Spring Boot集成MinIO


在之前的文章中我介绍了 MinIO 文件数据库的安装及其 SDK 使用,今天就结合 Spring BootVue 实现文件的上传与下载,话不多说,直奔主题。

一、连接配置

1. 依赖导入

在项目中导入 MinIO 相关的 Maven 依赖。

<dependency>
    <groupId>io.minio</groupId>
    <artifactId>minio</artifactId>
    <version>8.0.3</version>
    </dependency>
<dependency>
    <groupId>commons-io</groupId>
    <artifactId>commons-io</artifactId>
    <version>2.6</version>
</dependency>
<dependency>
    <groupId>commons-fileupload</groupId>
    <artifactId>commons-fileupload</artifactId>
    <version>1.4</version>
</dependency>

2. 连接配置

在项目的 yml 文件添加 MinIO 相关配置。

# Minio数据库
minio:
  endpoint: http://127.0.0.1:9000
  accessKey: minioadmin
  secretKey: minioadmin

3. 实体类

public class MinioProp {

    private String endpoint;

    private String accessKey;

    private String secretKey;
}

4. 配置类

在创建连接前需要向 Spring Boot 中注入 Bean 对象。

@Configuration
@EnableConfigurationProperties(MinioProp.class)
public class MinioConfig {

    @Autowired
    private MinioProp minioProp;

    /**
     * 初始化 Minio 对象
     */
    @Bean
    public MinioClient minioClient() {
        return io.minio.MinioClient.builder()
                .endpoint(minioProp.getEndpoint())
                .credentials(minioProp.getAccessKey(), minioProp.getSecretKey())
                .build();
    }
}

二、MinIO工具

1. 返回值

封装请求 MinIO API 相关的返回值,用于设计接口时与前端进行交互。

@Data
@AllArgsConstructor
public class MinioRespond {

    // 原始文件名
    String originName;

    // 存入桶中的文件名
    String fileName;

    ObjectWriteResponse objectWriteResponse;

}

2. 工具类

为了方便后续的操作,这里封装了一些常用的方法,如上传、下载和删除等等。

@Component
public class MinioUtil {

    @Autowired
    private MinioClient client;

    /**
     * 判断 bucket 是否存在
     *
     * @param bucketName
     */
    public boolean bucketExist(String bucketName) throws Exception {
        return client.bucketExists(BucketExistsArgs.builder()
                .bucket(bucketName)
                .build());
    }

    /**
     * 创建 bucket
     *
     * @param bucketName
     */
    public void createBucket(String bucketName) throws Exception {
        if (!bucketExist(bucketName)) {
            client.makeBucket(MakeBucketArgs.builder()
                    .bucket(bucketName)
                    .build());
        }
    }

    /**
     * 删除 bucket
     *
     * @param bucketName
     */
    public void removeBucket(String bucketName) throws Exception {
        if (bucketExist(bucketName)) {
            client.removeBucket(RemoveBucketArgs.builder()
                    .bucket(bucketName)
                    .build());
        }
    }

    /**
     * 获取全部 bucket
     *
     * @return
     */
    public List<Bucket> getAllBuckets() throws Exception {
        return client.listBuckets();
    }

    /**
     * 上传文件
     *
     * MiniO 对于同名文件会直接覆盖,所以这里给文件名拼接一个UUID
     */
    public MinioRespond uploadFile(MultipartFile file, String bucketName) throws Exception {
        // 拼接文件名: <UUID>_<FileName>.Suffix
        String originName = file.getOriginalFilename();
        StringBuilder fileName = new StringBuilder();
        fileName.append(UUID.randomUUID());
        fileName.append("_");
        fileName.append(originName);

        ObjectWriteResponse objectWriteResponse = null;

        // 上传
        objectWriteResponse = client.putObject(
                PutObjectArgs.builder().bucket(bucketName)
                        .object(fileName.toString())
                        .stream(file.getInputStream(), file.getSize(), -1)
                        .build());

        MinioRespond respond = new MinioRespond(originName,
                fileName.toString(), objectWriteResponse);
        return respond;
    }

    /**
     * 获取⽂件
     *
     * @param bucketName bucket名称
     * @param objectName ⽂件名称
     * @return ⼆进制流
     */
    public InputStream getObject(String bucketName, String objectName) throws Exception {
        return client.getObject(GetObjectArgs.builder()
                .bucket(bucketName)
                .object(objectName)
                .build());
    }

    /**
     * 获取⽂件信息
     *
     * @param bucketName bucket名称
     * @param objectName ⽂件名称
     */
    public StatObjectResponse getObjectInfo(String bucketName, String objectName) throws Exception {
        return client.statObject(StatObjectArgs.builder()
                .bucket(bucketName)
                .object(objectName)
                .build());
    }

    /**
     * 删除⽂件
     *
     * @param bucketName bucket名称
     * @param objectName ⽂件名称
     */
    public void removeObject(String bucketName, String objectName) throws Exception {
        client.removeObject(RemoveObjectArgs.builder()
                .bucket(bucketName)
                .object(objectName)
                .build());
    }

}

二、上传文件

1. 接口设计

Spring Boot 提供 MultipartFile 用于文件流的相应操作,这里调用了上面封装的上传方法。

@PostMapping("/upload")
public boolean UploadFile(@RequestParam(name = "files") MultipartFile multipartFile) {
    // 文件判空
    if(multipartFile.isEmpty()){
        return false;
    }
    
    // 目标存储桶
    String bucketName = "webtest";
    boolean tag = false;
    MinioRespond minioRespond = null;
    try {
        minioRespond = minioUtil.uploadFile(multipartFile, bucketName);
        if(minioRespond.getObjectWriteResponse() != null){
            tag = true;
        }
    } catch (Exception e) {
        log.error("上传失败 : [{}]", Arrays.asList(e.getStackTrace()));
    }
    
    return tag;
}

2. 接口封装

接口请求这里使用的是 Axios ,具体使用教程参考之前文章:Axios二次封装

export function UploadFile(params) {
  return request({
    url: '/files/upload',
    method: 'post',
    data: params,
    headers: {
      'Content-Type': 'multipart/form-data'
    }
  })
}

3. 页面设计

前端页面这里选用了 Antdupload 组件,只是更改了请求接口方法。

<template>
  <div class="clearfix">
    <a-upload :file-list="fileList" :remove="handleRemove" :before-upload="beforeUpload">
      <a-button> <a-icon type="upload" /> 选择文件 </a-button>
    </a-upload>

    <a-button
      type="primary"
      :disabled="fileList.length === 0"
      :loading="uploading"
      style="margin-top: 16px"
      @click="handleUpload"
    >
      {{ uploading ? '上传中' : '上传' }}
    </a-button>
  </div>
</template>

<script>
import { UploadFile } from '@/api/files.js';
export default {
  data() {
    return {
      fileList: [],
      uploading: false,
      fileID: ''
    }
  },
  methods: {
    handleRemove(file) {
      const index = this.fileList.indexOf(file);
      const newFileList = this.fileList.slice();
      newFileList.splice(index, 1);
      this.fileList = newFileList;
    },
    beforeUpload(file) {
      this.fileList = [...this.fileList, file];
      return false;
    },
    handleUpload() {
      const { fileList } = this;
      const formData = new FormData();
      fileList.forEach(file => {
        // 这里 "files" 要和后端接口参数名称一致 
        formData.append('files', file);
      });
      this.uploading = true;

      UploadFile(formData).then(res => {
        if (res) {
          this.fileList = [];
          this.uploading = false;
          this.$message.success('上传成功');
        } else {
          this.uploading = false;
          this.$message.error('上传失败');
        }
      })
    }
  }
}
</script>

三、下载文件

1. 接口设计

这里我将文件在 MinIO 中的文件名通过请求头传回前端,这样前端就无需对文件类型进行再次判断,可以下载任何格式文件。

需要注意一点:默认请求头无法正常传输中文字符,会出现乱码的情况,所以在传送前通过 URLEncoder.encode() 将其进行编码,前端获取时需要通过 decodeURI() 进行解码。

@PostMapping("/download")
public ResponseEntity<byte[]> Download(@RequestParam(name = "fileName") String fileName,
                                       @RequestParam(name = "bucketName") String bucketName) throws Exception {
    ResponseEntity<byte[]> responseEntity = null;

    try(InputStream in = minioUtil.getObject(bucketName, fileName);
        ByteArrayOutputStream out = new ByteArrayOutputStream();) {
        if (in == null) {
            throw new PrinterException("文件不存在");
        }

        byte[] buffer = new byte[4096];
        int n = 0;
        while (-1 != (n = in.read(buffer))) {
            out.write(buffer, 0, n);
        }
        byte[] bytes = out.toByteArray();

        // 设置header
        HttpHeaders httpHeaders = new HttpHeaders();
        httpHeaders.add("Accept-Ranges", "bytes");
        httpHeaders.add("Content-Length", bytes.length + "");
        // 加密文件名
        httpHeaders.add("Content-disposition", "attachment; filename=" + URLEncoder.encode(fileName, "UTF-8"));
        httpHeaders.add("Content-Type", "text/plain;charset=utf-8");

        responseEntity = new ResponseEntity<byte[]>(bytes, httpHeaders, HttpStatus.CREATED);
    } catch (MinioException e) {
        e.printStackTrace();
    }

    return responseEntity;
  }

2. 接口封装

需要注意一点这里要通过 decodeURI() 方法对文件名进行解码。

import request from "./axios";

export function download(params) {
    return request({
        method: 'post',
        url: '/files/download',
        data: params,
        responseType: 'blob'
    }).then((res) => {
        // 获取文件名
        let fileName = res.headers["content-disposition"]
        // 解密文件名
        fileName = decodeURI(fileName)
        fileName = fileName.substring(fileName.lastIndexOf("="))
        fileName = fileName.slice(1)

        const content = res.data
        const blob = new Blob([content])
          if ('download' in document.createElement('a')) {
            // 非IE下载
            const elink = document.createElement('a')
            elink.download = fileName
            elink.style.display = 'none'
            elink.href = URL.createObjectURL(blob)
            document.body.appendChild(elink)
            elink.click()
            URL.revokeObjectURL(elink.href) // 释放URL 对象
            document.body.removeChild(elink)
        } else {
            // IE10+下载
            navigator.msSaveBlob(blob, fileName)
        }
  }).catch(error => {
          console.log(error)
      })
  }

3. 页面设计

用户在页面输入文件相关信息后点击下载按钮将自动发送请求生成下载。

<template>
  <div id="app">
    <a-input
      v-model="fileBucket"
      style="width: 15%"
      placeholder="请输入桶名"
    />
    <a-input
      v-model="fileName"
      style="width: 15%"
      placeholder="请输入文件名"
    />
    <a-button @click="download()">下载</a-button>
  </div>
</template>

<script>
import { download } from '@/api/files.js';
export default {
  data() {
      return {
          fileName: '',
          fileBucket: '',
      }
  },
  methods: {
      download(){
          if(this.fileName !== '' && this.fileBucket !== '') {
              const formData = new FormData()
              formData.append("fileName", this.fileName)
              formData.append("bucketName", this.fileBucket)
              download(formData)
          } else {
              this.$message.error('文件信息不能为空!')
          }
      }
  }
}
</script>

参考文档Spring Boot 集成 MiniO


文章作者: 烽火戏诸诸诸侯
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 烽火戏诸诸诸侯 !
  目录