Spring Boot 集成 MinIO 实现分布式文件存储与管理

一、MinIO 简介
MinIO 是一个高性能的分布式对象存储服务器,兼容 Amazon S3 API。它具有以下特点:
- 轻量级且易于部署
- 高性能(读写速度可达每秒数GB)
- 支持数据加密和访问控制
- 提供多种语言的SDK
- 开源且社区活跃
二、Spring Boot 集成 MinIO
1. 添加依赖
在 pom.xml
中添加 MinIO Java SDK 依赖:
xml
<dependency>
<groupId>io.minio</groupId>
<artifactId>minio</artifactId>
<version>8.5.7</version>
</dependency>
2. 配置 MinIO 连接
在 application.yml
中配置:
yaml
minio:
endpoint: http://localhost:9000
accessKey: minioadmin
secretKey: minioadmin
bucketName: default-bucket
secure: false
3. 创建配置类
java
@Configuration
public class MinioConfig {
@Value("${minio.endpoint}")
private String endpoint;
@Value("${minio.accessKey}")
private String accessKey;
@Value("${minio.secretKey}")
private String secretKey;
@Bean
public MinioClient minioClient() {
return MinioClient.builder()
.endpoint(endpoint)
.credentials(accessKey, secretKey)
.build();
}
}
三、实现文件服务
java
@Service
@RequiredArgsConstructor
public class MinioService {
private final MinioClient minioClient;
@Value("${minio.bucketName}")
private String bucketName;
/**
* 检查存储桶是否存在
*
* @param bucketName 存储桶名称
* @return 存储桶是否存在 状态码 true:存在 false:不存在
*/
public boolean bucketExists(String bucketName) throws Exception {
return !minioClient.bucketExists(BucketExistsArgs.builder().bucket(bucketName).build());
}
/**
* 创建存储桶
*/
public void makeBucket(String bucketName) throws Exception {
if (bucketExists(bucketName)) {
minioClient.makeBucket(MakeBucketArgs.builder().bucket(bucketName).build());
}
}
/**
* 列出所有存储桶
*/
public List<Bucket> listBuckets() throws Exception {
return minioClient.listBuckets();
}
/**
* 上传文件
*
* @param file 文件
* @param bucketName 存储桶名称
* @param rename 是否重命名
*/
public String uploadFile(MultipartFile file, String bucketName, boolean rename) throws Exception {
// 如果未指定bucketName,使用默认的
bucketName = getBucketName(bucketName);
// 检查存储桶是否存在,不存在则创建
ensureBucketExists(bucketName);
// 生成唯一文件名
String objectName = generateObjectName(file, rename);
// 上传文件
minioClient.putObject(
PutObjectArgs.builder()
.bucket(bucketName)
.object(objectName)
.stream(file.getInputStream(), file.getSize(), -1)
.contentType(file.getContentType())
.build());
return objectName;
}
/**
* 下载文件
*/
public InputStream downloadFile(String objectName, String bucketName) throws Exception {
return minioClient.getObject(
GetObjectArgs.builder()
.bucket(getBucketName(bucketName))
.object(objectName)
.build());
}
/**
* 删除文件
*/
public void removeFile(String objectName, String bucketName) throws Exception {
minioClient.removeObject(
RemoveObjectArgs.builder()
.bucket(getBucketName(bucketName))
.object(objectName)
.build());
}
/**
* 获取文件URL(先检查文件是否存在)
*/
public String getFileUrl(String objectName, String bucketName) throws Exception {
try {
minioClient.statObject(
StatObjectArgs.builder()
.bucket(getBucketName(bucketName))
.object(objectName)
.build());
} catch (ErrorResponseException e) {
// 文件不存在时抛出异常
throw new FileNotFoundException("File not found: " + objectName);
}
// 文件存在,生成URL
return minioClient.getPresignedObjectUrl(
GetPresignedObjectUrlArgs.builder()
.method(Method.GET)
.bucket(getBucketName(bucketName))
.object(objectName)
.build());
}
/**
* 生成唯一文件名
*/
private static @Nullable String generateObjectName(MultipartFile file, boolean rename) {
String fileName = file.getOriginalFilename();
String objectName = fileName;
if (rename && fileName != null) {
objectName = UUID.randomUUID().toString().replaceAll("-", "")
+ fileName.substring(fileName.lastIndexOf("."));
}
return objectName;
}
/**
* 检查存储桶是否存在,不存在则创建
*/
private void ensureBucketExists(String bucketName) throws Exception {
if (bucketExists(bucketName)) {
makeBucket(bucketName);
}
}
/**
* 获取存储桶名称
*/
private String getBucketName(String bucketName) {
if (bucketName == null || bucketName.isEmpty()) {
bucketName = this.bucketName;
}
return bucketName;
}
}
四、REST API 实现
java
@RestController
@RequiredArgsConstructor
@RequestMapping("/api/file")
public class MinioController {
private final MinioService minioService;
@PostMapping("/upload")
public ResponseEntity<String> uploadFile(@RequestParam("file") MultipartFile file,
@RequestParam(value = "bucketName", required = false) String bucketName) {
try {
String objectName = minioService.uploadFile(file, bucketName, false);
return ResponseEntity.ok(objectName);
} catch (Exception e) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(e.getMessage());
}
}
@GetMapping("/download")
public ResponseEntity<byte[]> downloadFile(@RequestParam String objectName,
@RequestParam(value = "bucketName", required = false) String bucketName) {
try {
InputStream stream = minioService.downloadFile(objectName, bucketName);
byte[] bytes = stream.readAllBytes();
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_OCTET_STREAM);
headers.setContentDispositionFormData("attachment", objectName);
return new ResponseEntity<>(bytes, headers, HttpStatus.OK);
} catch (Exception e) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(null);
}
}
@DeleteMapping("/delete")
public ResponseEntity<String> deleteFile(@RequestParam String objectName,
@RequestParam(value = "bucketName", required = false) String bucketName) {
try {
minioService.removeFile(objectName, bucketName);
return ResponseEntity.ok("File deleted successfully");
} catch (Exception e) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(e.getMessage());
}
}
@GetMapping("/url")
public ResponseEntity<String> getFileUrl(@RequestParam String objectName,
@RequestParam(value = "bucketName", required = false) String bucketName) {
try {
String url = minioService.getFileUrl(objectName, bucketName);
return ResponseEntity.ok(url);
} catch (FileNotFoundException e) {
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(e.getMessage());
} catch (Exception e) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(e.getMessage());
}
}
}
五、高级功能实现
1. 分片上传
java
public String multipartUpload(MultipartFile file, String bucketName) {
// 1. 初始化分片上传
String uploadId = minioClient.initiateMultipartUpload(...);
// 2. 分片上传
Map<Integer, String> etags = new HashMap<>();
for (int partNumber = 1; partNumber <= totalParts; partNumber++) {
PartSource partSource = getPartSource(file, partNumber);
String etag = minioClient.uploadPart(...);
etags.put(partNumber, etag);
}
// 3. 完成分片上传
minioClient.completeMultipartUpload(...);
return objectName;
}
2. 文件预览
java
@GetMapping("/preview/{objectName}")
public ResponseEntity<Resource> previewFile(
@PathVariable String objectName,
@RequestParam(value = "bucketName", required = false) String bucketName) throws Exception {
String contentType = minioService.getFileContentType(objectName, bucketName);
InputStream inputStream = minioService.downloadFile(objectName, bucketName);
return ResponseEntity.ok()
.contentType(MediaType.parseMediaType(contentType))
.body(new InputStreamResource(inputStream));
}
六、最佳实践
-
安全性考虑:
- 为预签名URL设置合理的过期时间
- 实现细粒度的访问控制
- 对上传文件进行病毒扫描
-
性能优化:
- 使用CDN加速文件访问
- 对大文件使用分片上传
- 实现客户端直传(Presigned URL)
-
监控与日志:
- 记录所有文件操作
- 监控存储空间使用情况
- 设置自动清理策略
七、常见问题解决
-
连接超时问题:
java@Bean public MinioClient minioClient() { return MinioClient.builder() .endpoint(endpoint) .credentials(accessKey, secretKey) .httpClient(HttpClient.newBuilder() .connectTimeout(Duration.ofSeconds(30)) .build()) .build(); }
-
文件存在性检查优化:
javapublic boolean fileExists(String objectName, String bucketName) { try { minioClient.statObject( StatObjectArgs.builder() .bucket(getBucketName(bucketName)) .object(objectName) .build()); return true; } catch (ErrorResponseException e) { if (e.errorResponse().code().equals("NoSuchKey")) { return false; } throw new FileStorageException("检查文件存在性失败", e); } catch (Exception e) { throw new FileStorageException("检查文件存在性失败", e); } }
八、总结
通过本文的介绍,我们实现了:
- Spring Boot 与 MinIO 的基本集成
- 文件上传、下载、删除等基础功能
- 文件预览、分片上传等高级功能
- 安全性、性能等方面的最佳实践
MinIO 作为轻量级的对象存储解决方案,非常适合中小型项目使用。结合 Spring Boot 可以快速构建强大的文件存储服务。