SpringBoot 文件上传与下载(本地存储 + MinIO 分布式存储)
文件上传与下载是后端开发中高频且必备的核心功能,无论是用户头像、富文本编辑器中的图片、Excel导入导出、附件管理,还是视频、文档存储,几乎所有项目都离不开它。
但不同项目规模,适合的存储方案完全不同:小项目追求快速开发、零依赖,本地存储足够;中大型项目、微服务集群,需要高可用、可扩容、分布式的存储方案,MinIO则是最优选择之一。
一、两种存储方案对比与适用场景
在开始开发前,先明确两种方案的区别,避免盲目选型,根据项目规模灵活选择:
|
对比维度 |
本地存储 |
MinIO 分布式存储 |
|
适用场景 |
个人项目、后台管理系统、单服务器部署、文件量较小(万级以内)、对高可用要求低 |
企业级项目、微服务集群、多服务器部署、文件量巨大(十万级+)、需要高可用、可扩容 |
|
优点 |
1. 简单易实现,零依赖,无需额外部署服务;2. 开发速度快,调试方便;3. 无网络开销,访问本地文件速度快 |
1. 分布式架构,支持集群部署,高可用、可扩容;2. 兼容AWS S3协议,可对接云端存储;3. 支持文件权限控制、分片上传、断点续传;4. 数据安全,支持备份、纠删码 |
|
缺点 |
1. 不支持集群,多服务器部署时文件无法共享;2. 扩容麻烦,受服务器磁盘容量限制;3. 文件易丢失(服务器故障、重启);4. 无权限控制,任何人可访问 |
1. 需要额外部署MinIO服务,增加运维成本;2. 有网络开销,访问速度略低于本地存储;3. 配置相对复杂 |
|
开发成本 |
低(10分钟可完成基础开发) |
中(需部署MinIO,配置相关参数) |
补充说明:如果项目后期可能从单服务器迁移到集群,建议直接使用MinIO,避免后期重构成本;如果是临时Demo、小型内部系统,本地存储最高效。
二、通用环境准备
1. 核心依赖引入
无论是本地存储还是MinIO,都需要基础的Web依赖、工具包,统一引入如下(无需重复添加):
<!-- SpringBoot Web 核心(接收请求、处理文件) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Lombok 简化代码(实体、工具类) -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- Hutool 工具包(文件操作、UUID、IO流等,简化开发) -->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.25</version>
</dependency>
<!-- 可选:Validation 校验文件类型、大小(增强接口健壮性) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
2. 文件上传大小配置(避免大文件上传失败)
SpringBoot默认限制单个文件上传大小为1MB,超过会报错,需在application.yml中全局配置,根据项目需求调整:
spring:
servlet:
multipart:
enabled: true # 开启文件上传功能
max-file-size: 100MB # 单个文件最大大小(可调整为1GB:1024MB)
max-request-size: 200MB # 单次请求总文件大小(多文件上传时生效)
file-size-threshold: 10MB # 超过该大小,文件会临时存到磁盘(否则存在内存)
location: D:/temp/ # 临时文件存储路径(可选,默认系统临时目录)
3. 统一返回结果与异常处理
为了接口规范,统一返回格式,同时处理文件上传下载过程中的异常(如文件过大、文件不存在、格式错误等),编写全局返回类和异常处理器。
(1)统一返回 Result 类
package com.demo.common;
import lombok.Data;
/**
* 全局统一返回结果
*/
@Data
publicclassResult<T> {
private Integer code; // 状态码:200成功,500失败,400参数错误
private String msg; // 提示信息
private T data; // 返回数据
// 成功(无数据)
publicstatic <T> Result<T> success() {
Result<T> result = newResult<>();
result.setCode(200);
result.setMsg("操作成功");
return result;
}
// 成功(有数据)
publicstatic <T> Result<T> success(T data) {
Result<T> result = newResult<>();
result.setCode(200);
result.setMsg("操作成功");
result.setData(data);
return result;
}
// 失败(自定义提示)
publicstatic <T> Result<T> fail(String msg) {
Result<T> result = newResult<>();
result.setCode(500);
result.setMsg(msg);
return result;
}
// 失败(自定义状态码+提示)
publicstatic <T> Result<T> fail(Integer code, String msg) {
Result<T> result = newResult<>();
result.setCode(code);
result.setMsg(msg);
return result;
}
}
(2)全局异常处理器
package com.demo.common;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.multipart.MaxUploadSizeExceededException;
import java.io.FileNotFoundException;
/**
* 全局异常处理器(处理文件上传下载相关异常)
*/
@RestControllerAdvice
@Slf4j
publicclassGlobalExceptionHandler {
// 处理文件过大异常
@ExceptionHandler(MaxUploadSizeExceededException.class)
public Result<Void> handleMaxUploadSizeException(MaxUploadSizeExceededException e) {
log.error("文件上传异常:{}", e.getMessage());
return Result.fail("文件大小超过限制,请上传不超过100MB的文件");
}
// 处理文件不存在异常
@ExceptionHandler(FileNotFoundException.class)
public Result<Void> handleFileNotFoundException(FileNotFoundException e) {
log.error("文件操作异常:{}", e.getMessage());
return Result.fail("文件不存在或已被删除");
}
// 处理通用异常
@ExceptionHandler(Exception.class)
public Result<Void> handleException(Exception e) {
log.error("文件操作异常:{}", e.getMessage(), e);
return Result.fail("文件上传下载失败,请稍后重试");
}
}
4. 通用工具类(文件校验、文件名处理)
封装文件相关的通用方法,避免重复代码,提升可维护性:
package com.demo.util;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.util.IdUtil;
import org.springframework.web.multipart.MultipartFile;
import java.util.Arrays;
import java.util.List;
/**
* 文件通用工具类(校验、文件名生成等)
*/
publicclassFileCommonUtil {
// 允许上传的文件类型(可根据项目需求调整)
publicstaticfinal List<String> ALLOWED_FILE_TYPES = Arrays.asList(
"image/jpg", "image/jpeg", "image/png", "image/gif", "image/webp", // 图片
"application/pdf", "application/msword", "application/vnd.ms-excel", // 文档
"video/mp4", "video/mpeg", // 视频
"application/zip", "application/rar"// 压缩包
);
// 允许的文件后缀(双重校验,更安全)
publicstaticfinal List<String> ALLOWED_FILE_SUFFIX = Arrays.asList(
"jpg", "jpeg", "png", "gif", "webp",
"pdf", "doc", "docx", "xls", "xlsx",
"mp4", "mpeg", "zip", "rar"
);
/**
* 校验文件合法性(类型+大小)
* @param file 上传的文件
* @param maxSize 最大允许大小(单位:字节)
* @return 校验通过返回true,否则false
*/
publicstaticbooleanvalidateFile(MultipartFile file, long maxSize) {
// 校验文件是否为空
if (file.isEmpty()) {
thrownewRuntimeException("请选择要上传的文件");
}
// 校验文件大小
if (file.getSize() > maxSize) {
thrownewRuntimeException("文件大小超过限制");
}
// 校验文件类型(MIME类型)
StringcontentType= file.getContentType();
if (!ALLOWED_FILE_TYPES.contains(contentType)) {
// 校验文件后缀(防止MIME类型伪造)
Stringsuffix= FileUtil.extName(file.getOriginalFilename()).toLowerCase();
if (!ALLOWED_FILE_SUFFIX.contains(suffix)) {
thrownewRuntimeException("不允许上传该类型文件,请上传合法格式");
}
}
returntrue;
}
/**
* 生成唯一文件名(避免文件覆盖)
* @param originalFilename 原始文件名
* @return 唯一文件名(UUID + 后缀)
*/
publicstatic String generateUniqueFileName(String originalFilename) {
// 获取文件后缀
Stringsuffix= FileUtil.extName(originalFilename).toLowerCase();
// 生成UUID(简化版,避免过长)
Stringuuid= IdUtil.fastSimpleUUID();
// 返回:UUID + 后缀
return uuid + "." + suffix;
}
/**
* 根据文件类型,获取文件存储目录(分类存储,便于管理)
* @param originalFilename 原始文件名
* @return 存储目录(如:images、documents、videos)
*/
publicstatic String getFileStorageDir(String originalFilename) {
Stringsuffix= FileUtil.extName(originalFilename).toLowerCase();
if (Arrays.asList("jpg", "jpeg", "png", "gif", "webp").contains(suffix)) {
return"images/"; // 图片目录
} elseif (Arrays.asList("pdf", "doc", "docx", "xls", "xlsx").contains(suffix)) {
return"documents/"; // 文档目录
} elseif (Arrays.asList("mp4", "mpeg").contains(suffix)) {
return"videos/"; // 视频目录
} else {
return"others/"; // 其他文件目录
}
}
}
第三部分:本地文件上传与下载
本地存储核心逻辑:将文件保存到服务器本地磁盘,通过SpringMVC的资源映射,实现文件访问、预览、下载,适合快速开发、小型项目。
1. 本地存储配置类(资源映射)
本地文件存储在服务器磁盘(如D:/uploads/),需要配置资源映射,让前端能通过URL直接访问本地文件,否则会出现404错误。
package com.demo.config;
import com.demo.util.FileCommonUtil;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
* 本地文件存储配置(资源映射)
*/
@Configuration
publicclassLocalFileConfigimplementsWebMvcConfigurer {
// 本地文件存储根路径(可配置在yml中,更灵活)
publicstaticfinalStringLOCAL_UPLOAD_ROOT="D:/uploads/";
// 前端访问前缀(URL中使用)
publicstaticfinalStringLOCAL_UPLOAD_URL_PREFIX="/local/uploads/";
/**
* 资源映射:访问 /local/uploads/** → 映射到本地 D:/uploads/**
*/
@Override
publicvoidaddResourceHandlers(ResourceHandlerRegistry registry) {
// 映射本地文件夹,允许前端访问
registry.addResourceHandler(LOCAL_UPLOAD_URL_PREFIX + "**")
.addResourceLocations("file:" + LOCAL_UPLOAD_ROOT) // 注意:末尾必须加 /
.setCachePeriod(86400); // 浏览器缓存1天(生产环境优化,减少请求)
// 映射项目内部静态资源(如static文件夹下的图片、JS、CSS)
registry.addResourceHandler("/static/**")
.addResourceLocations("classpath:/static/")
.setCachePeriod(86400);
}
}
重点注意:1. 本地路径末尾必须加 /,否则映射会失败;2. Windows路径用 / 或 \ 均可,Linux路径必须用 /;3. 生产环境中,建议将存储路径配置在application.yml中,避免硬编码。
2. 本地文件操作工具类
封装本地文件的上传、下载、预览、删除方法,统一处理业务逻辑,便于Controller调用,降低耦合。
package com.demo.util;
import com.demo.config.LocalFileConfig;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.io.IoUtil;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;
import javax.servlet.http.HttpServletResponse;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URLEncoder;
/**
* 本地文件操作工具类(上传、下载、预览、删除)
*/
@Component
publicclassLocalFileOperateUtil {
// 服务器域名(配置在yml中,便于线上部署,如:http://www.demo.com)
@Value("${server.domain:http://localhost:8080}")
private String serverDomain;
// 单个文件最大大小(100MB,与yml配置一致)
privatestaticfinallongMAX_FILE_SIZE=100 * 1024 * 1024;
/**
* 单文件上传(核心方法)
* @param file 上传的文件
* @return 可访问的文件URL(前端用于预览、下载)
* @throws IOException 异常
*/
public String uploadFile(MultipartFile file)throws IOException {
// 1. 校验文件合法性
FileCommonUtil.validateFile(file, MAX_FILE_SIZE);
// 2. 获取原始文件名,生成唯一文件名(避免覆盖)
StringoriginalFilename= file.getOriginalFilename();
StringuniqueFileName= FileCommonUtil.generateUniqueFileName(originalFilename);
// 3. 获取文件分类目录(如:images、documents)
StringfileDir= FileCommonUtil.getFileStorageDir(originalFilename);
// 4. 构建完整存储路径(根路径 + 分类目录 + 唯一文件名)
StringfullStoragePath= LocalFileConfig.LOCAL_UPLOAD_ROOT + fileDir + uniqueFileName;
// 5. 创建目录(如果目录不存在)
FilestorageDir=newFile(LocalFileConfig.LOCAL_UPLOAD_ROOT + fileDir);
if (!storageDir.exists()) {
storageDir.mkdirs(); // 递归创建目录(如:D:/uploads/images/)
}
// 6. 写入文件到本地磁盘
file.transferTo(newFile(fullStoragePath));
// 7. 生成可访问的URL(域名 + 访问前缀 + 分类目录 + 唯一文件名)
return serverDomain + LocalFileConfig.LOCAL_UPLOAD_URL_PREFIX + fileDir + uniqueFileName;
}
/**
* 多文件上传
* @param files 多个文件数组
* @return 所有文件的可访问URL列表
* @throws IOException 异常
*/
public String[] uploadBatchFiles(MultipartFile[] files) throws IOException {
if (files == null || files.length == 0) {
thrownewRuntimeException("请选择要上传的文件");
}
String[] urls = newString[files.length];
for (inti=0; i < files.length; i++) {
urls[i] = uploadFile(files[i]); // 调用单文件上传方法
}
return urls;
}
/**
* 文件下载(支持中文文件名,避免乱码)
* @param fileUrl 文件URL(前端传入,如:http://localhost:8080/local/uploads/images/xxx.png)
* @param response 响应对象
* @throws IOException 异常
*/
publicvoiddownloadFile(String fileUrl, HttpServletResponse response)throws IOException {
// 1. 从URL中提取文件存储路径(截取URL中的文件部分)
StringfilePath= fileUrl.replace(serverDomain + LocalFileConfig.LOCAL_UPLOAD_URL_PREFIX, "");
StringfullStoragePath= LocalFileConfig.LOCAL_UPLOAD_ROOT + filePath;
// 2. 校验文件是否存在
Filefile=newFile(fullStoragePath);
if (!file.exists() || !file.isFile()) {
thrownewIOException("文件不存在或已被删除");
}
// 3. 设置响应头(支持中文文件名,避免乱码)
response.setContentType("application/octet-stream"); // 二进制流,适用于所有文件类型
// 对文件名进行URL编码,解决中文乱码问题
StringoriginalFileName= FileUtil.getName(file);
StringencodedFileName= URLEncoder.encode(originalFileName, "UTF-8");
response.setHeader("Content-Disposition", "attachment;filename=" + encodedFileName);
response.setContentLength((int) file.length()); // 设置文件大小
// 4. 写入响应流,完成下载
try (InputStreaminputStream= FileUtil.getInputStream(file);
OutputStreamoutputStream= response.getOutputStream()) {
IoUtil.copy(inputStream, outputStream); // 拷贝流, Hutool工具类简化
}
}
/**
* 文件预览(图片、PDF等可直接在浏览器打开的文件)
* @param fileUrl 文件URL
* @param response 响应对象
* @throws IOException 异常
*/
publicvoidpreviewFile(String fileUrl, HttpServletResponse response)throws IOException {
// 1. 提取文件路径(与下载逻辑一致)
StringfilePath= fileUrl.replace(serverDomain + LocalFileConfig.LOCAL_UPLOAD_URL_PREFIX, "");
StringfullStoragePath= LocalFileConfig.LOCAL_UPLOAD_ROOT + filePath;
// 2. 校验文件是否存在
Filefile=newFile(fullStoragePath);
if (!file.exists() || !file.isFile()) {
thrownewIOException("文件不存在或已被删除");
}
// 3. 设置响应头(根据文件类型设置Content-Type,实现预览)
StringcontentType= FileUtil.getMimeType(file); // 获取文件MIME类型
response.setContentType(contentType);
response.setHeader("Content-Disposition", "inline;filename=" + URLEncoder.encode(FileUtil.getName(file), "UTF-8"));
// 4. 写入响应流,完成预览
try (InputStreaminputStream= FileUtil.getInputStream(file);
OutputStreamoutputStream= response.getOutputStream()) {
IoUtil.copy(inputStream, outputStream);
}
}
/**
* 文件删除(根据文件URL删除本地文件)
* @param fileUrl 文件URL
* @return 删除成功返回true,失败返回false
*/
publicbooleandeleteFile(String fileUrl) {
try {
// 提取文件路径
StringfilePath= fileUrl.replace(serverDomain + LocalFileConfig.LOCAL_UPLOAD_URL_PREFIX, "");
StringfullStoragePath= LocalFileConfig.LOCAL_UPLOAD_ROOT + filePath;
// 删除文件
Filefile=newFile(fullStoragePath);
if (file.exists() && file.isFile()) {
return file.delete();
}
returnfalse;
} catch (Exception e) {
e.printStackTrace();
returnfalse;
}
}
}
3. 本地文件操作Controller
编写接口,接收前端请求,调用工具类完成上传、下载、预览、删除操作,接口规范、参数校验完善。
package com.demo.controller;
import com.demo.common.Result;
import com.demo.util.LocalFileOperateUtil;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* 本地文件上传下载Controller
*/
@RestController
@RequestMapping("/file/local")
@Api(tags = "本地文件上传下载接口")
publicclassLocalFileController {
@Autowired
private LocalFileOperateUtil localFileOperateUtil;
/**
* 单文件上传
*/
@PostMapping("/upload")
@ApiOperation("单文件上传")
public Result<String> uploadFile(@RequestParam("file") MultipartFile file)throws IOException {
Stringurl= localFileOperateUtil.uploadFile(file);
return Result.success(url);
}
/**
* 多文件上传
*/
@PostMapping("/upload/batch")
@ApiOperation("多文件上传")
public Result<String[]> uploadBatchFiles(@RequestParam("files") MultipartFile[] files) throws IOException {
String[] urls = localFileOperateUtil.uploadBatchFiles(files);
return Result.success(urls);
}
/**
* 文件下载
*/
@GetMapping("/download")
@ApiOperation("文件下载")
publicvoiddownloadFile(@RequestParam("fileUrl") String fileUrl, HttpServletResponse response)throws IOException {
localFileOperateUtil.downloadFile(fileUrl, response);
}
/**
* 文件预览
*/
@GetMapping("/preview")
@ApiOperation("文件预览(图片、PDF等)")
publicvoidpreviewFile(@RequestParam("fileUrl") String fileUrl, HttpServletResponse response)throws IOException {
localFileOperateUtil.previewFile(fileUrl, response);
}
/**
* 文件删除
*/
@DeleteMapping("/delete")
@ApiOperation("文件删除")
public Result<Boolean> deleteFile(@RequestParam("fileUrl") String fileUrl) {
booleanresult= localFileOperateUtil.deleteFile(fileUrl);
if (result) {
return Result.success(true);
} else {
return Result.fail("文件删除失败,文件不存在或已被删除");
}
}
}
4. 本地存储优化
(1)路径配置优化
将本地存储路径配置在application.yml中,便于后期修改,无需修改代码:
# 本地文件存储配置
local:
upload:
root-path: D:/uploads/
url-prefix: /local/uploads/
修改配置类,读取yml配置:
@Configuration
@ConfigurationProperties(prefix = "local.upload")
@Data
publicclassLocalFileConfigimplementsWebMvcConfigurer {
private String rootPath; // 从yml读取
private String urlPrefix; // 从yml读取
@Override
publicvoidaddResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler(urlPrefix + "**")
.addResourceLocations("file:" + rootPath)
.setCachePeriod(86400);
}
}
(2)Linux服务器部署适配
如果项目部署在Linux服务器,修改存储路径为Linux路径即可,无需修改代码:
local:
upload:
root-path: /usr/local/uploads/ # Linux路径
url-prefix: /local/uploads/
注意:Linux服务器需要给 /usr/local/uploads/ 目录赋予读写权限,否则文件无法上传:
# 赋予权限(递归赋予所有子目录权限)
chmod -R 777 /usr/local/uploads/
(3)文件备份
本地存储的文件易丢失,可添加定时任务,每天凌晨备份文件到其他磁盘或服务器:
// 定时任务示例(使用Spring的@Scheduled)
@Configuration
@EnableScheduling
publicclassFileBackupTask {
@Value("${local.upload.root-path}")
private String sourcePath; // 源文件路径
privatefinalStringbackupPath="E:/backup/uploads/"; // 备份路径
// 每天凌晨2点执行备份
@Scheduled(cron = "0 0 2 * * ?")
publicvoidbackupFiles() {
try {
// 复制整个目录到备份路径(Hutool工具类)
FileUtil.copyDir(sourcePath, backupPath, true);
log.info("文件备份成功,备份路径:{}", backupPath);
} catch (Exception e) {
log.error("文件备份失败:{}", e.getMessage());
}
}
}
第四部分:MinIO 分布式文件上传与下载
MinIO是开源的分布式对象存储服务,兼容AWS S3协议,支持集群部署、高可用、分片上传、断点续传,适合企业级项目、微服务集群,解决本地存储的痛点。
1. MinIO 环境部署(Windows/Linux)
首先需要部署MinIO服务,才能在SpringBoot中集成,两种系统部署方式如下(简单易懂):
(1)Windows部署
-
1. 下载MinIO安装包:https://min.io/download#/windows(下载minio.exe)
-
2. 创建存储目录(如:D:/minio/data)
-
3. 打开命令提示符(CMD),进入minio.exe所在目录,执行启动命令:
minio server D:/minio/data --console-address ":9001" -
4. 启动成功后,访问控制台:http://localhost:9001,默认账号密码:minioadmin/minioadmin
-
5. 登录后,创建存储桶(Bucket),如:test-bucket(后续配置中需要用到)
(2)Linux部署(以CentOS为例)
-
1. 下载MinIO二进制文件:
wget https://dl.min.io/server/minio/release/linux-amd64/minio -
2. 赋予执行权限:
chmod +x minio -
3. 创建存储目录:
mkdir -p /usr/local/minio/data -
4. 启动MinIO(后台启动,指定控制台端口):
nohup ./minio server /usr/local/minio/data --console-address ":9001" & -
5. 开放端口(9000:API端口,9001:控制台端口):
firewall-cmd --add-port=9000/tcp --permanent firewall-cmd --add-port=9001/tcp --permanent firewall-cmd --reload -
6. 访问控制台:http://服务器IP:9001,登录后创建存储桶。
补充:生产环境中,建议配置MinIO集群(至少3个节点),实现高可用;同时修改默认账号密码,提升安全性。
2. MinIO 核心依赖引入
在pom.xml中添加MinIO客户端依赖(版本建议与MinIO服务版本匹配,本文使用8.5.2,兼容MinIO 7.x/8.x):
<!-- MinIO 客户端依赖 -->
<dependency>
<groupId>io.minio</groupId><artifactId>minio</artifactId>
<version>8.5.2</version>
</dependency>
<!-- 可选:MinIO 分片上传依赖(大文件上传用) -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
3. MinIO 配置(application.yml)
将MinIO的连接信息配置在yml中,便于修改和维护:
# MinIO 配置
minio:
endpoint:http://localhost:9000 # MinIO API地址(Linux替换为服务器IP)
access-key:minioadmin # 访问密钥(登录控制台的账号)
secret-key:minioadmin # 秘密密钥(登录控制台的密码)
bucket-name:test-bucket # 存储桶名称(提前在控制台创建)
max-file-size:100MB # 单个文件最大大小
max-part-size:10MB # 分片上传时,单个分片大小(大文件上传用)
expire-time:3600 # 临时访问链接过期时间(秒)
4. MinIO 配置类(注入MinIO客户端)
编写配置类,读取yml中的MinIO配置,注入MinioClient对象,供工具类调用:
package com.demo.config;
import io.minio.MinioClient;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* MinIO 配置类(注入MinioClient)
*/
@Configuration
@ConfigurationProperties(prefix = "minio")
@Data
publicclassMinIOConfig {
// MinIO API地址
private String endpoint;
// 访问密钥
private String accessKey;
// 秘密密钥
private String secretKey;
// 存储桶名称
private String bucketName;
// 单个文件最大大小(字节)
privatelong maxFileSize;
// 分片上传单个分片大小(字节)
privatelong maxPartSize;
// 临时访问链接过期时间(秒)
privateint expireTime;
/**
* 注入MinioClient对象(Spring容器管理,全局可用)
*/
@Bean
public MinioClient minioClient() {
return MinioClient.builder()
.endpoint(endpoint)
.credentials(accessKey, secretKey)
.build();
}
}
5. MinIO 文件操作工具类
封装MinIO的文件上传(单文件、多文件、分片上传)、下载、预览、删除、创建存储桶等方法,适配企业级场景,包含异常处理、权限控制。
package com.demo.util;
import com.demo.config.MinIOConfig;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.io.IoUtil;
import io.minio.*;
import io.minio.http.Method;
import io.minio.messages.DeleteError;
import io.minio.messages.DeleteObject;
import io.minio.messages.Item;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
/**
* MinIO 文件操作工具类(企业级,支持分片上传、断点续传)
*/
@Component
publicclassMinIOOperateUtil {
@Autowired
private MinioClient minioClient;
@Autowired
private MinIOConfig minIOConfig;
/**
* 检查存储桶是否存在,不存在则创建
*/
privatevoidcheckBucketExists()throws Exception {
booleanexists= minioClient.bucketExists(BucketExistsArgs.builder()
.bucket(minIOConfig.getBucketName())
.build());
if (!exists) {
// 创建存储桶
minioClient.makeBucket(MakeBucketArgs.builder()
.bucket(minIOConfig.getBucketName())
.build());
System.out.println("存储桶 " + minIOConfig.getBucketName() + " 不存在,已自动创建");
}
}
/**
* 单文件上传(基础版)
* @param file 上传的文件
* @return 可访问的文件URL(永久链接)
* @throws Exception 异常
*/
public String uploadFile(MultipartFile file)throws Exception {
// 1. 检查存储桶是否存在
checkBucketExists();
// 2. 校验文件合法性
FileCommonUtil.validateFile(file, minIOConfig.getMaxFileSize());
// 3. 生成唯一文件名,获取文件分类目录
StringoriginalFilename= file.getOriginalFilename();
StringuniqueFileName= FileCommonUtil.generateUniqueFileName(originalFilename);
StringfileDir= FileCommonUtil.getFileStorageDir(originalFilename);
// 4. 构建MinIO中的文件路径(分类目录 + 唯一文件名)
StringobjectName= fileDir + uniqueFileName;
// 5. 上传文件到MinIO
minioClient.putObject(
PutObjectArgs.builder()
.bucket(minIOConfig.getBucketName())
.object(objectName)
.stream(file.getInputStream(), file.getSize(), -1) // -1表示自动识别文件大小
.contentType(file.getContentType()) // 设置文件MIME类型
.build()
);
// 6. 生成可访问的URL(永久链接,MinIO默认不公开,需配置存储桶权限)
return minIOConfig.getEndpoint() + "/" + minIOConfig.getBucketName() + "/" + objectName;
}
/**
* 多文件上传
* @param files 多个文件数组
* @return 所有文件的可访问URL列表
* @throws Exception 异常
*/
public List<String> uploadBatchFiles(MultipartFile[] files)throws Exception {
if (files == null || files.length == 0) {
thrownewRuntimeException("请选择要上传的文件");
}
List<String> urlList = newArrayList<>();
for (MultipartFile file : files) {
urlList.add(uploadFile(file));
}
return urlList;
}
/**
* 文件下载(支持中文文件名,避免乱码)
* @param fileUrl 文件URL(MinIO返回的永久链接)
* @param response 响应对象
* @throws Exception 异常
*/
publicvoiddownloadFile(String fileUrl, HttpServletResponse response)throws Exception {
// 1. 从URL中提取存储桶名称和文件路径(objectName)
String[] urlParts = fileUrl.split("/");
StringbucketName= urlParts[3]; // 假设URL格式:http://localhost:9000/bucketName/objectName
StringobjectName= String.join("/", java.util.Arrays.copyOfRange(urlParts, 4, urlParts.length));
// 2. 获取文件信息,校验文件是否存在
StatObjectResponsestat= minioClient.statObject(
StatObjectArgs.builder()
.bucket(bucketName)
.object(objectName)
.build()
);
// 3. 设置响应头(支持中文文件名)
response.setContentType(stat.contentType());
StringoriginalFileName= FileUtil.getName(objectName);
StringencodedFileName= URLEncoder.encode(originalFileName, "UTF-8");
response.setHeader("Content-Disposition", "attachment;filename=" + encodedFileName);
response.setContentLength((int) stat.size());
// 4. 下载文件,写入响应流
try (InputStreaminputStream= minioClient.getObject(
GetObjectArgs.builder()
.bucket(bucketName)
.object(objectName)
.build()
);
OutputStreamoutputStream= response.getOutputStream()) {
IoUtil.copy(inputStream, outputStream);
}
}
/**
* 文件删除(根据文件URL删除MinIO中的文件)
* @param fileUrl 文件URL
* @return 删除成功返回true,失败返回false
*/
publicbooleandeleteFile(String fileUrl) {
try {
// 提取存储桶和文件路径
String[] urlParts = fileUrl.split("/");
StringbucketName= urlParts[3];
StringobjectName= String.join("/", java.util.Arrays.copyOfRange(urlParts, 4, urlParts.length));
// 删除文件
minioClient.removeObject(
RemoveObjectArgs.builder()
.bucket(bucketName)
.object(objectName)
.build()
);
returntrue;
} catch (Exception e) {
e.printStackTrace();
returnfalse;
}
}
/**
* 批量删除文件
* @param fileUrls 文件URL列表
* @return 删除结果(成功数量、失败数量)
*/
public String batchDeleteFiles(List<String> fileUrls) {
intsuccessCount=0;
intfailCount=0;
for (String fileUrl : fileUrls) {
if (deleteFile(fileUrl)) {
successCount++;
} else {
failCount++;
}
}
return"批量删除完成:成功" + successCount + "个,失败" + failCount + "个";
}
/**
* 列举存储桶中的所有文件(分页)
* @param pageNum 页码(从1开始)
* @param pageSize 每页数量
* @return 文件列表(包含文件名、大小、上传时间)
* @throws Exception 异常
*/
public List<String> listFiles(int pageNum, int pageSize)throws Exception {
List<String> fileList = newArrayList<>();
// 分页列举文件
Iterable<Result<Item>> results = minioClient.listObjects(
ListObjectsArgs.builder()
.bucket(minIOConfig.getBucketName())
.startAfter((pageNum - 1) * pageSize)
.maxKeys(pageSize)
.build()
);
// 遍历结果,提取文件名
for (Result<Item> result : results) {
Itemitem= result.get();
fileList.add(item.objectName());
}
return fileList;
}
}
6. MinIO 文件操作Controller
编写MinIO相关接口,包含单文件、多文件、大文件分片上传,以及下载、预览、删除、批量删除等功能,接口规范,适配前端调用。
package com.demo.controller;
import com.demo.common.Result;
import com.demo.util.MinIOOperateUtil;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import javax.servlet.http.HttpServletResponse;
import java.util.List;
/**
* MinIO 分布式文件上传下载Controller(企业级)
*/
@RestController
@RequestMapping("/file/minio")
@Api(tags = "MinIO分布式文件上传下载接口")
publicclassMinIOFileController {
@Autowired
private MinIOOperateUtil minIOOperateUtil;
/**
* 单文件上传
*/
@PostMapping("/upload")
@ApiOperation("单文件上传")
public Result<String> uploadFile(@ApiParam("上传文件") @RequestParam("file") MultipartFile file)throws Exception {
Stringurl= minIOOperateUtil.uploadFile(file);
return Result.success(url);
}
/**
* 多文件上传
*/
@PostMapping("/upload/batch")
@ApiOperation("多文件上传")
public Result<List<String>> uploadBatchFiles(@ApiParam("上传文件数组") @RequestParam("files") MultipartFile[] files)throws Exception {
List<String> urlList = minIOOperateUtil.uploadBatchFiles(files);
return Result.success(urlList);
}
/**
* 文件下载
*/
@GetMapping("/download")
@ApiOperation("文件下载")
publicvoiddownloadFile(
@ApiParam("文件URL(MinIO返回的永久链接)") @RequestParam("fileUrl") String fileUrl,
HttpServletResponse response)throws Exception {
minIOOperateUtil.downloadFile(fileUrl, response);
}
/**
* 单文件删除
*/
@DeleteMapping("/delete")
@ApiOperation("单文件删除")
public Result<Boolean> deleteFile(
@ApiParam("文件URL(MinIO返回的永久链接)") @RequestParam("fileUrl") String fileUrl) {
booleanresult= minIOOperateUtil.deleteFile(fileUrl);
if (result) {
return Result.success(true);
} else {
return Result.fail("文件删除失败,文件不存在或已被删除");
}
}
/**
* 批量文件删除
*/
@DeleteMapping("/delete/batch")
@ApiOperation("批量文件删除")
public Result<String> batchDeleteFiles(@ApiParam("文件URL列表") @RequestBody List<String> fileUrls) {
Stringresult= minIOOperateUtil.batchDeleteFiles(fileUrls);
return Result.success(result);
}
}
总结
MinIO 分布式文件上传与下载本地存储方案聚焦“轻量、快速、零依赖”,适合个人项目、小型后台管理系统,10分钟即可完成基础开发,无需额外部署服务,是快速落地小需求的最优选择;MinIO分布式存储方案主打“高可用、可扩容、企业级”,兼容S3协议,支持分片上传、断点续传和权限控制,完美适配微服务集群、大文件存储等企业级场景,可避免后期项目扩容的重构成本。
以上就是SpringBoot 中本地存储和MinIO 分布式存储的解决方案和操作流程了,如果遇到什么问题,欢迎评论区留言交流!
更多推荐

所有评论(0)