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 可以快速構建強大的文件存儲服務。