1. 什么是 MinIO?
MinIO 是一个高性能、分布式、云原生的对象存储系统,专为大规模数据存储和检索而设计。它兼容 Amazon S3 API,可以作为私有云或混合云环境中的存储解决方案,适用于现代数据密集型应用,如大数据分析、AI/ML、备份和归档等。
下载:https://dl.minio.org.cn/server/minio/release/windows-amd64/minio.exe
2. 启动 Windows 版本 MinIO
@echo off
minio.exe server D:\MinIO\data --address "localhost:9000" --console-address "localhost:9001"
3. MinioClient 常用 API
3.1 MinIO 中的 Bucket 和 Object
(1)Bucket(存储桶):是存储 Object 的逻辑空间(相当于顶层文件夹),每个Bucket之间的数据相互隔离。MinIO 兼容S3,S3 桶名只能包含小写字母、数字(0-9)、连字符(-)和点号(.),所以 Bucket 命名需满足 S3 桶名命名规则。
(2)Object(对象):是 MinIO 存储的对象(相当于文件)。
3.2 pom.xml 引入 MinIO 依赖
<!-- minio -->
<dependency>
<groupId>io.minio</groupId>
<artifactId>minio</artifactId>
<version>8.5.17</version>
</dependency>
3.3 application.yml 自定义 MinIO 配置
spring:
servlet:
multipart:
max-file-size: 20MB # 上传的最大文件大小
minio:
enabled: true # 是否开启 minio 配置
endpoint: http://127.0.0.1:9000 # MinIO API 地址
accessKey: minioadmin # 用户名
secretKey: minioadmin # 密码
bucket: my-bucket # Bucket(存储桶)
3.4 MinIO 客户端配置
package com.dragon.springboot3vue3.config;
import io.minio.MinioClient;
import lombok.Data;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
@ConditionalOnProperty(prefix = "minio",name = "enabled", havingValue = "true")
public class MinioConfig {
@Value("${minio.endpoint}")
private String endpoint;
@Value("${minio.accessKey}")
private String accessKey;
@Value("${minio.secretKey}")
private String secretKey;
/**
* minio 客户端(这是单例Bean,但是线程安全)
* @return
*/
@Bean
public MinioClient minioClient() {
return MinioClient.builder()
.endpoint(endpoint)
.credentials(accessKey, secretKey)
.build();
}
}
3.5 MinioClient 常用 API
package com.dragon.springboot3vue3.controller;
import cn.dev33.satoken.util.SaResult;
import com.dragon.springboot3vue3.controller.dto.entityDto.MinioObjectDto;
import com.dragon.springboot3vue3.controller.dto.entityDto.MinioUploadObjectDto;
import com.dragon.springboot3vue3.utils.StringDTO;
import io.minio.*;
import io.minio.http.Method;
import io.minio.messages.Bucket;
import io.minio.messages.Item;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@Tag(name = "Minio接口")
@RestController
@RequestMapping("/minio")
public class MinioController {
@Autowired
private MinioClient minioClient;
// 1. 操作 Bucket 存储桶 -----------------------------------------------------------------
@Operation(summary = "创建 Bucket 存储桶")
@PostMapping("/makeBucket")
public SaResult makeBucket(@RequestBody StringDTO stringDTO) {
try{
// 判断是否存在 Bucket 存储桶
boolean b = minioClient.bucketExists(BucketExistsArgs.builder()
.bucket(stringDTO.getStr()) // 存储桶名
.build());
if(!b){
minioClient.makeBucket(MakeBucketArgs.builder()
.bucket(stringDTO.getStr()) // 存储桶名
.build());
return SaResult.ok();
}
return SaResult.error("Bucket 已存在,创建失败");
} catch (Exception e) {
return SaResult.error(e.getMessage());
}
}
@Operation(summary = "获取 Bucket 存储桶列表")
@GetMapping("/listBuckets")
public SaResult listBuckets() {
try{
List<Bucket> buckets = minioClient.listBuckets();
List<String> bucketNameList = buckets.stream().map(Bucket::name).toList();
return SaResult.ok().setData(bucketNameList);
} catch (Exception e) {
return SaResult.error(e.getMessage());
}
}
@Operation(summary = "删除 Bucket 存储桶")
@GetMapping("/removeBuckets")
public SaResult removeBuckets(@RequestBody StringDTO stringDTO) {
try{
minioClient.removeBucket(RemoveBucketArgs.builder()
.bucket(stringDTO.getStr()) // 存储桶名
.build());
return SaResult.ok();
} catch (Exception e) {
return SaResult.error(e.getMessage());
}
}
// 2. 操作 Object 对象 -----------------------------------------------------------------
@Operation(summary = "上传 Object")
@PostMapping("/uploadObject")
public SaResult uploadObject(@RequestBody MinioUploadObjectDto minioUploadObjectDto) {
try{
if(StringUtils.isBlank(minioUploadObjectDto.getObjectName())){
// 从文件路径中提取文件名(如 "D:\\Files\\bg1.jpg" → "bg1.jpg")
String name = Paths.get(minioUploadObjectDto.getFilePath()).getFileName().toString();
minioUploadObjectDto.setObjectName(name);
}
ObjectWriteResponse objectWriteResponse = minioClient.uploadObject(UploadObjectArgs.builder()
.bucket(minioUploadObjectDto.getBucketName()) // 存储桶名
.object(minioUploadObjectDto.getObjectName()) // 对象名
.filename(minioUploadObjectDto.getFilePath()) // 文件路径,如 "D:\\Files\\bg1.jpg"
.build());
return SaResult.ok();
} catch (Exception e) {
return SaResult.error(e.getMessage());
}
}
@Operation(summary = "获取 Object 状态信息")
@PostMapping("/statObject")
public SaResult statObject(@RequestBody MinioObjectDto minioObjectDto) {
try{
StatObjectResponse statObjectResponse = minioClient.statObject(StatObjectArgs.builder()
.bucket(minioObjectDto.getBucket()) // 存储桶名
.object(minioObjectDto.getObject()) // 对象名
.build());
Map<String, Object> map = new HashMap<>();
map.put("bucket", statObjectResponse.bucket());
map.put("object", statObjectResponse.object());
map.put("size", statObjectResponse.size());
map.put("etag", statObjectResponse.etag());
map.put("lastModified", statObjectResponse.lastModified());
map.put("contentType", statObjectResponse.contentType());
// 不能直接返回 statObjectResponse,是 MinIO 内部类,无法直接序列化为 JSON
return SaResult.ok().setData(map);
} catch (Exception e) {
return SaResult.error(e.getMessage());
}
}
/**
*
* @param minioObjectDto
* @return
* @throws Exception
*/
@Operation(summary = "获取访问 Object 的预签名 URL")
@PostMapping("/getPresignedObjectUrl")
public SaResult getPresignedObjectUrl(@RequestBody MinioObjectDto minioObjectDto) {
try{
String presignedObjectUrl = minioClient.getPresignedObjectUrl(GetPresignedObjectUrlArgs.builder()
.bucket(minioObjectDto.getBucket()) // 存储桶名
.object(minioObjectDto.getObject()) // 对象名
.method(Method.GET) // URL 请求方法
// .expiry(5, TimeUnit.MINUTES) // URL 过期时间
.build());
return SaResult.ok().setData(presignedObjectUrl);
} catch (Exception e) {
return SaResult.error(e.getMessage());
}
}
@Operation(summary = "获取所有 Object")
@PostMapping("/listObjects")
public SaResult listObjects(@RequestBody StringDTO stringDTO) {
try{
// 1. 获取存储桶中的对象列表
Iterable<Result<Item>> results = minioClient.listObjects(ListObjectsArgs.builder()
.bucket(stringDTO.getStr()) // 存储桶名
.build());
// 2. 提取对象名到 List<String>
List<String> list = new ArrayList<>();
for (Result<Item> item : results) {
list.add(item.get().objectName()); // 获取每个对象的名称
}
return SaResult.ok().setData(list);
} catch (Exception e) {
return SaResult.error(e.getMessage());
}
}
@Operation(summary = "删除 Object")
@PostMapping("/removeObject")
public SaResult removeObject(@RequestBody MinioObjectDto minioObjectDto) {
try{
minioClient.removeObject(RemoveObjectArgs.builder()
.bucket(minioObjectDto.getBucket()) // 存储桶名
.object(minioObjectDto.getObject()) // 对象名
.build());
return SaResult.ok();
} catch (Exception e) {
return SaResult.error(e.getMessage());
}
}
}
4. SpringBoot 3 项目开发 MinIO Object 管理功能
4.1 前端页面展示
4.2 minio_object 表 SQL
CREATE TABLE `minio_object` (
`id` varchar(40) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '主键',
`user_id` varchar(40) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '用户ID',
`bucket` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT 'Bucket 桶名',
`object` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT 'Object 对象名',
`name` varchar(255) DEFAULT NULL COMMENT '文件名',
`url` varchar(1024) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT 'URL',
`type` varchar(40) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '文件类型',
`size` bigint DEFAULT NULL COMMENT '文件大小(B)',
`md5` varchar(255) DEFAULT NULL COMMENT 'md5 文件唯一标识',
`creator_id` varchar(40) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '创建人ID',
`create_time` datetime NOT NULL COMMENT '创建时间',
`ts` datetime NOT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`delete_flag` int NOT NULL DEFAULT '0' COMMENT '逻辑删除字段(0:正常,1:删除)',
PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci ROW_FORMAT=DYNAMIC COMMENT='MinIO Object表';
4.3 Java 代码
4.3.1 文件上传,object 命名和使用 md5 的作用
4.3.2 文件下载(根据 ID 下载)
4.3.3 文件下载(根据 URL 下载)
4.3.4 使用 MyBatis-Plus 分页、连接查询
4.3.5 完整代码
package com.dragon.springboot3vue3.controller;
import cn.dev33.satoken.util.SaResult;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.io.unit.DataSize;
import cn.hutool.core.lang.Dict;
import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.crypto.SecureUtil;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.dragon.springboot3vue3.common.Constant;
import com.dragon.springboot3vue3.controller.dto.entityDto.MinioObjectDto;
import com.dragon.springboot3vue3.controller.dto.pageDto.MinioObjectPageDto;
import com.dragon.springboot3vue3.entity.MinioObject;
import com.dragon.springboot3vue3.entity.User;
import com.dragon.springboot3vue3.service.IMinioObjectService;
import com.dragon.springboot3vue3.utils.StringDTO;
import com.github.yulichang.wrapper.MPJLambdaWrapper;
import io.minio.*;
import io.minio.http.Method;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletResponse;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import java.io.BufferedInputStream;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
/**
* <p>
* MinIO Object表 前端控制器
* </p>
*
* @author dragon
* @since 2025-05-13
*/
@Tag(name = "Minio Object 接口")
@RestController
@RequestMapping("/minioObject")
public class MinioObjectController {
@Autowired
private IMinioObjectService minioObjectService;
@Autowired
private MinioClient minioClient; // MinIO 客户端,主要用来调用 API
@Value("${spring.servlet.multipart.max-file-size}")
private String maxSize; // yml 配置文件获取
@Value("${minio.bucket}")
private String minIOBucket; // yml 配置文件获取
@Transactional(rollbackFor = Exception.class)
@Operation(summary = "文件上传")
@PostMapping("/upload/{userId}")
public SaResult upload(@RequestParam MultipartFile file,@PathVariable(value = "userId") String userId) {
try {
if(file.getSize() > DataSize.parse(maxSize).toBytes()){
return SaResult.error("上传失败,上传文件过大");
}
// 文件大小(字节 B)
long size = file.getSize();
// 原文件名(bg.jpg)
String originalFilename = file.getOriginalFilename();
// 原文件类型(jpg)
String type = FileUtil.extName(originalFilename);
// 保证对象名唯一性(文件名由 uuid 拼接生成)
String object = IdUtil.fastSimpleUUID() + StrUtil.DOT + type;
// 生成文件唯一标识 md5,保证不会存储重复的文件
String md5;
try (BufferedInputStream bis = new BufferedInputStream(file.getInputStream())) {
// 使用流计算 md5
md5 = SecureUtil.md5(bis);
}
// 查找是否存在 md5 值,如果存在直接返回该对象
MinioObject one = minioObjectService.lambdaQuery()
.eq(MinioObject::getMd5, md5)
.eq(MinioObject::getDeleteFlag, Constant.ZERO)
.one();
if(Objects.nonNull(one)){
return SaResult.ok().setData(one);
}
// 上传到 MinIO 服务器,object 对象名同名会被覆盖
minioClient.putObject(PutObjectArgs.builder()
.bucket(minIOBucket) // 存储桶名
.object(object) // 对象名
.stream(file.getInputStream(),size,-1) // 文件输入流,文件大小(字节),-1表示默认的分片大小(通常为5MB)
.build());
// 获取 MinIO 中 Object Url
String presignedObjectUrl = minioClient.getPresignedObjectUrl(GetPresignedObjectUrlArgs.builder()
.bucket(minIOBucket) // 存储桶名
.object(object) // 对象名
.method(Method.GET) // URL 请求方法
.build());
// 保存到数据库
MinioObject minioObject = new MinioObject();
minioObject.setUserId(userId);
minioObject.setBucket(minIOBucket);
minioObject.setObject(object);
minioObject.setName(originalFilename);
minioObject.setUrl(presignedObjectUrl);
minioObject.setType(type);
minioObject.setSize(size);
minioObject.setMd5(md5);
minioObjectService.saveOrUpdate(minioObject);
return SaResult.ok().setData(minioObject);
}catch (Exception e) {
return SaResult.error(e.getMessage());
}
}
@Operation(summary = "wang-editor 富文本编辑器文件上传")
@PostMapping("/wangEditorUpload/{userId}")
public Map<String, Object> wangEditorUpload(@RequestParam MultipartFile file,@PathVariable(value = "userId") String userId) {
try{
if(file.getSize() > DataSize.parse(maxSize).toBytes()){
return SaResult.error("上传失败,上传文件过大");
}
// 文件大小(字节)
long size = file.getSize();
// 原文件名
String originalFilename = file.getOriginalFilename();
// 原文件类型
String type = FileUtil.extName(originalFilename);
// 保证对象名唯一性(文件名由 uuid 拼接生成)
String object = IdUtil.fastSimpleUUID() + StrUtil.DOT + type;
// 生成文件唯一标识 md5,保证不会在磁盘存储重复的文件
String md5;
try (BufferedInputStream bis = new BufferedInputStream(file.getInputStream())) {
// 使用流计算 md5
md5 = SecureUtil.md5(bis);
}
// 存在 md5 值,直接返回该对象
MinioObject one = minioObjectService.lambdaQuery()
.eq(MinioObject::getMd5, md5)
.eq(MinioObject::getDeleteFlag, Constant.ZERO)
.one();
if(Objects.nonNull(one)){
return SaResult.ok().setData(one);
}
// 上传到 MinIO 服务器,object 对象名同名会被覆盖
minioClient.putObject(PutObjectArgs.builder()
.bucket(minIOBucket) // 存储桶名
.object(object) // 对象名
.stream(file.getInputStream(),size,-1) // 文件输入流,文件大小(字节),-1表示默认的分片大小(通常为5MB)
.build());
// 获取 Object Url
String presignedObjectUrl = minioClient.getPresignedObjectUrl(GetPresignedObjectUrlArgs.builder()
.bucket(minIOBucket) // 存储桶名
.object(object) // 对象名
.method(Method.GET) // URL 请求方法
.build());
// 保存到数据库
MinioObject minioObject = new MinioObject();
minioObject.setUserId(userId);
minioObject.setBucket(minIOBucket);
minioObject.setObject(object);
minioObject.setName(originalFilename);
minioObject.setUrl(presignedObjectUrl);
minioObject.setType(type);
minioObject.setSize(size);
minioObject.setMd5(md5);
minioObjectService.saveOrUpdate(minioObject);
// 封装 wang-editor 数据返回格式
Map<String, Object> map =new HashMap<>();
map.put("errno",0);
map.put("data", CollUtil.newArrayList(Dict.create().set("url",presignedObjectUrl)));
return map;
} catch (Exception e) {
return SaResult.error(e.getMessage());
}
}
/**
*
* @param id : MinioObject ID
* @param response
* @throws Exception
*/
@Operation(summary = "根据 ID 文件下载")
@GetMapping("/download/{id}")
public void download(@PathVariable String id, HttpServletResponse response) throws Exception {
MinioObject one = minioObjectService.lambdaQuery()
.eq(MinioObject::getId, id)
.eq(MinioObject::getDeleteFlag, Constant.ZERO)
.one();
if (one == null) {
response.setStatus(404);
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write("{\"code\": 404, \"msg\": \"文件不存在\"}");
return;
}
String bucket = one.getBucket();
String object = one.getObject();
String name = one.getName();
// 设置输出流格式
// 添加 CORS 暴露请求头(必须!)
response.addHeader("Access-Control-Expose-Headers", "Content-Disposition");
// 设置下载文件名
String encodedName = URLEncoder.encode(name, StandardCharsets.UTF_8);
response.addHeader("Content-Disposition","attachment;filename=" + encodedName);
response.setContentType("application/octet-stream");
try {
// 从MinIO获取文件流
GetObjectResponse getObjectResponse = minioClient.getObject(GetObjectArgs.builder()
.bucket(bucket)
.object(object)
.build());
// 流式传输到HTTP响应
getObjectResponse.transferTo(response.getOutputStream());
} catch (Exception e) {
response.setStatus(404);
}
response.getOutputStream().flush();
}
@Operation(summary = "根据 URL 文件下载")
@PostMapping("/downloadByUrl")
public void downloadByUrl(@RequestBody StringDTO stringDTO, HttpServletResponse response) throws Exception {
// 解密 url
String decodedUrl = URLDecoder.decode(stringDTO.getStr(), StandardCharsets.UTF_8);
MinioObject one = minioObjectService.lambdaQuery()
.eq(MinioObject::getUrl, decodedUrl)
.eq(MinioObject::getDeleteFlag, Constant.ZERO)
.one();
if (one == null) {
response.setStatus(404);
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write("{\"code\": 404, \"msg\": \"文件不存在\"}");
return;
}
String bucket = one.getBucket();
String object = one.getObject();
String name = one.getName();
// 设置输出流格式
// 添加 CORS 暴露请求头(必须!)
response.addHeader("Access-Control-Expose-Headers", "Content-Disposition");
// 设置下载文件名
String encodedName = URLEncoder.encode(name, StandardCharsets.UTF_8);
response.addHeader("Content-Disposition","attachment;filename=" + encodedName);
response.setContentType("application/octet-stream");
try {
// 从MinIO获取文件流
GetObjectResponse getObjectResponse = minioClient.getObject(GetObjectArgs.builder()
.bucket(bucket)
.object(object)
.build());
// 流式传输到HTTP响应
getObjectResponse.transferTo(response.getOutputStream());
} catch (Exception e) {
response.setStatus(404);
}
response.getOutputStream().flush();
}
@Operation(summary = "分页列表")
@PostMapping("/list")
public SaResult list(@RequestBody MinioObjectPageDto pageDto){
// 创建分页对象
Page<MinioObjectDto> page=new Page<>(pageDto.getCurrentPage(), pageDto.getPageSize());
// 构造询条件
MPJLambdaWrapper<MinioObject> qw = new MPJLambdaWrapper<MinioObject>()
.selectAll(MinioObject.class)
.selectAs(User::getName, MinioObjectDto::getUserName)
.leftJoin(User.class, User::getId, MinioObject::getUserId)
.like(StringUtils.isNotBlank(pageDto.getName()), MinioObject::getName, pageDto.getName())
.like(StringUtils.isNotBlank(pageDto.getBucket()), MinioObject::getBucket, pageDto.getBucket())
.like(StringUtils.isNotBlank(pageDto.getType()), MinioObject::getType, pageDto.getType())
.orderByDesc(MinioObject::getCreateTime);
// 根据查询条件,将结果封装到分页对象
Page<MinioObjectDto> response = minioObjectService.selectJoinListPage(page, MinioObjectDto.class, qw);
return SaResult.ok().setData(response);
}
@Transactional(rollbackFor = Exception.class)
@Operation(summary = "删除")
@DeleteMapping("/remove")
public SaResult remove(@RequestBody @Validated StringDTO stringDTO){
MinioObject object = minioObjectService.lambdaQuery().eq(MinioObject::getId, stringDTO.getStr()).one();
try {
minioClient.removeObject(RemoveObjectArgs.builder()
.bucket(minIOBucket) // 存储桶名
.object(object.getObject()) // 对象名
.build());
} catch (Exception e) {
return SaResult.error(e.getMessage());
}
minioObjectService.removeById(stringDTO.getStr());
return SaResult.ok();
}
}