1. 概述

Spring WebFlux 是一个响应式 Web 框架,通过非阻塞事件循环异步处理 I/O 操作。它使用 Mono 和 Flux 作为响应式流发布器,在订阅时发出数据。

这种响应式方式能帮助应用处理大量请求和数据,而无需分配过多资源。本教程将通过分步指南,学习如何使用 Spring WebFlux 将多个文件上传到目录,并将文件名映射到实体类以便后续检索。

2. 项目设置

创建一个简单的响应式 Spring Boot 项目,将多个文件上传到目录。为简化演示,我们将文件存储在项目根目录下。生产环境中建议使用 AWS S3、Azure Blob Storage 或 Oracle Cloud Infrastructure 等文件系统

2.1 Maven 依赖

首先在 pom.xml 中添加 spring-boot-starter-webflux 依赖:

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

这提供了构建响应式 Web 应用所需的核心 WebFlux API 和嵌入式 Netty 服务器

接着添加 spring-boot-starter-data-r2dbc 和 H2 数据库依赖:

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

WebFlux 的 R2DBC 是响应式数据库连接器,H2 是内存数据库。最后添加 R2DBC 原生驱动:

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

2.2 实体、仓库和控制器

创建实体类 FileRecord

class FileRecord {
    @Id
    private int id;
    private List<String> filenames;
    
   // 标准的 getter/setter/构造方法  
}

创建仓库接口 FileRecordRepository

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

创建控制器类:

@RestController
class FileRecordController {
}

后续我们将文件名和扩展名映射到 fileName 字段。

3. 上传文件到目录

有时我们需要将多个文件上传到文件系统而不映射文件名到数据库实体。这种情况下后续检索文件会变得困难

以下代码示例将多个文件上传到根目录,但不映射文件名到实体:

@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"));
}

首先创建 uploadFileWithoutEntity() 方法接收 FilePartFlux。**然后对每个 FilePart 调用 flatMap() 传输文件并返回 Mono**。这会为每个文件传输操作创建单独的 Mono,并将 Mono 流扁平化为单个 Mono

使用 Postman 测试该接口:

上传多个文件但不映射到数据库实体

上图中我们上传了三个文件到项目根目录。接口返回 OK 表示操作成功。

注意我们使用 onErrorResume() 显式处理文件上传错误。如果上传失败,接口返回错误信息

但失败前已上传的文件可能已成功传输,这种情况下需要清理部分上传的文件。为简化演示,我们未实现清理逻辑。

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

进一步可以将文件名映射到数据库实体,这样后续可通过 Id 检索文件。这在需要显示图像或进行额外计算时很有用。

4.1 数据库配置

在资源文件夹创建 schema.sql 定义表结构:

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

创建 file_record 表存储文件名和扩展名。接着编写配置在启动时初始化 schema:

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

    return initializer;
}

application.properties 中定义数据库 URL:

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

为简化演示,数据库未设置密码保护。

4.2 服务层

创建服务类处理数据持久化:

@Service
public class FileRecordService {

    private FileRecordRepository fileRecordRepository;

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

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

在服务类中注入 FileRecordRepository 接口,定义保存文件名和扩展名的逻辑

接着在控制器类中注入 FileRecordService

private FileRecordService fileRecordService;

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

使数据持久化逻辑在控制器中可用。

4.3 上传接口

编写接口将多个文件上传到根目录,并将文件名映射到实体类:

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

创建接口返回 Mono接收 FilePartFlux 并上传每个文件,然后收集文件名和扩展名映射到 FileRecord 实体

使用 Postman 测试:

上传多个文件并映射到数据库实体

上传 spring-config.xmlserver_name.png 两个文件。POST 请求返回包含请求详情的 Mono

为简化演示,我们未验证文件名、类型和大小。

4.4 根据 Id 获取文件记录

实现接口通过 Id 检索文件记录以查看关联的文件名。

先在服务类添加通过 Id 检索的逻辑:

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

调用 bookRepositoryfindById() 通过 Id 检索存储的 Book。

接着编写获取文件记录的接口:

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

该接口返回包含文件 Id 和文件名的 Mono

使用 Postman 测试:

通过 id 获取关联文件

上图返回文件信息。图像文件 URL 可包含在 API 响应中,客户端可通过这些 URL 检索和显示图像。通过处理服务传递文件可实现额外的图像处理和编辑功能。

5. 结论

本文学习了如何使用 Spring WebFlux 将多个文件上传到服务器文件系统,以及如何上传文件时选择是否将文件名和扩展名映射到数据库实体。

最后展示了一种同时上传文件并将文件名和扩展名持久化到数据库的方法。

完整示例代码可在 GitHub 获取。


原始标题:Upload Multiple Files Using WebFlux | Baeldung