Upload Multiple Files Using WebFlux – 使用 WebFlux 上传多个文件

最后修改: 2024年 1月 19日

中文/混合/英文(键盘快捷键:t)

1. Overview

1.概述

Spring WebFlux is a reactive web framework that provides a non-blocking event loop to handle I/O operations asynchronously. Also, it uses Mono and Flux reactive stream publishers to emit data when subscribed to.

Spring WebFlux 是一个反应式 Web 框架,它提供了一个非阻塞事件循环来异步处理 I/O 操作。此外,它还使用MonoFlux反应式stream发布器在订阅时发布数据。

This reactive approach helps applications handle large volumes of requests and data without allocating huge resources.

这种反应式方法可帮助应用程序处理大量请求和数据,而无需分配大量资源。

In this tutorial, we’ll learn how to upload multiple files to a directory using Spring WebFlux through a step-by-step guide. Also, we’ll map the filename to an entity class for easy retrieval.

在本教程中,我们将逐步学习如何使用 Spring WebFlux 将多个文件上传到一个目录。此外,我们还将把文件名映射到实体类,以便于检索。

2. Project Setup

2.项目设置

Let’s create a simple reactive Spring Boot project that uploads multiple files to a directory. For simplicity, we’ll use the root directory of our project to store the files. In production, we can use a file system like AWS S3, Azure Blob Storage, Oracle Cloud Infrastructure storage, etc.

让我们创建一个简单的反应式 Spring Boot 项目,将多个文件上传到一个目录。为简单起见,我们将使用项目的根目录来存储文件。在生产中,我们可以使用文件系统,如 AWS S3、Azure Blob 存储、Oracle 云基础架构存储等

2.1. Maven Dependencies

2.1.Maven 依赖项

To begin with, let’s bootstrap a Spring WebFlux application by adding the spring-boot-starter-webflux dependency to the pom.xml:

首先,让我们通过在 pom.xml 中添加 spring-boot-starter-webflux 依赖关系来引导 Spring WebFlux 应用程序:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-webflux</artifactId>
    <version>3.2.0</version>
</dependency>

This provides the core Spring WebFlux APIs and embedded Netty server to build reactive web applications.

它提供了核心 Spring WebFlux API 和嵌入式 Netty 服务器,用于构建反应式 Web 应用程序

Also, let’s add the spring-boot-starter-data-r2dbc and H2 database dependencies to the pom.xml file:

此外,让我们在 pom.xml 文件中添加 spring-boot-starter-data-r2dbcH2 数据库依赖项:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-r2dbc</artifactId>
    <version>3.2.0</version>
</dependency>
<dependency>
    <groupId>com.h2database</groupId>
    <artifactId>h2</artifactId>
    <version>2.2.224</version>
</dependency>

Spring WebFlux R2DBC is a reactive database connector and the H2 database is an in-memory database.

Spring WebFlux R2DBC 是一个反应式数据库连接器,而 H2 数据库是一个内存数据库。

Finally, let’s add the R2DBC native driver dependency to the pom.xml:

最后,让我们将 R2DBC 本机驱动程序 依赖关系添加到 pom.xml 中:

<dependency>
    <groupId>io.r2dbc</groupId>
    <artifactId>r2dbc-h2</artifactId>
    <version>1.0.0.RELEASE</version>
</dependency>

This native driver is implemented for the H2 database.

该本地驱动程序是为 H2 数据库实施的。

2.2. Entity, Repository and Controller

2.2.实体、存储库和控制器

Let’s create an entity class named FileRecord:

让我们创建一个名为 FileRecord 实体类:

class FileRecord {
    @Id
    private int id;
    private List<String> filenames;
    
   // standard getters, setters, constructor  
}

Next, let’s create a repository named FileRecordRepository:

接下来,让我们创建一个名为 FileRecordRepository 的存储库:

@Repository
interface FileRecordRepository extends R2dbcRepository<FileRecord, Integer> {
}

Finally, let’s create a controller class:

最后,让我们创建一个控制器类:

@RestController
class FileRecordController {
}

In the subsequent sections, we’ll map our file name and its extension to the fileName field.

在随后的章节中,我们将把文件名及其扩展名映射到 fileName 字段。

3. Uploading Files to a Directory

3.将文件上传到目录

Occasionally, we may upload multiple files to a filesystem without mapping the filenames to database entities. In this case, retrieving the files later may be challenging.

有时,我们可能会将多个文件上传到文件系统,但没有将文件名映射到数据库实体。在这种情况下,以后检索文件可能会很困难

Let’s see an example code that uploads multiple files to our root directory without mapping the file name to an entity:

让我们来看一个示例代码,它可以在不将文件名映射到实体的情况下将多个文件上传到我们的根目录:

PostMapping("/upload-files")
Mono uploadFileWithoutEntity(@RequestPart("files") Flux<FilePart> filePartFlux) {
    return filePartFlux.flatMap(file -> file.transferTo(Paths.get(file.filename())))
      .then(Mono.just("OK"))
      .onErrorResume(error -> Mono.just("Error uploading files"));
}

First, we create a method named uploadFileWithoutEntity() which accepts Flux of FilePart objects. Then, we invoke the flatMap() method on each FilePart object to transfer the file and return a Mono. This creates a separate Mono for each file transfer operation and flattens the stream of Monos into a single Mono.

首先,我们创建一个名为 uploadFileWithoutEntity() 的方法,该方法接受 Flux FilePart 对象。然后,我们在每个 FilePart 对象上调用 flatMap() 方法来传输文件,并返回一个 Mono 。这会为每个文件传输操作创建一个单独的 Mono 并将 Mono 流扁平化为一个 Mono

Let’s test the endpoint by uploading multiple files through Postman:

让我们通过 Postman 上传多个文件来测试端点:

Upload multiple files and do not map to database entity

In the image above, we upload three files to the project root directory. The endpoint returns OK to show that the operation was completed successfully.

在上图中,我们向项目根目录上传了三个文件。端点返回 OK 表示操作已成功完成。

Notably, we use the onErrorResume() method to handle errors related to file upload explicitly. In a case where failure occurs while uploading, the endpoint returns the error message.

值得注意的是,我们使用 onErrorResume() 方法来明确处理与文件上传相关的错误。在上传失败的情况下,端点会返回错误信息

However, files uploaded earlier may have been transferred successfully before the failure. In this case, clean-up may be needed to delete partially uploaded files on error. For simplicity, we didn’t cover the clean-up process.

不过,在出现故障之前,之前上传的文件可能已经成功传输。在这种情况下,可能需要进行清理,删除错误上传的部分文件。为了简单起见,我们没有介绍清理过程。

4. Mapping Uploaded Files to Database Entities

4.将上传的文件映射到数据库实体

Furthermore, we can map the file name to the database entity. This gives us the flexibility to retrieve files by their Id later. This is useful when we want to display an image or perform further computation.

此外,我们还可以将文件名映射到数据库实体。这样,我们就可以灵活地根据文件的 Id 来检索文件。这在我们要显示图像或执行进一步计算时非常有用。

4.1. Database Configuration

4.1.数据库配置

First, let’s create a schema.sql file in the resource folder to define the database table structure:

首先,让我们在资源文件夹中创建一个 schema.sql 文件,以定义数据库表结构:

CREATE TABLE IF NOT EXISTS file_record (
    id INT NOT NULL AUTO_INCREMENT,
    filenames VARCHAR(255),
    PRIMARY KEY (id)
);

Here, we create a file record table to store the uploaded file name and its extension. Next, let’s write a configuration to initialize the schema on startup:

在这里,我们创建了一个文件记录表,用于存储上传的文件名及其扩展名。接下来,让我们编写一个配置,以便在启动时初始化模式:

@Bean
ConnectionFactoryInitializer initializer(ConnectionFactory connectionFactory) {
    ConnectionFactoryInitializer initializer = new ConnectionFactoryInitializer();
    initializer.setConnectionFactory(connectionFactory);
    initializer.setDatabasePopulator(new ResourceDatabasePopulator(new ClassPathResource("schema.sql")));

    return initializer;
}

Also, let’s define the database URL in the application.properties file:

此外,让我们在 application.properties 文件中定义数据库 URL:

spring.r2dbc.url=r2dbc:h2:file:///./testdb

Here, we define the R2DBC URL to connect to the H2 database. For simplicity, the database isn’t protected with a password.

在此,我们定义 R2DBC URL 以连接 H2 数据库。为简单起见,数据库不使用密码保护。

4.2. Service Layer

4.2.服务层

First, let’s create a service class and add the logic to handle data persistence:

首先,让我们创建一个服务类,并添加处理数据持久性的逻辑:

@Service
public class FileRecordService {

    private FileRecordRepository fileRecordRepository;

    public FileRecordService(FileRecordRepository fileRecordRepository) {
        this.fileRecordRepository = fileRecordRepository;
    }

    public Mono<FileRecord> save(FileRecord fileRecord) {
        return fileRecordRepository.save(fileRecord);
    }
}

Here, we inject the FileRecordRepository interface in the service class and define the logic to save the file name and its extension in the database.

在此,我们将在服务类中注入 FileRecordRepository 接口,并定义在数据库中保存文件名及其扩展名的逻辑

Next, let’s inject the FileRecordService class into the controller class:

接下来,让我们将 FileRecordService 类注入控制器类:

private FileRecordService fileRecordService;

public FileRecordController(FileRecordService fileRecordService) {
    this.fileRecordService = fileRecordService;
}

The code above makes the logic to persist data available in the controller class.

上面的代码在控制器类中提供了持久化数据的逻辑。

4.3. Upload Endpoint

4.3.上传终端

Finally, let’s write an endpoint that uploads multiple files to the root directory and maps the file names and their extension to an entity class:

最后,让我们编写一个端点,将多个文件上传到根目录,并将文件名及其扩展名映射到实体类:

@PostMapping("/upload-files-entity")
Mono uploadFileWithEntity(@RequestPart("files") Flux<FilePart> filePartFlux) {
    FileRecord fileRecord = new FileRecord();

    return filePartFlux.flatMap(filePart -> filePart.transferTo(Paths.get(filePart.filename()))
      .then(Mono.just(filePart.filename())))
      .collectList()
      .flatMap(filenames -> {
          fileRecord.setFilenames(filenames);
          return fileRecordService.save(fileRecord);
      })
      .onErrorResume(error -> Mono.error(error));
}

Here, we create an endpoint that emits Mono. It accepts Flux of FilePart and uploads each file. Next, it collects the file name and its extension and maps them to the FileRecord entity.

在此,我们创建了一个可发出 Mono 的端点。它接受 FluxFilePart 并上传每个文件。接下来,它会收集文件名及其扩展名,并将它们映射到 FileRecord 实体。

Let’s test the endpoint with Postman:

让我们用 Postman 测试端点:

Upload multiple files and map to database entity

 

Here, we upload two files named spring-config.xml and server_name.png to the server. The POST request emits a Mono showing details of the request.

在此,我们将两个名为 spring-config.xmlserver_name.png 的文件上传到服务器。POST 请求会发出一个 Mono 显示请求的详细信息。

For simplicity, we didn’t validate the file name, type, and size.

为了简单起见,我们没有验证文件名、类型和大小。

4.4. Retrieve Book by Id

4.4.按 Id 检索图书

Let’s implement an endpoint to retrieve stored a file record by its Id to view the associated file names.

让我们实现一个端点,根据文件记录Id检索存储的文件记录,以查看相关的文件名。

First, let’s add the logic to retrieve a file record by its Id to the service class:

首先,让我们在服务类中添加通过 Id 检索文件记录的逻辑:

Mono findById(int id) {
    return fileRecordRepository.findById(id);
}

Here, we invoke the findById() on bookRepository to retrieve the store Book by its id.

在这里,我们在 bookRepository 上调用 findById() 以通过其 id 检索存储 Book。

Next, let’s write an endpoint to retrieve the file record:

接下来,让我们编写一个端点来检索文件记录:

@GetMapping("/files/{id}")
Mono geFilesById(@PathVariable("id") int id) {
    return fileRecordService.findById(id)
      .onErrorResume(error -> Mono.error(error));
}

This endpoint returns a Mono containing the file Id and file name.

该端点会返回一个包含文件 Id 和文件名的 Mono

Let’s see the endpoint in action using Postman:

让我们使用 Postman 来看看端点的运行情况:

get associated files by it id

The image above shows the return file information. The image file URLs can be returned in the API response. The client can use these URLs to retrieve and display the images. Additional processing and editing of the images can be implemented by passing the files through processing services.

上图显示了返回的文件信息。可在 API 响应中返回图像文件 URL。客户端可以使用这些 URL 来检索和显示图像。通过处理服务传递文件,可以对图像进行其他处理和编辑。

5. Conclusion

5.结论

In this article, we learned how to upload multiple files to the server file system using Spring WebFlux.  Also, we saw how to upload files with or without mapping the file name and extension to a database entity.

在本文中,我们学习了如何使用 Spring WebFlux 将多个文件上传到服务器文件系统。 此外,我们还了解了如何在将文件名和扩展名映射到数据库实体的情况下上传文件。

Finally, we saw an approach that uploads the files and persists the file name and its extension to the database.

最后,我们看到了一种上传文件并将文件名及其扩展名持久保存到数据库的方法。

As always, the full source code for the example is available over on GitHub.

一如既往,该示例的完整源代码可在 GitHub 上获取。