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的架构才好上手部署开发,没有官方文档,没有控制台
思路
- 方案一:
前端 将附件与表单属性一同提交 - 方案二:
前端分两步
2.1 前端 上传图片 调用“附件上传接口” -> 成功,返回attachmentId;
2.2 前端 提交表单 将 attachmentId 和其他表单信息一同提交。
选择方案二,理由:解耦,成功率更高。
附件信息表 数据库表结构设计参考
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有时效性,注意使用定时任务去完成刷新,自行把握时间窗口。