SpringBoot 整合 MinIO 实现对象存储文件上传下载实战

举报
JiHua 发表于 2026/10/27 10:55:25 2026/10/27
【摘要】 传统服务器本地存储文件存在扩容难、备份麻烦、单点故障等问题,MinIO 是一款轻量、高性能的对象存储服务,兼容 S3 协议。本文基于 SpringBoot 整合 MinIO,封装文件上传、下载、预览、删除接口,包含桶策略配置、文件类型校验、文件名防重处理,附带完整可运行代码,适合图片、附件、视频等文件存储场景快速落地。

前言

在业务系统开发中,图片、合同附件、视频等资源存储是刚需。如果直接把文件存到应用服务器磁盘,集群部署时文件无法共享,扩容和迁移都很麻烦。MinIO 兼容 AWS S3 标准接口,部署简单、资源占用低,私有化部署也很方便,是中小型项目非常合适的对象存储方案。

本文搭建一套通用 MinIO 文件工具类,统一处理文件上传、临时访问链接生成、文件删除,同时梳理开发中高频踩坑点。

一、环境准备

  1. 启动 MinIO 服务(docker 快速启动参考)
docker run -d -p 9000:9000 -p 9001:9001 \
  --name minio \
  -e MINIO_ROOT_USER=minioadmin \
  -e MINIO_ROOT_PASSWORD=minioadmin123 \
  -v /data/minio:/data \
  minio/minio server /data --console-address ":9001"
  • 9000:API端口
  • 9001:可视化控制台
  1. Maven 依赖
<!-- MinIO SDK -->
<dependency>
    <groupId>io.minio</groupId>
    <artifactId>minio</artifactId>
    <version>8.5.7</version>
</dependency>
<!-- lombok -->
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <optional>true</optional>
</dependency>

二、yml 配置文件

minio:
  endpoint: [http://127.0.0.1:9000](http://127.0.0.1:9000)
  access-key: minioadmin
  secret-key: minioadmin123
  bucket-name: file-bucket
  # 分片大小 5MB
  part-size: 5242880

三、配置 MinIO Client

import io.minio.MinioClient;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class MinioConfig {

    @Value("${minio.endpoint}")
    private String endpoint;
    @Value("${minio.access-key}")
    private String accessKey;
    @Value("${minio.secret-key}")
    private String secretKey;

    @Bean
    public MinioClient minioClient() {
        return MinioClient.builder()
                .endpoint(endpoint)
                .credentials(accessKey, secretKey)
                .build();
    }
}

四、封装 MinIO 工具类(核心)

import io.minio.*;
import io.minio.http.Method;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;

import java.io.InputStream;
import java.util.UUID;
import java.util.concurrent.TimeUnit;

@Component
@RequiredArgsConstructor
public class MinioUtil {

    private final MinioClient minioClient;

    @Value("${minio.bucket-name}")
    private String bucketName;

    /**
     * 检查桶是否存在,不存在则创建
     */
    public void checkBucket() throws Exception {
        boolean exists = minioClient.bucketExists(BucketExistsArgs.builder().bucket(bucketName).build());
        if (!exists) {
            minioClient.makeBucket(MakeBucketArgs.builder().bucket(bucketName).build());
        }
    }

    /**
     * 文件上传
     * @param file 文件
     * @return 文件访问路径
     */
    public String upload(MultipartFile file) throws Exception {
        checkBucket();
        // 原始文件名
        String originalName = file.getOriginalFilename();
        String suffix = originalName.substring(originalName.lastIndexOf("."));
        // UUID重命名,防止文件名覆盖冲突
        String objectName = UUID.randomUUID() + suffix;

        try (InputStream is = file.getInputStream()) {
            minioClient.putObject(
                    PutObjectArgs.builder()
                            .bucket(bucketName)
                            .object(objectName)
                            .stream(is, file.getSize(), -1)
                            .contentType(file.getContentType())
                            .build()
            );
        }
        return objectName;
    }

    /**
     * 获取文件临时预览URL(有效期30分钟)
     */
    public String getPreviewUrl(String objectName) throws Exception {
        return minioClient.getPresignedObjectUrl(
                GetPresignedObjectUrlArgs.builder()
                        .bucket(bucketName)
                        .object(objectName)
                        .method(Method.GET)
                        .expiry(30, TimeUnit.MINUTES)
                        .build()
        );
    }

    /**
     * 删除文件
     */
    public void delete(String objectName) throws Exception {
        minioClient.removeObject(
                RemoveObjectArgs.builder()
                        .bucket(bucketName)
                        .object(objectName)
                        .build()
        );
    }
}

五、Controller 接口

import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;

@RestController
@RequestMapping("/file")
@RequiredArgsConstructor
public class FileController {

    private final MinioUtil minioUtil;

    @PostMapping("/upload")
    public String upload(@RequestParam("file") MultipartFile file) throws Exception {
        String objectName = minioUtil.upload(file);
        return minioUtil.getPreviewUrl(objectName);
    }

    @DeleteMapping("/delete")
    public String delete(@RequestParam("objectName") String objectName) throws Exception {
        minioUtil.delete(objectName);
        return "删除成功";
    }
}

六、业务扩展方案

  1. 文件分类目录:objectName 可以加上日期前缀,比如 2026/10/27/xxx.jpg,方便归档管理;
  2. 文件白名单校验:限制后缀 jpg/png/pdf,禁止上传可执行文件;
  3. 大文件分片上传:MinIO 原生支持分片,适合几十MB以上视频;
  4. 永久公开访问:修改桶策略为 public,不推荐业务使用,优先用带时效签名URL;
  5. 文件水印、图片压缩:上传成功后搭配 thumbnail 服务处理图片。

七、常见坑点

  1. 跨域问题:前端直传MinIO需要在控制台配置CORS;
  2. 时区、签名过期:服务器时间不准会导致签名链接直接失效;
  3. 内网部署:生成的url默认是minio endpoint内网地址,需要配置反向代理和域名;
  4. 桶名称规范:不能大写、不能带下划线,只能小写字母、数字、短横线;
  5. 资源释放:InputStream必须关闭,工具类用try-with-resources自动关闭。

八、总结

MinIO 上手简单,API统一,私有化部署可控性强,对比云厂商OSS,本地开发、测试环境非常友好。这套封装可以直接集成到后台管理系统,统一接管全部文件存储。如果后续要迁移到华为云OBS、阿里云OSS,只需要替换存储实现层,上层业务代码几乎不用改动,符合面向接口编程思想。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。