文章目录
- 前言
- 一、配置
-
- 1.配置文件:application.yml
- 2.配置类:MinioProperties
- 3.工具类:MinioUtil
-
- [3.1 初始化方法](#3.1 初始化方法)
- [3.2 核心功能](#3.2 核心功能)
- [3.3 关键技术点](#3.3 关键技术点)
- 二、使用示例
- 总结
前言
- Minio 是一个高性能的分布式对象存储系统,专为云原生应用而设计
- 作为 Amazon S3 的兼容替代品,它提供了简单易用的 API,支持海量非结构化数据存储
- 在微服务架构中,文件存储是常见需求,而 Minio 以其轻量级、高可用和易部署的特点成为理想选择
一、配置
1.配置文件:application.yml
yml
vehicle:
minio:
url: http://localhost:9000 # 连接地址,如果是线上的将:localhost->ip
username: minio # 登录用户名
password: 12345678 # 登录密码
bucketName: vehicle # 存储文件的桶的名字
- url:Minio 服务器地址,线上环境替换为实际 IP 或域
- username/password:Minio 控制台登录凭证
- bucketName:文件存储桶名称,类似文件夹概念
- HTTPS 注意:若配置域名访问,URL 需写为 https://your.domain.name:9090
2.配置类:MinioProperties
java
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
java
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 {
//创建客户端
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);
// 上传到 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);
}
}
/*
* 根据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
java
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
java
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
java
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
测试下三个接口
图片上传
图片下载
删除图片
总结
本文通过 "配置 - 工具 - 业务" 三层架构,实现了 Spring Boot 与 MinIO 的集成,核心优势如下:
- 易用性:通过配置绑定和工具类封装,简化 MinIO 操作,开发者无需关注底层 API 细节。
- 灵活性:支持多种文件类型(表单文件、字节流、本地文件),满足不同场景需求(如图片压缩、Excel 生成)。
- 可扩展性:可基于此框架扩展功能,如添加文件权限控制(通过 MinIO 的 Policy)、文件分片上传(大文件处理)、定期清理过期文件等。
MinIO 作为轻量级对象存储方案,非常适合中小项目替代本地存储或云厂商 OSS(降低成本)。实际应用中需注意:生产环境需配置 MinIO 集群确保高可用;敏感文件需通过预签名 URL 控制访问权限;定期备份桶数据以防丢失。通过本文的方案,开发者可快速搭建稳定、可扩展的文件存储服务,为应用提供可靠的非结构化数据管理能力。