Let's create a versatile file storage (?) Operation library by abstracting file storage / acquisition in Java

Nanikore

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.

Overview of what to do

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.

Implementation overview

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.

Dependent libraries

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'
}

Prepare API for operating "file storage"

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));

Prepare an implementation of FileStorageService

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.)

Replace implementation for each environment with Spring Boot

Thing you want to do

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.

Realization policy

[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`)

Profile settings

Make sure that the appropriate profile is enabled in each environment.

During local development: 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

During automated testing: 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: production

When you start the Spring Boot application in production, start it with the option --spring.profiles.active = production.

Register the implementation of FileStorageService according to Profile as Bean

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

Let's create a versatile file storage (?) Operation library by abstracting file storage / acquisition in Java
Let's create a super-simple web framework in Java
[Java] Let's create a mod for Minecraft 1.14.4 [0. Basic file]
[Java] Let's create a mod for Minecraft 1.16.1 [Basic file]
[Java] Create a temporary file
Let's create a file upload system using Azure Computer Vision API and Azure Storage Java SDK
Let's create a TODO application in Java 4 Implementation of posting function
Let's create a TODO application in Java 6 Implementation of search function
Let's create a TODO application in Java 8 Implementation of editing function
Let's create a TODO application in Java 1 Brief explanation of MVC
Let's create a TODO application in Java 5 Switch the display of TODO
To create a Zip file while grouping database search results in Java
Let's create a Java development environment (updating)
Create a TODO app in Java 7 Create Header
[Java] Let's make a DB access library!
Let's create a TODO application in Java 9 Create TODO display Sort by date and time + Set due date default to today's date
Android-Upload image files to Azure Blob Storage in Java
Let's create a versatile file storage (?) Operation library by abstracting file storage / acquisition in Java
[Java] File system operation
Azure functions in java
Create Azure Functions in Java
Read Java properties file in C #
Run Java application in Azure Batch
Unzip the zip file in Java
Log output to file in Java
About file copy processing in Java
Read xlsx file in Java with Selenium
Sample to unzip gz file in Java
Element operation method in appium TIPS (Java)
Read a string in a PDF file with Java
Create a CSR with extended information in Java
Let's create a timed process with Java Timer! !!
Try to create a bulletin board in Java
[Java] Let's create a mod for Minecraft 1.14.4 [Introduction]
A bat file that uses Java in windows
[Java] Let's create a mod for Minecraft 1.16.1 [Introduction]
[Java] Let's create a mod for Minecraft 1.14.4 [99. Mod output]
Let's create a TODO application in Java 11 Exception handling when accessing TODO with a non-existent ID
Create a Java Servlet and JSP WAR file to deploy to Apache Tomcat 9 in Gradle
[Java] Let's create a mod for Minecraft 1.14.4 [4. Add tools]
How to create a Java environment in just 3 seconds
[Java] Let's create a mod for Minecraft 1.14.4 [5. Add armor]
[CentOS, Eclipse] Load a library file in a C project
[Java] Let's create a mod for Minecraft 1.14.4 [Extra edition]
[Java] Let's create a mod for Minecraft 1.14.4 [7. Add progress]
[Java] Let's create a mod for Minecraft 1.14.4 [6. Add recipe]
[Java] Let's create a mod for Minecraft 1.16.1 [Add item]
I tried to create a Clova skill in Java
[Java] Let's create a mod for Minecraft 1.14.4 [1. Add items]
How to create a data URI (base64) in Java
How to convert a file to a byte array in Java
[Java] Let's create a mod for Minecraft 1.14.4 [2. Add block]
Java11: Run Java code in a single file as is
[Java] Let's create a mod for Minecraft 1.16.1 [Add block]
A library that realizes multi-line strings in Java multiline-string
[Java] File system operation
[Java] Create a filter
Create JSON in Java
Update your Java knowledge by writing a gRPC server in Java (2)
[Java] Let's create a mod for Minecraft 1.14.4 [3. Add creative tab]
Let's make a calculator application in Java ~ Display the application window
Create a jar file that can be executed in Gradle
Update your Java knowledge by writing a gRPC server in Java (1)
Create a SlackBot with AWS lambda & API Gateway in Java
Create a method to return the tax rate in Java
I want to create a Parquet file even in Ruby
Let's touch on Deep Java Library (DJL), a library that can handle Deep Learning in Java released from AWS.
Let's create a TODO application in Java 13 TODO form validation 1: Character limit ・ Gradle update to use @Validated
Let's create a TODO application in Java 3 Save temporary data in MySQL-> Get all-> Display on top