SpringBoot × MinIO 极速开发指南:从入门到高并发实战

发布于:2025-04-04 ⋅ 阅读:(34) ⋅ 点赞:(0)

目录

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

本章的知识网络:

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>

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

2.3. 桶的数量配置

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

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

以下是部分示例:

minio:
  buckets:
    user: app-user-data
    product: app-product-images
    log: app-system-logs
minio:
  buckets:
    dev: app-dev-bucket
    test: app-test-bucket
    prod: app-prod-bucket

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();
    }
}

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());
    }
}

3.2. MinIO 控制器

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

@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());
        }
    }
}

4. 功能测试验证

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

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

4.1. 程序启动

访问地址是127.0.0.1:8080

4.2. 文件上传接口验证

ApiPost验证文件上传:

验证文件确实上传成功:

验证返回的文件url:

4.3. 文件下载接口验证

ApiPost验证文件下载:

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

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

验证文件预览URL:

4.5. 文件删除接口验证

准备删除的文件

ApiPost验证文件删除:

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

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

ApiPost验证获取文件列表:

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

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

ApiPost验证生成临时URL:

验证临时访问URL

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

ApiPost验证获取永久访问URL:

验证永久访问URL:

4.9. 创建存储桶接口验证

ApiPost验证创建存储桶:

验证桶确实创建成功:

再次调用返回桶已存在

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

ApiPost验证获取所有存储桶:

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

4.11. 删除存储桶接口验证

ApiPost验证删除存储桶:

桶已删除

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