使用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,如有錯誤請批評指正。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM