添加链接
link之家
链接快照平台
  • 输入网页链接,自动生成快照
  • 标签化管理网页链接

本文只是简单使用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,如有错误请批评指正。