目录
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 桶的数量取决于具体的业务需求和技术架构设计,以下是不同场景的实践建议:
- 小型项目或简单业务在配置文件中配置一个桶即可,简单高效,维护成本低,但是灵活性差。
- 中大型项目或复杂业务需要权限控制、数据隔离和多租户情况时,可以再配置文件中预设高频使用的桶(例如用户头像、日志文件等),在接口中动态制定低频使用的桶(租户个人空间)
以下是部分示例:
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验证删除存储桶:
编辑
桶已删除
编辑
再次调用返回该桶已不存在
编辑