使用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界面看到上传后的结果。

img

其他

作为一个完整的微服务,还需要考虑到服务的发现和服务的监控,这里我选择了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,如有错误请批评指正。

posted @ 2020-08-10 15:02  ZongweiBai  阅读(2759)  评论(2编辑  收藏  举报