Spring Boot 集成 MinIO 实现分布式文件存储与管理

Spring Boot 集成 MinIO 实现分布式文件存储与管理

一、MinIO 简介

MinIO 是一个高性能的分布式对象存储服务器,兼容 Amazon S3 API。它具有以下特点:

  • 轻量级且易于部署
  • 高性能(读写速度可达每秒数GB)
  • 支持数据加密和访问控制
  • 提供多种语言的SDK
  • 开源且社区活跃

二、Spring Boot 集成 MinIO

1. 添加依赖

pom.xml 中添加 MinIO Java SDK 依赖:

xml 复制代码
<dependency>
    <groupId>io.minio</groupId>
    <artifactId>minio</artifactId>
    <version>8.5.7</version>
</dependency>

2. 配置 MinIO 连接

application.yml 中配置:

yaml 复制代码
minio:
  endpoint: http://localhost:9000
  accessKey: minioadmin
  secretKey: minioadmin
  bucketName: default-bucket
  secure: false

3. 创建配置类

java 复制代码
@Configuration
public class MinioConfig {
    
    @Value("${minio.endpoint}")
    private String endpoint;
    
    @Value("${minio.accessKey}")
    private String accessKey;
    
    @Value("${minio.secretKey}")
    private String secretKey;
    
    @Bean
    public MinioClient minioClient() {
        return MinioClient.builder()
                .endpoint(endpoint)
                .credentials(accessKey, secretKey)
                .build();
    }
}

三、实现文件服务

java 复制代码
@Service
@RequiredArgsConstructor
public class MinioService {

    private final MinioClient minioClient;

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

    /**
     * 检查存储桶是否存在
     *
     * @param bucketName 存储桶名称
     * @return 存储桶是否存在 状态码 true:存在 false:不存在
     */
    public boolean bucketExists(String bucketName) throws Exception {
        return !minioClient.bucketExists(BucketExistsArgs.builder().bucket(bucketName).build());
    }

    /**
     * 创建存储桶
     */
    public void makeBucket(String bucketName) throws Exception {
        if (bucketExists(bucketName)) {
            minioClient.makeBucket(MakeBucketArgs.builder().bucket(bucketName).build());
        }
    }

    /**
     * 列出所有存储桶
     */
    public List<Bucket> listBuckets() throws Exception {
        return minioClient.listBuckets();
    }

    /**
     * 上传文件
     *
     * @param file       文件
     * @param bucketName 存储桶名称
     * @param rename     是否重命名
     */
    public String uploadFile(MultipartFile file, String bucketName, boolean rename) throws Exception {
        // 如果未指定bucketName,使用默认的
        bucketName = getBucketName(bucketName);

        // 检查存储桶是否存在,不存在则创建
        ensureBucketExists(bucketName);

        // 生成唯一文件名
        String objectName = generateObjectName(file, rename);
        // 上传文件
        minioClient.putObject(
                PutObjectArgs.builder()
                        .bucket(bucketName)
                        .object(objectName)
                        .stream(file.getInputStream(), file.getSize(), -1)
                        .contentType(file.getContentType())
                        .build());
        return objectName;
    }


    /**
     * 下载文件
     */
    public InputStream downloadFile(String objectName, String bucketName) throws Exception {
        return minioClient.getObject(
                GetObjectArgs.builder()
                        .bucket(getBucketName(bucketName))
                        .object(objectName)
                        .build());
    }

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

    /**
     * 获取文件URL(先检查文件是否存在)
     */
    public String getFileUrl(String objectName, String bucketName) throws Exception {
        try {
            minioClient.statObject(
                    StatObjectArgs.builder()
                            .bucket(getBucketName(bucketName))
                            .object(objectName)
                            .build());
        } catch (ErrorResponseException e) {
            // 文件不存在时抛出异常
            throw new FileNotFoundException("File not found: " + objectName);
        }

        // 文件存在,生成URL
        return minioClient.getPresignedObjectUrl(
                GetPresignedObjectUrlArgs.builder()
                        .method(Method.GET)
                        .bucket(getBucketName(bucketName))
                        .object(objectName)
                        .build());
    }

    /**
     * 生成唯一文件名
     */
    private static @Nullable String generateObjectName(MultipartFile file, boolean rename) {
        String fileName = file.getOriginalFilename();
        String objectName = fileName;
        if (rename && fileName != null) {
            objectName = UUID.randomUUID().toString().replaceAll("-", "")
                    + fileName.substring(fileName.lastIndexOf("."));
        }
        return objectName;
    }

    /**
     * 检查存储桶是否存在,不存在则创建
     */
    private void ensureBucketExists(String bucketName) throws Exception {
        if (bucketExists(bucketName)) {
            makeBucket(bucketName);
        }
    }

    /**
     * 获取存储桶名称
     */
    private String getBucketName(String bucketName) {
        if (bucketName == null || bucketName.isEmpty()) {
            bucketName = this.bucketName;
        }
        return bucketName;
    }
}

四、REST API 实现

java 复制代码
@RestController
@RequiredArgsConstructor
@RequestMapping("/api/file")
public class MinioController {

    private final MinioService minioService;

    @PostMapping("/upload")
    public ResponseEntity<String> uploadFile(@RequestParam("file") MultipartFile file,
                                             @RequestParam(value = "bucketName", required = false) String bucketName) {
        try {
            String objectName = minioService.uploadFile(file, bucketName, false);
            return ResponseEntity.ok(objectName);
        } catch (Exception e) {
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(e.getMessage());
        }
    }

    @GetMapping("/download")
    public ResponseEntity<byte[]> downloadFile(@RequestParam String objectName,
                                               @RequestParam(value = "bucketName", required = false) String bucketName) {
        try {
            InputStream stream = minioService.downloadFile(objectName, bucketName);
            byte[] bytes = stream.readAllBytes();

            HttpHeaders headers = new HttpHeaders();
            headers.setContentType(MediaType.APPLICATION_OCTET_STREAM);
            headers.setContentDispositionFormData("attachment", objectName);

            return new ResponseEntity<>(bytes, headers, HttpStatus.OK);
        } catch (Exception e) {
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(null);
        }
    }

    @DeleteMapping("/delete")
    public ResponseEntity<String> deleteFile(@RequestParam String objectName,
                                             @RequestParam(value = "bucketName", required = false) String bucketName) {
        try {
            minioService.removeFile(objectName, bucketName);
            return ResponseEntity.ok("File deleted successfully");
        } catch (Exception e) {
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(e.getMessage());
        }
    }

    @GetMapping("/url")
    public ResponseEntity<String> getFileUrl(@RequestParam String objectName,
                                             @RequestParam(value = "bucketName", required = false) String bucketName) {
        try {
            String url = minioService.getFileUrl(objectName, bucketName);
            return ResponseEntity.ok(url);
        } catch (FileNotFoundException e) {
            return ResponseEntity.status(HttpStatus.NOT_FOUND).body(e.getMessage());
        } catch (Exception e) {
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(e.getMessage());
        }
    }
}

五、高级功能实现

1. 分片上传

java 复制代码
public String multipartUpload(MultipartFile file, String bucketName) {
    // 1. 初始化分片上传
    String uploadId = minioClient.initiateMultipartUpload(...);
    
    // 2. 分片上传
    Map<Integer, String> etags = new HashMap<>();
    for (int partNumber = 1; partNumber <= totalParts; partNumber++) {
        PartSource partSource = getPartSource(file, partNumber);
        String etag = minioClient.uploadPart(...);
        etags.put(partNumber, etag);
    }
    
    // 3. 完成分片上传
    minioClient.completeMultipartUpload(...);
    
    return objectName;
}

2. 文件预览

java 复制代码
    @GetMapping("/preview/{objectName}")
    public ResponseEntity<Resource> previewFile(
            @PathVariable String objectName,
            @RequestParam(value = "bucketName", required = false) String bucketName) throws Exception {

        String contentType = minioService.getFileContentType(objectName, bucketName);
        InputStream inputStream = minioService.downloadFile(objectName, bucketName);

        return ResponseEntity.ok()
                .contentType(MediaType.parseMediaType(contentType))
                .body(new InputStreamResource(inputStream));
    }

六、最佳实践

  1. 安全性考虑

    • 为预签名URL设置合理的过期时间
    • 实现细粒度的访问控制
    • 对上传文件进行病毒扫描
  2. 性能优化

    • 使用CDN加速文件访问
    • 对大文件使用分片上传
    • 实现客户端直传(Presigned URL)
  3. 监控与日志

    • 记录所有文件操作
    • 监控存储空间使用情况
    • 设置自动清理策略

七、常见问题解决

  1. 连接超时问题

    java 复制代码
    @Bean
    public MinioClient minioClient() {
        return MinioClient.builder()
                .endpoint(endpoint)
                .credentials(accessKey, secretKey)
                .httpClient(HttpClient.newBuilder()
                        .connectTimeout(Duration.ofSeconds(30))
                        .build())
                .build();
    }
  2. 文件存在性检查优化

    java 复制代码
    public boolean fileExists(String objectName, String bucketName) {
        try {
            minioClient.statObject(
                StatObjectArgs.builder()
                    .bucket(getBucketName(bucketName))
                    .object(objectName)
                    .build());
            return true;
        } catch (ErrorResponseException e) {
            if (e.errorResponse().code().equals("NoSuchKey")) {
                return false;
            }
            throw new FileStorageException("检查文件存在性失败", e);
        } catch (Exception e) {
            throw new FileStorageException("检查文件存在性失败", e);
        }
    }

八、总结

通过本文的介绍,我们实现了:

  1. Spring Boot 与 MinIO 的基本集成
  2. 文件上传、下载、删除等基础功能
  3. 文件预览、分片上传等高级功能
  4. 安全性、性能等方面的最佳实践

MinIO 作为轻量级的对象存储解决方案,非常适合中小型项目使用。结合 Spring Boot 可以快速构建强大的文件存储服务。

相关推荐
郝学胜-神的一滴2 小时前
SpringBoot实战指南:从快速入门到生产级部署(2025最新版)
java·spring boot·后端·程序人生
Rancemy5 小时前
rabbitmq 03
java·分布式·rabbitmq
爷_6 小时前
字节跳动震撼开源Coze平台!手把手教你本地搭建AI智能体开发环境
前端·人工智能·后端
小马爱打代码8 小时前
Spring Boot 接口安全设计:接口限流、防重放攻击、签名验证
网络·spring boot·安全
黄雪超8 小时前
Kafka——多线程开发消费者实例
大数据·分布式·kafka
阿里巴巴淘系技术团队官网博客8 小时前
面向互联网2C业务的分布式类Manus Java框架
java·开发语言·分布式
sniper_fandc9 小时前
RabbitMQ—HAProxy负载均衡
分布式·rabbitmq·负载均衡
不过普通话一乙不改名10 小时前
第一章:Go语言基础入门之函数
开发语言·后端·golang
苹果醋310 小时前
iview中实现点击表格单元格完成编辑和查看(span和input切换)
运维·vue.js·spring boot·nginx·课程设计
武昌库里写JAVA10 小时前
iView Table组件二次封装
vue.js·spring boot·毕业设计·layui·课程设计