Spring Boot 2.x 整合 MinIO 8.x

Spring Boot 2.x 整合 MinIO 8.x

MinIO概要

MinIO 是一个基于Apache License v2.0开源协议的对象存储服务。它兼容亚马逊S3云存储服务接口,非常适合于存储大容量非结构化的数据,例如图片、视频、日志文件、备份数据和容器/虚拟机镜像等,而一个对象文件可以是任意大小,从几kb到最大5T不等。

MinIO是一个非常轻量的服务,可以很简单的和其他应用的结合,类似 NodeJS, Redis 或者 MySQL。

快速入门地址 -> http://docs.minio.org.cn/docs/
PS: 之前官网文档API有些已废弃,提过issue,官方已更新,也许还有部分文档未更新

MinIO & FastDFS 对比

MinIO:
Kubernetes原生支持,高性能,对象存储,有官方文档,API简单,有控制台

FastDFS:
部署较为复杂,要理解FastDFS的架构才好上手部署开发,没有官方文档,没有控制台

思路

  1. 方案一:
    前端 将附件与表单属性一同提交
  2. 方案二:
    前端分两步
    2.1 前端 上传图片 调用“附件上传接口” -> 成功,返回attachmentId;
    2.2 前端 提交表单 将 attachmentId 和其他表单信息一同提交。

选择方案二,理由:解耦,成功率更高。

附件信息表 数据库表结构设计参考

Spring Boot 2.x 整合 MinIO 8.x

talk is cheap -> show me the code(核心代码)

Spring Boot 工程 application.yml

spring:
  servlet:
    # 附件上传限制大小
    multipart:
      # 单个文件的最大值
      max-file-size: 10MB
      # 最大请求文件的大小
      max-request-size: 100MB

# MinIO
minio:
  url: http://127.0.0.1:9000
  access-key: minioadmin
  secret-key: minioadmin
  bucket-name: spring-festival

MinIOConfig.java

import io.minio.MinioClient;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * MinIO config
 *
 * @author Jiahai
 */
@Data
@Configuration
@ConfigurationProperties(value = "minio")
public class MinIOConfig {
    /**
     * URL
     */
    private String url;

    /**
     * access-key
     */
    private String accessKey;

    /**
     * secretKey
     */
    private String secretKey;

    /**
     * bucket name
     */
    private String bucketName;

    /**
     * 初始化 MinIO Client
     *
     * @return
     */
    @Bean
    public MinioClient initMinioClient () {
        return MinioClient.builder().endpoint(url).credentials(accessKey, secretKey).build();
    }
}

MinIOComponent.java

import com.wisdom.attachment.service.AttachmentInfoService;
import com.wisdom.config.MinIOConfig;
import com.wisdom.exception.BusinessException;
import io.minio.*;
import io.minio.http.Method;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.IOUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletResponse;
import java.io.InputStream;
import java.net.URLEncoder;
import java.util.concurrent.ThreadPoolExecutor;

/**
 * MinIO 组件
 *
 * @author Jiahai
 */
@Slf4j
@Component
public class MinIOComponent {
    @Autowired
    private MinIOConfig minIOConfig;

    @Autowired
    private MinioClient minioClient;

	/**
	 * 自定义的线程池
	**/
    @Resource(name = "updateAttachmentInfoThreadPoolExecutor")
    private ThreadPoolExecutor updateAttachmentInfoThreadPoolExecutor;

    @Autowired
    private AttachmentInfoService attachmentInfoService;

    /**
     * 附件上传
     *
     * @param attachmentInfoId       附件ID
     * @param multipartFile          附件
     * @param attachmentNameInServer 存储在文件服务器中的附件名称
     */
    public String upload(Long attachmentInfoId, MultipartFile multipartFile, String attachmentNameInServer) {
        // 获取 原始文件名,例如 hello.txt
        String originalFilename = multipartFile.getOriginalFilename();
        String contentType = multipartFile.getContentType();

        // 桶名称
        String bucketName = minIOConfig.getBucketName();

        try (InputStream inputStream = multipartFile.getInputStream()) {
            boolean bucketExists = minioClient.bucketExists(BucketExistsArgs.builder().bucket(bucketName).build());
            if (!bucketExists) {
                minioClient.makeBucket(MakeBucketArgs.builder().bucket(bucketName).build());
            }
            minioClient.putObject(PutObjectArgs.builder().bucket(bucketName)
                    .object(attachmentNameInServer)
                    .stream(inputStream, multipartFile.getSize(), -1)
                    .contentType(contentType).build()
            );
            log.info("附件上传成功, fileName: {}, contentType: {}, size(Byte): {}", originalFilename, contentType, multipartFile.getSize());

            // 异步更新附件URL
            String presignedObjectUrl = minioClient.getPresignedObjectUrl(GetPresignedObjectUrlArgs.builder().method(Method.GET).bucket(bucketName).object(attachmentNameInServer).build());
            updateAttachmentInfoThreadPoolExecutor.execute(() -> {
                try {
                    int row = attachmentInfoService.updateAttachmentUrlByAttachmentInfoId(attachmentInfoId, presignedObjectUrl);
                    log.info("ID为{}的附件,URL更新{}", attachmentInfoId, row > 0 ? "成功" : "失败");
                } catch (Exception e) {
                    e.printStackTrace();
                }
            });
            return presignedObjectUrl;
        } catch (Exception e) {
            log.error("附件上传失败, fileName: {}, contentType: {}, size(Byte): {}", originalFilename, contentType, multipartFile.getSize());
            e.printStackTrace();
            throw new BusinessException(HttpStatus.INTERNAL_SERVER_ERROR.value(), "附件上传失败");
        }
    }

    /**
     * 附件下载
     *
     * @param attachmentName         附件原始名称
     * @param bucketName             桶名称
     * @param attachmentNameInServer 存储在文件服务器中的附件名称
     * @param httpServletResponse    httpServletResponse
     */
    public void download(String attachmentName, String bucketName, String attachmentNameInServer, HttpServletResponse httpServletResponse) {
        try {
            StatObjectResponse statObjectResponse = minioClient.statObject(StatObjectArgs.builder().bucket(bucketName).object(attachmentNameInServer).build());
            httpServletResponse.setHeader("Content-Disposition", "attachment;filename=" + URLEncoder.encode(attachmentName, "UTF-8"));
            httpServletResponse.setContentType(statObjectResponse.contentType());
            httpServletResponse.setCharacterEncoding("UTF-8");
            InputStream inputStream = minioClient.getObject(GetObjectArgs.builder().bucket(bucketName).object(attachmentNameInServer).build());
            IOUtils.copy(inputStream, httpServletResponse.getOutputStream());
        } catch (Exception e) {
            log.error("附件下载失败, fileName: {}, bucketName: {}, attachmentNameInServer: {}", attachmentName, bucketName, attachmentNameInServer);
            e.printStackTrace();
        }
    }

    /**
     * 获取预览URL
     * 注:.txt的附件会乱码,PDF正常
     * 注:默认有效期7天
     *
     * @param bucketName             桶名称
     * @param attachmentNameInServer 存储在文件服务器中的附件名称
     * @return
     */
    public String getPresignedUrl(String bucketName, String attachmentNameInServer) {
        try {
            minioClient.statObject(StatObjectArgs.builder().bucket(bucketName).object(attachmentNameInServer).build());
            return minioClient.getPresignedObjectUrl(GetPresignedObjectUrlArgs.builder().method(Method.GET).bucket(bucketName).object(attachmentNameInServer).build());
        } catch (Exception e) {
            throw new BusinessException(HttpStatus.BAD_REQUEST.value(), e.getMessage());
        }
    }

    /**
     * 删除附件
     *
     * @param bucketName             桶名称
     * @param attachmentNameInServer 存储在文件服务器中的附件名称
     */
    public void remove(String bucketName, String attachmentNameInServer) {
        try {
            minioClient.removeObject(RemoveObjectArgs.builder().bucket(bucketName).object(attachmentNameInServer).build());
            log.info("附件: {}, 删除成功!", attachmentNameInServer);
        } catch (Exception e) {
            log.error("附件: {}, 删除失败...", attachmentNameInServer);
            e.printStackTrace();
        }
    }
}

PS:

  • 新版MinIO的API主要使用建造者模式;
  • MinIO的预览URL有时效性,注意使用定时任务去完成刷新,自行把握时间窗口。
上一篇:Python:数据库操作


下一篇:minio实现文件上传下载和删除功能