Spring Boot 整合 Minio 實(shí)現(xiàn)高效文件存儲(chǔ)解決方案(本地和線上)
前言
- Minio 是一個(gè)高性能的分布式對(duì)象存儲(chǔ)系統(tǒng),專為云原生應(yīng)用而設(shè)計(jì)
- 作為 Amazon S3 的兼容替代品,它提供了簡單易用的 API,支持海量非結(jié)構(gòu)化數(shù)據(jù)存儲(chǔ)
- 在微服務(wù)架構(gòu)中,文件存儲(chǔ)是常見需求,而 Minio 以其輕量級(jí)、高可用和易部署的特點(diǎn)成為理想選擇
一、配置
1.配置文件:application.yml
vehicle:
minio:
url: http://localhost:9000 # 連接地址,如果是線上的將:localhost->ip
username: minio # 登錄用戶名
password: 12345678 # 登錄密碼
bucketName: vehicle # 存儲(chǔ)文件的桶的名字- url:Minio 服務(wù)器地址,線上環(huán)境替換為實(shí)際 IP 或域
- username/password:Minio 控制臺(tái)登錄憑證
- bucketName:文件存儲(chǔ)桶名稱,類似文件夾概念
- 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 文件擴(kuò)展名(如 ".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("文件擴(kuò)展名不能為空");
}
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 臨時(shí)文件到 MinIO
* @param localFile 本地臨時(shí)文件路徑
* @param extension 擴(kuò)展名
* @return MinIO 存儲(chǔ)路徑,格式:/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中提取對(duì)象路徑和文件名
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ū)默認(rèn)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中提取對(duì)象路徑
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 初始化后自動(dòng)執(zhí)行
- 創(chuàng)建 MinioClient 客戶端實(shí)例
- 檢查并創(chuàng)建存儲(chǔ)桶(若不存在)
3.2 核心功能
| 方法名 | 功能描述 | 參數(shù)說明 | 返回值 |
|---|---|---|---|
| uploadFile() | 上傳MultipartFile文件 | 文件對(duì)象,擴(kuò)展名 | 文件路徑 |
| uploadFileByte() | 上傳字節(jié)數(shù)組 | 字節(jié)數(shù)據(jù),擴(kuò)展名,MIME類型 | 文件路徑 |
| uploadLocalExcel() | 上傳本地Excel文件 | 文件路徑,擴(kuò)展名 | 文件路徑 |
| downloadFile() | 下載文件到響應(yīng)流 | HTTP響應(yīng)對(duì)象,文件URL | 無 |
| parseGetUrl() | 生成帶簽名直鏈 | 文件路徑,有效期(分鐘) | 直鏈URL |
| deleteFile() | 刪除文件 | 文件URL | 無 |
| fileExists() | 檢查文件是否存在 | 文件URL | 布爾值 |
3.3 關(guān)鍵技術(shù)點(diǎn)
- 唯一文件名生成:日期目錄/UUID.擴(kuò)展名 格式避免重名
- 大文件流式傳輸:避免內(nèi)存溢出
- 響應(yīng)頭編碼處理:解決中文文件名亂碼問題
- 異常統(tǒng)一處理:Minio 異常轉(zhuǎn)換為運(yùn)行時(shí)異常
- 預(yù)簽名URL:生成臨時(shí)訪問鏈接
二、使用示例
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測試下三個(gè)接口
圖片上傳


圖片下載

刪除圖片


總結(jié)
本文通過 “配置 - 工具 - 業(yè)務(wù)” 三層架構(gòu),實(shí)現(xiàn)了 Spring Boot 與 MinIO 的集成,核心優(yōu)勢如下:
- 易用性:通過配置綁定和工具類封裝,簡化 MinIO 操作,開發(fā)者無需關(guān)注底層 API 細(xì)節(jié)。
- 靈活性:支持多種文件類型(表單文件、字節(jié)流、本地文件),滿足不同場景需求(如圖片壓縮、Excel 生成)。
- 可擴(kuò)展性:可基于此框架擴(kuò)展功能,如添加文件權(quán)限控制(通過 MinIO 的 Policy)、文件分片上傳(大文件處理)、定期清理過期文件等。
MinIO 作為輕量級(jí)對(duì)象存儲(chǔ)方案,非常適合中小項(xiàng)目替代本地存儲(chǔ)或云廠商 OSS(降低成本)。實(shí)際應(yīng)用中需注意:生產(chǎn)環(huán)境需配置 MinIO 集群確保高可用;敏感文件需通過預(yù)簽名 URL 控制訪問權(quán)限;定期備份桶數(shù)據(jù)以防丟失。通過本文的方案,開發(fā)者可快速搭建穩(wěn)定、可擴(kuò)展的文件存儲(chǔ)服務(wù),為應(yīng)用提供可靠的非結(jié)構(gòu)化數(shù)據(jù)管理能力。
到此這篇關(guān)于Spring Boot 整合 Minio 實(shí)現(xiàn)高效文件存儲(chǔ)解決方案(本地和線上)的文章就介紹到這了,更多相關(guān)Spring Boot Minio 文件存儲(chǔ)內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Dwr3.0純注解(純Java Code配置)配置與應(yīng)用淺析三之后端反向調(diào)用前端
Dwr是為人所熟知的前端框架,其異步推送功能是為人所津津樂道的,下來主要研究一下它的這個(gè)功能是怎么應(yīng)用的;2016-04-04
java入門概念個(gè)人理解之package與import淺析
下面小編就為大家?guī)硪黄猨ava入門概念個(gè)人理解之package與import淺析。小編覺得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2016-08-08
Jmeter的接口測試詳細(xì)步驟并實(shí)現(xiàn)業(yè)務(wù)閉環(huán)
這篇文章主要介紹了Jmeter的接口測試詳細(xì)步驟并實(shí)現(xiàn)業(yè)務(wù)閉環(huán),文章圍繞主題展開詳細(xì)的內(nèi)容介紹,具有一定的參考價(jià)值,需要的小伙伴可以參考一下2022-08-08
玩轉(zhuǎn)spring boot 結(jié)合AngularJs和JDBC(4)
玩轉(zhuǎn)spring boot,這篇文章主要介紹了結(jié)合AngularJs和JDBC,玩轉(zhuǎn)spring boot,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-01-01

