Spring Boot 集成 MinIO 实现分布式文件存储与管理
一、MinIO 简介
MinIO 是一个高性能的分布式对象存储服务器,兼容 Amazon S3 API。它具有以下特点:
- 轻量级且易于部署
- 高性能(读写速度可达每秒数GB)
- 支持数据加密和访问控制
- 提供多种语言的SDK
- 开源且社区活跃
二、Spring Boot 集成 MinIO
1. 添加依赖
在 pom.xml
中添加 MinIO Java SDK 依赖:
<dependency><groupId>io.minio</groupId><artifactId>minio</artifactId><version>8.5.7</version>
</dependency>
2. 配置 MinIO 连接
在 application.yml
中配置:
minio:endpoint: http://localhost:9000accessKey: minioadminsecretKey: minioadminbucketName: default-bucketsecure: false
3. 创建配置类
@Configuration
public class MinioConfig {@Value("${minio.endpoint}")private String endpoint;@Value("${minio.accessKey}")private String accessKey;@Value("${minio.secretKey}")private String secretKey;@Beanpublic MinioClient minioClient() {return MinioClient.builder().endpoint(endpoint).credentials(accessKey, secretKey).build();}
}
三、实现文件服务
@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);}// 文件存在,生成URLreturn 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 实现
@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. 分片上传
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. 文件预览
@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)
-
监控与日志:
- 记录所有文件操作
- 监控存储空间使用情况
- 设置自动清理策略
七、常见问题解决
-
连接超时问题:
@Bean public MinioClient minioClient() {return MinioClient.builder().endpoint(endpoint).credentials(accessKey, secretKey).httpClient(HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(30)).build()).build(); }
-
文件存在性检查优化:
public 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 可以快速构建强大的文件存储服务。