SpringBoot教程(三十三)| SpringBoot集成MinIO(文件服务器)

一、MinIO 是什么?

MinIO 采用 Go 语言开发,是一款高性能、兼容 S3 协议的分布式对象存储系统,常被用作文件服务器,专门存储图片、视频、文档等 “非结构化数据”(没法存进数据库的文件类数据)。

二、MinIO 的核心定位:不止是 “文件服务器”

  • 本质是 对象存储:和传统的 FTP 服务器、本地磁盘存储不同,MinIO 以 “对象” 为单位存储文件(每个对象包含文件数据 + 元数据,如文件名、大小、创建时间),而非按文件夹层级的文件系统。
  • 天生适配文件服务器场景:因为支持海量文件存储、高并发上传下载,且能分布式部署,所以常被用来替代传统文件服务器,承接 Web 应用的文件存储需求。

三、MinIO 的核心特性(为什么适合当文件服务器?)

  • 轻量易部署:单节点部署只需一个 JAR 包或二进制文件,无需复杂依赖;分布式部署也只需多台服务器配置集群,支持动态扩容。
  • 兼容 S3 协议:这是核心优势!AWS S3 是对象存储的行业标准,MinIO 完全兼容其 API,意味着你写的代码能轻松迁移到 S3,或同时对接多个兼容 S3 的存储服务。
  • 高性能:专门优化了大文件、高并发场景,支持 GB/TB 级文件快速上传下载,读写速度远超传统文件服务器。
  • 高可用:分布式模式下支持纠删码(Erasure Code),比如 10 台服务器组成集群,哪怕坏了 3 台,数据也不会丢,还能正常访问(类似 RAID 但更灵活)。
  • 支持分片与合并:虽然没有内置分片上传 API,但提供了 composeObject 接口,能轻松实现分片合并,完美适配大文件上传 + 断点续传场景。

四、MinIO 适用场景界定

  1. 私有化部署场景:企业对数据私密性要求较高(如合同文档、内部研发资料),需将数据存储在自有服务器集群,MinIO 可通过分布式部署实现数据本地化管控。

  2. 业务数据存储场景:电商平台商品图片、社交 APP 用户头像、教育平台课程视频等非结构化数据存储,MinIO 支持高并发读写,适配业务流量波动。

  3. 开发测试场景:开发者需快速搭建轻量化存储服务用于功能测试,MinIO 单机部署仅需 1 个可执行文件,5 分钟内即可完成环境搭建。

  4. 数据备份场景:企业核心业务数据需异地备份,MinIO 支持跨节点数据同步与多副本存储,保障数据冗余安全。

五、MinIO 与主流存储方案的对比分析

  • MinIO的独特优势:在分布式存储领域中,是唯一能同时满足“轻量部署、S3兼容、低硬件成本、云原生适配”的方案,尤其在云原生大文件高并发场景中,无其他方案可替代。
  • 其他方案核心壁垒:本地存储胜在“零成本零门槛”,NAS胜在“跨终端协作便捷性”,公有云对象存储胜在“零运维与全球分发”,分布式文件系统胜在“传统文件接口与分布式能力的结合”。
方案类型突出差异化特点(独有的/显著优于其他方案)适用场景
MinIO(分布式对象存储)1. 轻量极致:单机部署仅需单个二进制文件,集群部署无复杂依赖;
2. S3协议完美兼容:无缝对接AWS生态,迁移成本趋近于零;
3. 纠删码轻量化实现:同等冗余级别下硬件成本低30%+;
4. 云原生无缝适配:Docker/K8s一键部署,支持动态扩缩容
1. 云原生/容器化环境下的文件存储(唯一轻量分布式选择);
2. 需私有部署且追求低硬件成本的分布式存储场景;
3. 需对接S3生态或未来可能迁移至AWS的业务;
4. 大文件(GB/TB级)高并发上传下载场景(性能碾压其他方案)
本地存储(DAS架构)1. 零门槛部署:无需额外配置,直接复用服务器本地硬盘;
2. 无网络开销:文件读写延迟极低,仅受硬盘IO限制;
3. 成本趋近于零:无需额外采购硬件/软件
1. 开发/测试环境临时文件存储(最快上手无成本);
2. 单机版应用本地数据存储(如小型工具类软件);
3. 非核心业务的日志、缓存等临时数据存储
NAS(网络附加存储)1. 开箱即用:专用硬件+预制系统,通电配置即可提供文件共享;
2. 多终端跨系统兼容:完美支持Windows/Mac/Linux/移动端,共享无壁垒;
3. 一体化文件管理:内置权限管控、版本快照、增量备份,无需额外工具
1. 企业内部团队办公文件协作(如文档、设计稿,协作体验最优);
2. 家庭/小型办公多媒体集中存储(照片、视频、音乐);
3. 中小型企业固定容量的文件归档与共享(无需专业运维)
公有云对象存储(如华为云OBS、AWS S3)1. 零运维负担:硬件维护、容灾备份、安全防护全由厂商负责;
2. 无限弹性扩容:存储容量随用量自动增长,无需规划硬件;
3. 增值服务丰富:自带CDN加速、内容审核、跨区域复制
1. 无专业IT运维团队的中小企业(彻底免除运维成本);
2. 流量波动大(如电商大促、活动推广)的文件存储;
3. 需全球访问的内容分发场景(如跨境APP资源存储,全球节点覆盖最优)
分布式文件系统(如Ceph FS、GlusterFS)1. 统一存储池:整合多品牌、多规格服务器存储资源,单一访问入口;
2. POSIX接口兼容:保留传统文件系统操作习惯,无需修改现有应用代码;
3. 极致扩展性:性能与容量随节点线性提升,支持PB级存储
1. 大型企业数据中心统一存储平台(多资源整合能力最优);
2. 大数据分析/AI训练场景(需标准文件系统接口访问海量数据);
3. 需保留复杂目录结构的分布式存储场景(唯一兼顾分布式与目录结构的方案)

六、Windows 环境下 MinIO 部署与配置

1. 下载 MinIO

硬件要求:Windows 7 及以上 64 位系统,建议 CPU 2 核及以上、内存 4GB 及以上,存储盘预留至少 10GB 可用空间(用于存储服务数据)。

软件下载:(推荐方式二)

  • 方式一:访问 MinIO 官方下载页(https://min.io/download)(太慢了),
    下载核心服务端程序 minio.exe;若需命令行管理工具,可同步下载客户端 mc.exe(可选)。
    在这里插入图片描述
  • 方式二:访问 MinIO 国内镜像地址(https://www.minio.org.cn/)(速度飞起)
    下载核心服务端程序 minio.exe;若需命令行管理工具,可同步下载客户端 mc.exe(可选)。
    在这里插入图片描述

2. 启动 MinIO

(1)规划目录

首先在本地磁盘(建议非系统盘,如 D 盘)创建标准化目录结构,用于统一管理程序与数据:
以下是我存放的位置(你们可以根据情况自行更改)

  • 主目录:D:\Develop\Env\MinIO(存放部署相关文件)
  • 程序目录:D:\Develop\Env\MinIO\bin(存放 minio.exemc.exe 等可执行文件)
  • 数据目录:D:\Develop\Env\MinIO\data(用于存储 MinIO 管理的对象数据,避免临时目录导致数据丢失)
  • 日志目录:D:\Develop\Env\MinIO\logs(用于存储服务启动日志,便于问题排查)

在这里插入图片描述

在这里插入图片描述
(2)创建启动脚本
创建一个txt文件,然后复制下面的命令(脚本中的位置根据你的存放的位置自行修改),在改成bat后缀

@echo off
:: 切换编码为 UTF-8,避免中文乱码
chcp 65001 > nul
echo 正在启动MinIO服务...
:: 切换至程序目录
cd D:\Develop\Env\MinIO\bin
:: 启动服务,指定数据目录、控制台端口
minio.exe server D:\Develop\Env\MinIO\data --console-address ":9001"
pause

然后放到 和 bin 目录同级别

在这里插入图片描述

(3)双击启动

在这里插入图片描述
访问 http://127.0.0.1:9001,用户名和密码默认都为 minioadmin
在这里插入图片描述
在这里插入图片描述

3. 汉化操作

目前没有汉化配置。有的话也是和源码相关的(忒麻烦),所以最快的方式就是浏览器翻译
在这里插入图片描述
在这里插入图片描述

4. MinIO 核心概念解析

  1. 桶(Bucket):MinIO 的基本存储单元,用于对对象进行分类管理,类似文件系统中的 “文件夹”,但无层级嵌套限制(可通过对象名中的 “/” 模拟层级)。
    命名规则:3-63 个字符,仅支持小写字母、数字、横线,且不能以横线开头或结尾。

  2. 对象(Object):MinIO 中存储的核心数据单元,对应实际的文件(如图片、文档),由 “文件内容 + 元数据” 组成。
    元数据包含文件大小、类型、创建时间等基础信息,支持自定义扩展(如给文件添加业务标签)。

  3. API 服务端口:默认 9000 端口,用于客户端(如 Spring Boot 应用)通过 SDK 集成 MinIO,实现文件上传、下载等自动化操作。

  4. Web 控制台端口:默认 9001 端口,提供可视化管理界面,支持手动创建桶、上传文件、配置权限等操作。

5. MinIO 基础操作指南(Web 控制台)

(1)创建存储桶

登录 Web 控制台后,点击左侧菜单栏 “Create Bucket”。输入桶名称(如 test-123

在这里插入图片描述
在这里插入图片描述

你可以看看data下面,就出现了你创建的那个桶了

在这里插入图片描述

(2)对象操作—桶下面创建文件夹

前提是你创建文件夹以后,还得再到这个新文件夹里面上传一个资源才行,不然就属于无效

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

(3)对象操作—桶下面创建文件

在这里插入图片描述

你可以看看data下面,就出现了《API命名规范.txt》的这个文件夹

在这里插入图片描述

和windows的不一样,windows 就直接是文件了,而它是以文件夹方式的,里面还有一层,这个 xl.meta 才是《API命名规范.txt》的真正的数据

在这里插入图片描述

七、SpringBoot集成MinIO

1. 引入maven

我这边是jdk1.8 建议选择 8.5.6

<!-- MinIO 官方 Java SDK -->
<dependency>
    <groupId>io.minio</groupId>
    <artifactId>minio</artifactId>
    <version>8.5.6</version> <!-- JDK 1.8 推荐版本 -->
</dependency>

<!-- 用于文件流处理的工具类(可选) -->
<dependency>
    <groupId>commons-io</groupId>
    <artifactId>commons-io</artifactId>
    <version>2.11.0</version>
</dependency>

2. 配置 MinIO 连接信息

在 application.yml 中配置 MinIO 连接参数:

spring:
  minio:
    endpoint: http://localhost:9000  # MinIO API 地址
    access-key: minioadmin          # 管理员账号
    secret-key: minioadmin          # 管理员密码
    bucket-name: test-bucket        # 默认操作的桶(提前创建)
    base-url: http://localhost:9000/test-bucket/  # 公开桶的文件访问基础路径

3. 创建 MinIO 客户端配置类

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 {

    // 从配置文件读取 MinIO 连接参数
    @Value("${spring.minio.endpoint}")
    private String endpoint;
    @Value("${spring.minio.access-key}")
    private String accessKey;
    @Value("${spring.minio.secret-key}")
    private String secretKey;

    /**
     * 初始化 MinIO 客户端,交给 Spring 容器管理
     * @return MinioClient 实例
     */
    @Bean
    public MinioClient minioClient() {
        return MinioClient.builder()
                .endpoint(endpoint)  // 设置 API 地址
                .credentials(accessKey, secretKey)  // 设置账号密码
                .build();
    }
}

4. 封装 MinIO 工具类

import io.minio.*;
import io.minio.http.Method;
import io.minio.messages.Bucket;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.List;
import java.util.concurrent.TimeUnit;

@Component
public class MinioUtil {

    @Autowired
    private MinioClient minioClient;

    @Value("${spring.minio.bucket-name}")
    private String defaultBucket;  // 默认桶名
    @Value("${spring.minio.base-url}")
    private String baseUrl;        // 公开桶的文件访问基础路径

    /**
     * 创建桶(如果桶不存在)
     * @param bucketName 桶名
     * @throws Exception 异常
     */
    public void createBucket(String bucketName) throws Exception {
        // 判断桶是否存在
        if (!minioClient.bucketExists(BucketExistsArgs.builder().bucket(bucketName).build())) {
            // 不存在则创建桶
            minioClient.makeBucket(MakeBucketArgs.builder().bucket(bucketName).build());
        }
    }

    /**
     * 上传文件(支持 MultipartFile 类型,Spring 接收前端文件的标准类型)
     * @param file 前端上传的文件
     * @param bucketName 桶名(可为空,使用默认桶)
     * @return 文件访问路径
     * @throws Exception 异常
     */
    public String uploadFile(MultipartFile file, String bucketName) throws Exception {
        // 若未指定桶名,使用默认桶
        if (bucketName == null || bucketName.isEmpty()) {
            bucketName = defaultBucket;
        }
        // 确保桶存在
        createBucket(bucketName);

        // 生成唯一文件名(前缀+时间戳+原文件名,避免重复)
        String originalFilename = file.getOriginalFilename();
        String fileName = "upload/" + System.currentTimeMillis() + "_" + originalFilename;

        // 上传文件到 MinIO
        minioClient.putObject(
                PutObjectArgs.builder()
                        .bucket(bucketName)  // 桶名
                        .object(fileName)   // 存储在 MinIO 中的文件名(可包含路径,如 "img/123.jpg")
                        // 文件流(MultipartFile 的输入流)
                        .stream(file.getInputStream(), file.getSize(), -1)
                        .contentType(file.getContentType())  // 文件类型(如 image/jpeg)
                        .build()
        );

        // 返回文件访问路径(公开桶可直接访问)
        return baseUrl + fileName;
    }

    /**
     * 下载文件(返回文件输入流,用于前端下载)
     * @param fileName 文件名(与上传时的 fileName 一致)
     * @param bucketName 桶名(可为空,使用默认桶)
     * @return 文件输入流
     * @throws Exception 异常
     */
    public InputStream downloadFile(String fileName, String bucketName) throws Exception {
        if (bucketName == null || bucketName.isEmpty()) {
            bucketName = defaultBucket;
        }
        // 获取文件输入流
        return minioClient.getObject(
                GetObjectArgs.builder()
                        .bucket(bucketName)
                        .object(fileName)
                        .build()
        );
    }

    /**
     * 删除文件
     * @param fileName 文件名
     * @param bucketName 桶名(可为空,使用默认桶)
     * @throws Exception 异常
     */
    public void deleteFile(String fileName, String bucketName) throws Exception {
        if (bucketName == null || bucketName.isEmpty()) {
            bucketName = defaultBucket;
        }
        // 删除文件
        minioClient.removeObject(
                RemoveObjectArgs.builder()
                        .bucket(bucketName)
                        .object(fileName)
                        .build()
        );
    }

    /**
     * 获取桶列表
     * @return 桶列表
     * @throws Exception 异常
     */
    public List<Bucket> getBucketList() throws Exception {
        return minioClient.listBuckets();
    }

    /**
     * 获取私有文件的临时访问链接(适用于私有桶,链接有有效期)
     * @param fileName 文件名
     * @param bucketName 桶名(可为空,使用默认桶)
     * @param expireSeconds 链接有效期(秒)
     * @return 临时访问链接
     * @throws Exception 异常
     */
    public String getPresignedUrl(String fileName, String bucketName, int expireSeconds) throws Exception {
        if (bucketName == null || bucketName.isEmpty()) {
            bucketName = defaultBucket;
        }
        // 生成临时访问链接
        return minioClient.getPresignedObjectUrl(
                GetPresignedObjectUrlArgs.builder()
                        .method(Method.GET)  // 请求方法
                        .bucket(bucketName)
                        .object(fileName)
                        .expiry(expireSeconds, TimeUnit.SECONDS)  // 链接有效期
                        .build()
        );
    }
}

5. 编写测试接口

import org.apache.commons.io.IOUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;

import java.io.InputStream;

@RestController
@RequestMapping("/minio")
public class MinioController {

    @Autowired
    private MinioUtil minioUtil;

    /**
     * 测试上传文件
     * @param file 前端上传的文件(form-data 格式)
     * @return 文件访问路径
     */
    @PostMapping("/upload")
    public String uploadFile(@RequestParam("file") MultipartFile file) {
        try {
            // 上传到默认桶,返回文件访问路径
            return minioUtil.uploadFile(file, null);
        } catch (Exception e) {
            return "上传失败:" + e.getMessage();
        }
    }

    /**
     * 测试下载文件
     * @param fileName 文件名(与上传时返回的路径中的文件名部分一致)
     * @return 响应实体(文件字节流)
     */
    @GetMapping("/download")
    public ResponseEntity<byte[]> downloadFile(@RequestParam("fileName") String fileName) {
        try {
            // 获取文件输入流
            InputStream in = minioUtil.downloadFile(fileName, null);
            // 转换为字节数组
            byte[] bytes = IOUtils.toByteArray(in);
            // 设置响应头(解决中文文件名乱码)
            HttpHeaders headers = new HttpHeaders();
            headers.setContentDispositionFormData("attachment", new String(fileName.getBytes("UTF-8"), "ISO-8859-1"));
            headers.setContentType(MediaType.APPLICATION_OCTET_STREAM);
            // 返回文件字节流
            return new ResponseEntity<>(bytes, headers, HttpStatus.OK);
        } catch (Exception e) {
            return ResponseEntity.status(500).body(("下载失败:" + e.getMessage()).getBytes());
        }
    }

    /**
     * 测试删除文件
     * @param fileName 文件名
     * @return 操作结果
     */
    @DeleteMapping("/delete")
    public String deleteFile(@RequestParam("fileName") String fileName) {
        try {
            // 删除文件
            minioUtil.deleteFile(fileName, null);
            return "删除成功";
        } catch (Exception e) {
            return "删除失败:" + e.getMessage();
        }
    }

    /**
     * 测试获取私有文件的临时访问链接
     * @param fileName 文件名
     * @return 临时访问链接(有效期 1 小时)
     */
    @GetMapping("/getUrl")
    public String getFileUrl(@RequestParam("fileName") String fileName) {
        try {
            // 获取临时链接,有效期 3600 秒(1 小时)
            return minioUtil.getPresignedUrl(fileName, null, 3600);
        } catch (Exception e) {
            return "获取链接失败:" + e.getMessage();
        }
    }
}

6. 测试验证

  1. 启动 SpringBoot 项目和 MinIO 服务。
  2. 使用 Postman 测试接口:
    • 上传文件:发送 POST 请求到 http://localhost:8080/minio/upload,参数类型 form-data,键 file,值选择本地文件,返回文件访问路径。
    • 下载文件:发送 GET 请求到 http://localhost:8080/minio/download?fileName=上传返回的文件名,浏览器自动下载文件。
    • 删除文件:发送 DELETE 请求到 http://localhost:8080/minio/delete?fileName=文件名,返回删除结果。
    • 获取临时链接:发送 GET 请求到 http://localhost:8080/minio/getUrl?fileName=文件名,返回有效期 1 小时的临时访问链接。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值