Spring WebFlux Multipart 文件上传

1、概览

Spring WebFlux 是一个响应式 Web 框架,它提供了一个非阻塞 Event Loop,可异步处理 I/O 操作。此外,它还使用 MonoFlux Reactive Stream Publisher 在订阅时发布数据。

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

本文将带你了解如何在 Spring WebFlux 中处理 Multipart 文件上传。

2、项目设置

创建一个简单的响应式 Spring Boot 项目,将 Multipart 文件上传到一个目录。

为简单起见,使用项目的根目录来存储文件。在生产中,可以使用 云厂商 OSSMinio 存储等文件系统。

2.1、Maven 依赖

首先,在 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>

它提供核心 Spring WebFlux API 和嵌入式 Netty 服务器,用于构建响应式 Web 应用。

另外,还要在 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 是一个响应式数据库连接器(Database Connector),而 H2 数据库是一个内存数据库。

最后,在 pom.xml 中添加 R2DBC 原生驱动:

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

这个原生驱动是为H2数据库实现的。

2.2、Entity、Repository 和 Controller

创建 FileRecord 实体类

class FileRecord {
    @Id
    private int id;
    private List<String> filenames;
    
   // Get、Set、构造器省略 
}

创建 FileRecordRepository Repository:

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

最后,创建 Controller:

@RestController
class FileRecordController {
}

3、将文件上传到目录

示例如下,把 Multipart 文件上传到根目录。但没有把文件名称和扩展名保存到数据库。

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() 的方法,该方法接受 Flux<FilePart> 对象。然后,在每个 FilePart 对象上调用 flatMap() 方法来传输文件并返回一个 Mono。这将为每个文件传输操作创建一个单独的 Mono,并将 Mono 流扁平化为一个单一的 Mono

还使用了 onErrorResume() 方法来明确处理与文件上传相关的异常。如果上传失败,端点会返回错误信息。

注意,在出现异常之前,之前上传的文件可能已经成功传输。在这种情况下,可能需要进行清理,删除错误上传的部分文件。

通过 Postman 上传多个 Multipart 文件来测试端点:

Postman 上传文件

如上,向项目根目录上传了两个文件。端点返回 OK,表明操作已成功完成。

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

还可以将文件名映射到数据库实体。这样,以后就可以灵活地通过文件 Id 检索文件。

4.1、数据源配置

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

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

如上,创建了一个文件记录表,用于存储上传的文件名及其扩展名。接下来,编写一个配置类,以在启动时初始化 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

定义 R2DBC URL 以连接 H2 数据库。为简单起见,数据库没有设置密码。

4.2、Service 层

创建一个 Service 类,实现持久化数据。

@Service
public class FileRecordService {

    private FileRecordRepository fileRecordRepository;

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

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

如上,在 Service 类中注入 FileRecordRepository 接口,并通过 save 方法保存实体。

接下来,将 FileRecordService 类注入 Controller 类:

private FileRecordService fileRecordService;

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

4、上传端点

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

@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 的端点。它接受 Flux<FilePart> 并上传每个文件。然后,它会收集文件名及其扩展名,并将它们映射到 FileRecord 实体。

用 Postman 测试端点:

Postman 文件上传

如上,将两个名为 spring-config.xmlserver_name.png 的文件上传到服务器,并返回请求的详细信息。

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

4.4、根据 Id 检索文件

再实现一个端点,通过 Id 检索存储的文件记录,以查看对应的文件名。

首先,在 Service 类中添加通过 Id 检索文件记录的逻辑:

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

如上,调用 fileRecordRepository 上的 findById() 方法,通过其 id 来检索存储的 FileRecord

最后,实现根据 Id 检索文件的端点:

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

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

使用 Postman 来测试端点:

Postman 根据 ID 检索记录

如上,成功地返回了对应的文件信息。

5、总结

本文介绍了如何在 Spring WebFlux 实现 Multipart 文件上传,以及如何把上传文件的文件名称和扩展名存储到数据库。


Ref:https://www.baeldung.com/spring-webflux-upload-multiple-files