When creating an application (* server-side Java is assumed here), I think there is often a requirement to save and retrieve files. There are various candidates for storing files.
--Local file system
--Cloud storage such as S3
and Cloud Storage
--RDB (BLOB type)
here
--Define an interface that abstracts file storage that does not depend on a specific storage method, --The implementation on the back side can be easily switched depending on the environment etc.
I will show you how to do that.
The completed form can be found on GitHub.
We will implement the file storage as "file storage". "File storage" is like a key-value store that manages files.
--Key (file location) --Value (contents of file)
Suppose you manage files in the form of.
We will provide an interface called FileStorageService
, through which files can be saved / retrieved.
There are three types of implementation of FileStorageService
.
--LocalFileStorageService
(Save the file in the file system of the local host. Assuming that it will be used during local development)
--S3FileStorageService
(Save to S3. Assuming use in production environment)
--ʻInMemoryFileStorageService` (Keeps files in memory, assuming use during CI and automated testing)
Finally, set Spring Boot to replace these implementations depending on your environment.
This time, we will use commons-io
and ʻaws-java-sdk-s3. Let's dive into
dependencies. I usually use
Lombok`, but I will not use it this time because it will be a relatively simple implementation.
dependencies {
implementation 'commons-io:commons-io:2.6'
implementation 'com.amazonaws:aws-java-sdk-s3:1.11.774'
}
First, we will make the "side" side. I will make three.
--FileStorageService
... The one who handles the processing such as saving / retrieving files. The leading role.
--FileLocation
... A value object that represents the location of a file on storage.
--FileStorageObject
... An object that represents the contents of a file
It is an image to use like this.
FileStorageService fileStorageService = ...;
//Save the file
fileStorageService.putFile(FileLocation.of("hoge/fuga/sample.txt"), file);
//Extract the saved file
FileStorageObject fileStorageObject = fileStorageService.getFile(FileLocation.of("hoge/fuga/sample.txt"));
InputStream is = fileStorageObject.getInputStream(); //The contents of the file can be obtained with InputStream
The key that represents the location of the file can be String
, but let's prepare a value object called FileLocation
that wraps String
.
FileLocation.java
First, create an object that represents the location (key) of the file.
import java.util.Objects;
/**
*The location of the file on the file storage.
*/
public class FileLocation {
/**
*path."parent/child/file.txt"Assuming a value like.
*/
private final String value;
private FileLocation(String value) {
this.value = value.startsWith("/") ? value.substring(1) : value;
}
/**
*From a string{@link FileLocation}Create an instance of.
*
* @param value path
* @return instance
*/
public static FileLocation of(String value) {
return new FileLocation(value);
}
/**
*From multiple strings{@link FileLocation}Create an instance of.
*Each string is"/"It is connected by.
*
* @param parts Multiple strings that make up the path
* @return instance
*/
public static FileLocation of(String... parts) {
if (parts.length == 1) {
return new FileLocation(parts[0]);
}
return new FileLocation(String.join("/", parts));
}
@Override
public String toString() {
return value;
}
//Implement hashCode and equals
...
}
You can get the FileLocation
object like this.
FileLocation fileLocation = FileLocation.of("key/to/file.txt");
This is the same. (I referred to the Java standard API Paths.get ()
)
FileLocation fileLocation = FileLocation.of("key", "to", "file.txt");
FileStorageObject.java
Next, create an object that represents the file retrieved from storage. It is used in the return value of FileStorageService # getFile ()
.
import java.io.IOException;
import java.io.InputStream;
import java.io.UncheckedIOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import org.apache.commons.io.IOUtils;
/**
*An object that represents the contents of a file on storage.
*/
public interface FileStorageObject {
/**
*The contents of the file{@link InputStream}Get in.
*
* @return {@link InputStream}
*/
InputStream getInputStream();
}
Create as an interface. Here, only the method that returns ʻInputStream` is used, but it is OK to use various convenient methods.
FileStorageService.java
Finally, make a guy who works with files. It is the leading role.
import java.io.InputStream;
import java.nio.file.Path;
/**
*A service for manipulating files in file storage.
*/
public interface FileStorageService {
/**
*Save the file.
*
* @param fileLocation Destination on storage
* @Contents of the param inputStream file
*/
void putFile(FileLocation fileLocation, InputStream inputStream);
/**
*Save the file.
*
* @param fileLocation Destination on storage
* @param localFile File to save
*/
void putFile(FileLocation fileLocation, Path localFile);
/**
*Delete the file.
*If the file does not exist, do nothing.
*
* @param fileLocation Destination on storage
*/
void deleteFile(FileLocation fileLocation);
/**
*Get the file.
*
* @param fileLocation Location on storage
* @return File object. Null if it does not exist
*/
FileStorageObject getFile(FileLocation fileLocation);
}
It's like a map with FileLocation
as the key and the contents of the file as the value.
The putFile ()
method provides putFile (InputStream)
and putFile (Path)
,
You can save anything as long as you have ʻInputStream. If you want to specify the contents of the file with
byte []`, it looks like this.
byte[] bytes = ...;
fileStorageService.putFile(FileLocation.of("hoge"), new ByteArrayInputStream(bytes));
In ↑, FileStorageService
is created as an interface, so there is no content.
Here, we will implement three, LocalFileStorageService
, S3FileStorageService
, and ʻInMemoryFileStorageService`.
LocalFileStorageService.java
First, create an implementation of FileStorageService that stores files in your local file system. Receives the root directory of the file storage location as a constructor argument.
public class LocalFileStorageService implements FileStorageService {
private final Path rootDirPath;
public LocalFileStorageService(Path rootDirPath) {
this.rootDirPath = Objects.requireNonNull(rootDirPath);
}
@Override
public void putFile(FileLocation targetLocation, InputStream inputStream) {
Path target = rootDirPath.resolve(targetLocation.toString());
ensureDirectoryExists(target.getParent());
try (InputStream is = inputStream) {
Files.write(target, IOUtils.toByteArray(inputStream));
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}
@Override
public void putFile(FileLocation targetLocation, Path localFile) {
Path target = rootDirPath.resolve(targetLocation.toString());
ensureDirectoryExists(target.getParent());
try {
Files.copy(localFile, target, StandardCopyOption.REPLACE_EXISTING);
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}
@Override
public void deleteFile(FileLocation targetLocation) {
Path path = rootDirPath.resolve(targetLocation.toString());
if (!Files.exists(path)) {
return;
}
try {
Files.delete(path);
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}
@Override
public FileStorageObject getFile(FileLocation fileLocation) {
Path path = rootDirPath.resolve(fileLocation.toString());
if (!Files.exists(path)) {
return null;
}
return new LocalFileStorageObject(path);
}
private void ensureDirectoryExists(Path directory) {
if (!Files.exists(directory)) {
try {
Files.createDirectories(directory);
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}
}
private static class LocalFileStorageObject implements FileStorageObject {
private final Path path;
private LocalFileStorageObject(Path path) {
this.path = path;
}
@Override
public InputStream getInputStream() {
try {
return Files.newInputStream(path);
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}
}
}
If you do like ↓, a file will be created in /hoge/fuga/abc/efg.txt
.
FileStorageService fileStorageService = new LocalFileStorageService(Paths.get("/hoge/fuga"));
fileStorageService.putFile(FileLocation.of("abc/efg.txt"), file);
S3FileStorageService.java
Next, create an implementation of FileStorageService that stores files in AWS S3.
public class S3FileStorageService implements FileStorageService {
private final AmazonS3 s3Client;
private final String bucketName;
public S3FileStorageService(AmazonS3 s3Client, String bucketName) {
this.s3Client = Objects.requireNonNull(s3Client);
this.bucketName = Objects.requireNonNull(bucketName);
}
@Override
public void putFile(FileLocation targetLocation, InputStream inputStream) {
Path scratchFile = null;
try (InputStream is = inputStream) {
//If you try to upload directly with InputStream, you have to set ContentLength, so write it to a file once
//PutFile if you care about performance(FileLocation, InputStream, int contentLength)Or maybe you can prepare
scratchFile = Files.createTempFile("s3put", ".tmp");
Files.copy(inputStream, scratchFile);
putFile(targetLocation, scratchFile);
} catch (IOException e) {
throw new UncheckedIOException(e);
} finally {
if (scratchFile != null) {
FileUtils.deleteQuietly(scratchFile.toFile());
}
}
}
@Override
public void putFile(FileLocation targetLocation, Path localFile) {
if (!Files.exists(localFile)) {
throw new IllegalArgumentException(localFile + " does not exists.");
}
s3Client.putObject(new PutObjectRequest(bucketName, targetLocation.toString(), localFile.toFile()));
}
@Override
public void deleteFile(FileLocation targetLocation) {
s3Client.deleteObject(bucketName, targetLocation.toString());
}
@Override
public FileStorageObject getFile(FileLocation fileLocation) {
S3Object s3Object = s3Client.getObject(new GetObjectRequest(bucketName, fileLocation.toString()));
if (s3Object == null) {
return null;
}
return new S3FileStorageObject(s3Object);
}
private static class S3FileStorageObject implements FileStorageObject {
private final S3Object s3Object;
private S3FileStorageObject(S3Object s3Object) {
this.s3Object = s3Object;
}
@Override
public InputStream getInputStream() {
return s3Object.getObjectContent();
}
}
}
InMemoryFileStorageService.java
Finally, create a FileStorageService that holds the files in memory.
public class InMemoryFileStorageService implements FileStorageService {
private final Map<FileLocation, byte[]> files = new ConcurrentHashMap<>();
@Override
public void putFile(FileLocation targetLocation, InputStream inputStream) {
try (InputStream is = inputStream) {
files.put(targetLocation, IOUtils.toByteArray(inputStream));
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}
@Override
public void putFile(FileLocation targetLocation, Path localFile) {
try {
byte[] bytes = Files.readAllBytes(localFile);
files.put(targetLocation, bytes);
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}
@Override
public void deleteFile(FileLocation targetLocation) {
files.remove(targetLocation);
}
@Override
public FileStorageObject getFile(FileLocation fileLocation) {
byte[] bytes = files.get(fileLocation);
if (bytes == null) {
return null;
}
return new InMemoryFileStorageObject(bytes);
}
private static class InMemoryFileStorageObject implements FileStorageObject {
private final byte[] bytes;
private InMemoryFileStorageObject(byte[] bytes) {
this.bytes = bytes;
}
@Override
public InputStream getInputStream() {
return new ByteArrayInputStream(bytes);
}
}
}
At this point, the implementation of the library that handles "file storage" is complete. (Slightly improved ones are on GitHub.)
Let's replace the implementation of 3 types of FileStorageService depending on the environment. The side that uses the FileStorageService asks Spring to inject an instance of the FileStorageService so that it doesn't have to know which implementation is being used.
@Service
public class SampleService {
private final FileStorageService fileStorageService; //It is injected. You don't have to know which implementation is used
public SampleService(FileStorageService fileStorageService) { //Constructor injection
this.fileStorageService = fileStorageService;
}
public void doSomething() {
fileStorageService.getFile(...);
}
}
After that, it is perfect if the instance of FileStorageService to be injected is switched depending on the environment.
[Profiles](https://docs.spring.io/spring-boot/docs/current/reference/html/spring-boot-features.html#boot-features-profiles] is a mechanism for switching settings for each environment in Spring. ). Let's use this to replace the implementation of FileStorageService for each environment.
Here, set three Profiles according to the environment.
--During local development: development
(→ use LocalFileStorageService
)
--During automatic testing: test
(→ ʻInMemoryFileStorageServiceis used) --Production:
production (→ Use
S3FileStorageService`)
Make sure that the appropriate profile is enabled in each environment.
development
Allows the profile development
to be enabled by default.
If you write ↓ in src / main / resources / application.yml
, this profile will be enabled when you start the Spring Boot application normally.
src/main/resources/application.yml
spring:
profiles:
active: development
test
Next, make sure that the profile test
is enabled when you run the test.
Write as ↓ in src / test / resources / application.yml
. (At the time of testing, this is prioritized over ↑)
src/test/resources/application.yml
spring:
profiles:
active: test
production
When you start the Spring Boot application in production, start it with the option --spring.profiles.active = production
.
Register the bean using Java Config.
You can use the annotation @Profile
to generate & register a bean only when a specific Profile is valid.
FileStorageConfiguration.java
@Configuration
public class FileStorageConfiguration {
@Bean
@Profile("development")
FileStorageService localFileStorageService(
@Value("${app.fileStorage.local.rootDir}") String rootDir) {
return new LocalFileStorageServiceFactory(Paths.get(rootDir));
}
@Bean
@Profile("test")
FileStorageService inMemoryFileStorageService() {
return new InMemoryFileStorageService();
}
@Bean
@Profile("production")
FileStorageService s3FileStorageService(AmazonS3 amazonS3,
@Value("${app.fileStorage.s3.bucketName}") String bucketName) {
return new S3FileStorageService(amazonS3, bucketName);
}
@Bean
AmazonS3 amazonS3() {
return AmazonS3ClientBuilder.defaultClient();
}
}
In the part of ↑ where it is @Value ("$ {key} ")
, if you write the setting corresponding to ʻapplication- {profile} .yml`, the value will be injected automatically.
src/main/resources/application-development.yml
app:
fileStorage:
local:
rootDir: "/opt/app/file_storage"
src/main/resources/application-production.yml
app:
fileStorage:
s3:
bucketName: smaple_bucket
Recommended Posts