1、概览 {#1概览}
Spring WebFlux 是一个响应式 Web 框架,它提供了一个非阻塞 Event Loop,可异步处理 I/O 操作。此外,它还使用 Mono
和 Flux
Reactive Stream Publisher 在订阅时发布数据。
这种响应式方式可帮助应用处理大量请求和数据,而无需分配大量资源。
本文将带你了解如何在 Spring WebFlux 中处理 Multipart 文件上传。
2、项目设置 {#2项目设置}
创建一个简单的响应式 Spring Boot 项目,将 Multipart 文件上传到一个目录。
为简单起见,使用项目的根目录来存储文件。在生产中,可以使用 云厂商 OSS 、Minio 存储等文件系统。
2.1、Maven 依赖 {#21maven-依赖}
首先,在 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-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>
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 {#22entityrepository-和-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、将文件上传到目录 {#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 文件来测试端点:
如上,向项目根目录上传了两个文件。端点返回 OK,表明操作已成功完成。
4、将上传的文件映射到数据库实体 {#4将上传的文件映射到数据库实体}
还可以将文件名映射到数据库实体。这样,以后就可以灵活地通过文件 Id 检索文件。
4.1、数据源配置 {#41数据源配置}
首先,在 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 层 {#42service-层}
创建一个 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、上传端点 {#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 测试端点:
如上,将两个名为 spring-config.xml
和 server_name.png
的文件上传到服务器,并返回请求的详细信息。
为了简单起见,这里没有验证文件名、类型和大小。
4.4、根据 Id 检索文件 {#44根据-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 来测试端点:
如上,成功地返回了对应的文件信息。
5、总结 {#5总结}
本文介绍了如何在 Spring WebFlux 实现 Multipart 文件上传,以及如何把上传文件的文件名称和扩展名存储到数据库。
Ref:https://www.baeldung.com/spring-webflux-upload-multiple-files