目录

一 入门

二 开发集成

1 首先在SpringBoot项目当中添加对应的依赖

2 常见API的使用

3 文件上传

4 文件下载


一 入门

1 基础概念

 网址:https://minio.org.cn/docs/minio/kubernetes/upstream/

对象存储(Object Storage)是一种数据存储架构,他以对象为单位来处理、存储和检索数据,每个对象都包含了数据本身以及元数据

MinIO存储的元数据主要包括对象的描述信息,如用户(account)、桶(bucket)以及存储桶索引(bucket index)等;

对象存储系统通常通过基于HTTP或者HTTPS协议的API(应用程序编程接口)进行数据读写。

2 环境部署

存储路径:/mnt/data/

minio路径:/usr/local/minio/

环境部署对应的网址:

GitHub: https://github.com/minio

Gitee: https://gitee.com/mirrors/minio

下载地址:https://www.min.io/download?platform=linux&arch=amd64

wget https://dl.min.io/server/minio/release/linux-amd64/minio

在本地linux系统当中去进行下载

文件下载结束之后需要给文件授予一个可执行权限

chmod +x minio

软件使用对应的启动服务器

查看如何去使用

对应的方法

MINIO_ROOT_USER=admin MINIO_ROOT_PASSWORD=password ./minio server /mnt/data --console-address ":9001"

启动MinIO:

- 执行命令:
  - MINIO_ROOT_USER=admin MINIO_ROOT_PASSWORD=password ./minio server /mnt/data --console-address ":9001"
- 或者:
  - ./minio server /mnt/data --console-address ":9001"
- MINIO_ROOT_USER 是指定minio的用户名;
  - MINIO_ROOT_PASSWORD 是指定minio的密码;
  - /mnt/data 是指定MinIO服务器用于存储数据的目录;
  - console-address ":9001" 指定 MinIO 控制台的监听地址和端口

展示:这里的内核版本优点低

现在去访问对应的客户端,可能会出现超时的情况,是因为防火墙关闭的原因。

也可以先看看防火墙是否开启

systemctl status firewalld

如果说其防火墙开启了可以临时将这个端口的防火墙给关闭

firewall-cmd --add-port=9001/tcp

本地访问成功

查看进程程序

使用Docker的方式进行启动部署(可以根据实际情况进行实现)

https://anxian.blog.csdn.net/article/details/148144715

Docker启动命令

docker run -d --name minio -p 9000:9000 -p 9001:9001 -e "MINIO_ROOT_USER=admin" -e "MINIO_ROOT_PASSWORD=password" -v /opt/minio/data:/mnt/data minio/minio server /mnt/data --console-address ":9001"
docker run -d \
  --name minio \  # 容器名称(自定义)
  -p 9000:9000 \  # 映射API端口(容器内9000 -> 宿主机9000)
  -p 9001:9001 \  # 映射控制台端口(容器内9001 -> 宿主机9001)
  -e "MINIO_ROOT_USER=admin" \  # 管理员账号(对应你的配置)
  -e "MINIO_ROOT_PASSWORD=password" \  # 管理员密码(对应你的配置)
  -v /opt/minio/data:/mnt/data \  # 挂载数据目录(宿主机/opt/minio/data -> 容器内/mnt/data,确保数据持久化)
  minio/minio \  # 社区版镜像(无需许可证)
  server /mnt/data --console-address ":9001"  # 启动命令(指定数据目录和控制台地址,对应你的参数)

最终结果

界面也是一样可以访问

二 开发集成

1 首先在SpringBoot项目当中添加对应的依赖

https://mvnrepository.com/artifact/io.minio/minio/8.5.17

<dependency>
    <groupId>io.minio</groupId>
    <artifactId>minio</artifactId>
    <version>8.5.17</version>
</dependency>

介绍一下两个基础概念:

Bucket:是存储Object的逻辑空间,每个Bucket之间的数据是相互隔离的,对用户而言,相当于存放文件的顶层文件夹。

Object:是存储到MinIO的基本对象,对用户而言,相当于文件。

2 常见API的使用

minio:
  url: http://192.168.100.100:9000
  access-key: admin
  secret-key: password
servlet:
  multipart:
    max-file-size: 1000MB    # 单个文件最大100MB
    max-request-size: 1000MB # 整个请求最大100MB
    enabled: true
package com.ax.demo123.config;

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

/**
 * @author ax
 */
@Configuration
public class MinioConfig {

    @Value("${minio.url}")
    private String url;
    @Value("${minio.accessKey}")
    private String accessKey;
    @Value("${minio.secretKey}")
    private String secretKey;

    @Bean
    public MinioClient minioClient() {
        // 链式编程创建MinIO客户端
        return MinioClient.builder()
                .endpoint(url)
                .credentials(accessKey, secretKey)
                .build();
    }
}

对于存储桶常见API

  1. putObject:用于上传已知大小的数据流,或者来自文件、字节数组等的数据。它要求指定对象的大小(除非使用-1表示未知大小,但通常建议已知大小)。它更通用,可以上传任何输入流。

  2. uploadObject:专门用于上传大型文件,并且支持分片上传( multipart upload )。它通常用于大文件,因为分片上传可以提高大文件上传的可靠性和效率。使用 uploadObject 时,MinIO客户端会自动将大文件分成多个部分进行上传。

示例代码展示:


    @Autowired
    private MinioClient minioClient;

    /**
     * 判断bucket是否存在,返回值为boolean
     */
    @Test
    void test1() throws Exception {
        BucketExistsArgs args = BucketExistsArgs.builder().bucket("bu2").build();
        boolean b = minioClient.bucketExists(args);
    }

    /**
     * 创建bucket
     */
    @Test
    void test2() throws Exception {
        MakeBucketArgs makeArgs = MakeBucketArgs.builder().bucket("bu2").build();
        minioClient.makeBucket(makeArgs);

    }

    /**
     * 列出所有bucket
     */
    @Test
    void test3() throws Exception {
        List<Bucket> buckets = minioClient.listBuckets();
        for (Bucket bucket : buckets) {
            System.out.println(bucket.name() + "创建时间" + bucket.creationDate());
        }
    }

    /**
     * 删除bucket
     */
    @Test
    void test4() throws Exception {
        RemoveBucketArgs removeArgs = RemoveBucketArgs.builder().bucket("bu2").build();
        minioClient.removeBucket(removeArgs);
    }

    /**
     * 在指定路径下上传文件
     */
    @Test
    void test5() throws Exception {
        //日期路径
        String datePath = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy/MM/dd"));

        //1流式上传(适用于小文件)
        File file1 = new File("D:\\excel\\axminio.txt");
        //创建PutObjectArgs构造器
        PutObjectArgs putArgs = PutObjectArgs.builder()
                .bucket("bu1")// 桶名
                .object("imagfile/" + datePath + "/testminio.txt") // 包含目录路径的文件名
                .stream(new FileInputStream(file1), file1.length(), -1).build();// 文件流
        //上传文件
        minioClient.putObject(putArgs);
        //2磁盘读取(适用于大文件分片上传)
        File file2 = new File("D:\\excel\\图片1.jpg");
        //UploadObjectArgs构造器
        UploadObjectArgs uploadArgs = UploadObjectArgs.builder()
                .bucket("bu1")
                .object("imagfile/" + datePath + "/testminio.jpg")// 包含目录路径的文件名
                .filename(file2.getAbsolutePath())// 临时文件路径
                .build();
        //上传文件
        minioClient.uploadObject(uploadArgs);
    }

    /**
     * 获取文件访问路径:作用:生成一个有时限的临时访问链接,在有效期内可以直接访问文件,无需认证。
     * 前端可以直接显示或者下载文件
     */
    @Test
    void test6() throws Exception {
        // 生成签名串
        String presignedUrl = minioClient.getPresignedObjectUrl(
                GetPresignedObjectUrlArgs.builder()
                        .method(Method.GET)
                        .bucket("jarbucket")
                        .object("2025/11/15/mescloud-daemon-elastic-job.jar")
                        .expiry(5, TimeUnit.MINUTES)
                        .build());
        System.out.println(presignedUrl);
        // 这里存在访问策略,如果说存储桶是私有的,就需要生成对应的签名串拼接进行访问
        // 但是共有的存储桶,那么这里不需要设置访问策略,可以直接使用路径直接访问
    }

    /**
     * 下载文件:这里是读取到本地指定文件夹当中
     */
    @Test
    void test7() {
        GetObjectArgs args = GetObjectArgs.builder()
                .bucket("jarbucket")// 桶名
                .object("2025/11/15/mescloud-daemon-elastic-job.jar")// 文件名
                .build();
        try (GetObjectResponse response = minioClient.getObject(args);
             FileOutputStream outputStream = new FileOutputStream("D:\\excel\\mescloud-daemon-elastic-job.jar")) {
            // 读取数据并写入文件
            byte[] buffer = new byte[1024];
            int bytesRead;
            while ((bytesRead = response.read(buffer)) != -1) {
                outputStream.write(buffer, 0, bytesRead);
            }
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }


    /**
     * 遍历路径文件(完整的文件名)
     */


    @Test
    void test8() throws Exception {
        ListObjectsArgs args = ListObjectsArgs.builder()
                .bucket("jarbucket")
                .prefix("2025/11/15/")
                .build();

        Iterable<Result<Item>> results = minioClient.listObjects(args);

        // 补充获取的文件名为完整的路径加文件名,可以借助这个调价进行文件夹批量下载操作,或者说文件夹内容查询操作
        for (Result<Item> result : results) {
            Item item = result.get();
            String objectName = item.objectName();
            System.out.println("名称:" + objectName + ",大小" + item.size());

            // 下载到本地D://Excel目录
            try {
                // 构建本地文件路径
                String fileName = objectName.substring(objectName.lastIndexOf("/") + 1);
                String localPath = "D://excel/" + System.currentTimeMillis() + fileName;

                // 下载文件
                minioClient.downloadObject(
                        DownloadObjectArgs.builder()
                                .bucket("jarbucket")
                                .object(objectName)
                                .filename(localPath)
                                .build()
                );

                System.out.println("下载成功: " + localPath);
            } catch (Exception e) {
                System.err.println("下载失败: " + objectName + " - " + e.getMessage());
            }
        }
    }

    /**
     * 删除路径文件
     */
    @Test
    void test9() throws Exception {
        RemoveObjectArgs args = RemoveObjectArgs.builder()
                .bucket("jarbucket")
                .object("2025/11/15/mescloud-daemon-elastic-job.jar")
                .build();
        minioClient.removeObject(args);
        System.out.println("删除成功");
    }

对文件访问路径访问策略的补充:修改方式有后台修改与api修改

@RequestParam(“前端传递的参数名称”)

3 文件上传

Controller层代码,前提:已经提前创建好了名称为excel的桶

    /**
     * 上传Excel文件到Minio的bucket存储桶
     *
     * @return
     */
    @PostMapping("/uploadFile")
    public R uploadFile(@RequestParam MultipartFile file,String bucketName) {
        return R.ok(importService.uploadFile(file,bucketName));
    }

Service层

     String uploadFile(MultipartFile file,String bucketName);

Service实现类

整体的一个需求设计:

首先,文件上传对文件进行非空判断,然后对存储桶的存在性进行判断,不存在就创建一个,设置存储桶内部的文件结构路径(这里是根据上传的日期实践进行设置),对路径下文件是否存在进行判断,存在则返回,防止进行覆盖。然后就需要进行判断文件大小采用不同的上传策略。

第一种(小于100MB的):传递文件,日期格式路径,文件名,大小,桶名

第二种(大于100MB的):传递文件,日期格式路径,桶名

第一种采用的是流式传输,其在stream()当中需要传入对于的1 文件输入流 2 对象总大小 3 分片大小(这里使用默认即可:-1)

第二种采用的是磁盘文件上传,其内部大致思路是有一个临时文件的存储,然后读取这个临时文件,适用于大文件,在其的Filename()当中就传入指定的路径即可

临时文件会创建在本地,但是后续会对其进行删除

代码展示:

 /**
     * 文件上传-到excel桶的日期规范之下
     *
     * @param file
     * @param bucketName
     * @return
     */
    @Override
    public String uploadFile(MultipartFile file, String bucketName) {
        // 非空校验
        if (file == null || file.isEmpty()) {
            throw new IllegalArgumentException("文件不能为空");
        }
        // 判断桶是否存在,不存在则创建一个
        ensureBucketExists(bucketName);
        // 对一些参数进行设置(日期路径,文件名称,大小,尺寸限制)
        String datePath = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy/MM/dd"));
        String fileName = file.getOriginalFilename();
        long size = file.getSize();
        long limit = 1024 * 1024 * 100;
        // 判断路径下文件是否存在
        boolean exists = fileExists(bucketName, datePath + "/" + fileName);
        if (exists) {
            log.info("该文件已存在,请勿重复上传");
            throw new RuntimeException("文件已经存在请勿重复上传。");
        }
        try {
            // 判断文件大小选择上传方式
            if (size < limit) {
                return uploadSmallFile(file, datePath, fileName, size, bucketName);
            } else {
                return uploadLargeFile(file, datePath, fileName, bucketName);
            }
        } catch (Exception e) {
            log.error("上传文件失败:文件名:" + fileName);
            throw new RuntimeException("上传文件失败" + e.getMessage());
        }
    }

    /**
     * 文件上传 (<100MB) - 内存流式传输
     */
    private String uploadSmallFile(MultipartFile file, String datePath, String fileName, long size, String bucketName) {
        try (InputStream inputStream = file.getInputStream()) {
            PutObjectArgs args = PutObjectArgs.builder()
                    .bucket(bucketName)
                    .object(datePath + "/" + fileName)
                    .stream(inputStream, size, -1)
                    .build();
            minioClient.putObject(args);
            log.info("上传成功,文件存储在Minio的" + bucketName + "/datePath/" + fileName);
            return "上传成功(小于100M方式)";
        } catch (Exception e) {
            log.error("小文件上传失败:文件名" + fileName);
            throw new RuntimeException("小文件上传失败" + e.getMessage());
        }
    }

    /**
     * 文件上传 (≥100MB) - 磁盘文件上传
     */
    private String uploadLargeFile(MultipartFile file, String datePath, String fileName, String bucketName) {
        File tempFile = null;
        try {
            // 在本地的临时文件夹路径之下创建临时文件
            tempFile = File.createTempFile("minio_upload_", "_" + fileName);

            // 将MultipartFile转换为临时文件
            file.transferTo(tempFile);

            // 使用uploadObject上传大文件
            UploadObjectArgs uploadArgs = UploadObjectArgs.builder()
                    .bucket(bucketName)
                    .object(datePath + "/" + fileName)
                    .filename(tempFile.getAbsolutePath())
                    .build();
            minioClient.uploadObject(uploadArgs);
            log.info("上传成功,文件存储在Minio的" + bucketName + "/" + datePath + "/" + fileName);
            return "上传成功(大于100M方式)";

        } catch (Exception e) {
            log.error("大文件上传失败: {}", fileName);
            throw new RuntimeException("大文件上传失败" + e.getMessage());
        } finally {
            // 确保临时文件被删除
            if (tempFile != null && tempFile.exists()) {
                try {
                    tempFile.delete();
                } catch (Exception e) {
                    log.warn("删除临时文件失败: {}", tempFile.getAbsolutePath());
                }
            }
        }
    }

    /**
     * 使用 statObject 判断文件是否存在
     */
    public boolean fileExists(String bucketName, String objectName) {
        try {
            StatObjectArgs statArgs = StatObjectArgs.builder()
                    .bucket(bucketName)
                    .object(objectName)
                    .build();

            StatObjectResponse response = minioClient.statObject(statArgs);

            // 如果能执行到这里,说明文件存在
            log.info("文件存在: {}/{}, 大小: {}, 最后修改: {}",
                    bucketName, objectName,
                    response.size(), response.lastModified());
            return true;

        } catch (ErrorResponseException e) {
            // 明确捕获 MinIO 错误响应
            String errorCode = e.errorResponse().code();

            if ("NoSuchKey".equals(errorCode)) {
                // 文件不存在是正常情况,返回 false
                log.info("文件不存在: {}/{}", bucketName, objectName);
                return false;
            } else {
                // 其他 MinIO 错误(如权限问题、存储桶不存在等)
                log.info("MinIO错误响应: {}/{}, code={}, message={}",
                        bucketName, objectName, errorCode, e.getMessage());
                throw new RuntimeException("文件检查失败: " + e.getMessage());
            }

        } catch (Exception e) {
            // 其他异常(如网络问题、连接超时等)
            log.info("检查文件存在性时发生异常: {}/{}", bucketName, objectName, e);
            throw new RuntimeException("文件检查失败: " + e.getMessage());
        }
    }
    /**
     * 确保存储桶存在
     */
    private void ensureBucketExists(String bucketName) {
        try {
            BucketExistsArgs existsArgs = BucketExistsArgs.builder().bucket(bucketName).build();
            if (!minioClient.bucketExists(existsArgs)) {
                MakeBucketArgs makeArgs = MakeBucketArgs.builder().bucket(bucketName).build();
                minioClient.makeBucket(makeArgs);
                log.info("创建存储桶: {}", bucketName);
            }
        } catch (Exception e) {
            log.error("存储桶操作失败: bucket={}", bucketName, e);
            throw new RuntimeException("存储桶操作失败: " + e.getMessage());
        }
    }

结果展示:

控制台展示:

4 文件下载

传递参数存储桶名,存储的格式化路径,这里的存储是基于年月日yyyyMMdd的格式进行传递,还有文件名称以及后缀。对文件的桶以及路径进行存在性判断,然后获取对应的存储对象将其进行读取到本地。

controller

    /**
     * 获取Minio的指定bucketName存储桶中的文件
     *
     * @return
     */
    @GetMapping("/downloadFile")
    public R downloadFile(@RequestParam("bucketName") String bucketName, @RequestParam("objectName") String objectName, @RequestParam("fileName") String fileName) {
        return R.ok(importService.downloadFile(bucketName, objectName, fileName));
    }

Service

String downloadFile(String bucketName, String objectName, String fileName);

实现类

    /**
     * 文件下载(指定桶名,时间路径yyyyMMdd,文件名)
     */
    @Override
    public String downloadFile(String bucketName, String objectName, String fileName) {

        // 生成完整的对象路径
        objectName = timeTransfer(objectName) + "/" + fileName;
        log.info("开始下载文件: {}/{}", bucketName, objectName);
        if (!bucketExists(bucketName)) {
            log.error("存储桶不存在: {}", bucketName);
            return "路径错误无法找到文件";
        } else {
            if (!fileExists(bucketName, objectName)) {
                log.error("文件不存在: {}/{}", bucketName, objectName);
                return "路径错误,无法找到文件";
            }
        }
        GetObjectArgs args = GetObjectArgs.builder()
                .bucket(bucketName)
                .object(objectName)
                .build();
        try (GetObjectResponse response = minioClient.getObject(args);
             FileOutputStream outputStream = new FileOutputStream("D:\\excel\\" + fileName)) {
            // 读取数据并写入文件
            byte[] buffer = new byte[1024];
            int bytesRead;
            while ((bytesRead = response.read(buffer)) != -1) {
                outputStream.write(buffer, 0, bytesRead);
            }
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
        log.info("下载成功: {}/{}", bucketName, objectName);
        return "下载成功" + fileName;
    }

    /**
     * 判断存储桶是否存在
     */
    public boolean bucketExists(String bucketName) {
        try {
            BucketExistsArgs args = BucketExistsArgs.builder().bucket(bucketName).build();
            return minioClient.bucketExists(args);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * 时间转换yyyyMMdd转yyyy/MM/dd
     */
    public String timeTransfer(String objectName) {
        try {
            return LocalDate.parse(objectName, DateTimeFormatter.BASIC_ISO_DATE)
                    .format(DateTimeFormatter.ofPattern("yyyy/MM/dd"));
        } catch (Exception e) {
            throw new IllegalArgumentException("日期格式错误: " + objectName);
        }
    }
    /**
     * 使用 statObject 判断文件是否存在
     */
    public boolean fileExists(String bucketName, String objectName) {
        try {
            StatObjectArgs statArgs = StatObjectArgs.builder()
                    .bucket(bucketName)
                    .object(objectName)
                    .build();

            StatObjectResponse response = minioClient.statObject(statArgs);

            // 如果能执行到这里,说明文件存在
            log.info("文件存在: {}/{}, 大小: {}, 最后修改: {}",
                    bucketName, objectName,
                    response.size(), response.lastModified());
            return true;

        } catch (ErrorResponseException e) {
            // 明确捕获 MinIO 错误响应
            String errorCode = e.errorResponse().code();

            if ("NoSuchKey".equals(errorCode)) {
                // 文件不存在是正常情况,返回 false
                log.info("文件不存在: {}/{}", bucketName, objectName);
                return false;
            } else {
                // 其他 MinIO 错误(如权限问题、存储桶不存在等)
                log.info("MinIO错误响应: {}/{}, code={}, message={}",
                        bucketName, objectName, errorCode, e.getMessage());
                throw new RuntimeException("文件检查失败: " + e.getMessage());
            }

        } catch (Exception e) {
            // 其他异常(如网络问题、连接超时等)
            log.info("检查文件存在性时发生异常: {}/{}", bucketName, objectName, e);
            throw new RuntimeException("文件检查失败: " + e.getMessage());
        }
    }

Logo

腾讯云面向开发者汇聚海量精品云计算使用和开发经验,营造开放的云计算技术生态圈。

更多推荐