大数跨境
0
0

SpringBoot+Minio实现高效文件存储解决方案

SpringBoot+Minio实现高效文件存储解决方案 终码一生
2025-09-02
0
点击“终码一生”,关注,置顶公众号
每日技术干货,第一时间送达!
  • Minio 是一个高性能的分布式对象存储系统,专为云原生应用而设计
  • 作为 Amazon S3 的兼容替代品,它提供了简单易用的 API,支持海量非结构化数据存储
  • 在微服务架构中,文件存储是常见需求,而 Minio 以其轻量级、高可用和易部署的特点成为理想选择
01
配置
图片
1.配置文件:application.yml
vehicle:
  minio:
    url:http://localhost:9000# 连接地址,如果是线上的将:localhost->ip
    username:minio# 登录用户名
    password:12345678# 登录密码
    bucketName:vehicle# 存储文件的桶的名字
  • url: Minio 服务器地址,线上环境替换为实际 IP 或域
  • username/password: Minio 控制台登录凭证
  • bucketName: 文件存储桶名称,类似文件夹概念
  • HTTPS注意:若配置域名访问,URL 需写为 https://your.domain.name:9090
2.配置类:MinioProperties

@Component
@Data
@ConfigurationProperties(prefix = "vehicle.minio")
publicclassMinioProperties {
    private String url;
    private String username;
    private String password;
    private String bucketName;
}
  • @ConfigurationProperties:将配置文件中的属性绑定到类字段
  • @Component:使该类成为 Spring 管理的 Bean
  • 提供 Minio 连接所需的所有配置参数
3.工具类:MinioUtil

/**
 * 文件操作工具类
 */

@RequiredArgsConstructor
@Component
publicclassMinioUtil {
    privatefinal MinioProperties minioProperties;//配置类
    private MinioClient minioClient;//连接客户端
    private String bucketName;//桶的名字
    // 初始化 Minio 客户端
    @PostConstruct
    publicvoidinit() {
        try {
            //创建客户端
            minioClient = MinioClient.builder()
                    .endpoint(minioProperties.getUrl())
                    .credentials(minioProperties.getUsername(), minioProperties.getPassword())
                    .build();
            bucketName = minioProperties.getBucketName();

            // 检查桶是否存在,不存在则创建
            booleanbucketExists= minioClient.bucketExists(BucketExistsArgs.builder().bucket(bucketName).build());
            if (!bucketExists) {
                minioClient.makeBucket(MakeBucketArgs.builder().bucket(bucketName).build());
            }
        } catch (Exception e) {
            thrownewRuntimeException("Minio 初始化失败", e);
        }
    }

    /*
     * 上传文件
     */

    public String uploadFile(MultipartFile file,String extension) {
        if (file == null || file.isEmpty()) {
            thrownewRuntimeException("上传文件不能为空");
        }

        try {
            // 生成唯一文件名
            StringuniqueFilename= generateUniqueFilename(extension);

            // 上传文件
            minioClient.putObject(PutObjectArgs.builder()
                    .bucket(bucketName)
                    .object(uniqueFilename)
                    .stream(file.getInputStream(), file.getSize(), -1)
                    .contentType(file.getContentType())
                    .build());

            return"/" + bucketName + "/" + uniqueFilename;
        } catch (Exception e) {
            thrownewRuntimeException("文件上传失败", e);
        }
    }



    /**
     * 上传已处理的图片字节数组到 MinIO
     *
     * @param imageData 处理后的图片字节数组
     * @param extension 文件扩展名(如 ".jpg", ".png")
     * @param contentType 文件 MIME 类型(如 "image/jpeg", "image/png")
     * @return MinIO 中的文件路径(格式:/bucketName/yyyy-MM-dd/uuid.extension)
     */

    public String uploadFileByte(byte[] imageData, String extension, String contentType) {
        if (imageData == null || imageData.length == 0) {
            thrownewRuntimeException("上传的图片数据不能为空");
        }
        if (extension == null || extension.isEmpty()) {
            thrownewIllegalArgumentException("文件扩展名不能为空");
        }
        if (contentType == null || contentType.isEmpty()) {
            thrownewIllegalArgumentException("文件 MIME 类型不能为空");
        }

        try {
            // 生成唯一文件名
            StringuniqueFilename= generateUniqueFilename(extension);

            // 上传到 MinIO
            minioClient.putObject(
                    PutObjectArgs.builder()
                            .bucket(bucketName)
                            .object(uniqueFilename)
                            .stream(newByteArrayInputStream(imageData), imageData.length, -1)
                            .contentType(contentType)
                            .build()
            );

            return"/" + bucketName + "/" + uniqueFilename;
        } catch (Exception e) {
            thrownewRuntimeException("处理后的图片上传失败", e);
        }
    }

    /**
     * 上传本地生成的 Excel 临时文件到 MinIO
     * @param localFile  本地临时文件路径
     * @param extension 扩展名
     * @return MinIO 存储路径,格式:/bucketName/yyyy-MM-dd/targetName
     */

    public String uploadLocalExcel(Path localFile, String extension) {
        if (localFile == null || !Files.exists(localFile)) {
            thrownewRuntimeException("本地文件不存在");
        }
        try (InputStreamin= Files.newInputStream(localFile)) {
            StringobjectKey= generateUniqueFilename(extension); // 保留日期目录
            minioClient.putObject(
                    PutObjectArgs.builder()
                            .bucket(bucketName)
                            .object(objectKey)
                            .stream(in, Files.size(localFile), -1)
                            .contentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")
                            .build());
            return"/" + bucketName + "/" + objectKey;
        } catch (Exception e) {
            thrownewRuntimeException("Excel 上传失败", e);
        }
    }

    /*
     * 根据URL下载文件
     */

    publicvoiddownloadFile(HttpServletResponse response, String fileUrl) {
        if (fileUrl == null || !fileUrl.contains(bucketName + "/")) {
            thrownewIllegalArgumentException("无效的文件URL");
        }

        try {
            // 从URL中提取对象路径和文件名
            StringobjectUrl= fileUrl.split(bucketName + "/")[1];
            StringfileName= objectUrl.substring(objectUrl.lastIndexOf("/") + 1);

            // 设置响应头
            response.setContentType("application/octet-stream");
            StringencodedFileName= URLEncoder.encode(fileName, StandardCharsets.UTF_8).replaceAll("\\+""%20");
            response.setHeader("Content-Disposition""attachment; filename=\"" + encodedFileName + "\"");

            // 下载文件
            try (InputStreaminputStream= minioClient.getObject(GetObjectArgs.builder()
                    .bucket(bucketName)
                    .object(objectUrl)
                    .build());
                 OutputStreamoutputStream= response.getOutputStream()) {

                // 用IOUtils.copy高效拷贝(内部缓冲区默认8KB)
                IOUtils.copy(inputStream, outputStream);
            }
        } catch (Exception e) {
            thrownewRuntimeException("文件下载失败", e);
        }
    }

    /**
     * 根据 MinIO 路径生成带签名的直链
     * @param objectUrl 已存在的 MinIO 路径(/bucketName/...)
     * @param minutes   链接有效期(分钟)
     * @return 可直接访问的 HTTPS 下载地址
     */

    public String parseGetUrl(String objectUrl, int minutes) {
        if (objectUrl == null || !objectUrl.startsWith("/" + bucketName + "/")) {
            thrownewIllegalArgumentException("非法的 objectUrl");
        }
        StringobjectKey= objectUrl.substring(("/" + bucketName + "/").length());
        try {
            return minioClient.getPresignedObjectUrl(
                    GetPresignedObjectUrlArgs.builder()
                            .method(Method.GET)
                            .bucket(bucketName)
                            .object(objectKey)
                            .expiry(minutes, TimeUnit.MINUTES)
                            .build());
        } catch (Exception e) {
            thrownewRuntimeException("生成直链失败", e);
        }
    }

    /*
     * 根据URL删除文件
     */

    publicvoiddeleteFile(String fileUrl) {
        try {
            // 从URL中提取对象路径
            StringobjectUrl= fileUrl.split(bucketName + "/")[1];
            minioClient.removeObject(RemoveObjectArgs.builder()
                    .bucket(bucketName)
                    .object(objectUrl)
                    .build());
        } catch (Exception e) {
            thrownewRuntimeException("文件删除失败", e);
        }
    }

    /*
     * 检查文件是否存在
     */

    publicbooleanfileExists(String fileUrl) {
        if (fileUrl == null || !fileUrl.contains(bucketName + "/")) {
            returnfalse;
        }

        try {
            StringobjectUrl= fileUrl.split(bucketName + "/")[1];
            minioClient.statObject(StatObjectArgs.builder()
                    .bucket(bucketName)
                    .object(objectUrl)
                    .build());
            returntrue;
        } catch (ErrorResponseException | InsufficientDataException | InternalException | InvalidKeyException |
                 InvalidResponseException | IOException | NoSuchAlgorithmException | ServerException |
                 XmlParserException e) {
            if (e instanceof ErrorResponseException && ((ErrorResponseException) e).errorResponse().code().equals("NoSuchKey")) {
                returnfalse;
            }
            thrownewRuntimeException("检查文件存在失败", e);
        }
    }


    /**
     * 生成唯一文件名(带日期路径 + UUID)
     */

    private String generateUniqueFilename(String extension) {
        StringdateFormat= LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd"));
        Stringuuid= UUID.randomUUID().toString().replace("-"""); // 去掉 UUID 中的 "-"
        return dateFormat + "/" + uuid + extension;
    }
}
3.1 初始化方法

使用 @PostConstruct 在 Bean 初始化后自动执行

创建 MinioClient 客户端实例

检查并创建存储桶(若不存在)
3.2 核心功能
3.3 关键技术点
  • 唯一文件名生成: 日期目录/UUID.扩展名 格式避免重名
  • 大文件流式传输: 避免内存溢出
  • 响应头编码处理: 解决中文文件名乱码问题
  • 异常统一处理: Minio 异常转换为运行时异常
  • 预签名URL: 生成临时访问链接
图片
02
使用示例
图片
1.控制器类:FileController

@Api(tags = "文件")
@RestController
@RequestMapping("/file")
@RequiredArgsConstructor
publicclassFileController {

    privatefinal FileService fileService;

    @ApiOperation("图片上传")
    @PostMapping("/image")
    public Result<String> imageUpload(MultipartFile file)throws IOException {
        Stringurl= fileService.imageUpload(file);
        return Result.success(url);
    }

    @ApiOperation("图片下载")
    @GetMapping("/image")
    publicvoidimageDownLoad(HttpServletResponse response, String url)throws IOException {
        fileService.imageDownload(response, url);
    }

    @ApiOperation("图片删除")
    @DeleteMapping("/image")
    public Result<Void> imageDelete(String url) {
        fileService.imageDelete(url);
        return Result.success();
    }

}
2.服务类
FileService

publicinterfaceFileService {

    String imageUpload(MultipartFile file)throws IOException;

    voidimageDownload(HttpServletResponse response, String url)throws IOException;

    voidimageDelete(String url);
}
FileServiceImpl

@Service
@RequiredArgsConstructor
publicclassFileServiceImplimplementsFileService {
    privatefinal MinioUtil minioUtil;

    @Override
    public String imageUpload(MultipartFile file)throws IOException {
        byte[] bytes = ImageUtil.compressImage(file, "JPEG");
        return minioUtil.uploadFileByte(bytes, ".jpeg""image/jpeg");
    }

    @Override
    publicvoidimageDownload(HttpServletResponse response, String url)throws IOException {
        minioUtil.downloadFile(response, url);
    }

    @Override
    publicvoidimageDelete(String url) {
        if (!minioUtil.fileExists(url)) {
            thrownewFileException("文件不存在");
        }
        minioUtil.deleteFile(url);
    }
}
3.效果展示
利用Apifox测试下三个接口
图片上传
图片
图片下载
图片
删除图片
图片
图片
03
总结
图片
本文通过 “配置 - 工具 - 业务” 三层架构,实现了 Spring Boot 与 MinIO 的集成,核心优势如下:
  • 易用性: 通过配置绑定和工具类封装,简化 MinIO 操作,开发者无需关注底层 API 细节。
  • 灵活性: 支持多种文件类型(表单文件、字节流、本地文件),满足不同场景需求(如图片压缩、Excel 生成)。
  • 可扩展性: 可基于此框架扩展功能,如添加文件权限控制(通过 MinIO 的 Policy)、文件分片上传(大文件处理)、定期清理过期文件等。
MinIO 作为轻量级对象存储方案,非常适合中小项目替代本地存储或云厂商 OSS(降低成本)。实际应用中需注意:生产环境需配置 MinIO 集群确保高可用;敏感文件需通过预签名 URL 控制访问权限;定期备份桶数据以防丢失。通过本文的方案,开发者可快速搭建稳定、可扩展的文件存储服务,为应用提供可靠的非结构化数据管理能力。
来源:https://blog.csdn.net/y_wu794/
END
PS:防止找不到本篇文章,可以收藏点赞,方便翻阅查找哦。



往期推荐



项目自从用了接口请求合并,效率直接加倍!

为什么要尽量避免使用 IN 和 NOT IN 呢?

微软发布史上最强虚拟机!流畅度堪比主机,附保姆级安装教程

SpringBoot + ResponseBodyEmitter 实时异步流式推送,优雅!

JEnv:新一代Java环境管理器,让多版本Java管理变得简单高效!

全网最全,部署一套完整的 Prometheus+Grafana 智能监控告警系统


【声明】内容源于网络
0
0
终码一生
开发者聚集地。分享Java相关开发技术(JVM,多线程,高并发,性能调优等),开源项目,常见开发问题和前沿科技资讯!
内容 1876
粉丝 0
终码一生 开发者聚集地。分享Java相关开发技术(JVM,多线程,高并发,性能调优等),开源项目,常见开发问题和前沿科技资讯!
总阅读1.1k
粉丝0
内容1.9k