SpringBoot × MinIO 极速开发指南:对象存储服务高可用实战

本文涉及的产品
对象存储 OSS,20GB 3个月
对象存储 OSS,内容安全 1000 次 1年
对象存储 OSS,恶意文件检测 1000次 1年
简介: 生成临时访问URL接口和文件预览其实是同一个方法,只是文件预览内定了七天访问,而这个方法可以自行制定,单位是秒。方法,所以返回的是地址信息,但是可以通过dubug看到Bucket中的属性,确实是当前所有桶信息。配置类中奖MinIOClient客户端注入到Springboot中。MinIO集群的在Linux上的部署可以参考:​​​​​​​。Nginx代理MinIO集群可以参考:​​​​​​​。从Bucket源码可以看出,并没有实现。

 目录

SpringBoot Integrate MinIO

1. MinIO 安装部署

2. SpringBoot 项目配置

2.1. 添加依赖

2.2. 配置文件

2.3. 桶的数量配置

2.4. 配置类

3. MinIO 核心功能实现

3.1. MinIO 工具类

3.2. MinIO 控制器

4. 功能测试验证

4.1. 程序启动

4.2. 文件上传接口验证

4.3. 文件下载接口验证

4.4. 文件预览(7天有效期)接口验证

4.5. 文件删除接口验证

4.6. 获取文件列表接口验证

4.7. 生成临时访问URL接口验证

4.8. 获取永久访问URL接口验证

4.9. 创建存储桶接口验证

4.10. 获取所有存储桶接口验证

4.11. 删除存储桶接口验证


SpringBoot Integrate MinIO

本章的知识网络:

image.gif 编辑

1. MinIO 安装部署

MinIO的在Linux上的部署可以参考:MinIO在Linux上的安装与部署_minio linux部署-CSDN博客

MinIO集群的在Linux上的部署可以参考:MinIO在Linux上的集群安装与部署-CSDN博客

Nginx代理MinIO集群可以参考:Nginx代理MinIO集群-CSDN博客

2. SpringBoot 项目配置

2.1. 添加依赖

添加相应的pom.xml文件依赖

<dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springdoc</groupId>
            <artifactId>springdoc-openapi-ui</artifactId>
            <version>1.6.14</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>io.minio</groupId>
            <artifactId>minio</artifactId>
            <version>8.5.6</version>
            <exclusions>
                <exclusion>
                    <groupId>com.squareup.okhttp3</groupId>
                    <artifactId>okhttp</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>com.squareup.okhttp3</groupId>
            <artifactId>okhttp</artifactId>
            <version>4.11.0</version> <!-- 使用最新稳定版 -->
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.30</version>
        </dependency>

image.gif

2.2. 配置文件

配置application.yml文件

minio:
  endpoint: http://192.168.33.205:9000 # Nginx 负载均衡器的地址
  access-key: gVqpDMoCHridNBp9wI1O # 访问密钥
  secret-key: H6E19mqETNxYFJ6PgSK1V6tgdWjeiqt8e6BMnkuV # 安全密钥
  bucketName: springboot-mino-test-bucket # 存储桶名称
# 文件上传大小限制
spring:
  servlet:
    multipart:
      enabled: true
      max-file-size: 100MB
      max-request-size: 100MB

image.gif

2.3. 桶的数量配置

在项目开发中,使用 MinIO 桶的数量取决于具体的业务需求和技术架构设计,以下是不同场景的实践建议:

  1. 小型项目或简单业务在配置文件中配置一个桶即可,简单高效,维护成本低,但是灵活性差。
  2. 中大型项目或复杂业务需要权限控制、数据隔离和多租户情况时,可以再配置文件中预设高频使用的桶(例如用户头像、日志文件等),在接口中动态制定低频使用的桶(租户个人空间)

以下是部分示例:

minio:
  buckets:
    user: app-user-data
    product: app-product-images
    log: app-system-logs

image.gif

minio:
  buckets:
    dev: app-dev-bucket
    test: app-test-bucket
    prod: app-prod-bucket

image.gif

2.4. 配置类

配置类中奖MinIOClient客户端注入到Springboot中

@Configuration
@Getter
public class MinioConfig {
    @Value("${minio.endpoint}")
    private String endpoint;
    @Value("${minio.access-key}")
    private String accessKey;
    @Value("${minio.secret-key}")
    private String secretKey;
    @Value("${minio.bucketName}")
    private String bucketName;
    @Bean
    public MinioClient minioClient() {
        return MinioClient.builder()
        .endpoint(endpoint)
        .credentials(accessKey, secretKey)
        .build();
    }
}

image.gif

3. MinIO 核心功能实现

3.1. MinIO 工具类

MinIO工具类中包含了操作MinIO服务的核心方法:

  • 文件上传(支持自动重命名)
  • 文件下载(文件输入流和浏览器直接下载)
  • 文件删除
  • 获取桶中文件列表
  • 存储桶管理(增、删、是否存在、获取桶列表)
  • 生成临时访问链接
  • 图片预览
/**
 * Minio对象存储操作工具类
 * 包含存储桶管理、文件操作、浏览器下载等完整功能
 */
@Component
public class MinioUtil {
    private final MinioClient minioClient;
    private final MinioConfig minioConfig;
    @Autowired
    public MinioUtil(MinioClient minioClient, MinioConfig minioConfig) {
        this.minioClient = minioClient;
        this.minioConfig = minioConfig;
    }
    /* 存储桶操作系列方法 */
    /**
     * 检查存储桶是否存在
     *
     * @param bucketName 存储桶名称
     * @return 是否存在
     * @throws Exception Minio操作异常
     */
    public boolean bucketExists(String bucketName) throws Exception {
        return minioClient.bucketExists(BucketExistsArgs.builder()
                .bucket(bucketName)
                .build());
    }
    /**
     * 创建新存储桶
     *
     * @param bucketName 存储桶名称
     * @return 是否创建成功
     * @throws Exception Minio操作异常
     */
    public boolean createBucket(String bucketName) throws Exception {
        if (!bucketExists(bucketName)) {
            minioClient.makeBucket(MakeBucketArgs.builder()
                    .bucket(bucketName)
                    .build());
            return true;
        }
        return false;
    }
    /**
     * 删除存储桶(存储桶必须为空)
     *
     * @param bucketName 存储桶名称
     * @throws Exception Minio操作异常
     */
    public void removeBucket(String bucketName) throws Exception {
        minioClient.removeBucket(RemoveBucketArgs.builder()
                .bucket(bucketName)
                .build());
    }
    /**
     * 获取全部存储桶列表
     *
     * @return 存储桶信息列表
     * @throws Exception Minio操作异常
     */
    public List<Bucket> listAllBuckets() throws Exception {
        return minioClient.listBuckets();
    }
    /* 文件操作系列方法 */
    /**
     * 上传文件到默认存储桶
     *
     * @param file       上传的文件对象
     * @param objectName 存储对象名称(包含路径)
     * @return 文件访问URL
     * @throws Exception Minio操作异常
     */
    public String uploadFile(MultipartFile file, String objectName) throws Exception {
        // 自动生成唯一文件名
        if (objectName == null || objectName.isEmpty()) {
            objectName = generateUniqueName(file.getOriginalFilename());
        }
        minioClient.putObject(PutObjectArgs.builder()
                .bucket(minioConfig.getBucketName())
                .object(objectName)
                .stream(file.getInputStream(), file.getSize(), -1)
                .contentType(file.getContentType())
                .build());
        return getFileUrl(objectName);
    }
    /**
     * 获取文件临时访问URL
     *
     * @param objectName 存储对象名称
     * @param expiry     有效期(单位:秒)
     * @return 临时访问URL
     * @throws Exception Minio操作异常
     */
    public String getPresignedUrl(String objectName, int expiry) throws Exception {
        return minioClient.getPresignedObjectUrl(
                GetPresignedObjectUrlArgs.builder()
                        .method(Method.GET)
                        .bucket(minioConfig.getBucketName())
                        .object(objectName)
                        .expiry(expiry, TimeUnit.SECONDS)
                        .build());
    }
    /**
     * 图片预览(生成7天有效的URL)
     *
     * @param objectName 存储对象名称
     * @return 预览URL
     * @throws Exception Minio操作异常
     */
    public String previewImage(String objectName) throws Exception {
        return minioClient.getPresignedObjectUrl(
                GetPresignedObjectUrlArgs.builder()
                        .method(Method.GET)
                        .bucket(minioConfig.getBucketName())
                        .object(objectName)
                        .expiry(7, TimeUnit.DAYS)
                        .build());
    }
    /**
     * 获取文件输入流
     *
     * @param objectName 存储对象名称
     * @return 文件流
     * @throws Exception Minio操作异常
     */
    public InputStream downloadFile(String objectName) throws Exception {
        return minioClient.getObject(GetObjectArgs.builder()
                .bucket(minioConfig.getBucketName())
                .object(objectName)
                .build());
    }
    /**
     * 直接下载文件到HttpServletResponse(浏览器下载)
     *
     * @param objectName 存储对象名称
     * @param response   HttpServletResponse
     * @throws Exception Minio操作异常或IO异常
     */
    public void downloadToResponse(String objectName, HttpServletResponse response) throws Exception {
        // 获取文件元数据
        StatObjectResponse stat = minioClient.statObject(StatObjectArgs.builder()
                .bucket(minioConfig.getBucketName())
                .object(objectName)
                .build());
        // 设置响应头
        response.setContentType(stat.contentType());
        response.setHeader("Content-Disposition",
                "attachment; filename=\"" + URLEncoder.encode(objectName, StandardCharsets.UTF_8.name()) + "\"");
        response.setContentLengthLong(stat.size());
        // 流式传输文件内容
        try (InputStream is = downloadFile(objectName);
             OutputStream os = response.getOutputStream()) {
            IOUtils.copy(is, os);
            os.flush();
        }
    }
    /**
     * 删除文件
     *
     * @param objectName 存储对象名称
     * @throws Exception Minio操作异常
     */
    public void deleteFile(String objectName) throws Exception {
        minioClient.removeObject(RemoveObjectArgs.builder()
                .bucket(minioConfig.getBucketName())
                .object(objectName)
                .build());
    }
    /**
     * 列出存储桶中的所有文件
     *
     * @param bucketName 存储桶名称
     * @return 文件信息列表
     * @throws Exception Minio操作异常
     */
    public List<String> listAllFiles(String bucketName) throws Exception {
        List<String> list = new ArrayList<>();
        for (Result<Item> result : minioClient.listObjects(
                ListObjectsArgs.builder().bucket(bucketName).build())) {
            list.add(result.get().objectName());
        }
        return list;
    }
    /**
     * 获取文件元数据
     *
     * @param objectName 存储对象名称
     * @return 文件元数据
     * @throws Exception Minio操作异常
     */
    public StatObjectResponse getObjectStat(String objectName) throws Exception {
        return minioClient.statObject(StatObjectArgs.builder()
                .bucket(minioConfig.getBucketName())
                .object(objectName)
                .build());
    }
    /**
     * 生成永久访问URL(需要存储桶设置为公开)
     *
     * @param objectName 存储对象名称
     * @return 直接访问URL
     */
    public String getPermanentUrl(String objectName) {
        return String.format("%s/%s/%s",
                minioConfig.getEndpoint(),
                minioConfig.getBucketName(),
                objectName);
    }
    /**
     * 验证文件类型白名单
     *
     * @param file         上传文件
     * @param allowedTypes 允许的类型列表
     * @throws IllegalArgumentException 文件类型不合法
     */
    public void validateFileType(MultipartFile file, List<String> allowedTypes) {
        String fileType = file.getContentType();
        if (!allowedTypes.contains(fileType)) {
            throw new IllegalArgumentException("不支持的文件类型: " + fileType);
        }
    }
    /* 辅助方法 */
    private String generateUniqueName(String originalFileName) {
        return UUID.randomUUID().toString().replace("-", "")
                + "_" + originalFileName;
    }
    private String getFileUrl(String objectName) throws Exception {
        return minioConfig.getEndpoint() + "/"
                + minioConfig.getBucketName() + "/" + objectName;
    }
    /* 扩展功能方法 */
    /**
     * 复制文件到新位置
     *
     * @param sourceBucket 源存储桶
     * @param sourceObject 源文件
     * @param destBucket   目标存储桶
     * @param destObject   目标文件
     * @throws Exception Minio操作异常
     */
    public void copyObject(String sourceBucket, String sourceObject,
                           String destBucket, String destObject) throws Exception {
        minioClient.copyObject(CopyObjectArgs.builder()
                .source(CopySource.builder()
                        .bucket(sourceBucket)
                        .object(sourceObject)
                        .build())
                .bucket(destBucket)
                .object(destObject)
                .build());
    }
}

image.gif

3.2. MinIO 控制器

FileController在MinIO工具基础上实现了以下接口:

image.gif 编辑

@Tag(name = "文件管理接口")
@RestController
@RequestMapping("/minio")
public class MinioController {
    private final MinioUtil minioUtil;
    private final MinioConfig minioConfig;
    @Autowired
    public MinioController(MinioUtil minioUtil, MinioConfig minioConfig) {
        this.minioUtil = minioUtil;
        this.minioConfig = minioConfig;
    }
    @Operation(summary = "文件上传")
    @PostMapping("/upload")
    public R<String> uploadFile(@RequestParam("file") MultipartFile file,
                                @RequestParam(value = "objectName", required = false) String objectName) {
        try {
            if (file.isEmpty()) {
                return R.error(HttpStatus.BAD_REQUEST.value(), "上传文件不能为空");
            }
            String url = minioUtil.uploadFile(file, objectName);
            return R.success("上传成功", url);
        } catch (Exception e) {
            return R.error(HttpStatus.INTERNAL_SERVER_ERROR.value(), e.getMessage());
        }
    }
    @Operation(summary ="文件下载")
    @GetMapping("/download/{objectName}")
    public void downloadFile(@PathVariable String objectName, HttpServletResponse response) {
        try {
            minioUtil.downloadToResponse(objectName, response);
        } catch (Exception e) {
            response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
        }
    }
    @Operation(summary ="文件预览(7天有效期)")
    @GetMapping("/preview/{objectName}")
    public R<String> previewFile(@PathVariable String objectName) {
        try {
            String url = minioUtil.previewImage(objectName);
            return R.success("获取预览地址成功", url);
        } catch (Exception e) {
            return R.error(HttpStatus.INTERNAL_SERVER_ERROR.value(), e.getMessage());
        }
    }
    @Operation(summary ="删除文件")
    @DeleteMapping("/delete/{objectName}")
    public R<Void> deleteFile(@PathVariable String objectName) {
        try {
            minioUtil.deleteFile(objectName);
            return R.success("删除成功");
        } catch (Exception e) {
            return R.error(HttpStatus.INTERNAL_SERVER_ERROR.value(), e.getMessage());
        }
    }
    @Operation(summary ="获取文件列表")
    @GetMapping("/list")
    public R<List<String>> listFiles(@RequestParam(required = false) String bucketName) {
        try {
            String targetBucket = (bucketName != null && !bucketName.isEmpty())
                    ? bucketName : minioConfig.getBucketName();
            List<String> files = minioUtil.listAllFiles(targetBucket);
            return R.success("获取成功", files);
        } catch (Exception e) {
            return R.error(HttpStatus.INTERNAL_SERVER_ERROR.value(), e.getMessage());
        }
    }
    @Operation(summary ="生成临时访问URL")
    @GetMapping("/presigned-url")
    public R<String> getPresignedUrl(@RequestParam String objectName,
                                    @RequestParam(defaultValue = "3600") int expiry) {
        try {
            String url = minioUtil.getPresignedUrl(objectName, expiry);
            return R.success("生成成功", url);
        } catch (Exception e) {
            return R.error(HttpStatus.INTERNAL_SERVER_ERROR.value(), e.getMessage());
        }
    }
    @Operation(summary ="获取永久访问URL")
    @GetMapping("/permanent-url/{objectName}")
    public R<String> getPermanentUrl(@PathVariable String objectName) {
        try {
            String url = minioUtil.getPermanentUrl(objectName);
            return R.success("获取成功", url);
        } catch (Exception e) {
            return R.error(HttpStatus.INTERNAL_SERVER_ERROR.value(), e.getMessage());
        }
    }
    // 存储桶管理相关接口
    @Operation(summary ="创建存储桶")
    @PostMapping("/buckets/{bucketName}")
    public R<Void> createBucket(@PathVariable String bucketName) {
        try {
            boolean created = minioUtil.createBucket(bucketName);
            return created ? R.success("创建成功") : R.error(HttpStatus.INTERNAL_SERVER_ERROR.value(),"存储桶已存在");
        } catch (Exception e) {
            return R.error(HttpStatus.INTERNAL_SERVER_ERROR.value(), e.getMessage());
        }
    }
    @Operation(summary ="删除存储桶")
    @DeleteMapping("/buckets/{bucketName}")
    public R<Void> deleteBucket(@PathVariable String bucketName) {
        try {
            minioUtil.removeBucket(bucketName);
            return R.success("删除成功");
        } catch (Exception e) {
            return R.error(HttpStatus.INTERNAL_SERVER_ERROR.value(), e.getMessage());
        }
    }
    @Operation(summary ="获取所有存储桶")
    @GetMapping("/buckets")
    public R<List<io.minio.messages.Bucket>> listBuckets() {
        try {
            List<io.minio.messages.Bucket> buckets = minioUtil.listAllBuckets();
            return R.success("获取成功", buckets);
        } catch (Exception e) {
            return R.error(HttpStatus.INTERNAL_SERVER_ERROR.value(), e.getMessage());
        }
    }
}

image.gif

4. 功能测试验证

下方提供了功能测试的图例,我将apipost的地址共享在这里,有需要测试的朋友可以自行测试:

Apipost-基于协作, 不止于API文档、调试、Mock、自动化测试

4.1. 程序启动

访问地址是127.0.0.1:8080

image.gif 编辑

4.2. 文件上传接口验证

ApiPost验证文件上传:

image.gif 编辑

验证文件确实上传成功:

image.gif 编辑

image.gif 编辑

验证返回的文件url:

image.gif 编辑

4.3. 文件下载接口验证

ApiPost验证文件下载:

image.gif 编辑

4.4. 文件预览(7天有效期)接口验证

ApiPost验证文件预览(7天有效期):

image.gif 编辑

验证文件预览URL:

image.gif 编辑

4.5. 文件删除接口验证

准备删除的文件

image.gif 编辑

ApiPost验证文件删除:

image.gif 编辑

准备删除的文件已经删除了

image.gif 编辑

4.6. 获取文件列表接口验证

ApiPost验证获取文件列表:

image.gif 编辑

4.7. 生成临时访问URL接口验证

生成临时访问URL接口和文件预览其实是同一个方法,只是文件预览内定了七天访问,而这个方法可以自行制定,单位是秒

ApiPost验证生成临时URL:

image.gif 编辑

验证临时访问URL

image.gif 编辑

4.8. 获取永久访问URL接口验证

ApiPost验证获取永久访问URL:

image.gif 编辑

验证永久访问URL:

image.gif 编辑

4.9. 创建存储桶接口验证

ApiPost验证创建存储桶:

image.gif 编辑

验证桶确实创建成功:

image.gif 编辑

再次调用返回桶已存在

image.gif 编辑

4.10. 获取所有存储桶接口验证

ApiPost验证获取所有存储桶:

image.gif 编辑

从Bucket源码可以看出,并没有实现toString()方法,所以返回的是地址信息,但是可以通过dubug看到Bucket中的属性,确实是当前所有桶信息

image.gif 编辑

image.gif 编辑

4.11. 删除存储桶接口验证

ApiPost验证删除存储桶:

image.gif 编辑

桶已删除

image.gif 编辑

再次调用返回该桶已不存在

image.gif 编辑


相关实践学习
通义万相文本绘图与人像美化
本解决方案展示了如何利用自研的通义万相AIGC技术在Web服务中实现先进的图像生成。
目录
相关文章
|
4月前
|
编解码 网络协议 算法
SpringBoot × TCP 极速开发指南:工业级TCP通信协议栈操作手册
🌟 ​大家好,我是摘星!​ 🌟今天为大家带来的是并发编程的SpringBoot × TCP 极速开发指南,废话不多说直接开始~
272 0
|
6月前
|
存储 Java 文件存储
🗄️Spring Boot 3 整合 MinIO 实现分布式文件存储
本文介绍了如何基于Spring Boot 3和MinIO实现分布式文件存储。随着应用规模扩大,传统的单机文件存储方案难以应对大规模数据和高并发访问,分布式文件存储系统成为更好的选择。文章详细讲解了MinIO的安装、配置及与Spring Boot的整合步骤,包括Docker部署、MinIO控制台操作、Spring Boot项目中的依赖引入、配置类编写及工具类封装等内容。最后通过一个上传头像的接口示例展示了具体的开发和测试过程,强调了将API操作封装成通用工具类以提高代码复用性和可维护性的重要性。
1209 7
🗄️Spring Boot 3 整合 MinIO 实现分布式文件存储
|
8月前
|
存储 Java 文件存储
Spring Boot 3 整合 Minio 实现文件存储
本文介绍了如何使用 Spring Boot 3 整合 MinIO 实现文件存储服务。MinIO 是一款高性能的对象存储服务器,适合大规模数据存储与分析,支持多种部署环境且文档完备、开源免费。从 MinIO 的快速安装、配置文件公开访问,到 Spring Boot 中集成 MinIO 客户端的步骤,包括创建用户访问密钥、引入依赖包、添加配置信息、编写 MinIO 客户端配置类及上传和预览文件的服务代码。最后通过 Apifox 进行文件上传测试,并验证文件是否成功存储及预览功能是否正常。关注公众号“Harry技术”,回复 minio 获取源码地址。
601 76
|
9月前
|
XML Java API
Spring Boot集成MinIO
本文介绍了如何在Spring Boot项目中集成MinIO,一个高性能的分布式对象存储服务。主要步骤包括:引入MinIO依赖、配置MinIO属性、创建MinIO配置类和服务类、使用服务类实现文件上传和下载功能,以及运行应用进行测试。通过这些步骤,可以轻松地在项目中使用MinIO的对象存储功能。
1158 5
|
10月前
|
存储 Java API
开源对象存储服务(MinIO),正在备受欢迎!
本文介绍了MinIO,一个高性能、开源的对象存储服务器,兼容Amazon S3 API,适合存储大量非结构化数据。文章详细讲解了MinIO在Java中的使用方法,包括添加依赖、初始化客户端、基本操作(创建桶、上传/下载/删除对象)和高级功能(设置桶策略、使用预签名URL),并提供了Spring Boot集成MinIO的示例。
921 4
|
29天前
|
存储 运维 安全
阿里云国际站OSS与自建存储的区别
阿里云国际站对象存储OSS提供海量、安全、低成本的云存储解决方案。相比自建存储,OSS具备易用性强、稳定性高、安全性好、成本更低等优势,支持无限扩展、自动冗余、多层防护及丰富增值服务,助力企业高效管理数据。
|
1月前
|
存储 域名解析 前端开发
震惊!不买服务器,还可以用阿里云国际站 OSS 轻松搭建静态网站
在数字化时代,利用阿里云国际站OSS可低成本搭建静态网站。本文详解OSS优势及步骤:创建Bucket、上传文件、配置首页与404页面、绑定域名等,助你快速上线个人或小型业务网站,操作简单,成本低廉,适合初学者与中小企业。
|
1月前
|
存储 安全 API
某网盘不好用?有没有类似某网盘的存储软件?阿里云国际站 OSS:云存储的全能助手,你 get 了吗?
在数据爆炸时代,阿里云国际站OSS提供海量、安全、低成本的云存储服务,支持多种数据类型存储与灵活访问,助力企业与个人高效管理数据,降低存储成本。开通简便,操作友好,是理想的云端数据解决方案。
|
7月前
|
SQL 分布式计算 Serverless
基于阿里云 EMR Serverless Spark 版快速搭建OSS日志分析应用
基于阿里云 EMR Serverless Spark 版快速搭建OSS日志分析应用
149 0

热门文章

最新文章