一、MinIO简介与优势
MinIO是一款基于Go语言开发的高性能、分布式对象存储系统,完全兼容Amazon S3 API协议。作为开源项目,它采用Apache License v2.0协议,非常适合存储大容量非结构化数据,如图片、视频、日志文件等。
核心优势:
- 轻量级:二进制文件小,部署简单,资源要求低
- 高性能:通过优化的存储引擎和缓存机制提供极高读写性能
- 易扩展:支持水平扩展,可通过增加节点提升存储能力和吞吐量
- S3兼容:完全兼容Amazon S3 API,现有S3客户端和工具可直接使用
- 开源免费:采用Apache 2.0开源协议,可自由使用和修改
二、环境准备
1. 安装MinIO服务器
Docker方式(推荐):
docker run -p 9000:9000 -p 9001:9001 \
--name minio \
-v /data/minio/data:/data \
-v /data/minio/config:/root/.minio \
-e "MINIO_ROOT_USER=minioadmin" \
-e "MINIO_ROOT_PASSWORD=minioadmin" \
minio/minio server /data --console-address ":9001"
Windows二进制方式:
- 下载MinIO二进制文件:https://min.io/download#/windows
- 创建目录结构:
F:\Minio\
├── bin\ (存放minio.exe)
├── data\ (存放上传的文件)
└── log\ (日志目录,可选)
- 创建启动脚本(start.bat):
minio.exe server F:/Minio/data --console-address ":9005" --address ":9000"
启动成功后,访问控制台:http://localhost:9001,使用默认账号密码(minioadmin/minioadmin)登录。
2. 创建存储桶(Bucket)
登录MinIO控制台后,点击”Create Bucket”按钮,输入桶名称(如file-upload-bucket)并创建。
三、SpringBoot项目集成
1. 添加Maven依赖
在pom.xml中添加MinIO Java SDK依赖:
<dependency>
<groupId>io.minio</groupId>
<artifactId>minio</artifactId>
<version>8.5.9</version>
</dependency>
同时添加其他常用依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.11.0</version>
</dependency>
2. 配置文件
在application.yml中配置MinIO连接参数:
minio:
endpoint: http://localhost:9000
access-key: minioadmin
secret-key: minioadmin
bucket-name: file-upload-bucket
part-size: 104857600 # 分片大小(100MB)
3. 配置类
创建MinIO配置类,将MinioClient注入Spring容器:
@Configuration
public class MinioConfig {
@Value("${minio.endpoint}")
private String endpoint;
@Value("${minio.access-key}")
private String accessKey;
@Value("${minio.secret-key}")
private String secretKey;
@Bean
public MinioClient minioClient() {
return MinioClient.builder()
.endpoint(endpoint)
.credentials(accessKey, secretKey)
.build();
}
}
四、基础文件操作实现
1. 文件上传
@Service
public class MinioService {
@Autowired
private MinioClient minioClient;
@Value("${minio.bucket-name}")
private String bucketName;
/**
* 上传文件
*/
public String uploadFile(MultipartFile file, String objectName) throws Exception {
// 检查存储桶是否存在,不存在则创建
boolean found = minioClient.bucketExists(BucketExistsArgs.builder()
.bucket(bucketName)
.build());
if (!found) {
minioClient.makeBucket(MakeBucketArgs.builder()
.bucket(bucketName)
.build());
}
// 上传文件
minioClient.putObject(
PutObjectArgs.builder()
.bucket(bucketName)
.object(objectName)
.stream(file.getInputStream(), file.getSize(), -1)
.contentType(file.getContentType())
.build());
return getFileUrl(objectName);
}
/**
* 获取文件访问URL
*/
public String getFileUrl(String objectName) {
try {
return minioClient.getPresignedObjectUrl(
GetPresignedObjectUrlArgs.builder()
.bucket(bucketName)
.object(objectName)
.method(Method.GET)
.expiry(7, TimeUnit.DAYS) // 7天有效期
.build());
} catch (Exception e) {
throw new RuntimeException("获取文件URL失败", e);
}
}
}
2. 文件下载
/**
* 下载文件
*/
public void downloadFile(String objectName, HttpServletResponse response) {
try {
// 获取文件信息
StatObjectResponse stat = minioClient.statObject(
StatObjectArgs.builder()
.bucket(bucketName)
.object(objectName)
.build());
// 设置响应头
response.setContentType(stat.contentType());
response.setHeader("Content-Disposition",
"attachment; filename=\"" + URLEncoder.encode(objectName, "UTF-8") + "\"");
response.setHeader("Content-Length", String.valueOf(stat.size()));
// 下载文件
try (InputStream inputStream = minioClient.getObject(
GetObjectArgs.builder()
.bucket(bucketName)
.object(objectName)
.build())) {
IOUtils.copy(inputStream, response.getOutputStream());
}
} catch (Exception e) {
throw new RuntimeException("下载文件失败", e);
}
}
3. 文件删除
/**
* 删除文件
*/
public void deleteFile(String objectName) {
try {
minioClient.removeObject(
RemoveObjectArgs.builder()
.bucket(bucketName)
.object(objectName)
.build());
} catch (Exception e) {
throw new RuntimeException("删除文件失败", e);
}
}
五、大文件分片上传实现
1. 自定义MinIO客户端(关键)
由于MinIO 8.5.9版本的分片上传核心方法在S3Base类中,且权限为protected,需要自定义客户端继承才能调用:
public class CustomMinioClient extends MinioAsyncClient {
public CustomMinioClient(MinioClient minioClient) {
super(minioClient);
}
/**
* 创建分片上传任务
*/
public String createMultipartUpload(String bucketName, String objectName) throws Exception {
CreateMultipartUploadResponse response = this.createMultipartUpload(
bucketName,
null,
objectName,
null,
null,
null);
return response.uploadId();
}
/**
* 上传分片
*/
public String uploadPart(String bucketName, String objectName, String uploadId,
int partNumber, InputStream inputStream, long partSize) throws Exception {
UploadPartResponse response = this.uploadPart(
bucketName,
null,
objectName,
uploadId,
partNumber,
inputStream,
partSize,
null,
null);
return response.etag();
}
/**
* 完成分片上传
*/
public void completeMultipartUpload(String bucketName, String objectName,
String uploadId, Map<Integer, String> partETags) throws Exception {
this.completeMultipartUpload(
bucketName,
null,
objectName,
uploadId,
partETags,
null,
null);
}
}
2. 分片上传服务
@Service
public class ChunkUploadService {
@Autowired
private MinioClient minioClient;
@Value("${minio.bucket-name}")
private String bucketName;
@Value("${minio.part-size}")
private long partSize;
// 存储分片上传任务信息
private Map<String, ChunkUploadTask> uploadTasks = new ConcurrentHashMap<>();
/**
* 初始化分片上传
*/
public ChunkUploadInitResponse initChunkUpload(String fileName, long fileSize) {
String uploadId = UUID.randomUUID().toString();
int totalParts = (int) Math.ceil((double) fileSize / partSize);
ChunkUploadTask task = new ChunkUploadTask();
task.setUploadId(uploadId);
task.setFileName(fileName);
task.setFileSize(fileSize);
task.setTotalParts(totalParts);
task.setUploadedParts(new ConcurrentHashMap<>());
uploadTasks.put(uploadId, task);
return new ChunkUploadInitResponse(uploadId, totalParts);
}
/**
* 上传分片
*/
public void uploadChunk(String uploadId, int partNumber, InputStream inputStream, long chunkSize) {
ChunkUploadTask task = uploadTasks.get(uploadId);
if (task == null) {
throw new RuntimeException("上传任务不存在");
}
try {
String etag = ((CustomMinioClient) minioClient).uploadPart(
bucketName,
task.getFileName(),
uploadId,
partNumber,
inputStream,
chunkSize);
task.getUploadedParts().put(partNumber, etag);
} catch (Exception e) {
throw new RuntimeException("分片上传失败", e);
}
}
/**
* 完成分片上传
*/
public void completeChunkUpload(String uploadId) {
ChunkUploadTask task = uploadTasks.get(uploadId);
if (task == null) {
throw new RuntimeException("上传任务不存在");
}
try {
Map<Integer, String> partETags = task.getUploadedParts();
((CustomMinioClient) minioClient).completeMultipartUpload(
bucketName,
task.getFileName(),
uploadId,
partETags);
uploadTasks.remove(uploadId);
} catch (Exception e) {
throw new RuntimeException("分片合并失败", e);
}
}
}
3. 分片上传任务实体
@Data
public class ChunkUploadTask {
private String uploadId;
private String fileName;
private long fileSize;
private int totalParts;
private Map<Integer, String> uploadedParts; // partNumber -> etag
}
@Data
public class ChunkUploadInitResponse {
private String uploadId;
private int totalParts;
public ChunkUploadInitResponse(String uploadId, int totalParts) {
this.uploadId = uploadId;
this.totalParts = totalParts;
}
}
六、Controller层实现
1. 基础文件操作接口
@RestController
@RequestMapping("/api/file")
public class FileController {
@Autowired
private MinioService minioService;
@Autowired
private ChunkUploadService chunkUploadService;
/**
* 文件上传
*/
@PostMapping("/upload")
public String uploadFile(@RequestParam("file") MultipartFile file) {
try {
String objectName = UUID.randomUUID().toString() + "_" + file.getOriginalFilename();
return minioService.uploadFile(file, objectName);
} catch (Exception e) {
throw new RuntimeException("文件上传失败", e);
}
}
/**
* 文件下载
*/
@GetMapping("/download/{fileName}")
public void downloadFile(@PathVariable String fileName, HttpServletResponse response) {
minioService.downloadFile(fileName, response);
}
/**
* 删除文件
*/
@DeleteMapping("/{fileName}")
public void deleteFile(@PathVariable String fileName) {
minioService.deleteFile(fileName);
}
}
2. 分片上传接口
@RestController
@RequestMapping("/api/chunk")
public class ChunkUploadController {
@Autowired
private ChunkUploadService chunkUploadService;
/**
* 初始化分片上传
*/
@PostMapping("/init")
public ChunkUploadInitResponse initChunkUpload(@RequestParam String fileName,
@RequestParam long fileSize) {
return chunkUploadService.initChunkUpload(fileName, fileSize);
}
/**
* 上传分片
*/
@PostMapping("/upload/{uploadId}/{partNumber}")
public void uploadChunk(@PathVariable String uploadId,
@PathVariable int partNumber,
@RequestParam("file") MultipartFile file) {
try {
chunkUploadService.uploadChunk(uploadId, partNumber,
file.getInputStream(), file.getSize());
} catch (Exception e) {
throw new RuntimeException("分片上传失败", e);
}
}
/**
* 完成分片上传
*/
@PostMapping("/complete/{uploadId}")
public void completeChunkUpload(@PathVariable String uploadId) {
chunkUploadService.completeChunkUpload(uploadId);
}
}
七、高级功能扩展
1. 秒传功能实现
/**
* 秒传检查
*/
public boolean checkFastUpload(String fileMd5, String fileName) {
try {
// 检查文件是否已存在
minioClient.statObject(
StatObjectArgs.builder()
.bucket(bucketName)
.object(fileName)
.build());
return true;
} catch (ErrorResponseException e) {
if (e.errorResponse().code().equals("NoSuchKey")) {
return false;
}
throw new RuntimeException("检查文件失败", e);
} catch (Exception e) {
throw new RuntimeException("检查文件失败", e);
}
}
2. 断点续传
通过Redis存储分片上传任务状态,实现断点续传:
@Service
public class ChunkUploadService {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
private static final String UPLOAD_TASK_KEY = "chunk:upload:task:";
/**
* 获取上传任务状态
*/
public ChunkUploadTask getUploadTask(String uploadId) {
return (ChunkUploadTask) redisTemplate.opsForValue().get(UPLOAD_TASK_KEY + uploadId);
}
/**
* 保存上传任务状态
*/
private void saveUploadTask(ChunkUploadTask task) {
redisTemplate.opsForValue().set(
UPLOAD_TASK_KEY + task.getUploadId(),
task,
24, // 24小时过期
TimeUnit.HOURS);
}
}
八、最佳实践与优化建议
1. 连接池优化
@Bean
public MinioClient minioClient() {
return MinioClient.builder()
.endpoint(endpoint)
.credentials(accessKey, secretKey)
.httpClient(HttpClient.newBuilder()
.connectionTimeout(Duration.ofSeconds(30))
.build())
.build();
}
2. 异常统一处理
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(Exception.class)
public ResponseEntity<Result<?>> handleException(Exception e) {
log.error("系统异常", e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(Result.error("系统异常,请稍后重试"));
}
@ExceptionHandler(MinioException.class)
public ResponseEntity<Result<?>> handleMinioException(MinioException e) {
log.error("MinIO操作异常", e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(Result.error("文件服务异常,请稍后重试"));
}
}
3. 跨域配置
@Configuration
public class CorsConfig {
@Bean
public CorsFilter corsFilter() {
CorsConfiguration config = new CorsConfiguration();
config.addAllowedOrigin("*");
config.addAllowedMethod("*");
config.addAllowedHeader("*");
config.setAllowCredentials(true);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", config);
return new CorsFilter(source);
}
}
九、总结
本文详细介绍了SpringBoot整合MinIO的完整方案,从基础文件操作到高级的分片上传功能。通过自定义MinIO客户端实现大文件分片上传,结合Redis实现断点续传,能够有效提升大文件上传的稳定性和用户体验。在实际项目中,建议根据业务需求进一步优化,如添加文件元数据管理、访问权限控制、文件预览等功能。
核心价值:
- 小文件(<100MB)采用完整流传输,高效低开销
- 大文件(≥100MB)采用分片上传+断点续传,稳定抗中断
- 支持秒传、断点续传等企业级功能
- 完全兼容S3协议,便于迁移和扩展
通过这套方案,可以构建出高性能、高可用的文件存储系统,满足企业级文档管理、视频存储、报表导出等业务需求。