目录
SpringBoot Integrate MinIO
本章的知识网络:
1. MinIO 安装部署
MinIO的在Linux上的部署可以参考:MinIO在Linux上的安装与部署_minio linux部署-CSDN博客
MinIO集群的在Linux上的部署可以参考:MinIO在Linux上的集群安装与部署-CSDN博客
Nginx代理MinIO集群可以参考:Nginx代理MinIO集群-CSDN博客
2. SpringBoot 项目配置
2.1. 添加依赖
添加相应的pom.xml
文件依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-ui</artifactId>
<version>1.6.14</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.minio</groupId>
<artifactId>minio</artifactId>
<version>8.5.6</version>
<exclusions>
<exclusion>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
<version>4.11.0</version> <!-- 使用最新稳定版 -->
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.30</version>
</dependency>
2.2. 配置文件
配置application.yml
文件
minio:
endpoint: http://192.168.33.205:9000 # Nginx 负载均衡器的地址
access-key: gVqpDMoCHridNBp9wI1O # 访问密钥
secret-key: H6E19mqETNxYFJ6PgSK1V6tgdWjeiqt8e6BMnkuV # 安全密钥
bucketName: springboot-mino-test-bucket # 存储桶名称
# 文件上传大小限制
spring:
servlet:
multipart:
enabled: true
max-file-size: 100MB
max-request-size: 100MB
2.3. 桶的数量配置
在项目开发中,使用 MinIO 桶的数量取决于具体的业务需求和技术架构设计,以下是不同场景的实践建议:
- 小型项目或简单业务在配置文件中配置一个桶即可,简单高效,维护成本低,但是灵活性差。
- 中大型项目或复杂业务需要权限控制、数据隔离和多租户情况时,可以再配置文件中预设高频使用的桶(例如用户头像、日志文件等),在接口中动态制定低频使用的桶(租户个人空间)
以下是部分示例:
minio:
buckets:
user: app-user-data
product: app-product-images
log: app-system-logs
minio:
buckets:
dev: app-dev-bucket
test: app-test-bucket
prod: app-prod-bucket
2.4. 配置类
配置类中奖MinIOClient客户端注入到Springboot中
@Configuration
@Getter
public class MinioConfig {
@Value("${minio.endpoint}")
private String endpoint;
@Value("${minio.access-key}")
private String accessKey;
@Value("${minio.secret-key}")
private String secretKey;
@Value("${minio.bucketName}")
private String bucketName;
@Bean
public MinioClient minioClient() {
return MinioClient.builder()
.endpoint(endpoint)
.credentials(accessKey, secretKey)
.build();
}
}
3. MinIO 核心功能实现
3.1. MinIO 工具类
MinIO工具类中包含了操作MinIO服务的核心方法:
- 文件上传(支持自动重命名)
- 文件下载(文件输入流和浏览器直接下载)
- 文件删除
- 获取桶中文件列表
- 存储桶管理(增、删、是否存在、获取桶列表)
- 生成临时访问链接
- 图片预览
/**
* Minio对象存储操作工具类
* 包含存储桶管理、文件操作、浏览器下载等完整功能
*/
@Component
public class MinioUtil {
private final MinioClient minioClient;
private final MinioConfig minioConfig;
@Autowired
public MinioUtil(MinioClient minioClient, MinioConfig minioConfig) {
this.minioClient = minioClient;
this.minioConfig = minioConfig;
}
/* 存储桶操作系列方法 */
/**
* 检查存储桶是否存在
*
* @param bucketName 存储桶名称
* @return 是否存在
* @throws Exception Minio操作异常
*/
public boolean bucketExists(String bucketName) throws Exception {
return minioClient.bucketExists(BucketExistsArgs.builder()
.bucket(bucketName)
.build());
}
/**
* 创建新存储桶
*
* @param bucketName 存储桶名称
* @return 是否创建成功
* @throws Exception Minio操作异常
*/
public boolean createBucket(String bucketName) throws Exception {
if (!bucketExists(bucketName)) {
minioClient.makeBucket(MakeBucketArgs.builder()
.bucket(bucketName)
.build());
return true;
}
return false;
}
/**
* 删除存储桶(存储桶必须为空)
*
* @param bucketName 存储桶名称
* @throws Exception Minio操作异常
*/
public void removeBucket(String bucketName) throws Exception {
minioClient.removeBucket(RemoveBucketArgs.builder()
.bucket(bucketName)
.build());
}
/**
* 获取全部存储桶列表
*
* @return 存储桶信息列表
* @throws Exception Minio操作异常
*/
public List<Bucket> listAllBuckets() throws Exception {
return minioClient.listBuckets();
}
/* 文件操作系列方法 */
/**
* 上传文件到默认存储桶
*
* @param file 上传的文件对象
* @param objectName 存储对象名称(包含路径)
* @return 文件访问URL
* @throws Exception Minio操作异常
*/
public String uploadFile(MultipartFile file, String objectName) throws Exception {
// 自动生成唯一文件名
if (objectName == null || objectName.isEmpty()) {
objectName = generateUniqueName(file.getOriginalFilename());
}
minioClient.putObject(PutObjectArgs.builder()
.bucket(minioConfig.getBucketName())
.object(objectName)
.stream(file.getInputStream(), file.getSize(), -1)
.contentType(file.getContentType())
.build());
return getFileUrl(objectName);
}
/**
* 获取文件临时访问URL
*
* @param objectName 存储对象名称
* @param expiry 有效期(单位:秒)
* @return 临时访问URL
* @throws Exception Minio操作异常
*/
public String getPresignedUrl(String objectName, int expiry) throws Exception {
return minioClient.getPresignedObjectUrl(
GetPresignedObjectUrlArgs.builder()
.method(Method.GET)
.bucket(minioConfig.getBucketName())
.object(objectName)
.expiry(expiry, TimeUnit.SECONDS)
.build());
}
/**
* 图片预览(生成7天有效的URL)
*
* @param objectName 存储对象名称
* @return 预览URL
* @throws Exception Minio操作异常
*/
public String previewImage(String objectName) throws Exception {
return minioClient.getPresignedObjectUrl(
GetPresignedObjectUrlArgs.builder()
.method(Method.GET)
.bucket(minioConfig.getBucketName())
.object(objectName)
.expiry(7, TimeUnit.DAYS)
.build());
}
/**
* 获取文件输入流
*
* @param objectName 存储对象名称
* @return 文件流
* @throws Exception Minio操作异常
*/
public InputStream downloadFile(String objectName) throws Exception {
return minioClient.getObject(GetObjectArgs.builder()
.bucket(minioConfig.getBucketName())
.object(objectName)
.build());
}
/**
* 直接下载文件到HttpServletResponse(浏览器下载)
*
* @param objectName 存储对象名称
* @param response HttpServletResponse
* @throws Exception Minio操作异常或IO异常
*/
public void downloadToResponse(String objectName, HttpServletResponse response) throws Exception {
// 获取文件元数据
StatObjectResponse stat = minioClient.statObject(StatObjectArgs.builder()
.bucket(minioConfig.getBucketName())
.object(objectName)
.build());
// 设置响应头
response.setContentType(stat.contentType());
response.setHeader("Content-Disposition",
"attachment; filename=\"" + URLEncoder.encode(objectName, StandardCharsets.UTF_8.name()) + "\"");
response.setContentLengthLong(stat.size());
// 流式传输文件内容
try (InputStream is = downloadFile(objectName);
OutputStream os = response.getOutputStream()) {
IOUtils.copy(is, os);
os.flush();
}
}
/**
* 删除文件
*
* @param objectName 存储对象名称
* @throws Exception Minio操作异常
*/
public void deleteFile(String objectName) throws Exception {
minioClient.removeObject(RemoveObjectArgs.builder()
.bucket(minioConfig.getBucketName())
.object(objectName)
.build());
}
/**
* 列出存储桶中的所有文件
*
* @param bucketName 存储桶名称
* @return 文件信息列表
* @throws Exception Minio操作异常
*/
public List<String> listAllFiles(String bucketName) throws Exception {
List<String> list = new ArrayList<>();
for (Result<Item> result : minioClient.listObjects(
ListObjectsArgs.builder().bucket(bucketName).build())) {
list.add(result.get().objectName());
}
return list;
}
/**
* 获取文件元数据
*
* @param objectName 存储对象名称
* @return 文件元数据
* @throws Exception Minio操作异常
*/
public StatObjectResponse getObjectStat(String objectName) throws Exception {
return minioClient.statObject(StatObjectArgs.builder()
.bucket(minioConfig.getBucketName())
.object(objectName)
.build());
}
/**
* 生成永久访问URL(需要存储桶设置为公开)
*
* @param objectName 存储对象名称
* @return 直接访问URL
*/
public String getPermanentUrl(String objectName) {
return String.format("%s/%s/%s",
minioConfig.getEndpoint(),
minioConfig.getBucketName(),
objectName);
}
/**
* 验证文件类型白名单
*
* @param file 上传文件
* @param allowedTypes 允许的类型列表
* @throws IllegalArgumentException 文件类型不合法
*/
public void validateFileType(MultipartFile file, List<String> allowedTypes) {
String fileType = file.getContentType();
if (!allowedTypes.contains(fileType)) {
throw new IllegalArgumentException("不支持的文件类型: " + fileType);
}
}
/* 辅助方法 */
private String generateUniqueName(String originalFileName) {
return UUID.randomUUID().toString().replace("-", "")
+ "_" + originalFileName;
}
private String getFileUrl(String objectName) throws Exception {
return minioConfig.getEndpoint() + "/"
+ minioConfig.getBucketName() + "/" + objectName;
}
/* 扩展功能方法 */
/**
* 复制文件到新位置
*
* @param sourceBucket 源存储桶
* @param sourceObject 源文件
* @param destBucket 目标存储桶
* @param destObject 目标文件
* @throws Exception Minio操作异常
*/
public void copyObject(String sourceBucket, String sourceObject,
String destBucket, String destObject) throws Exception {
minioClient.copyObject(CopyObjectArgs.builder()
.source(CopySource.builder()
.bucket(sourceBucket)
.object(sourceObject)
.build())
.bucket(destBucket)
.object(destObject)
.build());
}
}
3.2. MinIO 控制器
FileController在MinIO工具基础上实现了以下接口:
@Tag(name = "文件管理接口")
@RestController
@RequestMapping("/minio")
public class MinioController {
private final MinioUtil minioUtil;
private final MinioConfig minioConfig;
@Autowired
public MinioController(MinioUtil minioUtil, MinioConfig minioConfig) {
this.minioUtil = minioUtil;
this.minioConfig = minioConfig;
}
@Operation(summary = "文件上传")
@PostMapping("/upload")
public R<String> uploadFile(@RequestParam("file") MultipartFile file,
@RequestParam(value = "objectName", required = false) String objectName) {
try {
if (file.isEmpty()) {
return R.error(HttpStatus.BAD_REQUEST.value(), "上传文件不能为空");
}
String url = minioUtil.uploadFile(file, objectName);
return R.success("上传成功", url);
} catch (Exception e) {
return R.error(HttpStatus.INTERNAL_SERVER_ERROR.value(), e.getMessage());
}
}
@Operation(summary ="文件下载")
@GetMapping("/download/{objectName}")
public void downloadFile(@PathVariable String objectName, HttpServletResponse response) {
try {
minioUtil.downloadToResponse(objectName, response);
} catch (Exception e) {
response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
}
}
@Operation(summary ="文件预览(7天有效期)")
@GetMapping("/preview/{objectName}")
public R<String> previewFile(@PathVariable String objectName) {
try {
String url = minioUtil.previewImage(objectName);
return R.success("获取预览地址成功", url);
} catch (Exception e) {
return R.error(HttpStatus.INTERNAL_SERVER_ERROR.value(), e.getMessage());
}
}
@Operation(summary ="删除文件")
@DeleteMapping("/delete/{objectName}")
public R<Void> deleteFile(@PathVariable String objectName) {
try {
minioUtil.deleteFile(objectName);
return R.success("删除成功");
} catch (Exception e) {
return R.error(HttpStatus.INTERNAL_SERVER_ERROR.value(), e.getMessage());
}
}
@Operation(summary ="获取文件列表")
@GetMapping("/list")
public R<List<String>> listFiles(@RequestParam(required = false) String bucketName) {
try {
String targetBucket = (bucketName != null && !bucketName.isEmpty())
? bucketName : minioConfig.getBucketName();
List<String> files = minioUtil.listAllFiles(targetBucket);
return R.success("获取成功", files);
} catch (Exception e) {
return R.error(HttpStatus.INTERNAL_SERVER_ERROR.value(), e.getMessage());
}
}
@Operation(summary ="生成临时访问URL")
@GetMapping("/presigned-url")
public R<String> getPresignedUrl(@RequestParam String objectName,
@RequestParam(defaultValue = "3600") int expiry) {
try {
String url = minioUtil.getPresignedUrl(objectName, expiry);
return R.success("生成成功", url);
} catch (Exception e) {
return R.error(HttpStatus.INTERNAL_SERVER_ERROR.value(), e.getMessage());
}
}
@Operation(summary ="获取永久访问URL")
@GetMapping("/permanent-url/{objectName}")
public R<String> getPermanentUrl(@PathVariable String objectName) {
try {
String url = minioUtil.getPermanentUrl(objectName);
return R.success("获取成功", url);
} catch (Exception e) {
return R.error(HttpStatus.INTERNAL_SERVER_ERROR.value(), e.getMessage());
}
}
// 存储桶管理相关接口
@Operation(summary ="创建存储桶")
@PostMapping("/buckets/{bucketName}")
public R<Void> createBucket(@PathVariable String bucketName) {
try {
boolean created = minioUtil.createBucket(bucketName);
return created ? R.success("创建成功") : R.error(HttpStatus.INTERNAL_SERVER_ERROR.value(),"存储桶已存在");
} catch (Exception e) {
return R.error(HttpStatus.INTERNAL_SERVER_ERROR.value(), e.getMessage());
}
}
@Operation(summary ="删除存储桶")
@DeleteMapping("/buckets/{bucketName}")
public R<Void> deleteBucket(@PathVariable String bucketName) {
try {
minioUtil.removeBucket(bucketName);
return R.success("删除成功");
} catch (Exception e) {
return R.error(HttpStatus.INTERNAL_SERVER_ERROR.value(), e.getMessage());
}
}
@Operation(summary ="获取所有存储桶")
@GetMapping("/buckets")
public R<List<io.minio.messages.Bucket>> listBuckets() {
try {
List<io.minio.messages.Bucket> buckets = minioUtil.listAllBuckets();
return R.success("获取成功", buckets);
} catch (Exception e) {
return R.error(HttpStatus.INTERNAL_SERVER_ERROR.value(), e.getMessage());
}
}
}
4. 功能测试验证
下方提供了功能测试的图例,我将apipost的地址共享在这里,有需要测试的朋友可以自行测试:
Apipost-基于协作, 不止于API文档、调试、Mock、自动化测试
4.1. 程序启动
访问地址是127.0.0.1:8080
4.2. 文件上传接口验证
ApiPost验证文件上传:
验证文件确实上传成功:
验证返回的文件url:
4.3. 文件下载接口验证
ApiPost验证文件下载:
4.4. 文件预览(7天有效期)接口验证
ApiPost验证文件预览(7天有效期):
验证文件预览URL:
4.5. 文件删除接口验证
准备删除的文件
ApiPost验证文件删除:
准备删除的文件已经删除了
4.6. 获取文件列表接口验证
ApiPost验证获取文件列表:
4.7. 生成临时访问URL接口验证
生成临时访问URL接口和文件预览其实是同一个方法,只是文件预览内定了七天访问,而这个方法可以自行制定,单位是秒
ApiPost验证生成临时URL:
验证临时访问URL
4.8. 获取永久访问URL接口验证
ApiPost验证获取永久访问URL:
验证永久访问URL:
4.9. 创建存储桶接口验证
ApiPost验证创建存储桶:
验证桶确实创建成功:
再次调用返回桶已存在
4.10. 获取所有存储桶接口验证
ApiPost验证获取所有存储桶:
从Bucket源码可以看出,并没有实现toString()
方法,所以返回的是地址信息,但是可以通过dubug看到Bucket中的属性,确实是当前所有桶信息
4.11. 删除存储桶接口验证
ApiPost验证删除存储桶:
桶已删除
再次调用返回该桶已不存在