文章目錄
- 前言
- 一、配置
- 1.配置文件:application.yml
- 2.配置類:MinioProperties
- 3.工具類:MinioUtil
- 3.1 初始化方法
- 3.2 核心功能
- 3.3 關鍵技術點
- 二、使用示例
- 1.控制器類:FileController
- 2.服務類
- 3.效果展示
- 總結
前言
- Minio 是一個高性能的分布式對象存儲系統,專為云原生應用而設計
- 作為 Amazon S3 的兼容替代品,它提供了簡單易用的 API,支持海量非結構化數據存儲
- 在微服務架構中,文件存儲是常見需求,而 Minio 以其輕量級、高可用和易部署的特點成為理想選擇
一、配置
1.配置文件:application.yml
vehicle:minio:url: http://localhost:9000 # 連接地址,如果是線上的將:localhost->ipusername: minio # 登錄用戶名password: 12345678 # 登錄密碼bucketName: vehicle # 存儲文件的桶的名字
- url:Minio 服務器地址,線上環境替換為實際 IP 或域
- username/password:Minio 控制臺登錄憑證
- bucketName:文件存儲桶名稱,類似文件夾概念
- HTTPS 注意:若配置域名訪問,URL 需寫為 https://your.domain.name:9090
2.配置類:MinioProperties
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;@Component
@Data
@ConfigurationProperties(prefix = "vehicle.minio")
public class MinioProperties {private String url;private String username;private String password;private String bucketName;
}
- @ConfigurationProperties:將配置文件中的屬性綁定到類字段
- @Component:使該類成為 Spring 管理的 Bean
- 提供 Minio 連接所需的所有配置參數
3.工具類:MinioUtil
import cn.hutool.core.lang.UUID;
import com.fc.properties.MinioProperties;
import io.minio.*;
import io.minio.errors.*;
import lombok.RequiredArgsConstructor;
import org.apache.commons.compress.utils.IOUtils;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;
import io.minio.http.Method;
import javax.annotation.PostConstruct;
import javax.servlet.http.HttpServletResponse;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.concurrent.TimeUnit;/*** 文件操作工具類*/
@RequiredArgsConstructor
@Component
public class MinioUtil {private final MinioProperties minioProperties;//配置類private MinioClient minioClient;//連接客戶端private String bucketName;//桶的名字// 初始化 Minio 客戶端@PostConstructpublic void init() {try {//創建客戶端minioClient = MinioClient.builder().endpoint(minioProperties.getUrl()).credentials(minioProperties.getUsername(), minioProperties.getPassword()).build();bucketName = minioProperties.getBucketName();// 檢查桶是否存在,不存在則創建boolean bucketExists = minioClient.bucketExists(BucketExistsArgs.builder().bucket(bucketName).build());if (!bucketExists) {minioClient.makeBucket(MakeBucketArgs.builder().bucket(bucketName).build());}} catch (Exception e) {throw new RuntimeException("Minio 初始化失敗", e);}}/** 上傳文件*/public String uploadFile(MultipartFile file,String extension) {if (file == null || file.isEmpty()) {throw new RuntimeException("上傳文件不能為空");}try {// 生成唯一文件名String uniqueFilename = generateUniqueFilename(extension);// 上傳文件minioClient.putObject(PutObjectArgs.builder().bucket(bucketName).object(uniqueFilename).stream(file.getInputStream(), file.getSize(), -1).contentType(file.getContentType()).build());return "/" + bucketName + "/" + uniqueFilename;} catch (Exception e) {throw new RuntimeException("文件上傳失敗", e);}}/*** 上傳已處理的圖片字節數組到 MinIO** @param imageData 處理后的圖片字節數組* @param extension 文件擴展名(如 ".jpg", ".png")* @param contentType 文件 MIME 類型(如 "image/jpeg", "image/png")* @return MinIO 中的文件路徑(格式:/bucketName/yyyy-MM-dd/uuid.extension)*/public String uploadFileByte(byte[] imageData, String extension, String contentType) {if (imageData == null || imageData.length == 0) {throw new RuntimeException("上傳的圖片數據不能為空");}if (extension == null || extension.isEmpty()) {throw new IllegalArgumentException("文件擴展名不能為空");}if (contentType == null || contentType.isEmpty()) {throw new IllegalArgumentException("文件 MIME 類型不能為空");}try {// 生成唯一文件名String uniqueFilename = generateUniqueFilename(extension);// 上傳到 MinIOminioClient.putObject(PutObjectArgs.builder().bucket(bucketName).object(uniqueFilename).stream(new ByteArrayInputStream(imageData), imageData.length, -1).contentType(contentType).build());return "/" + bucketName + "/" + uniqueFilename;} catch (Exception e) {throw new RuntimeException("處理后的圖片上傳失敗", e);}}/*** 上傳本地生成的 Excel 臨時文件到 MinIO* @param localFile 本地臨時文件路徑* @param extension 擴展名* @return MinIO 存儲路徑,格式:/bucketName/yyyy-MM-dd/targetName*/public String uploadLocalExcel(Path localFile, String extension) {if (localFile == null || !Files.exists(localFile)) {throw new RuntimeException("本地文件不存在");}try (InputStream in = Files.newInputStream(localFile)) {String objectKey = generateUniqueFilename(extension); // 保留日期目錄minioClient.putObject(PutObjectArgs.builder().bucket(bucketName).object(objectKey).stream(in, Files.size(localFile), -1).contentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet").build());return "/" + bucketName + "/" + objectKey;} catch (Exception e) {throw new RuntimeException("Excel 上傳失敗", e);}}/** 根據URL下載文件*/public void downloadFile(HttpServletResponse response, String fileUrl) {if (fileUrl == null || !fileUrl.contains(bucketName + "/")) {throw new IllegalArgumentException("無效的文件URL");}try {// 從URL中提取對象路徑和文件名String objectUrl = fileUrl.split(bucketName + "/")[1];String fileName = objectUrl.substring(objectUrl.lastIndexOf("/") + 1);// 設置響應頭response.setContentType("application/octet-stream");String encodedFileName = URLEncoder.encode(fileName, StandardCharsets.UTF_8).replaceAll("\\+", "%20");response.setHeader("Content-Disposition", "attachment; filename=\"" + encodedFileName + "\"");// 下載文件try (InputStream inputStream = minioClient.getObject(GetObjectArgs.builder().bucket(bucketName).object(objectUrl).build());OutputStream outputStream = response.getOutputStream()) {// 用IOUtils.copy高效拷貝(內部緩沖區默認8KB)IOUtils.copy(inputStream, outputStream);}} catch (Exception e) {throw new RuntimeException("文件下載失敗", e);}}/*** 根據 MinIO 路徑生成帶簽名的直鏈* @param objectUrl 已存在的 MinIO 路徑(/bucketName/...)* @param minutes 鏈接有效期(分鐘)* @return 可直接訪問的 HTTPS 下載地址*/public String parseGetUrl(String objectUrl, int minutes) {if (objectUrl == null || !objectUrl.startsWith("/" + bucketName + "/")) {throw new IllegalArgumentException("非法的 objectUrl");}String objectKey = objectUrl.substring(("/" + bucketName + "/").length());try {return minioClient.getPresignedObjectUrl(GetPresignedObjectUrlArgs.builder().method(Method.GET).bucket(bucketName).object(objectKey).expiry(minutes, TimeUnit.MINUTES).build());} catch (Exception e) {throw new RuntimeException("生成直鏈失敗", e);}}/** 根據URL刪除文件*/public void deleteFile(String fileUrl) {try {// 從URL中提取對象路徑String objectUrl = fileUrl.split(bucketName + "/")[1];minioClient.removeObject(RemoveObjectArgs.builder().bucket(bucketName).object(objectUrl).build());} catch (Exception e) {throw new RuntimeException("文件刪除失敗", e);}}/** 檢查文件是否存在*/public boolean fileExists(String fileUrl) {if (fileUrl == null || !fileUrl.contains(bucketName + "/")) {return false;}try {String objectUrl = fileUrl.split(bucketName + "/")[1];minioClient.statObject(StatObjectArgs.builder().bucket(bucketName).object(objectUrl).build());return true;} catch (ErrorResponseException | InsufficientDataException | InternalException | InvalidKeyException |InvalidResponseException | IOException | NoSuchAlgorithmException | ServerException |XmlParserException e) {if (e instanceof ErrorResponseException && ((ErrorResponseException) e).errorResponse().code().equals("NoSuchKey")) {return false;}throw new RuntimeException("檢查文件存在失敗", e);}}/*** 生成唯一文件名(帶日期路徑 + UUID)*/private String generateUniqueFilename(String extension) {String dateFormat = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd"));String uuid = UUID.randomUUID().toString().replace("-", ""); // 去掉 UUID 中的 "-"return dateFormat + "/" + uuid + extension;}
}
3.1 初始化方法
-
使用 @PostConstruct 在 Bean 初始化后自動執行
-
創建 MinioClient 客戶端實例
-
檢查并創建存儲桶(若不存在)
3.2 核心功能
方法名 | 功能描述 | 參數說明 | 返回值 |
---|---|---|---|
uploadFile() | 上傳MultipartFile文件 | 文件對象,擴展名 | 文件路徑 |
uploadFileByte() | 上傳字節數組 | 字節數據,擴展名,MIME類型 | 文件路徑 |
uploadLocalExcel() | 上傳本地Excel文件 | 文件路徑,擴展名 | 文件路徑 |
downloadFile() | 下載文件到響應流 | HTTP響應對象,文件URL | 無 |
parseGetUrl() | 生成帶簽名直鏈 | 文件路徑,有效期(分鐘) | 直鏈URL |
deleteFile() | 刪除文件 | 文件URL | 無 |
fileExists() | 檢查文件是否存在 | 文件URL | 布爾值 |
3.3 關鍵技術點
-
唯一文件名生成:日期目錄/UUID.擴展名 格式避免重名
-
大文件流式傳輸:避免內存溢出
-
響應頭編碼處理:解決中文文件名亂碼問題
-
異常統一處理:Minio 異常轉換為運行時異常
-
預簽名URL:生成臨時訪問鏈接
二、使用示例
1.控制器類:FileController
import com.fc.result.Result;
import com.fc.service.FileService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;import javax.servlet.http.HttpServletResponse;
import java.io.IOException;@Api(tags = "文件")
@RestController
@RequestMapping("/file")
@RequiredArgsConstructor
public class FileController {private final FileService fileService;@ApiOperation("圖片上傳")@PostMapping("/image")public Result<String> imageUpload(MultipartFile file) throws IOException {String url = fileService.imageUpload(file);return Result.success(url);}@ApiOperation("圖片下載")@GetMapping("/image")public void imageDownLoad(HttpServletResponse response, String url) throws IOException {fileService.imageDownload(response, url);}@ApiOperation("圖片刪除")@DeleteMapping("/image")public Result<Void> imageDelete(String url) {fileService.imageDelete(url);return Result.success();}}
2.服務類
FileService
import org.springframework.web.multipart.MultipartFile;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;public interface FileService {String imageUpload(MultipartFile file) throws IOException;void imageDownload(HttpServletResponse response, String url) throws IOException;void imageDelete(String url);
}
FileServiceImpl
import com.fc.exception.FileException;
import com.fc.service.FileService;
import com.fc.utils.ImageUtil;
import com.fc.utils.MinioUtil;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;@Service
@RequiredArgsConstructor
public class FileServiceImpl implements FileService {private final MinioUtil minioUtil;@Overridepublic String imageUpload(MultipartFile file) throws IOException {byte[] bytes = ImageUtil.compressImage(file, "JPEG");return minioUtil.uploadFileByte(bytes, ".jpeg", "image/jpeg");}@Overridepublic void imageDownload(HttpServletResponse response, String url) throws IOException {minioUtil.downloadFile(response, url);}@Overridepublic void imageDelete(String url) {if (!minioUtil.fileExists(url)) {throw new FileException("文件不存在");}minioUtil.deleteFile(url);}
}
3.效果展示
利用
Apifox
測試下三個接口
圖片上傳
圖片下載
刪除圖片
總結
本文通過 “配置 - 工具 - 業務” 三層架構,實現了 Spring Boot 與 MinIO 的集成,核心優勢如下:
- 易用性:通過配置綁定和工具類封裝,簡化 MinIO 操作,開發者無需關注底層 API 細節。
- 靈活性:支持多種文件類型(表單文件、字節流、本地文件),滿足不同場景需求(如圖片壓縮、Excel 生成)。
- 可擴展性:可基于此框架擴展功能,如添加文件權限控制(通過 MinIO 的 Policy)、文件分片上傳(大文件處理)、定期清理過期文件等。
MinIO 作為輕量級對象存儲方案,非常適合中小項目替代本地存儲或云廠商 OSS(降低成本)。實際應用中需注意:生產環境需配置 MinIO 集群確保高可用;敏感文件需通過預簽名 URL 控制訪問權限;定期備份桶數據以防丟失。通過本文的方案,開發者可快速搭建穩定、可擴展的文件存儲服務,為應用提供可靠的非結構化數據管理能力。