文件上传与下载是后端开发中高频且必备的核心功能,无论是用户头像、富文本编辑器中的图片、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. 1. 下载MinIO安装包:https://min.io/download#/windows(下载minio.exe)

  2. 2. 创建存储目录(如:D:/minio/data)

  3. 3. 打开命令提示符(CMD),进入minio.exe所在目录,执行启动命令:
    minio server D:/minio/data --console-address ":9001"

  4. 4. 启动成功后,访问控制台:http://localhost:9001,默认账号密码:minioadmin/minioadmin

  5. 5. 登录后,创建存储桶(Bucket),如:test-bucket(后续配置中需要用到)

(2)Linux部署(以CentOS为例)
  1. 1. 下载MinIO二进制文件:
    wget https://dl.min.io/server/minio/release/linux-amd64/minio

  2. 2. 赋予执行权限:
    chmod +x minio

  3. 3. 创建存储目录:
    mkdir -p /usr/local/minio/data

  4. 4. 启动MinIO(后台启动,指定控制台端口):
    nohup ./minio server /usr/local/minio/data --console-address ":9001" &

  5. 5. 开放端口(9000:API端口,9001:控制台端口):
    firewall-cmd --add-port=9000/tcp --permanent firewall-cmd --add-port=9001/tcp --permanent firewall-cmd --reload

  6. 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 分布式存储的解决方案和操作流程了,如果遇到什么问题,欢迎评论区留言交流!

Logo

更多推荐