SpringBoot封裝MinIO工具的實現(xiàn)步驟
MinIO 簡介
MinIO 是一款高性能、開源、兼容Amazon S3 API的分布式對象存儲系統(tǒng),專為云原生架構(gòu)和大規(guī)模非結(jié)構(gòu)化數(shù)據(jù)場景設(shè)計。其核心定位是成為私有云/混合云環(huán)境中的標(biāo)準(zhǔn)存儲方案,適用于從數(shù)據(jù)湖到AI/ML、容器化部署等多樣化需求。
背景
為解決業(yè)務(wù)開時對接 MinIO繁瑣接口對工作
目錄結(jié)構(gòu)
cdkj-minio MinIO工具
annotation 注解
- EnableAutoMinio 啟用自動MinIO
config 配置
- MinioAutoConfiguration MinIO 自動配置
- MinioMarkerConfiguration MinIO 標(biāo)記配置
- MinioProperties MinIO 配置讀取
connectivity 連接庫
- MinioConfiguration MinIO 配置
enums 枚舉庫
- ContentTypeEnums 內(nèi)容類型枚舉
MinioUtils MinIO工具庫
項目介紹
POM引入包
<dependencies> <!-- MinIO --> <dependency> <groupId>io.minio</groupId> <artifactId>minio</artifactId> <exclusions> <exclusion> <groupId>com.squareup.okhttp3</groupId> <artifactId>okhttp</artifactId> </exclusion> </exclusions> </dependency> <dependency> <groupId>com.squareup.okhttp3</groupId> <artifactId>okhttp</artifactId> </dependency> </dependencies>
EnableAutoMinio 啟用自動MinIO
spring boot 啟動加載工具包
package com.cdkjframework.minio.annotation; import com.cdkjframework.minio.config.MinioMarkerConfiguration; import org.springframework.context.annotation.Import; import java.lang.annotation.*; /** * @ProjectName: cdkjframework * @Package: com.cdkjframework.minio.annotation * @ClassName: EnableAutoMinio * @Description: java類作用描述 * @Author: xiaLin * @Date: 2024/9/2 11:27 * @Version: 1.0 */ @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Documented @Import({MinioMarkerConfiguration.class}) public @interface EnableAutoMinio { }
Spring Boot 項目引入
package cn.qjwxlcps.ims.annotation; import com.cdkjframework.cloud.job.core.handler.annotation.EnableAutoCdkjJob; import com.cdkjframework.datasource.mongodb.annotation.EnableAutoMongo; import com.cdkjframework.minio.annotation.EnableAutoMinio; import com.cdkjframework.swagger.annotation.EnableAutoSwagger; import org.springframework.cloud.client.discovery.EnableDiscoveryClient; import org.springframework.cloud.openfeign.EnableFeignClients; import org.springframework.context.annotation.Configuration; import org.springframework.retry.annotation.EnableRetry; import org.springframework.scheduling.annotation.EnableAsync; import org.springframework.transaction.annotation.EnableTransactionManagement; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** * @ProjectName: com.lesmarthome.interface * @Package: com.lesmarthome.interfaces.annotation * @ClassName: EnableAutoApi * @Description: java類作用描述 * @Author: xiaLin * @Date: 2024/3/29 16:02 * @Version: 1.0 */ @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @EnableRetry @EnableAsync @Configuration @EnableAutoMinio @EnableAutoMongo @EnableAutoCdkjJob //@EnableAutoSwagger @EnableDiscoveryClient @EnableTransactionManagement @EnableFeignClients(basePackages = {"com.*.*.client"}) public @interface EnableAutoApi { }
MinIO 配置讀取
該類主要讀取用戶配置信息
package com.cdkjframework.minio.config; import lombok.Data; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.cloud.context.config.annotation.RefreshScope; import org.springframework.context.annotation.Configuration; /** * @ProjectName: cdkjframework * @Package: com.cdkjframework.minio.config * @ClassName: MinioProperties * @Description: mini配置 * @Author: xiaLin * @Date: 2024/9/2 11:28 * @Version: 1.0 */ @Data @RefreshScope @Configuration @ConfigurationProperties(prefix = "spring.minio") public class MinioProperties { /** * 訪問域名 */ private String domain; /** * 存儲端點 */ private String endpoint; /** * 端口 */ private Integer port; /** * 訪問密鑰 */ private String accessKey; /** * 密鑰 */ private String secretKey; /** * 存儲桶名稱 */ private String bucketName; /** * 分片對象過期時間 單位(天) */ private Integer expiry; /** * 斷點續(xù)傳有效時間,在redis存儲任務(wù)的時間 單位(天) */ private Integer breakpointTime; }
內(nèi)容類型枚舉
package com.cdkjframework.minio.enums; import com.cdkjframework.constant.IntegerConsts; import com.cdkjframework.util.tool.StringUtils; /** * @ProjectName: cdkjframework * @Package: com.cdkjframework.minio.enums * @ClassName: ContentTypeEnums * @Description: 內(nèi)容類型枚舉 * @Author: xiaLin * @Date: 2024/9/2 13:30 * @Version: 1.0 */ public enum ContentTypeEnums { /** * 默認類型 */ DEFAULT("default", "application/octet-stream"), JPG("jpg", "image/jpeg"), TIFF("tiff", "image/tiff"), GIF("gif", "image/gif"), JFIF("jfif", "image/jpeg"), PNG("png", "image/png"), TIF("tif", "image/tiff"), ICO("ico", "image/x-icon"), JPEG("jpeg", "image/jpeg"), WBMP("wbmp", "image/vnd.wap.wbmp"), FAX("fax", "image/fax"), NET("net", "image/pnetvue"), JPE("jpe", "image/jpeg"), RP("rp", "image/vnd.rn-realpix"), MP4("mp4", "video/mp4"); /** * 文件名后綴 */ private final String suffix; /** * 返回前端請求頭中,Content-Type具體的值 */ private final String value; ContentTypeEnums(String suffix, String value) { this.suffix = suffix; this.value = value; } /** * 根據(jù)文件后綴,獲取Content-Type * * @param suffix 文件后綴 * @return 返回結(jié)果 */ public static String formContentType(String suffix) { if (StringUtils.isNullAndSpaceOrEmpty(suffix)) { return DEFAULT.getValue(); } int beginIndex = suffix.lastIndexOf(StringUtils.POINT) + IntegerConsts.ONE; suffix = suffix.substring(beginIndex); for (ContentTypeEnums value : ContentTypeEnums.values()) { if (suffix.equalsIgnoreCase(value.getSuffix())) { return value.getValue(); } } return DEFAULT.getValue(); } public String getSuffix() { return suffix; } public String getValue() { return value; } }
MinIO 標(biāo)記配置
package com.cdkjframework.minio.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; /** * @ProjectName: cdkjframework * @Package: com.cdkjframework.minio.config * @ClassName: MinioMarkerConfiguration * @Description: Minio標(biāo)記配置 * @Author: xiaLin * @Date: 2024/9/2 11:28 * @Version: 1.0 */ @Configuration(proxyBeanMethods = false) public class MinioMarkerConfiguration { @Bean public Marker mybatisMarker() { return new Marker(); } public static class Marker { } }
MinIO 自動配置
注入 Bean 之前需要先加載配置 MinioProperties,且基于Bean的條件 MinioMarkerConfiguration.Marker 完成。開始調(diào)用 MinioConfiguration 中的start Bean方法。
package com.cdkjframework.minio.config; import com.cdkjframework.minio.connectivity.MinioConfiguration; import lombok.RequiredArgsConstructor; import org.springframework.boot.autoconfigure.AutoConfigureAfter; import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.boot.autoconfigure.web.reactive.function.client.WebClientAutoConfiguration; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.cloud.context.config.annotation.RefreshScope; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Lazy; /** * @ProjectName: cdkjframework * @Package: com.cdkjframework.minio.config * @ClassName: MinioAutoConfiguration * @Description: Minio自動配置 * @Author: xiaLin * @Date: 2024/9/2 11:29 * @Version: 1.0 */ @Lazy(false) @RequiredArgsConstructor @Configuration(proxyBeanMethods = false) @EnableConfigurationProperties({ MinioProperties.class }) @AutoConfigureAfter({WebClientAutoConfiguration.class}) @ConditionalOnBean(MinioMarkerConfiguration.Marker.class) public class MinioAutoConfiguration { /** * 配置信息 */ private final MinioProperties minioProperties; /** * minio配置 * * @return 返回配置信息 */ @Bean(initMethod = "start") public MinioConfiguration minioConfiguration() { return new MinioConfiguration(minioProperties); } }
MinIO 配置
package com.cdkjframework.minio.connectivity; import com.cdkjframework.minio.MinioUtils; import com.cdkjframework.minio.config.MinioProperties; import io.minio.MinioClient; import org.springframework.context.annotation.Bean; /** * @ProjectName: cdkjframework * @Package: com.cdkjframework.minio.connectivity * @ClassName: MinioConfiguration * @Description: minio配置 * @Author: xiaLin * @Date: 2024/9/2 11:30 * @Version: 1.0 */ public class MinioConfiguration { /** * 配置信息 */ private final MinioProperties minioProperties; /** * 構(gòu)建函數(shù) */ public MinioConfiguration(MinioProperties minioProperties) { this.minioProperties = minioProperties; } /** * 啟動 */ @Bean(name = "start") public void start() { MinioClient.Builder builder = MinioClient.builder(); if (minioProperties.getPort() == null) { builder.endpoint(minioProperties.getEndpoint()); } else { builder.endpoint(minioProperties.getEndpoint(), minioProperties.getPort(), Boolean.FALSE); } MinioClient client = builder // 服務(wù)端用戶名 、 服務(wù)端密碼 .credentials(minioProperties.getAccessKey(), minioProperties.getSecretKey()) .build(); // 實例化工具類 new MinioUtils(client); } }
MinIO工具庫
該工具庫主要提供靜態(tài)接口方法
package com.cdkjframework.minio; import com.cdkjframework.builder.ResponseBuilder; import com.cdkjframework.constant.IntegerConsts; import com.cdkjframework.exceptions.GlobalException; import com.cdkjframework.exceptions.GlobalRuntimeException; import com.cdkjframework.minio.enums.ContentTypeEnums; import com.cdkjframework.util.tool.StringUtils; import com.google.common.collect.Sets; import io.minio.*; import io.minio.http.Method; import io.minio.messages.Bucket; import io.minio.messages.DeleteError; import io.minio.messages.DeleteObject; import io.minio.messages.Item; import org.springframework.web.multipart.MultipartFile; import java.io.ByteArrayInputStream; import java.io.InputStream; import java.io.UnsupportedEncodingException; import java.net.URLDecoder; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.List; import java.util.Optional; import java.util.Set; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; import java.util.stream.Stream; /** * @ProjectName: cdkjframework * @Package: com.cdkjframework.minio * @ClassName: MinioUtils * @Description: minio工具類 * @Author: xiaLin * @Date: 2024/9/2 13:32 * @Version: 1.0 */ public class MinioUtils { /** * 默認的臨時文件存儲桶 */ private static final String DEFAULT_TEMP_BUCKET_NAME = "temp-bucket"; /** * minio客戶端 */ private static MinioClient client = null; /** * 構(gòu)造函數(shù) */ public MinioUtils(MinioClient client) { MinioUtils.client = client; } /** * 將URLDecoder編碼轉(zhuǎn)成UTF8 * * @param str 待轉(zhuǎn)碼的字符串 * @return 轉(zhuǎn)碼后的字符串 */ public static String getUtf8ByURLDecoder(String str) throws UnsupportedEncodingException { String url = str.replaceAll("%(?![0-9a-fA-F]{2})", "%25"); return URLDecoder.decode(url, StandardCharsets.UTF_8); } /** * 把路徑開頭的"/"去掉,并在末尾添加"/",這個是minio對象名的樣子。 * * @param projectPath 以"/"開頭、以字母結(jié)尾的路徑 * @return 去掉開頭"/" */ private static String trimHead(String projectPath) { return projectPath.substring(IntegerConsts.ONE); } /** * 把路徑開頭的"/"去掉,并在末尾添加"/",這個是minio對象名的樣子。 * * @param projectPath 以"/"開頭、以字母結(jié)尾的路徑 * @return 添加結(jié)尾"/" */ private static String addTail(String projectPath) { return projectPath + StringUtils.BACKSLASH; } /** * 獲取數(shù)值的位數(shù)(用于構(gòu)造臨時文件名) * * @param number * @return */ private static int countDigits(int number) { if (number == IntegerConsts.ZERO) { // 0 本身有一位 return IntegerConsts.ONE; } int count = IntegerConsts.ZERO; while (number != IntegerConsts.ZERO) { number /= IntegerConsts.TEN; count++; } return count; } /** * 檢查是否空 * * @param objects 待檢查的對象 */ private static void checkNull(Object... objects) { for (Object o : objects) { if (o == null) { throw new GlobalRuntimeException("Null param"); } if (o instanceof String && StringUtils.isNullAndSpaceOrEmpty(o)) { throw new GlobalRuntimeException("Empty string"); } } } /** * 給定一個字符串,返回其"/"符號后面的字符串 * * @param input 輸入字符串 * @return 截取后的字符串 */ private static String getContentAfterSlash(String input) { if (StringUtils.isNullAndSpaceOrEmpty(input)) { return StringUtils.Empty; } int slashIndex = input.indexOf(StringUtils.BACKSLASH); if (slashIndex != IntegerConsts.MINUS_ONE && slashIndex < input.length() - IntegerConsts.ONE) { return input.substring(slashIndex + IntegerConsts.ONE); } return StringUtils.Empty; } /** * 判斷Bucket是否存在 * * @return true:存在,false:不存在 */ private static boolean bucketExists(String bucketName) throws Exception { return client.bucketExists(BucketExistsArgs.builder().bucket(bucketName).build()); } /** * 如果一個桶不存在,則創(chuàng)建該桶 */ public static void createBucket(String bucketName) throws Exception { if (!bucketExists(bucketName)) { client.makeBucket(MakeBucketArgs.builder().bucket(bucketName).build()); } } /** * 獲取 Bucket 的相關(guān)信息 */ public static Optional<Bucket> getBucketInfo(String bucketName) throws Exception { return client.listBuckets().stream().filter(b -> b.name().equals(bucketName)).findFirst(); } /** * 使用MultipartFile進行文件上傳 * * @param bucketName 存儲桶 * @param file 文件 * @param fileName 對象名 * @param contentType 類型 * @return * @throws Exception */ public static ObjectWriteResponse uploadFile(String bucketName, MultipartFile file, String fileName, ContentTypeEnums contentType) throws Exception { InputStream inputStream = file.getInputStream(); return client.putObject( PutObjectArgs.builder() .bucket(bucketName) .object(fileName) .contentType(contentType.getValue()) .stream(inputStream, inputStream.available(), IntegerConsts.MINUS_ONE) .build()); } /** * 將文件進行分片上傳 * <p>有一個未處理的bug(雖然概率很低很低):</p> * 當(dāng)兩個線程同時上傳md5相同的文件時,由于兩者會定位到同一個桶的同一個臨時目錄,兩個線程會相互產(chǎn)生影響! * * @param file 分片文件 * @param currIndex 當(dāng)前文件的分片索引 * @param totalPieces 切片總數(shù)(對于同一個文件,請確保切片總數(shù)始終不變) * @param md5 整體文件MD5 * @return 剩余未上傳的文件索引集合 */ public static ResponseBuilder uploadFileFragment(MultipartFile file, Integer currIndex, Integer totalPieces, String md5) throws Exception { checkNull(currIndex, totalPieces, md5); // 把當(dāng)前分片上傳至臨時桶 if (!bucketExists(DEFAULT_TEMP_BUCKET_NAME)) { createBucket(DEFAULT_TEMP_BUCKET_NAME); } uploadFileStream(DEFAULT_TEMP_BUCKET_NAME, getFileTempPath(md5, currIndex, totalPieces), file.getInputStream()); // 得到已上傳的文件索引 Iterable<Result<Item>> results = getFilesByPrefix(DEFAULT_TEMP_BUCKET_NAME, md5.concat(StringUtils.BACKSLASH), Boolean.FALSE); Set<Integer> savedIndex = Sets.newHashSet(); boolean fileExists = Boolean.FALSE; for (Result<Item> item : results) { Integer idx = Integer.valueOf(getContentAfterSlash(item.get().objectName())); if (currIndex.equals(idx)) { fileExists = Boolean.TRUE; } savedIndex.add(idx); } // 得到未上傳的文件索引 Set<Integer> remainIndex = Sets.newTreeSet(); for (int i = IntegerConsts.ZERO; i < totalPieces; i++) { if (!savedIndex.contains(i)) { remainIndex.add(i); } } if (fileExists) { return ResponseBuilder.failBuilder("index [" + currIndex + "] exists"); } // 還剩一個索引未上傳,當(dāng)前上傳索引剛好是未上傳索引,上傳完當(dāng)前索引后就完全結(jié)束了。 if (remainIndex.size() == IntegerConsts.ONE && remainIndex.contains(currIndex)) { return ResponseBuilder.successBuilder("completed"); } return ResponseBuilder.failBuilder("index [" + currIndex + "] has been uploaded"); } /** * 合并分片文件,并放到指定目錄 * 前提是之前已把所有分片上傳完畢。 * * @param bucketName 目標(biāo)文件桶名 * @param targetName 目標(biāo)文件名(含完整路徑) * @param totalPieces 切片總數(shù)(對于同一個文件,請確保切片總數(shù)始終不變) * @param md5 文件md5 * @return minio原生對象,記錄了文件上傳信息 */ public static boolean composeFileFragment(String bucketName, String targetName, Integer totalPieces, String md5) throws Exception { checkNull(bucketName, targetName, totalPieces, md5); // 檢查文件索引是否都上傳完畢 Iterable<Result<Item>> results = getFilesByPrefix(DEFAULT_TEMP_BUCKET_NAME, md5.concat(StringUtils.BACKSLASH), false); Set<String> savedIndex = Sets.newTreeSet(); for (Result<Item> item : results) { savedIndex.add(item.get().objectName()); } if (savedIndex.size() == totalPieces) { // 文件路徑 轉(zhuǎn) 文件合并對象 List<ComposeSource> sourceObjectList = savedIndex.stream() .map(filePath -> ComposeSource.builder() .bucket(DEFAULT_TEMP_BUCKET_NAME) .object(filePath) .build()) .collect(Collectors.toList()); ObjectWriteResponse objectWriteResponse = client.composeObject( ComposeObjectArgs.builder() .bucket(bucketName) .object(targetName) .sources(sourceObjectList) .build()); // 上傳成功,則刪除所有的臨時分片文件 List<String> filePaths = Stream.iterate(IntegerConsts.ZERO, i -> ++i) .limit(totalPieces) .map(i -> getFileTempPath(md5, i, totalPieces)) .collect(Collectors.toList()); Iterable<Result<DeleteError>> deleteResults = removeFiles(DEFAULT_TEMP_BUCKET_NAME, filePaths); // 遍歷錯誤集合(無元素則成功) for (Result<DeleteError> result : deleteResults) { DeleteError error = result.get(); System.err.printf("[Bigfile] 分片'%s'刪除失敗! 錯誤信息: %s", error.objectName(), error.message()); } return true; } throw new GlobalException("The fragment index is not complete. Please check parameters [totalPieces] or [md5]"); } /** * 上傳本地文件 * * @param bucketName 存儲桶 * @param fileName 文件名稱 * @param filePath 本地文件路徑 */ public ObjectWriteResponse uploadFile(String bucketName, String fileName, String filePath) throws Exception { return client.uploadObject( UploadObjectArgs.builder() .bucket(bucketName) .object(fileName) .filename(filePath) .build()); } /** * 通過流上傳文件 * * @param bucketName 存儲桶 * @param fileName 文件名 * @param inputStream 文件流 */ public static ObjectWriteResponse uploadFileStream(String bucketName, String fileName, InputStream inputStream) throws Exception { return client.putObject( PutObjectArgs.builder() .bucket(bucketName) .object(fileName) .stream(inputStream, inputStream.available(), IntegerConsts.MINUS_ONE) .build()); } /** * 判斷文件是否存在 * * @param bucketName 存儲桶 * @param fileName 文件名 * @return true: 存在 */ public static boolean isFileExist(String bucketName, String fileName) { boolean exist = true; try { client.statObject(StatObjectArgs.builder().bucket(bucketName).object(fileName).build()); } catch (Exception e) { exist = false; } return exist; } /** * 判斷文件夾是否存在 * * @param bucketName 存儲桶 * @param folderName 目錄名稱:本項目約定路徑是以"/"開頭,不以"/"結(jié)尾 * @return true: 存在 */ public boolean isFolderExist(String bucketName, String folderName) { // 去掉頭"/",才能搜索到相關(guān)前綴 folderName = trimHead(folderName); boolean exist = false; try { Iterable<Result<Item>> results = client.listObjects( ListObjectsArgs.builder().bucket(bucketName).prefix(folderName).recursive(false).build()); for (Result<Item> result : results) { Item item = result.get(); // 增加尾"/",才能匹配到目錄名字 String objectName = addTail(folderName); if (item.isDir() && objectName.equals(item.objectName())) { exist = true; } } } catch (Exception e) { exist = false; } return exist; } /** * 獲取路徑下文件列表 * * @param bucketName 存儲桶 * @param prefix 文件名稱 * @param recursive 是否遞歸查找,false:模擬文件夾結(jié)構(gòu)查找 * @return 二進制流 */ public static Iterable<Result<Item>> getFilesByPrefix(String bucketName, String prefix, boolean recursive) { return client.listObjects( ListObjectsArgs.builder() .bucket(bucketName) .prefix(prefix) .recursive(recursive) .build()); } /** * 獲取文件信息, 如果拋出異常則說明文件不存在 * * @param bucketName 存儲桶 * @param fileName 文件名稱 */ public StatObjectResponse getFileStatusInfo(String bucketName, String fileName) throws Exception { return client.statObject( StatObjectArgs.builder() .bucket(bucketName) .object(fileName) .build()); } /** * 根據(jù)文件前綴查詢文件 * * @param bucketName 存儲桶 * @param prefix 前綴 * @param recursive 是否使用遞歸查詢 * @return MinioItem 列表 */ public List<Item> getAllFilesByPrefix(String bucketName, String prefix, boolean recursive) throws Exception { List<Item> list = new ArrayList<>(); Iterable<Result<Item>> objectsIterator = client.listObjects( ListObjectsArgs.builder().bucket(bucketName).prefix(prefix).recursive(recursive).build()); if (objectsIterator != null) { for (Result<Item> o : objectsIterator) { Item item = o.get(); list.add(item); } } return list; } /** * 批量刪除文件 * * @param bucketName 存儲桶 * @param filePaths<String> 需要刪除的文件列表 * @return Result */ public static Iterable<Result<DeleteError>> removeFiles(String bucketName, List<String> filePaths) { List<DeleteObject> objectPaths = filePaths.stream() .map(filePath -> new DeleteObject(filePath)) .collect(Collectors.toList()); return client.removeObjects( RemoveObjectsArgs.builder().bucket(bucketName).objects(objectPaths).build()); } /** * 獲取文件的二進制流 * * @param bucketName 存儲桶 * @param fileName 文件名 * @return 二進制流 */ public InputStream getFileStream(String bucketName, String fileName) throws Exception { return client.getObject(GetObjectArgs.builder().bucket(bucketName).object(fileName).build()); } /** * 斷點下載 * * @param bucketName 存儲桶 * @param fileName 文件名稱 * @param offset 起始字節(jié)的位置 * @param length 要讀取的長度 * @return 二進制流 */ public InputStream getFileStream(String bucketName, String fileName, long offset, long length) throws Exception { return client.getObject( GetObjectArgs.builder() .bucket(bucketName) .object(fileName) .offset(offset) .length(length) .build()); } /** * 拷貝文件 * * @param bucketName 存儲桶 * @param fileName 文件名 * @param srcBucketName 目標(biāo)存儲桶 * @param srcFileName 目標(biāo)文件名 */ public ObjectWriteResponse copyFile(String bucketName, String fileName, String srcBucketName, String srcFileName) throws Exception { return client.copyObject( CopyObjectArgs.builder() .source(CopySource.builder().bucket(bucketName).object(fileName).build()) .bucket(srcBucketName) .object(srcFileName) .build()); } /** * 刪除文件夾(未完成) * * @param bucketName 存儲桶 * @param fileName 路徑 */ @Deprecated public void removeFolder(String bucketName, String fileName) throws Exception { // 加尾 fileName = addTail(fileName); client.removeObject( RemoveObjectArgs.builder() .bucket(bucketName) .object(fileName) .build()); } /** * 刪除文件 * * @param bucketName 存儲桶 * @param fileName 文件名稱 */ public void removeFile(String bucketName, String fileName) throws Exception { // 掐頭 fileName = trimHead(fileName); client.removeObject( RemoveObjectArgs.builder() .bucket(bucketName) .object(fileName) .build()); } /** * 獲得文件外鏈 * * @param bucketName 存儲桶 * @param fileName 文件名 * @return url 返回地址 * @throws Exception */ public static String getPresignedObjectUrl(String bucketName, String fileName) throws Exception { GetPresignedObjectUrlArgs args = GetPresignedObjectUrlArgs.builder() .bucket(bucketName) .object(fileName) .method(Method.GET).build(); return client.getPresignedObjectUrl(args); } /** * 獲取文件外鏈 * * @param bucketName 存儲桶 * @param fileName 文件名 * @param expires 過期時間 <=7 秒 (外鏈有效時間(單位:秒)) * @return url * @throws Exception */ public String getPresignedObjectUrl(String bucketName, String fileName, Integer expires) throws Exception { GetPresignedObjectUrlArgs args = GetPresignedObjectUrlArgs.builder() .method(Method.GET) .expiry(expires, TimeUnit.SECONDS) .bucket(bucketName) .object(fileName) .build(); return client.getPresignedObjectUrl(args); } /** * 通過文件的md5,以及分片文件的索引,構(gòu)造分片文件的臨時存儲路徑 * * @param md5 文件md5 * @param currIndex 分片文件索引(從0開始) * @param totalPieces 總分片 * @return 臨時存儲路徑 */ private static String getFileTempPath(String md5, Integer currIndex, Integer totalPieces) { int zeroCnt = countDigits(totalPieces) - countDigits(currIndex); StringBuilder name = new StringBuilder(md5); name.append(StringUtils.BACKSLASH); for (int i = IntegerConsts.ZERO; i < zeroCnt; i++) { name.append(IntegerConsts.ZERO); } name.append(currIndex); return name.toString(); } /** * 創(chuàng)建目錄 * * @param bucketName 存儲桶 * @param folderName 目錄路徑:本項目約定路徑是以"/"開頭,不以"/"結(jié)尾 */ public ObjectWriteResponse createFolder(String bucketName, String folderName) throws Exception { // 這是minio的bug,只有在路徑的尾巴加上"/",才能當(dāng)成文件夾。 folderName = addTail(folderName); return client.putObject( PutObjectArgs.builder() .bucket(bucketName) .object(folderName) .stream(new ByteArrayInputStream(new byte[]{}), IntegerConsts.ZERO, IntegerConsts.MINUS_ONE) .build()); } }
資源目錄下新建 META-INF\spring
該方式支持 Spring Boot 3.x
新建文件 org.springframework.boot.autoconfigure.AutoConfiguration.imports 內(nèi)容
com.cdkjframework.minio.config.MinioAutoConfiguration
總結(jié)
Spring Boot 封裝 MinIO 工具的核心意義在于將分布式存儲能力轉(zhuǎn)化為可復(fù)用的基礎(chǔ)設(shè)施,通過標(biāo)準(zhǔn)化、模塊化的設(shè)計,顯著降低開發(fā)復(fù)雜度,提升系統(tǒng)健壯性和可維護性。這種封裝不僅是技術(shù)層面的優(yōu)化,更是工程實踐中的最佳選擇,尤其適用于需要快速迭代、高并發(fā)處理及多云兼容的現(xiàn)代應(yīng)用架構(gòu)。
到此這篇關(guān)于SpringBoot封裝MinIO工具的實現(xiàn)步驟的文章就介紹到這了,更多相關(guān)SpringBoot封裝MinIO內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
JAVA8 STREAM COLLECT GROUPBY分組實例解析
這篇文章主要介紹了JAVA8 STREAM COLLECT GROUPBY分組實例解析,文中通過示例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友可以參考下2020-01-01Springmvc發(fā)送json數(shù)據(jù)轉(zhuǎn)Java對象接收
這篇文章主要介紹了Springmvc發(fā)送json數(shù)據(jù)轉(zhuǎn)Java對象接收,文中通過示例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友可以參考下2020-10-10RocketMQ4.5.2 修改mqnamesrv 和 mqbroker的日志路徑操作
這篇文章主要介紹了RocketMQ 4.5.2 修改mqnamesrv 和 mqbroker的日志路徑操作,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-07-07springboot程序啟動慢-未配置hostname的解決
這篇文章主要介紹了springboot程序啟動慢-未配置hostname的解決方案,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-08-08

Mybatis-Plus進階分頁與樂觀鎖插件及通用枚舉和多數(shù)據(jù)源詳解