Spring Boot 整合 Minio 實現(xiàn)高效文件存儲解決方案(本地和線上)
前言
- Minio 是一個高性能的分布式對象存儲系統(tǒng),專為云原生應(yīng)用而設(shè)計
- 作為 Amazon S3 的兼容替代品,它提供了簡單易用的 API,支持海量非結(jié)構(gòu)化數(shù)據(jù)存儲
- 在微服務(wù)架構(gòu)中,文件存儲是常見需求,而 Minio 以其輕量級、高可用和易部署的特點成為理想選擇
一、配置
1.配置文件:application.yml
vehicle:
minio:
url: http://localhost:9000 # 連接地址,如果是線上的將:localhost->ip
username: minio # 登錄用戶名
password: 12345678 # 登錄密碼
bucketName: vehicle # 存儲文件的桶的名字- url:Minio 服務(wù)器地址,線上環(huán)境替換為實際 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 連接所需的所有配置參數(shù)
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 客戶端
@PostConstruct
public void init() {
try {
//創(chuàng)建客戶端
minioClient = MinioClient.builder()
.endpoint(minioProperties.getUrl())
.credentials(minioProperties.getUsername(), minioProperties.getPassword())
.build();
bucketName = minioProperties.getBucketName();
// 檢查桶是否存在,不存在則創(chuàng)建
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);
}
}
/**
* 上傳已處理的圖片字節(jié)數(shù)組到 MinIO
*
* @param imageData 處理后的圖片字節(jié)數(shù)組
* @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("上傳的圖片數(shù)據(jù)不能為空");
}
if (extension == null || extension.isEmpty()) {
throw new IllegalArgumentException("文件擴展名不能為空");
}
if (contentType == null || contentType.isEmpty()) {
throw new IllegalArgumentException("文件 MIME 類型不能為空");
}
try {
// 生成唯一文件名
String uniqueFilename = generateUniqueFilename(extension);
// 上傳到 MinIO
minioClient.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);
}
}
/*
* 根據(jù)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);
// 設(shè)置響應(yīng)頭
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高效拷貝(內(nèi)部緩沖區(qū)默認8KB)
IOUtils.copy(inputStream, outputStream);
}
} catch (Exception e) {
throw new RuntimeException("文件下載失敗", e);
}
}
/**
* 根據(jù) 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("生成直鏈?zhǔn)?, e);
}
}
/*
* 根據(jù)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 初始化后自動執(zhí)行
- 創(chuàng)建 MinioClient 客戶端實例
- 檢查并創(chuàng)建存儲桶(若不存在)
3.2 核心功能
| 方法名 | 功能描述 | 參數(shù)說明 | 返回值 |
|---|---|---|---|
| uploadFile() | 上傳MultipartFile文件 | 文件對象,擴展名 | 文件路徑 |
| uploadFileByte() | 上傳字節(jié)數(shù)組 | 字節(jié)數(shù)據(jù),擴展名,MIME類型 | 文件路徑 |
| uploadLocalExcel() | 上傳本地Excel文件 | 文件路徑,擴展名 | 文件路徑 |
| downloadFile() | 下載文件到響應(yīng)流 | HTTP響應(yīng)對象,文件URL | 無 |
| parseGetUrl() | 生成帶簽名直鏈 | 文件路徑,有效期(分鐘) | 直鏈URL |
| deleteFile() | 刪除文件 | 文件URL | 無 |
| fileExists() | 檢查文件是否存在 | 文件URL | 布爾值 |
3.3 關(guān)鍵技術(shù)點
- 唯一文件名生成:日期目錄/UUID.擴展名 格式避免重名
- 大文件流式傳輸:避免內(nèi)存溢出
- 響應(yīng)頭編碼處理:解決中文文件名亂碼問題
- 異常統(tǒng)一處理:Minio 異常轉(zhuǎn)換為運行時異常
- 預(yù)簽名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.服務(wù)類
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;
@Override
public String imageUpload(MultipartFile file) throws IOException {
byte[] bytes = ImageUtil.compressImage(file, "JPEG");
return minioUtil.uploadFileByte(bytes, ".jpeg", "image/jpeg");
}
@Override
public void imageDownload(HttpServletResponse response, String url) throws IOException {
minioUtil.downloadFile(response, url);
}
@Override
public void imageDelete(String url) {
if (!minioUtil.fileExists(url)) {
throw new FileException("文件不存在");
}
minioUtil.deleteFile(url);
}
}3.效果展示
利用Apifox測試下三個接口
圖片上傳


圖片下載

刪除圖片


總結(jié)
本文通過 “配置 - 工具 - 業(yè)務(wù)” 三層架構(gòu),實現(xiàn)了 Spring Boot 與 MinIO 的集成,核心優(yōu)勢如下:
- 易用性:通過配置綁定和工具類封裝,簡化 MinIO 操作,開發(fā)者無需關(guān)注底層 API 細節(jié)。
- 靈活性:支持多種文件類型(表單文件、字節(jié)流、本地文件),滿足不同場景需求(如圖片壓縮、Excel 生成)。
- 可擴展性:可基于此框架擴展功能,如添加文件權(quán)限控制(通過 MinIO 的 Policy)、文件分片上傳(大文件處理)、定期清理過期文件等。
MinIO 作為輕量級對象存儲方案,非常適合中小項目替代本地存儲或云廠商 OSS(降低成本)。實際應(yīng)用中需注意:生產(chǎn)環(huán)境需配置 MinIO 集群確保高可用;敏感文件需通過預(yù)簽名 URL 控制訪問權(quán)限;定期備份桶數(shù)據(jù)以防丟失。通過本文的方案,開發(fā)者可快速搭建穩(wěn)定、可擴展的文件存儲服務(wù),為應(yīng)用提供可靠的非結(jié)構(gòu)化數(shù)據(jù)管理能力。
到此這篇關(guān)于Spring Boot 整合 Minio 實現(xiàn)高效文件存儲解決方案(本地和線上)的文章就介紹到這了,更多相關(guān)Spring Boot Minio 文件存儲內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Spring Cloud Gateway使用Token驗證詳解
這篇文章主要介紹了Spring Cloud Gateway使用Token驗證詳解,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2019-02-02
自主配置數(shù)據(jù)源,mybatis/plus不打印sql日志問題
這篇文章主要介紹了自主配置數(shù)據(jù)源,mybatis/plus不打印sql日志問題,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2023-12-12
Springboot各版本與Java JDK的對應(yīng)版本梳理
本文介紹了SpringBoot各版本支持的JavaJDK版本,SpringBoot2.5.x支持JDK8、11、17,2.4.x支持JDK8、11、15等,商用JDK如OracleJDK、IBMJDK、AzulZuluEnterprise等包含額外功能,適用于企業(yè)級應(yīng)用2026-04-04
Spring Boot利用Lombok減少Java中樣板代碼的方法示例
spring Boot是非常高效的開發(fā)框架,lombok是一套代碼模板解決方案,將極大提升開發(fā)的效率,下面這篇文章主要給大家介紹了關(guān)于Spring Boot利用Lombok減少Java中樣板代碼的相關(guān)資料,需要的朋友可以參考借鑒,下面來一起看看吧。2017-09-09
關(guān)于maven項目引入maven庫沒有的jar處理辦法
這篇文章主要介紹了關(guān)于maven項目引入maven庫沒有的jar處理辦法,在平時開發(fā)中,有些jar包是不存在maven中央庫中的,那么此時該如何解決才能方便后續(xù)處理呢,需要的朋友可以參考下本文2023-03-03
SpringBoot集成Graphql Query實戰(zhàn)示例
這篇文章主要為大家介紹了SpringBoot集成Graphql Query實戰(zhàn)示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2022-09-09
SpringCloud中的Feign遠程調(diào)用接口傳參失敗問題
這篇文章主要介紹了SpringCloud中的Feign遠程調(diào)用接口傳參失敗問題,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2022-03-03

