Spring Boot 整合 Minio 实现高效文件存储解决方案(本地和线上)

文章目录

  • 前言
  • 一、配置
    • 1.配置文件:application.yml
    • 2.配置类:MinioProperties
    • 3.工具类:MinioUtil
      • 3.1 初始化方法
      • 3.2 核心功能
      • 3.3 关键技术点
  • 二、使用示例
    • 1.控制器类:FileController
    • 2.服务类
    • 3.效果展示
  • 总结


前言

  • Minio 是一个高性能的分布式对象存储系统,专为云原生应用而设计
  • 作为 Amazon S3 的兼容替代品,它提供了简单易用的 API,支持海量非结构化数据存储
  • 在微服务架构中,文件存储是常见需求,而 Minio 以其轻量级、高可用和易部署的特点成为理想选择

一、配置

1.配置文件:application.yml

vehicle:minio:url: http://localhost:9000 # 连接地址,如果是线上的将:localhost->ipusername: minio # 登录用户名password: 12345678 # 登录密码bucketName: vehicle # 存储文件的桶的名字
  • url:Minio 服务器地址,线上环境替换为实际 IP 或域
  • username/password:Minio 控制台登录凭证
  • bucketName:文件存储桶名称,类似文件夹概念
  • HTTPS 注意:若配置域名访问,URL 需写为 https://your.domain.name:9090

2.配置类:MinioProperties

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;@Component
@Data
@ConfigurationProperties(prefix = "vehicle.minio")
public class MinioProperties {private String url;private String username;private String password;private String bucketName;
}
  • @ConfigurationProperties:将配置文件中的属性绑定到类字段
  • @Component:使该类成为 Spring 管理的 Bean
  • 提供 Minio 连接所需的所有配置参数

3.工具类:MinioUtil

import cn.hutool.core.lang.UUID;
import com.fc.properties.MinioProperties;
import io.minio.*;
import io.minio.errors.*;
import lombok.RequiredArgsConstructor;
import org.apache.commons.compress.utils.IOUtils;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;
import io.minio.http.Method;
import javax.annotation.PostConstruct;
import javax.servlet.http.HttpServletResponse;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.concurrent.TimeUnit;/*** 文件操作工具类*/
@RequiredArgsConstructor
@Component
public class MinioUtil {private final MinioProperties minioProperties;//配置类private MinioClient minioClient;//连接客户端private String bucketName;//桶的名字// 初始化 Minio 客户端@PostConstructpublic void init() {try {//创建客户端minioClient = MinioClient.builder().endpoint(minioProperties.getUrl()).credentials(minioProperties.getUsername(), minioProperties.getPassword()).build();bucketName = minioProperties.getBucketName();// 检查桶是否存在,不存在则创建boolean bucketExists = minioClient.bucketExists(BucketExistsArgs.builder().bucket(bucketName).build());if (!bucketExists) {minioClient.makeBucket(MakeBucketArgs.builder().bucket(bucketName).build());}} catch (Exception e) {throw new RuntimeException("Minio 初始化失败", e);}}/** 上传文件*/public String uploadFile(MultipartFile file,String extension) {if (file == null || file.isEmpty()) {throw new RuntimeException("上传文件不能为空");}try {// 生成唯一文件名String uniqueFilename = 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) {throw new RuntimeException("文件上传失败", 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) {throw new RuntimeException("上传的图片数据不能为空");}if (extension == null || extension.isEmpty()) {throw new IllegalArgumentException("文件扩展名不能为空");}if (contentType == null || contentType.isEmpty()) {throw new IllegalArgumentException("文件 MIME 类型不能为空");}try {// 生成唯一文件名String uniqueFilename = generateUniqueFilename(extension);// 上传到 MinIOminioClient.putObject(PutObjectArgs.builder().bucket(bucketName).object(uniqueFilename).stream(new ByteArrayInputStream(imageData), imageData.length, -1).contentType(contentType).build());return "/" + bucketName + "/" + uniqueFilename;} catch (Exception e) {throw new RuntimeException("处理后的图片上传失败", 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)) {throw new RuntimeException("本地文件不存在");}try (InputStream in = Files.newInputStream(localFile)) {String objectKey = 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) {throw new RuntimeException("Excel 上传失败", e);}}/** 根据URL下载文件*/public void downloadFile(HttpServletResponse response, String fileUrl) {if (fileUrl == null || !fileUrl.contains(bucketName + "/")) {throw new IllegalArgumentException("无效的文件URL");}try {// 从URL中提取对象路径和文件名String objectUrl = fileUrl.split(bucketName + "/")[1];String fileName = objectUrl.substring(objectUrl.lastIndexOf("/") + 1);// 设置响应头response.setContentType("application/octet-stream");String encodedFileName = URLEncoder.encode(fileName, StandardCharsets.UTF_8).replaceAll("\\+", "%20");response.setHeader("Content-Disposition", "attachment; filename=\"" + encodedFileName + "\"");// 下载文件try (InputStream inputStream = minioClient.getObject(GetObjectArgs.builder().bucket(bucketName).object(objectUrl).build());OutputStream outputStream = response.getOutputStream()) {// 用IOUtils.copy高效拷贝(内部缓冲区默认8KB)IOUtils.copy(inputStream, outputStream);}} catch (Exception e) {throw new RuntimeException("文件下载失败", e);}}/*** 根据 MinIO 路径生成带签名的直链* @param objectUrl 已存在的 MinIO 路径(/bucketName/...)* @param minutes   链接有效期(分钟)* @return 可直接访问的 HTTPS 下载地址*/public String parseGetUrl(String objectUrl, int minutes) {if (objectUrl == null || !objectUrl.startsWith("/" + bucketName + "/")) {throw new IllegalArgumentException("非法的 objectUrl");}String objectKey = 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) {throw new RuntimeException("生成直链失败", e);}}/** 根据URL删除文件*/public void deleteFile(String fileUrl) {try {// 从URL中提取对象路径String objectUrl = fileUrl.split(bucketName + "/")[1];minioClient.removeObject(RemoveObjectArgs.builder().bucket(bucketName).object(objectUrl).build());} catch (Exception e) {throw new RuntimeException("文件删除失败", e);}}/** 检查文件是否存在*/public boolean fileExists(String fileUrl) {if (fileUrl == null || !fileUrl.contains(bucketName + "/")) {return false;}try {String objectUrl = fileUrl.split(bucketName + "/")[1];minioClient.statObject(StatObjectArgs.builder().bucket(bucketName).object(objectUrl).build());return true;} catch (ErrorResponseException | InsufficientDataException | InternalException | InvalidKeyException |InvalidResponseException | IOException | NoSuchAlgorithmException | ServerException |XmlParserException e) {if (e instanceof ErrorResponseException && ((ErrorResponseException) e).errorResponse().code().equals("NoSuchKey")) {return false;}throw new RuntimeException("检查文件存在失败", e);}}/*** 生成唯一文件名(带日期路径 + UUID)*/private String generateUniqueFilename(String extension) {String dateFormat = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd"));String uuid = UUID.randomUUID().toString().replace("-", ""); // 去掉 UUID 中的 "-"return dateFormat + "/" + uuid + extension;}
}

3.1 初始化方法

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

  • 创建 MinioClient 客户端实例

  • 检查并创建存储桶(若不存在)

3.2 核心功能

方法名功能描述参数说明返回值
uploadFile()上传MultipartFile文件文件对象,扩展名文件路径
uploadFileByte()上传字节数组字节数据,扩展名,MIME类型文件路径
uploadLocalExcel()上传本地Excel文件文件路径,扩展名文件路径
downloadFile()下载文件到响应流HTTP响应对象,文件URL
parseGetUrl()生成带签名直链文件路径,有效期(分钟)直链URL
deleteFile()删除文件文件URL
fileExists()检查文件是否存在文件URL布尔值

3.3 关键技术点

  • 唯一文件名生成:日期目录/UUID.扩展名 格式避免重名

  • 大文件流式传输:避免内存溢出

  • 响应头编码处理:解决中文文件名乱码问题

  • 异常统一处理:Minio 异常转换为运行时异常

  • 预签名URL:生成临时访问链接

二、使用示例

1.控制器类:FileController

import com.fc.result.Result;
import com.fc.service.FileService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;import javax.servlet.http.HttpServletResponse;
import java.io.IOException;@Api(tags = "文件")
@RestController
@RequestMapping("/file")
@RequiredArgsConstructor
public class FileController {private final FileService fileService;@ApiOperation("图片上传")@PostMapping("/image")public Result<String> imageUpload(MultipartFile file) throws IOException {String url = fileService.imageUpload(file);return Result.success(url);}@ApiOperation("图片下载")@GetMapping("/image")public void imageDownLoad(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

import org.springframework.web.multipart.MultipartFile;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;public interface FileService {String imageUpload(MultipartFile file) throws IOException;void imageDownload(HttpServletResponse response, String url) throws IOException;void imageDelete(String url);
}

FileServiceImpl

import com.fc.exception.FileException;
import com.fc.service.FileService;
import com.fc.utils.ImageUtil;
import com.fc.utils.MinioUtil;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;@Service
@RequiredArgsConstructor
public class FileServiceImpl implements FileService {private final MinioUtil minioUtil;@Overridepublic String imageUpload(MultipartFile file) throws IOException {byte[] bytes = ImageUtil.compressImage(file, "JPEG");return minioUtil.uploadFileByte(bytes, ".jpeg", "image/jpeg");}@Overridepublic void imageDownload(HttpServletResponse response, String url) throws IOException {minioUtil.downloadFile(response, url);}@Overridepublic void imageDelete(String url) {if (!minioUtil.fileExists(url)) {throw new FileException("文件不存在");}minioUtil.deleteFile(url);}
}

3.效果展示

利用Apifox测试下三个接口

图片上传

在这里插入图片描述
在这里插入图片描述

图片下载
图片下载
删除图片
在这里插入图片描述
在这里插入图片描述

总结

本文通过 “配置 - 工具 - 业务” 三层架构,实现了 Spring Boot 与 MinIO 的集成,核心优势如下:

  • 易用性:通过配置绑定和工具类封装,简化 MinIO 操作,开发者无需关注底层 API 细节。
  • 灵活性:支持多种文件类型(表单文件、字节流、本地文件),满足不同场景需求(如图片压缩、Excel 生成)。
  • 可扩展性:可基于此框架扩展功能,如添加文件权限控制(通过 MinIO 的 Policy)、文件分片上传(大文件处理)、定期清理过期文件等。

MinIO 作为轻量级对象存储方案,非常适合中小项目替代本地存储或云厂商 OSS(降低成本)。实际应用中需注意:生产环境需配置 MinIO 集群确保高可用;敏感文件需通过预签名 URL 控制访问权限;定期备份桶数据以防丢失。通过本文的方案,开发者可快速搭建稳定、可扩展的文件存储服务,为应用提供可靠的非结构化数据管理能力。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.pswp.cn/news/917919.shtml
繁体地址,请注明出处:http://hk.pswp.cn/news/917919.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【Unity3D实例-功能-镜头】第三人称视觉-镜头优化

这一篇我们一起来调整一下Cinemachine的第三人称视觉的镜头设置。一般用于ARPG角色扮演游戏的场景中。Unity里头&#xff0c;这种视角简直就是标配。来吧&#xff0c;咱们一起研究研究怎么调出这种视角效果&#xff01;目录&#xff1a;1.调整虚拟摄像机的Y轴2.调整虚拟摄像机的…

二叉树算法之【中序遍历】

目录 LeetCode-94题 LeetCode-94题 给定一个二叉树的根节点root&#xff0c;返回它的中序遍历结果。 class Solution {public List<Integer> inorderTraversal(TreeNode root) {List<Integer> result new ArrayList<>();order(root, result);return res…

Android14的QS面板的加载解析

/frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java QS 面板的创建 getNotificationShadeWindowView()&#xff1a;整个systemui的最顶级的视图容器&#xff08;super_notification_shade.xml&#xff09;R.id.qs_frame &…

解锁webpack核心技能(二):配置文件和devtool配置指南

一、配置文件webpack 提供的 cli 支持很多的参数&#xff0c;例如 --mode 。在我们平时的开发过程中&#xff0c;我们要学习很多的功能&#xff0c;这些很多都是可以用参数来完成的。那么后边就会导致参数越来越多&#xff0c;我们使用命令特别的不方便&#xff0c;所以我们会使…

Gitlab+Jenkins+K8S+Registry 建立 CI/CD 流水线

一、前言 DevOps是一种将开发&#xff08;Development&#xff09;和运维&#xff08;Operations&#xff09;相结合的软件开发方法论。它通过自动化和持续交付的方式&#xff0c;将软件开发、测试和部署等环节紧密集成&#xff0c;以提高效率和产品质量。在本篇博客中&#xf…

【Linux】特效爆满的Vim的配置方法 and make/Makefile原理

一、软件包管理器 1、Linux下安装软件的常见方式&#xff1a; 1&#xff09;源代码安装——不推荐。 2&#xff09;rpm包安装——不推荐。 3&#xff09;包管理器安装——推荐 2、安装软件命令 # Centos$ sudo yum install -y lrzsz# Ubuntu$ sudo apt install -y lrzsz 3、卸…

Spring Boot Actuator 监控功能的简介及禁用

Spring Boot Actuator: Production-ready Features 1. 添加依赖 <dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-actuator</artifactId></dependency> </dependencie…

Matlab(1)

一、基本操作1. matlab四则运算规则&#xff1a;先乘除后加减&#xff0c;从左到右2、对数和指数的表示sin(pi^0.5)log(tan(1))exp&#xff08;sin&#xff08;10&#xff09;&#xff09;3、类型&#xff1a;matlab变量默认为double4、who&whos&#xff1a;命令行输入who&…

Kotlin Android 开发脚手架封装

Kotlin Android 开发脚手架封装&#xff08;模块化版本&#xff09; 我将按照模块化设计原则&#xff0c;将脚手架拆分为多个文件&#xff0c;每个文件负责特定功能领域&#xff1a; 1. 核心初始化模块 文件路径: core/AppScaffold.kt object AppScaffold {lateinit var contex…

Flutter 报错解析:No TabController for TabBar 的完整解决方案

目录 Flutter 报错解析&#xff1a;No TabController for TabBar 的完整解决方案 一、错误场景&#xff1a;当 TabBar 失去 "指挥官" 二、为什么 TabBar 必须依赖 Controller&#xff1f; 1. TabBar 与 TabController 的协作关系 2. 状态管理的核心作用 3. 实战…

【24】C++实战篇——【 C++ 外部变量】 C++多个文件共用一个枚举变量,外部变量 extern,枚举外部变量 enum

文章目录1 方法2 外部变量 应用2.1 普通外部全局变量2.2 枚举外部全局变量 应用2.2.2 枚举外部变量优化c多个文件中如何共用一个全局变量 c头文件的使用和多个文件中如何共用一个全局变量 C共享枚举类型给QML 1 方法 ①头文件中 声明外部全局变量&#xff1b; ②在头文件对…

Linux SELinux 核心概念与管理

Linux SELinux 核心概念与管理一、SELinux 基本概念 SELinux 即安全增强型 Linux&#xff08;Security-Enhanced Linux&#xff09;&#xff0c;由美国国家安全局&#xff08;NSA&#xff09;开发&#xff0c;是一套基于强制访问控制&#xff08;MAC&#xff09;的安全机制&…

Git 中**未暂存**和**未跟踪**的区别:

文件状态分类 Git 中的文件有以下几种状态&#xff1a; 工作区文件状态&#xff1a; ├── 未跟踪 (Untracked) ├── 已跟踪 (Tracked)├── 未修改 (Unmodified) ├── 已修改未暂存 (Modified/Unstaged)└── 已暂存 (Staged)1. 未跟踪 (Untracked) 定义&#xff1a;Gi…

前端1.0

目录 一、 什么是前端 二、 HTML 1.0 概述 2.0 注释 三、开发环境的搭建 1.0 插件 2.0 笔记 四、 常见标签&#xff08;重点&#xff09; 四、案例展示&#xff08;图片代码&#xff09; 五、CSS引入 一、 什么是前端 web前端 用来直接给用户呈现一个一个的网页 …

Flutter镜像替换

一、核心镜像替换&#xff08;针对 Maven 仓库&#xff09; Flutter 依赖的 Google Maven 仓库&#xff08;https://maven.google.com 或 https://dl.google.com/dl/android/maven2&#xff09;可替换为国内镜像&#xff0c;常见的有&#xff1a;阿里云镜像&#xff08;推荐&am…

MATLAB实现的改进遗传算法用于有约束优化问题

基于MATLAB实现的改进遗传算法&#xff08;GA&#xff09;用于有约束优化问题的代码&#xff0c;包括处理非线性约束。此代码通过引入惩罚函数和修复机制&#xff0c;有效处理约束条件&#xff0c;提高算法的鲁棒性和收敛速度。 1. 定义优化问题 % 定义目标函数 function f ob…

Qt子类化QWidget后,使用setStyleSheet设置样式无效的解决方案

关键代码&#xff1a; #include <QPainter> #include <QStyleOption>void paintEvent(QPaintEvent *e) {QStyleOption opt;opt.init(this);QPainter p(this);style()->drawPrimitive(QStyle::PE_Widget, &opt, &p, this);QWidget::paintEvent(e); }定义…

【python中级】关于Flask服务在同一系统里如何只被运行一次

【python中级】关于Flask服务在同一系统里如何只被运行一次 1.背景 2.方案1 2.方案2 1.背景 python Flask实现的一个http服务,打包成应用程序exe后在windows10系统运行; 由于我会不断的更新这个http服务,我希望运行这个http服务的时候之前的http服务被停掉; 即实现 Pytho…

git配置公钥/密钥

遇到 “gitgithub.com: Permission denied (publickey)” 错误通常意味着你尝试通过 SSH 连接到 GitHub 时&#xff0c;SSH 密钥没有被正确设置或者 GitHub 无法识别你的公钥。这里有几个步骤可以帮助你解决这个问题&#xff1a; 检查 SSH 密钥 首先&#xff0c;确保你已经在本…

【机器学习】“回归“算法模型的三个评估指标:MAE(衡量预测准确性)、MSE(放大大误差)、R²(说明模型解释能力)

文章目录一、MAE、MSE、r概念说明二、MAE&#xff08;平均绝对误差&#xff09;&#xff1a;用"房价预测"理解误差测量三、MSE&#xff08;均方误差&#xff09;&#xff1a;误差的"放大镜"1、概念说明2、 sklearn代码实践3、流程总结四、R&#xff1a;理解…