本文只是簡單使用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,如有錯誤請批評指正。
