设为首页收藏本站
网站公告 | 这是第一条公告
     

 找回密码
 立即注册
缓存时间18 现在时间18 缓存数据 从头到尾 我要的只有感情 可没人能给我

从头到尾 我要的只有感情 可没人能给我 -- 情深深雨濛濛

查看: 599|回复: 0

SpringBoot封装MinIO工具的实现步骤

[复制链接]

  离线 

TA的专栏

  • 打卡等级:热心大叔
  • 打卡总天数:244
  • 打卡月天数:1
  • 打卡总奖励:3998
  • 最近打卡:2025-12-11 06:38:54
等级头衔

等級:晓枫资讯-上等兵

在线时间
4 小时

积分成就
威望
0
贡献
442
主题
402
精华
0
金钱
5356
积分
932
注册时间
2023-1-7
最后登录
2025-12-11

发表于 2025-8-27 19:58:29 | 显示全部楼层 |阅读模式

MinIO 简介

MinIO 是一款高性能、开源、兼容Amazon S3 API的分布式对象存储系统,专为云原生架构和大规模非结构化数据场景设计。其核心定位是成为私有云/混合云环境中的标准存储方案,适用于从数据湖到AI/ML、容器化部署等多样化需求。

背景

为解决业务开时对接 MinIO繁琐接口对工作

目录结构

  • cdkj-minio MinIO工具

    • annotation 注解

      • EnableAutoMinio 启用自动MinIO
    • config 配置

      • MinioAutoConfiguration MinIO 自动配置
      • MinioMarkerConfiguration MinIO 标记配置
      • MinioProperties MinIO 配置读取
    • connectivity 连接库

      • MinioConfiguration MinIO 配置
    • enums 枚举库

      • ContentTypeEnums 内容类型枚举
    • MinioUtils MinIO工具库

项目介绍

POM引入包

  1. <dependencies>
  2. <!-- MinIO -->
  3. <dependency>
  4. <groupId>io.minio</groupId>
  5. <artifactId>minio</artifactId>
  6. <exclusions>
  7. <exclusion>
  8. <groupId>com.squareup.okhttp3</groupId>
  9. <artifactId>okhttp</artifactId>
  10. </exclusion>
  11. </exclusions>
  12. </dependency>
  13. <dependency>
  14. <groupId>com.squareup.okhttp3</groupId>
  15. <artifactId>okhttp</artifactId>
  16. </dependency>
  17. </dependencies>
复制代码

EnableAutoMinio 启用自动MinIO

spring boot 启动加载工具包

  1. package com.cdkjframework.minio.annotation;
  2. import com.cdkjframework.minio.config.MinioMarkerConfiguration;
  3. import org.springframework.context.annotation.Import;
  4. import java.lang.annotation.*;
  5. /**
  6. * @ProjectName: cdkjframework
  7. * @Package: com.cdkjframework.minio.annotation
  8. * @ClassName: EnableAutoMinio
  9. * @Description: java类作用描述
  10. * @Author: xiaLin
  11. * @Date: 2024/9/2 11:27
  12. * @Version: 1.0
  13. */
  14. @Target(ElementType.TYPE)
  15. @Retention(RetentionPolicy.RUNTIME)
  16. @Documented
  17. @Import({MinioMarkerConfiguration.class})
  18. public @interface EnableAutoMinio {
  19. }
复制代码

Spring Boot 项目引入

  1. package cn.qjwxlcps.ims.annotation;
  2. import com.cdkjframework.cloud.job.core.handler.annotation.EnableAutoCdkjJob;
  3. import com.cdkjframework.datasource.mongodb.annotation.EnableAutoMongo;
  4. import com.cdkjframework.minio.annotation.EnableAutoMinio;
  5. import com.cdkjframework.swagger.annotation.EnableAutoSwagger;
  6. import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
  7. import org.springframework.cloud.openfeign.EnableFeignClients;
  8. import org.springframework.context.annotation.Configuration;
  9. import org.springframework.retry.annotation.EnableRetry;
  10. import org.springframework.scheduling.annotation.EnableAsync;
  11. import org.springframework.transaction.annotation.EnableTransactionManagement;
  12. import java.lang.annotation.ElementType;
  13. import java.lang.annotation.Retention;
  14. import java.lang.annotation.RetentionPolicy;
  15. import java.lang.annotation.Target;
  16. /**
  17. * @ProjectName: com.lesmarthome.interface
  18. * @Package: com.lesmarthome.interfaces.annotation
  19. * @ClassName: EnableAutoApi
  20. * @Description: java类作用描述
  21. * @Author: xiaLin
  22. * @Date: 2024/3/29 16:02
  23. * @Version: 1.0
  24. */
  25. @Target(ElementType.TYPE)
  26. @Retention(RetentionPolicy.RUNTIME)
  27. @EnableRetry
  28. @EnableAsync
  29. @Configuration
  30. @EnableAutoMinio
  31. @EnableAutoMongo
  32. @EnableAutoCdkjJob
  33. //@EnableAutoSwagger
  34. @EnableDiscoveryClient
  35. @EnableTransactionManagement
  36. @EnableFeignClients(basePackages = {"com.*.*.client"})
  37. public @interface EnableAutoApi {
  38. }
复制代码

MinIO 配置读取

该类主要读取用户配置信息

  1. package com.cdkjframework.minio.config;
  2. import lombok.Data;
  3. import org.springframework.boot.context.properties.ConfigurationProperties;
  4. import org.springframework.cloud.context.config.annotation.RefreshScope;
  5. import org.springframework.context.annotation.Configuration;
  6. /**
  7. * @ProjectName: cdkjframework
  8. * @Package: com.cdkjframework.minio.config
  9. * @ClassName: MinioProperties
  10. * @Description: mini配置
  11. * @Author: xiaLin
  12. * @Date: 2024/9/2 11:28
  13. * @Version: 1.0
  14. */
  15. @Data
  16. @RefreshScope
  17. @Configuration
  18. @ConfigurationProperties(prefix = "spring.minio")
  19. public class MinioProperties {
  20. /**
  21. * 访问域名
  22. */
  23. private String domain;
  24. /**
  25. * 存储端点
  26. */
  27. private String endpoint;
  28. /**
  29. * 端口
  30. */
  31. private Integer port;
  32. /**
  33. * 访问密钥
  34. */
  35. private String accessKey;
  36. /**
  37. * 密钥
  38. */
  39. private String secretKey;
  40. /**
  41. * 存储桶名称
  42. */
  43. private String bucketName;
  44. /**
  45. * 分片对象过期时间 单位(天)
  46. */
  47. private Integer expiry;
  48. /**
  49. * 断点续传有效时间,在redis存储任务的时间 单位(天)
  50. */
  51. private Integer breakpointTime;
  52. }
复制代码

内容类型枚举

  1. package com.cdkjframework.minio.enums;
  2. import com.cdkjframework.constant.IntegerConsts;
  3. import com.cdkjframework.util.tool.StringUtils;
  4. /**
  5. * @ProjectName: cdkjframework
  6. * @Package: com.cdkjframework.minio.enums
  7. * @ClassName: ContentTypeEnums
  8. * @Description: 内容类型枚举
  9. * @Author: xiaLin
  10. * @Date: 2024/9/2 13:30
  11. * @Version: 1.0
  12. */
  13. public enum ContentTypeEnums {
  14. /**
  15. * 默认类型
  16. */
  17. DEFAULT("default", "application/octet-stream"),
  18. JPG("jpg", "image/jpeg"),
  19. TIFF("tiff", "image/tiff"),
  20. GIF("gif", "image/gif"),
  21. JFIF("jfif", "image/jpeg"),
  22. PNG("png", "image/png"),
  23. TIF("tif", "image/tiff"),
  24. ICO("ico", "image/x-icon"),
  25. JPEG("jpeg", "image/jpeg"),
  26. WBMP("wbmp", "image/vnd.wap.wbmp"),
  27. FAX("fax", "image/fax"),
  28. NET("net", "image/pnetvue"),
  29. JPE("jpe", "image/jpeg"),
  30. RP("rp", "image/vnd.rn-realpix"),
  31. MP4("mp4", "video/mp4");
  32. /**
  33. * 文件名后缀
  34. */
  35. private final String suffix;
  36. /**
  37. * 返回前端请求头中,Content-Type具体的值
  38. */
  39. private final String value;
  40. ContentTypeEnums(String suffix, String value) {
  41. this.suffix = suffix;
  42. this.value = value;
  43. }
  44. /**
  45. * 根据文件后缀,获取Content-Type
  46. *
  47. * @param suffix 文件后缀
  48. * @return 返回结果
  49. */
  50. public static String formContentType(String suffix) {
  51. if (StringUtils.isNullAndSpaceOrEmpty(suffix)) {
  52. return DEFAULT.getValue();
  53. }
  54. int beginIndex = suffix.lastIndexOf(StringUtils.POINT) + IntegerConsts.ONE;
  55. suffix = suffix.substring(beginIndex);
  56. for (ContentTypeEnums value : ContentTypeEnums.values()) {
  57. if (suffix.equalsIgnoreCase(value.getSuffix())) {
  58. return value.getValue();
  59. }
  60. }
  61. return DEFAULT.getValue();
  62. }
  63. public String getSuffix() {
  64. return suffix;
  65. }
  66. public String getValue() {
  67. return value;
  68. }
  69. }
复制代码

MinIO 标记配置

  1. package com.cdkjframework.minio.config;
  2. import org.springframework.context.annotation.Bean;
  3. import org.springframework.context.annotation.Configuration;
  4. /**
  5. * @ProjectName: cdkjframework
  6. * @Package: com.cdkjframework.minio.config
  7. * @ClassName: MinioMarkerConfiguration
  8. * @Description: Minio标记配置
  9. * @Author: xiaLin
  10. * @Date: 2024/9/2 11:28
  11. * @Version: 1.0
  12. */
  13. @Configuration(proxyBeanMethods = false)
  14. public class MinioMarkerConfiguration {
  15. @Bean
  16. public Marker mybatisMarker() {
  17. return new Marker();
  18. }
  19. public static class Marker {
  20. }
  21. }
复制代码

MinIO 自动配置

注入 Bean 之前需要先加载配置 MinioProperties,且基于Bean的条件 MinioMarkerConfiguration.Marker 完成。开始调用 MinioConfiguration 中的start Bean方法。

  1. package com.cdkjframework.minio.config;
  2. import com.cdkjframework.minio.connectivity.MinioConfiguration;
  3. import lombok.RequiredArgsConstructor;
  4. import org.springframework.boot.autoconfigure.AutoConfigureAfter;
  5. import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
  6. import org.springframework.boot.autoconfigure.web.reactive.function.client.WebClientAutoConfiguration;
  7. import org.springframework.boot.context.properties.EnableConfigurationProperties;
  8. import org.springframework.cloud.context.config.annotation.RefreshScope;
  9. import org.springframework.context.annotation.Bean;
  10. import org.springframework.context.annotation.Configuration;
  11. import org.springframework.context.annotation.Lazy;
  12. /**
  13. * @ProjectName: cdkjframework
  14. * @Package: com.cdkjframework.minio.config
  15. * @ClassName: MinioAutoConfiguration
  16. * @Description: Minio自动配置
  17. * @Author: xiaLin
  18. * @Date: 2024/9/2 11:29
  19. * @Version: 1.0
  20. */
  21. @Lazy(false)
  22. @RequiredArgsConstructor
  23. @Configuration(proxyBeanMethods = false)
  24. @EnableConfigurationProperties({
  25. MinioProperties.class
  26. })
  27. @AutoConfigureAfter({WebClientAutoConfiguration.class})
  28. @ConditionalOnBean(MinioMarkerConfiguration.Marker.class)
  29. public class MinioAutoConfiguration {
  30. /**
  31. * 配置信息
  32. */
  33. private final MinioProperties minioProperties;
  34. /**
  35. * minio配置
  36. *
  37. * @return 返回配置信息
  38. */
  39. @Bean(initMethod = "start")
  40. public MinioConfiguration minioConfiguration() {
  41. return new MinioConfiguration(minioProperties);
  42. }
  43. }
复制代码

MinIO 配置

  1. package com.cdkjframework.minio.connectivity;
  2. import com.cdkjframework.minio.MinioUtils;
  3. import com.cdkjframework.minio.config.MinioProperties;
  4. import io.minio.MinioClient;
  5. import org.springframework.context.annotation.Bean;
  6. /**
  7. * @ProjectName: cdkjframework
  8. * @Package: com.cdkjframework.minio.connectivity
  9. * @ClassName: MinioConfiguration
  10. * @Description: minio配置
  11. * @Author: xiaLin
  12. * @Date: 2024/9/2 11:30
  13. * @Version: 1.0
  14. */
  15. public class MinioConfiguration {
  16. /**
  17. * 配置信息
  18. */
  19. private final MinioProperties minioProperties;
  20. /**
  21. * 构建函数
  22. */
  23. public MinioConfiguration(MinioProperties minioProperties) {
  24. this.minioProperties = minioProperties;
  25. }
  26. /**
  27. * 启动
  28. */
  29. @Bean(name = "start")
  30. public void start() {
  31. MinioClient.Builder builder = MinioClient.builder();
  32. if (minioProperties.getPort() == null) {
  33. builder.endpoint(minioProperties.getEndpoint());
  34. } else {
  35. builder.endpoint(minioProperties.getEndpoint(), minioProperties.getPort(), Boolean.FALSE);
  36. }
  37. MinioClient client = builder
  38. // 服务端用户名 、 服务端密码
  39. .credentials(minioProperties.getAccessKey(), minioProperties.getSecretKey())
  40. .build();
  41. // 实例化工具类
  42. new MinioUtils(client);
  43. }
  44. }
复制代码

MinIO工具库

该工具库主要提供静态接口方法

  1. package com.cdkjframework.minio;
  2. import com.cdkjframework.builder.ResponseBuilder;
  3. import com.cdkjframework.constant.IntegerConsts;
  4. import com.cdkjframework.exceptions.GlobalException;
  5. import com.cdkjframework.exceptions.GlobalRuntimeException;
  6. import com.cdkjframework.minio.enums.ContentTypeEnums;
  7. import com.cdkjframework.util.tool.StringUtils;
  8. import com.google.common.collect.Sets;
  9. import io.minio.*;
  10. import io.minio.http.Method;
  11. import io.minio.messages.Bucket;
  12. import io.minio.messages.DeleteError;
  13. import io.minio.messages.DeleteObject;
  14. import io.minio.messages.Item;
  15. import org.springframework.web.multipart.MultipartFile;
  16. import java.io.ByteArrayInputStream;
  17. import java.io.InputStream;
  18. import java.io.UnsupportedEncodingException;
  19. import java.net.URLDecoder;
  20. import java.nio.charset.StandardCharsets;
  21. import java.util.ArrayList;
  22. import java.util.List;
  23. import java.util.Optional;
  24. import java.util.Set;
  25. import java.util.concurrent.TimeUnit;
  26. import java.util.stream.Collectors;
  27. import java.util.stream.Stream;
  28. /**
  29. * @ProjectName: cdkjframework
  30. * @Package: com.cdkjframework.minio
  31. * @ClassName: MinioUtils
  32. * @Description: minio工具类
  33. * @Author: xiaLin
  34. * @Date: 2024/9/2 13:32
  35. * @Version: 1.0
  36. */
  37. public class MinioUtils {
  38. /**
  39. * 默认的临时文件存储桶
  40. */
  41. private static final String DEFAULT_TEMP_BUCKET_NAME = "temp-bucket";
  42. /**
  43. * minio客户端
  44. */
  45. private static MinioClient client = null;
  46. /**
  47. * 构造函数
  48. */
  49. public MinioUtils(MinioClient client) {
  50. MinioUtils.client = client;
  51. }
  52. /**
  53. * 将URLDecoder编码转成UTF8
  54. *
  55. * @param str 待转码的字符串
  56. * @return 转码后的字符串
  57. */
  58. public static String getUtf8ByURLDecoder(String str) throws UnsupportedEncodingException {
  59. String url = str.replaceAll("%(?![0-9a-fA-F]{2})", "%25");
  60. return URLDecoder.decode(url, StandardCharsets.UTF_8);
  61. }
  62. /**
  63. * 把路径开头的"/"去掉,并在末尾添加"/",这个是minio对象名的样子。
  64. *
  65. * @param projectPath 以"/"开头、以字母结尾的路径
  66. * @return 去掉开头"/"
  67. */
  68. private static String trimHead(String projectPath) {
  69. return projectPath.substring(IntegerConsts.ONE);
  70. }
  71. /**
  72. * 把路径开头的"/"去掉,并在末尾添加"/",这个是minio对象名的样子。
  73. *
  74. * @param projectPath 以"/"开头、以字母结尾的路径
  75. * @return 添加结尾"/"
  76. */
  77. private static String addTail(String projectPath) {
  78. return projectPath + StringUtils.BACKSLASH;
  79. }
  80. /**
  81. * 获取数值的位数(用于构造临时文件名)
  82. *
  83. * @param number
  84. * @return
  85. */
  86. private static int countDigits(int number) {
  87. if (number == IntegerConsts.ZERO) {
  88. // 0 本身有一位
  89. return IntegerConsts.ONE;
  90. }
  91. int count = IntegerConsts.ZERO;
  92. while (number != IntegerConsts.ZERO) {
  93. number /= IntegerConsts.TEN;
  94. count++;
  95. }
  96. return count;
  97. }
  98. /**
  99. * 检查是否空
  100. *
  101. * @param objects 待检查的对象
  102. */
  103. private static void checkNull(Object... objects) {
  104. for (Object o : objects) {
  105. if (o == null) {
  106. throw new GlobalRuntimeException("Null param");
  107. }
  108. if (o instanceof String && StringUtils.isNullAndSpaceOrEmpty(o)) {
  109. throw new GlobalRuntimeException("Empty string");
  110. }
  111. }
  112. }
  113. /**
  114. * 给定一个字符串,返回其"/"符号后面的字符串
  115. *
  116. * @param input 输入字符串
  117. * @return 截取后的字符串
  118. */
  119. private static String getContentAfterSlash(String input) {
  120. if (StringUtils.isNullAndSpaceOrEmpty(input)) {
  121. return StringUtils.Empty;
  122. }
  123. int slashIndex = input.indexOf(StringUtils.BACKSLASH);
  124. if (slashIndex != IntegerConsts.MINUS_ONE && slashIndex < input.length() - IntegerConsts.ONE) {
  125. return input.substring(slashIndex + IntegerConsts.ONE);
  126. }
  127. return StringUtils.Empty;
  128. }
  129. /**
  130. * 判断Bucket是否存在
  131. *
  132. * @return true:存在,false:不存在
  133. */
  134. private static boolean bucketExists(String bucketName) throws Exception {
  135. return client.bucketExists(BucketExistsArgs.builder().bucket(bucketName).build());
  136. }
  137. /**
  138. * 如果一个桶不存在,则创建该桶
  139. */
  140. public static void createBucket(String bucketName) throws Exception {
  141. if (!bucketExists(bucketName)) {
  142. client.makeBucket(MakeBucketArgs.builder().bucket(bucketName).build());
  143. }
  144. }
  145. /**
  146. * 获取 Bucket 的相关信息
  147. */
  148. public static Optional<Bucket> getBucketInfo(String bucketName) throws Exception {
  149. return client.listBuckets().stream().filter(b -> b.name().equals(bucketName)).findFirst();
  150. }
  151. /**
  152. * 使用MultipartFile进行文件上传
  153. *
  154. * @param bucketName 存储桶
  155. * @param file 文件
  156. * @param fileName 对象名
  157. * @param contentType 类型
  158. * @return
  159. * @throws Exception
  160. */
  161. public static ObjectWriteResponse uploadFile(String bucketName, MultipartFile file,
  162. String fileName, ContentTypeEnums contentType) throws Exception {
  163. InputStream inputStream = file.getInputStream();
  164. return client.putObject(
  165. PutObjectArgs.builder()
  166. .bucket(bucketName)
  167. .object(fileName)
  168. .contentType(contentType.getValue())
  169. .stream(inputStream, inputStream.available(), IntegerConsts.MINUS_ONE)
  170. .build());
  171. }
  172. /**
  173. * 将文件进行分片上传
  174. * <p>有一个未处理的bug(虽然概率很低很低):</p>
  175. * 当两个线程同时上传md5相同的文件时,由于两者会定位到同一个桶的同一个临时目录,两个线程会相互产生影响!
  176. *
  177. * @param file 分片文件
  178. * @param currIndex 当前文件的分片索引
  179. * @param totalPieces 切片总数(对于同一个文件,请确保切片总数始终不变)
  180. * @param md5 整体文件MD5
  181. * @return 剩余未上传的文件索引集合
  182. */
  183. public static ResponseBuilder uploadFileFragment(MultipartFile file,
  184. Integer currIndex, Integer totalPieces, String md5) throws Exception {
  185. checkNull(currIndex, totalPieces, md5);
  186. // 把当前分片上传至临时桶
  187. if (!bucketExists(DEFAULT_TEMP_BUCKET_NAME)) {
  188. createBucket(DEFAULT_TEMP_BUCKET_NAME);
  189. }
  190. uploadFileStream(DEFAULT_TEMP_BUCKET_NAME, getFileTempPath(md5, currIndex, totalPieces), file.getInputStream());
  191. // 得到已上传的文件索引
  192. Iterable<Result<Item>> results = getFilesByPrefix(DEFAULT_TEMP_BUCKET_NAME, md5.concat(StringUtils.BACKSLASH), Boolean.FALSE);
  193. Set<Integer> savedIndex = Sets.newHashSet();
  194. boolean fileExists = Boolean.FALSE;
  195. for (Result<Item> item : results) {
  196. Integer idx = Integer.valueOf(getContentAfterSlash(item.get().objectName()));
  197. if (currIndex.equals(idx)) {
  198. fileExists = Boolean.TRUE;
  199. }
  200. savedIndex.add(idx);
  201. }
  202. // 得到未上传的文件索引
  203. Set<Integer> remainIndex = Sets.newTreeSet();
  204. for (int i = IntegerConsts.ZERO; i < totalPieces; i++) {
  205. if (!savedIndex.contains(i)) {
  206. remainIndex.add(i);
  207. }
  208. }
  209. if (fileExists) {
  210. return ResponseBuilder.failBuilder("index [" + currIndex + "] exists");
  211. }
  212. // 还剩一个索引未上传,当前上传索引刚好是未上传索引,上传完当前索引后就完全结束了。
  213. if (remainIndex.size() == IntegerConsts.ONE && remainIndex.contains(currIndex)) {
  214. return ResponseBuilder.successBuilder("completed");
  215. }
  216. return ResponseBuilder.failBuilder("index [" + currIndex + "] has been uploaded");
  217. }
  218. /**
  219. * 合并分片文件,并放到指定目录
  220. * 前提是之前已把所有分片上传完毕。
  221. *
  222. * @param bucketName 目标文件桶名
  223. * @param targetName 目标文件名(含完整路径)
  224. * @param totalPieces 切片总数(对于同一个文件,请确保切片总数始终不变)
  225. * @param md5 文件md5
  226. * @return minio原生对象,记录了文件上传信息
  227. */
  228. public static boolean composeFileFragment(String bucketName, String targetName,
  229. Integer totalPieces, String md5) throws Exception {
  230. checkNull(bucketName, targetName, totalPieces, md5);
  231. // 检查文件索引是否都上传完毕
  232. Iterable<Result<Item>> results = getFilesByPrefix(DEFAULT_TEMP_BUCKET_NAME, md5.concat(StringUtils.BACKSLASH), false);
  233. Set<String> savedIndex = Sets.newTreeSet();
  234. for (Result<Item> item : results) {
  235. savedIndex.add(item.get().objectName());
  236. }
  237. if (savedIndex.size() == totalPieces) {
  238. // 文件路径 转 文件合并对象
  239. List<ComposeSource> sourceObjectList = savedIndex.stream()
  240. .map(filePath -> ComposeSource.builder()
  241. .bucket(DEFAULT_TEMP_BUCKET_NAME)
  242. .object(filePath)
  243. .build())
  244. .collect(Collectors.toList());
  245. ObjectWriteResponse objectWriteResponse = client.composeObject(
  246. ComposeObjectArgs.builder()
  247. .bucket(bucketName)
  248. .object(targetName)
  249. .sources(sourceObjectList)
  250. .build());
  251. // 上传成功,则删除所有的临时分片文件
  252. List<String> filePaths = Stream.iterate(IntegerConsts.ZERO, i -> ++i)
  253. .limit(totalPieces)
  254. .map(i -> getFileTempPath(md5, i, totalPieces))
  255. .collect(Collectors.toList());
  256. Iterable<Result<DeleteError>> deleteResults = removeFiles(DEFAULT_TEMP_BUCKET_NAME, filePaths);
  257. // 遍历错误集合(无元素则成功)
  258. for (Result<DeleteError> result : deleteResults) {
  259. DeleteError error = result.get();
  260. System.err.printf("[Bigfile] 分片'%s'删除失败! 错误信息: %s", error.objectName(), error.message());
  261. }
  262. return true;
  263. }
  264. throw new GlobalException("The fragment index is not complete. Please check parameters [totalPieces] or [md5]");
  265. }
  266. /**
  267. * 上传本地文件
  268. *
  269. * @param bucketName 存储桶
  270. * @param fileName 文件名称
  271. * @param filePath 本地文件路径
  272. */
  273. public ObjectWriteResponse uploadFile(String bucketName, String fileName,
  274. String filePath) throws Exception {
  275. return client.uploadObject(
  276. UploadObjectArgs.builder()
  277. .bucket(bucketName)
  278. .object(fileName)
  279. .filename(filePath)
  280. .build());
  281. }
  282. /**
  283. * 通过流上传文件
  284. *
  285. * @param bucketName 存储桶
  286. * @param fileName 文件名
  287. * @param inputStream 文件流
  288. */
  289. public static ObjectWriteResponse uploadFileStream(String bucketName, String fileName, InputStream inputStream) throws Exception {
  290. return client.putObject(
  291. PutObjectArgs.builder()
  292. .bucket(bucketName)
  293. .object(fileName)
  294. .stream(inputStream, inputStream.available(), IntegerConsts.MINUS_ONE)
  295. .build());
  296. }
  297. /**
  298. * 判断文件是否存在
  299. *
  300. * @param bucketName 存储桶
  301. * @param fileName 文件名
  302. * @return true: 存在
  303. */
  304. public static boolean isFileExist(String bucketName, String fileName) {
  305. boolean exist = true;
  306. try {
  307. client.statObject(StatObjectArgs.builder().bucket(bucketName).object(fileName).build());
  308. } catch (Exception e) {
  309. exist = false;
  310. }
  311. return exist;
  312. }
  313. /**
  314. * 判断文件夹是否存在
  315. *
  316. * @param bucketName 存储桶
  317. * @param folderName 目录名称:本项目约定路径是以"/"开头,不以"/"结尾
  318. * @return true: 存在
  319. */
  320. public boolean isFolderExist(String bucketName, String folderName) {
  321. // 去掉头"/",才能搜索到相关前缀
  322. folderName = trimHead(folderName);
  323. boolean exist = false;
  324. try {
  325. Iterable<Result<Item>> results = client.listObjects(
  326. ListObjectsArgs.builder().bucket(bucketName).prefix(folderName).recursive(false).build());
  327. for (Result<Item> result : results) {
  328. Item item = result.get();
  329. // 增加尾"/",才能匹配到目录名字
  330. String objectName = addTail(folderName);
  331. if (item.isDir() && objectName.equals(item.objectName())) {
  332. exist = true;
  333. }
  334. }
  335. } catch (Exception e) {
  336. exist = false;
  337. }
  338. return exist;
  339. }
  340. /**
  341. * 获取路径下文件列表
  342. *
  343. * @param bucketName 存储桶
  344. * @param prefix 文件名称
  345. * @param recursive 是否递归查找,false:模拟文件夹结构查找
  346. * @return 二进制流
  347. */
  348. public static Iterable<Result<Item>> getFilesByPrefix(String bucketName, String prefix,
  349. boolean recursive) {
  350. return client.listObjects(
  351. ListObjectsArgs.builder()
  352. .bucket(bucketName)
  353. .prefix(prefix)
  354. .recursive(recursive)
  355. .build());
  356. }
  357. /**
  358. * 获取文件信息, 如果抛出异常则说明文件不存在
  359. *
  360. * @param bucketName 存储桶
  361. * @param fileName 文件名称
  362. */
  363. public StatObjectResponse getFileStatusInfo(String bucketName, String fileName) throws Exception {
  364. return client.statObject(
  365. StatObjectArgs.builder()
  366. .bucket(bucketName)
  367. .object(fileName)
  368. .build());
  369. }
  370. /**
  371. * 根据文件前缀查询文件
  372. *
  373. * @param bucketName 存储桶
  374. * @param prefix 前缀
  375. * @param recursive 是否使用递归查询
  376. * @return MinioItem 列表
  377. */
  378. public List<Item> getAllFilesByPrefix(String bucketName,
  379. String prefix,
  380. boolean recursive) throws Exception {
  381. List<Item> list = new ArrayList<>();
  382. Iterable<Result<Item>> objectsIterator = client.listObjects(
  383. ListObjectsArgs.builder().bucket(bucketName).prefix(prefix).recursive(recursive).build());
  384. if (objectsIterator != null) {
  385. for (Result<Item> o : objectsIterator) {
  386. Item item = o.get();
  387. list.add(item);
  388. }
  389. }
  390. return list;
  391. }
  392. /**
  393. * 批量删除文件
  394. *
  395. * @param bucketName 存储桶
  396. * @param filePaths<String> 需要删除的文件列表
  397. * @return Result
  398. */
  399. public static Iterable<Result<DeleteError>> removeFiles(String bucketName, List<String> filePaths) {
  400. List<DeleteObject> objectPaths = filePaths.stream()
  401. .map(filePath -> new DeleteObject(filePath))
  402. .collect(Collectors.toList());
  403. return client.removeObjects(
  404. RemoveObjectsArgs.builder().bucket(bucketName).objects(objectPaths).build());
  405. }
  406. /**
  407. * 获取文件的二进制流
  408. *
  409. * @param bucketName 存储桶
  410. * @param fileName 文件名
  411. * @return 二进制流
  412. */
  413. public InputStream getFileStream(String bucketName, String fileName) throws Exception {
  414. return client.getObject(GetObjectArgs.builder().bucket(bucketName).object(fileName).build());
  415. }
  416. /**
  417. * 断点下载
  418. *
  419. * @param bucketName 存储桶
  420. * @param fileName 文件名称
  421. * @param offset 起始字节的位置
  422. * @param length 要读取的长度
  423. * @return 二进制流
  424. */
  425. public InputStream getFileStream(String bucketName, String fileName, long offset, long length) throws Exception {
  426. return client.getObject(
  427. GetObjectArgs.builder()
  428. .bucket(bucketName)
  429. .object(fileName)
  430. .offset(offset)
  431. .length(length)
  432. .build());
  433. }
  434. /**
  435. * 拷贝文件
  436. *
  437. * @param bucketName 存储桶
  438. * @param fileName 文件名
  439. * @param srcBucketName 目标存储桶
  440. * @param srcFileName 目标文件名
  441. */
  442. public ObjectWriteResponse copyFile(String bucketName, String fileName,
  443. String srcBucketName, String srcFileName) throws Exception {
  444. return client.copyObject(
  445. CopyObjectArgs.builder()
  446. .source(CopySource.builder().bucket(bucketName).object(fileName).build())
  447. .bucket(srcBucketName)
  448. .object(srcFileName)
  449. .build());
  450. }
  451. /**
  452. * 删除文件夹(未完成)
  453. *
  454. * @param bucketName 存储桶
  455. * @param fileName 路径
  456. */
  457. @Deprecated
  458. public void removeFolder(String bucketName, String fileName) throws Exception {
  459. // 加尾
  460. fileName = addTail(fileName);
  461. client.removeObject(
  462. RemoveObjectArgs.builder()
  463. .bucket(bucketName)
  464. .object(fileName)
  465. .build());
  466. }
  467. /**
  468. * 删除文件
  469. *
  470. * @param bucketName 存储桶
  471. * @param fileName 文件名称
  472. */
  473. public void removeFile(String bucketName, String fileName) throws Exception {
  474. // 掐头
  475. fileName = trimHead(fileName);
  476. client.removeObject(
  477. RemoveObjectArgs.builder()
  478. .bucket(bucketName)
  479. .object(fileName)
  480. .build());
  481. }
  482. /**
  483. * 获得文件外链
  484. *
  485. * @param bucketName 存储桶
  486. * @param fileName 文件名
  487. * @return url 返回地址
  488. * @throws Exception
  489. */
  490. public static String getPresignedObjectUrl(String bucketName, String fileName) throws Exception {
  491. GetPresignedObjectUrlArgs args = GetPresignedObjectUrlArgs.builder()
  492. .bucket(bucketName)
  493. .object(fileName)
  494. .method(Method.GET).build();
  495. return client.getPresignedObjectUrl(args);
  496. }
  497. /**
  498. * 获取文件外链
  499. *
  500. * @param bucketName 存储桶
  501. * @param fileName 文件名
  502. * @param expires 过期时间 <=7 秒 (外链有效时间(单位:秒))
  503. * @return url
  504. * @throws Exception
  505. */
  506. public String getPresignedObjectUrl(String bucketName, String fileName, Integer expires) throws Exception {
  507. GetPresignedObjectUrlArgs args = GetPresignedObjectUrlArgs.builder()
  508. .method(Method.GET)
  509. .expiry(expires, TimeUnit.SECONDS)
  510. .bucket(bucketName)
  511. .object(fileName)
  512. .build();
  513. return client.getPresignedObjectUrl(args);
  514. }
  515. /**
  516. * 通过文件的md5,以及分片文件的索引,构造分片文件的临时存储路径
  517. *
  518. * @param md5 文件md5
  519. * @param currIndex 分片文件索引(从0开始)
  520. * @param totalPieces 总分片
  521. * @return 临时存储路径
  522. */
  523. private static String getFileTempPath(String md5, Integer currIndex, Integer totalPieces) {
  524. int zeroCnt = countDigits(totalPieces) - countDigits(currIndex);
  525. StringBuilder name = new StringBuilder(md5);
  526. name.append(StringUtils.BACKSLASH);
  527. for (int i = IntegerConsts.ZERO; i < zeroCnt; i++) {
  528. name.append(IntegerConsts.ZERO);
  529. }
  530. name.append(currIndex);
  531. return name.toString();
  532. }
  533. /**
  534. * 创建目录
  535. *
  536. * @param bucketName 存储桶
  537. * @param folderName 目录路径:本项目约定路径是以"/"开头,不以"/"结尾
  538. */
  539. public ObjectWriteResponse createFolder(String bucketName, String folderName) throws Exception {
  540. // 这是minio的bug,只有在路径的尾巴加上"/",才能当成文件夹。
  541. folderName = addTail(folderName);
  542. return client.putObject(
  543. PutObjectArgs.builder()
  544. .bucket(bucketName)
  545. .object(folderName)
  546. .stream(new ByteArrayInputStream(new byte[]{}), IntegerConsts.ZERO, IntegerConsts.MINUS_ONE)
  547. .build());
  548. }
  549. }
复制代码

资源目录下新建 META-INF\spring

该方式支持 Spring Boot 3.x

新建文件 org.springframework.boot.autoconfigure.AutoConfiguration.imports 内容

  1. com.cdkjframework.minio.config.MinioAutoConfiguration
复制代码

总结

Spring Boot 封装 MinIO 工具的核心意义在于将分布式存储能力转化为可复用的基础设施,通过标准化、模块化的设计,显著降低开发复杂度,提升系统健壮性和可维护性。这种封装不仅是技术层面的优化,更是工程实践中的最佳选择,尤其适用于需要快速迭代、高并发处理及多云兼容的现代应用架构。

到此这篇关于SpringBoot封装MinIO工具的实现步骤的文章就介绍到这了,更多相关SpringBoot封装MinIO内容请搜索晓枫资讯以前的文章或继续浏览下面的相关文章希望大家以后多多支持晓枫资讯!


免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!
晓枫资讯-科技资讯社区-免责声明
免责声明:以上内容为本网站转自其它媒体,相关信息仅为传递更多信息之目的,不代表本网观点,亦不代表本网站赞同其观点或证实其内容的真实性。
      1、注册用户在本社区发表、转载的任何作品仅代表其个人观点,不代表本社区认同其观点。
      2、管理员及版主有权在不事先通知或不经作者准许的情况下删除其在本社区所发表的文章。
      3、本社区的文章部分内容可能来源于网络,仅供大家学习与参考,如有侵权,举报反馈:点击这里给我发消息进行删除处理。
      4、本社区一切资源不代表本站立场,并不代表本站赞同其观点和对其真实性负责。
      5、以上声明内容的最终解释权归《晓枫资讯-科技资讯社区》所有。
http://bbs.yzwlo.com 晓枫资讯--游戏IT新闻资讯~~~
严禁发布广告,淫秽、色情、赌博、暴力、凶杀、恐怖、间谍及其他违反国家法律法规的内容。!晓枫资讯-社区
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

手机版|晓枫资讯--科技资讯社区 本站已运行

CopyRight © 2022-2025 晓枫资讯--科技资讯社区 ( BBS.yzwlo.com ) . All Rights Reserved .

晓枫资讯--科技资讯社区

本站内容由用户自主分享和转载自互联网,转载目的在于传递更多信息,并不代表本网赞同其观点和对其真实性负责。

如有侵权、违反国家法律政策行为,请联系我们,我们会第一时间及时清除和处理! 举报反馈邮箱:点击这里给我发消息

Powered by Discuz! X3.5

快速回复 返回顶部 返回列表