从零用java实现 小红书 springboot vue uniapp(15) 集成minio存储 支持本地和minio切换
从零用java实现 小红书 springboot vue uniapp(15) 集成minio存储 支持本地和minio切换
移动端演示 http://8.146.211.120:8081/#/
管理端演示 http://8.146.211.120:8088/#/
项目整体介绍及演示
前言
随着我们的“小红书”项目功能逐渐丰富,特别是加入了视频笔记功能后,文件存储成为了一个亟待优化的核心问题。将用户上传的图片、视频等文件直接存储在应用服务器的本地磁盘上,虽然在开发初期简单快捷,但很快会暴露出一系列问题:服务器磁盘空间有限、数据难以迁移和备份、并且不利于未来服务的水平扩展。
为了解决这些问题,我们需要引入对象存储服务。MinIO 是一个开源的高性能对象存储,它与 Amazon S3 API 兼容,可以作为私有云存储的绝佳选择。本文将详细介绍如何在我们的 Spring Boot 项目中集成 MinIO,并设计一个灵活的切换机制,让我们能够一键在本地存储和MinIO存储之间切换,以适应不同的部署环境。
管理端
核心技术实现
我们的目标是让文件上传的业务逻辑与具体的存储方式解耦。无论文件最终存到哪里,对于上层调用者(Controller)来说,方法都是一样的。我们将通过配置、服务封装和策略模式的思想来实现这一目标。
1. 统一化配置:application.yml
配置是实现动态切换的第一步。我们在 application.yml
中定义所有与文件存储相关的参数,并增加一个关键的切换开关:storage-mode
。
dd: # 本地文件存储路径 uploadPath: C:/ddStore/ # 生产环境配置域名,如 example.com,本地开发环境留空 domain: # 是否使用HTTPS,生产环境设为true use-https: false # 文件存储模式:local(本地存储) 或 minio(MinIO对象存储) storage-mode: local# MinIO配置minio: # MinIO服务地址 endpoint: http://192.168.10.105:9000 # 访问密钥 accessKey: admin # 秘密密钥 secretKey: password # 存储桶名称 bucketName: book
dd.storage-mode
: 这是我们的核心开关。当值为local
时,系统使用本地磁盘存储;当值为minio
时,则使用 MinIO。dd.uploadPath
:local
模式下文件的存放根目录。minio.*
: 包含了连接 MinIO 服务所需的所有信息。
2. MinIO 核心服务:MinioService.java
我们首先创建一个 MinioService
,这个类是与 MinIO 服务器直接交互的“专家”。它封装了所有底层的 MinIO API 调用,如检查桶是否存在、上传、删除和获取文件URL等。
MinioService.java
:
@Slf4j@Servicepublic class MinioService { @Autowired private MinioClient minioClient; @Value(\"${minio.bucketName}\") private String bucketName; /** * 上传文件 * * @param file 文件 * @param objectName 在MinIO中存储的对象名称(路径+文件名) * @return 文件访问URL */ public String uploadFile(MultipartFile file, String objectName) { try { // 确保存储桶存在,不存在则自动创建 if (!minioClient.bucketExists(BucketExistsArgs.builder().bucket(bucketName).build())) { minioClient.makeBucket(MakeBucketArgs.builder().bucket(bucketName).build()); } // 执行上传 minioClient.putObject( PutObjectArgs.builder() .bucket(bucketName) .object(objectName) .stream(file.getInputStream(), file.getSize(), -1) .contentType(file.getContentType()) .build() ); log.info(\"文件上传成功 (MinIO): {}\", objectName); // 返回文件的预签名访问URL,有效期7天 return getFileUrl(objectName); } catch (Exception e) { log.error(\"文件上传至MinIO失败: {}\", e.getMessage()); throw new RuntimeException(\"文件上传失败\", e); } } /** * 获取文件访问URL (预签名URL) */ public String getFileUrl(String objectName) { try { return minioClient.getPresignedObjectUrl( GetPresignedObjectUrlArgs.builder() .method(Method.GET) .bucket(bucketName) .object(objectName) .expiry(7, TimeUnit.DAYS) // 7天有效期 .build() ); } catch (Exception e) { // ... } } // ... 其他方法如删除、下载等}
这个服务将复杂的 MinIO SDK 调用包装成了简单的方法,供我们上层的文件服务(FileService
)调用。
3. 策略切换核心:FileServiceImpl.java
这部分是实现存储模式切换的“大脑”。我们创建一个 FileService
接口和它的实现类 FileServiceImpl
。FileServiceImpl
会根据 application.yml
中的 dd.storage-mode
配置来决定具体的文件存储策略。
FileServiceImpl.java
:
@Servicepublic class FileServiceImpl implements FileService { @Value(\"${dd.storage-mode}\") private String storageMode; @Value(\"${dd.uploadPath}\") private String localUploadPath; @Autowired(required = false) // MinIO服务在local模式下可能不启用,允许不注入 private MinioService minioService; @Override public String upload(MultipartFile file, String bizPath) { String finalUrl; // 生成唯一的文件名 String fileName = bizPath + \"/\" + UUID.randomUUID().toString().replace(\"-\", \"\") + file.getOriginalFilename().substring(file.getOriginalFilename().lastIndexOf(\".\")); if (\"minio\".equals(storageMode)) { // MinIO 模式 finalUrl = minioService.uploadFile(file, fileName); } else { // Local 模式 File dest = new File(localUploadPath + fileName); if (!dest.getParentFile().exists()) { dest.getParentFile().mkdirs(); } try { file.transferTo(dest); } catch (IOException e) { throw new RuntimeException(\"本地文件上传失败\", e); } // 构造本地访问URL,需要配合静态资源映射 finalUrl = \"/upload/\" + fileName; } return finalUrl; }}
通过读取 storageMode
变量,upload
方法内部形成了一个 if-else
分支,从而实现了逻辑的动态分派。
4. 控制层调用:FileController.java
最后,我们的 Controller 层保持了极度的简洁。它只负责接收请求和调用 FileService
,完全不知道底层是用本地方式还是 MinIO 方式存储。
FileController.java
:
@RestController@RequestMapping(\"/appUpload\")public class FileController { @Autowired private FileService fileService; @PostMapping(\"/upload\") @ApiOperation(value = \"文件上传\", notes = \"通用文件上传接口\") public ResultBean<String> upload( @RequestParam(value = \"file\") MultipartFile file, @RequestParam(value = \"biz\", required = false, defaultValue = \"common\") String biz) { // 直接调用服务层,无需关心具体实现 String url = fileService.upload(file, biz); if (url != null && !url.isEmpty()) { return ResultBean.success(\"上传成功\", url); } return ResultBean.error(\"上传失败\"); }}
这种设计完美体现了“面向接口编程”和“单一职责”原则。Controller 的职责就是处理HTTP请求,存储的复杂性被完全封装在了服务层。
通过这种方式,我们不仅成功集成了 MinIO,还构建了一个高内聚、低耦合的文件存储系统。未来如果需要支持其他云存储(如阿里云OSS、七牛云Kodo),只需增加一个新的 else if
分支和对应的 Service 即可,对现有业务代码毫无影响。
我们可以根据存在业务表的图片id 关联文件表 然后返回相应的url
代码地址
https://gitee.com/ddeatrr/springboot_vue_xhs