使用Spring Webflux + MinIO构建响应式文件服务
本文只是简单使用SpringWebflux和MinIO的基本功能构建了一个基础的文件服务,仅作学习Demo使用。
前言
Spring Webflux是Spring5.0之后Spring推出的响应式编程框架,对标Spring MVC。Webflux并不能代替MVC,官方也并不推荐完全替代MVC的功能,对于日常数据库如MySQL、Oracle等,还没有响应式的ORM解决方案,在这种事务性的使用场景下并不适用于Spring Webflux,但是抛去事务性的使用场景,如API网关、文件服务等,Spring Webflux可以发挥出最大的优势。
MinIO是一个高性能的分布式对象存储系统,相对于fastDFS来说,具有易部署、易扩展、API更易用等特点,天然支持云原生,这里我们选择MinIO作为我们的文件存储底层服务。
环境依赖
首先需要安装Mongodb和MinIO,有了Docker之后可以很方便的进行环境的搭建,这部分不再赘述,大家可自行去docker hub参考官方说明进行部署,我也在github的源码上给出了一份docker-compose的部署文件以供参考。
代码实现
Maven依赖
我们还是使用SpringBoot2作为我们的开发框架,这里我选择目前最新的2.3.2版本,同时为了存储文件上传后的信息,选择支持响应式编程的MongoDB作为数据库。
关键依赖如下:
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.2.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
...
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-mongodb-reactive</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<dependency>
<groupId>io.minio</groupId>
<artifactId>minio</artifactId>
<version>${minio.version}</version>
</dependency>
</dependencies>
Spring Webflux支持两种开发模式:
- 类似于Spring WebMVC的基于注解(
@Controller
、@RequestMapping
)的开发模式; - Java 8 lambda 风格的函数式开发模式。
这里我们选择注解的方式。
编写Endpoint
实现三个API:文件上传,下载和删除:
@PostMapping(value = "/upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public Mono<FileInfo> uploadFile(@RequestPart("file") FilePart filePart) {
log.info("upload file:{}", filePart.filename());
return fileService.uploadFile(filePart);
}
@GetMapping("download/{fileId}")
public Mono<Void> downloadFile(@PathVariable String fileId, ServerHttpResponse response) {
Mono<FileInfo> fileInfoMono = fileService.getFileById(fileId);
Mono<FileInfo> fallback = Mono.error(new FileNotFoundException("No file was found with fileId: " + fileId));
return fileInfoMono
.switchIfEmpty(fallback)
.flatMap(fileInfo -> {
var fileName = new String(fileInfo.getDfsFileName().getBytes(Charset.defaultCharset()), StandardCharsets.ISO_8859_1);
ZeroCopyHttpOutputMessage zeroCopyResponse = (ZeroCopyHttpOutputMessage) response;
response.getHeaders().set(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=" + fileName);
response.getHeaders().setContentType(MediaType.IMAGE_PNG);
var file = new File(fileInfo.getDfsBucket());
return zeroCopyResponse.writeWith(file, 0, file.length());
});
}
@DeleteMapping("{fileId}")
public Mono<Void> deleteFile(@PathVariable String fileId, ServerHttpResponse response) {
return fileService.deleteById(fileId);
}
这里可以看到我们的响应都是Mono<T>
,可以理解为返回单个对象,同时用了 org.springframework.http.codec.multipart.FilePart
作为我们文件上传的载体,这是一个Spring5.0后引入的类,目的就是为了支持响应式的文件操作。
编写Service
主要的业务逻辑我们都通过Service去操作,对于上传文件,我们通过Spring5新的API将FilePart转为DataBuffer,再通过DataBuffer转为流,使用MinIO提供的API把流上传到MinIO中,最后将文件的基本信息写入到mongoDB中。代码如下:
@Override
public Mono<FileInfo> uploadFile(FilePart filePart) {
return DataBufferUtils.join(filePart.content())
.map(dataBuffer -> {
ObjectWriteResponse writeResponse = dfsRepository.uploadObject(filePart.filename(), dataBuffer.asInputStream());
FileInfo fileInfo = new FileInfo();
fileInfo.setOriginFileName(filePart.filename());
fileInfo.setDfsFileName(writeResponse.object());
fileInfo.setDfsBucket(writeResponse.bucket());
fileInfo.setCreatedAt(new Date());
return fileInfo;
})
.flatMap(fileInfo -> fileInfoRepository.insert(fileInfo))
.onErrorStop();
}
查询和删除文件的逻辑较为简单,这里给出代码:
@Override
public Mono<FileInfo> getFileById(String fileId) {
return fileInfoRepository.findById(fileId);
}
@Override
public Mono<Void> deleteById(String fileId) {
Mono<FileInfo> fileInfoMono = this.getFileById(fileId);
Mono<FileInfo> fallback = Mono.error(new FileNotFoundException("No file was found with fileId: " + fileId));
return fileInfoMono
.switchIfEmpty(fallback)
.flatMap(fileInfo -> {
dfsRepository.deleteObject(fileInfo.getDfsFileName());
return fileInfoRepository.deleteById(fileId);
}).then();
}
异常处理
在Spring webflux中,我们还是可以使用全局异常捕捉对异常进行处理,这里的用法和Spring MVC完全一致:
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(FileNotFoundException.class)
@ResponseStatus(code = HttpStatus.NOT_FOUND)
public ErrorInfo handleFileNotFoundException(FileNotFoundException e) {
log.error("FileNotFoundException occurred", e);
return new ErrorInfo("not_found", e.getMessage());
}
@ExceptionHandler(DfsServerException.class)
@ResponseStatus(code = HttpStatus.INTERNAL_SERVER_ERROR)
public ErrorInfo handleDfsServerException(DfsServerException e) {
log.error("DfsServerException occurred", e);
return new ErrorInfo("server_error", e.getMessage());
}
}
完成上述工作之后,一个简单的文件服务就完成了,可以使用Postman进行测试,可以通过MinIO提供的Web界面看到上传后的结果。
其他
作为一个完整的微服务,还需要考虑到服务的发现和服务的监控,这里我选择了Consul作为服务发现,Spring Boot Admin作为简单的监控工具,这里只需要引入pom依赖,再到 application.yml
里进行简单的配置即可:
# consul配置
spring:
cloud:
consul:
host: 192.168.3.168
port: 8501
discovery:
instance-id: ${spring.application.name}:${spring.cloud.client.ip-address}:${server.port}
serviceName: FILE-SERVER
prefer-ip-address: true
register-health-check: true
# 只能使用health-check-url,可以解决consul Service Checks failed的问题
# health-check-path: /actuator/health
health-check-url: http://${spring.cloud.client.ip-address}:${server.port}/actuator/health
health-check-critical-timeout: 30s
tags: 基础文件服务
最后
本文只是简单的演示了如何利用Spring Webflux完成一个简单的文件服务,同时也涉及了MinIO和MongoDB的使用,整个项目的源码我已提交到Github,spring-webflux-file-server,如有错误请批评指正。