SpringBoot封裝MinIO工具的實(shí)現(xiàn)步驟
MinIO 簡介
MinIO 是一款高性能、開源、兼容Amazon S3 API的分布式對象存儲系統(tǒng),專為云原生架構(gòu)和大規(guī)模非結(jié)構(gòu)化數(shù)據(jù)場景設(shè)計(jì)。其核心定位是成為私有云/混合云環(huán)境中的標(biāo)準(zhǔn)存儲方案,適用于從數(shù)據(jù)湖到AI/ML、容器化部署等多樣化需求。
背景
為解決業(yè)務(wù)開時(shí)對接 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工具庫
項(xiàng)目介紹
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 項(xiàng)目引入
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;
/**
* 存儲端點(diǎn)
*/
private String endpoint;
/**
* 端口
*/
private Integer port;
/**
* 訪問密鑰
*/
private String accessKey;
/**
* 密鑰
*/
private String secretKey;
/**
* 存儲桶名稱
*/
private String bucketName;
/**
* 分片對象過期時(shí)間 單位(天)
*/
private Integer expiry;
/**
* 斷點(diǎn)續(xù)傳有效時(shí)間,在redis存儲任務(wù)的時(shí)間 單位(天)
*/
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 {
/**
* 默認(rèn)類型
*/
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();
// 實(shí)例化工具類
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 {
/**
* 默認(rèn)的臨時(shí)文件存儲桶
*/
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);
}
/**
* 把路徑開頭的"/"去掉,并在末尾添加"/",這個(gè)是minio對象名的樣子。
*
* @param projectPath 以"/"開頭、以字母結(jié)尾的路徑
* @return 去掉開頭"/"
*/
private static String trimHead(String projectPath) {
return projectPath.substring(IntegerConsts.ONE);
}
/**
* 把路徑開頭的"/"去掉,并在末尾添加"/",這個(gè)是minio對象名的樣子。
*
* @param projectPath 以"/"開頭、以字母結(jié)尾的路徑
* @return 添加結(jié)尾"/"
*/
private static String addTail(String projectPath) {
return projectPath + StringUtils.BACKSLASH;
}
/**
* 獲取數(shù)值的位數(shù)(用于構(gòu)造臨時(shí)文件名)
*
* @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");
}
}
}
/**
* 給定一個(gè)字符串,返回其"/"符號后面的字符串
*
* @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());
}
/**
* 如果一個(gè)桶不存在,則創(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進(jìn)行文件上傳
*
* @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());
}
/**
* 將文件進(jìn)行分片上傳
* <p>有一個(gè)未處理的bug(雖然概率很低很低):</p>
* 當(dāng)兩個(gè)線程同時(shí)上傳md5相同的文件時(shí),由于兩者會定位到同一個(gè)桶的同一個(gè)臨時(shí)目錄,兩個(gè)線程會相互產(chǎn)生影響!
*
* @param file 分片文件
* @param currIndex 當(dāng)前文件的分片索引
* @param totalPieces 切片總數(shù)(對于同一個(gè)文件,請確保切片總數(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)前分片上傳至臨時(shí)桶
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");
}
// 還剩一個(gè)索引未上傳,當(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ù)(對于同一個(gè)文件,請確保切片總數(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());
// 上傳成功,則刪除所有的臨時(shí)分片文件
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);
// 遍歷錯(cuò)誤集合(無元素則成功)
for (Result<DeleteError> result : deleteResults) {
DeleteError error = result.get();
System.err.printf("[Bigfile] 分片'%s'刪除失敗! 錯(cuò)誤信息: %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 目錄名稱:本項(xiàng)目約定路徑是以"/"開頭,不以"/"結(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 二進(jìn)制流
*/
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());
}
/**
* 獲取文件的二進(jìn)制流
*
* @param bucketName 存儲桶
* @param fileName 文件名
* @return 二進(jìn)制流
*/
public InputStream getFileStream(String bucketName, String fileName) throws Exception {
return client.getObject(GetObjectArgs.builder().bucket(bucketName).object(fileName).build());
}
/**
* 斷點(diǎn)下載
*
* @param bucketName 存儲桶
* @param fileName 文件名稱
* @param offset 起始字節(jié)的位置
* @param length 要讀取的長度
* @return 二進(jìn)制流
*/
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 過期時(shí)間 <=7 秒 (外鏈有效時(shí)間(單位:秒))
* @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)造分片文件的臨時(shí)存儲路徑
*
* @param md5 文件md5
* @param currIndex 分片文件索引(從0開始)
* @param totalPieces 總分片
* @return 臨時(shí)存儲路徑
*/
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 目錄路徑:本項(xiàng)目約定路徑是以"/"開頭,不以"/"結(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è)計(jì),顯著降低開發(fā)復(fù)雜度,提升系統(tǒng)健壯性和可維護(hù)性。這種封裝不僅是技術(shù)層面的優(yōu)化,更是工程實(shí)踐中的最佳選擇,尤其適用于需要快速迭代、高并發(fā)處理及多云兼容的現(xiàn)代應(yīng)用架構(gòu)。
到此這篇關(guān)于SpringBoot封裝MinIO工具的實(shí)現(xiàn)步驟的文章就介紹到這了,更多相關(guān)SpringBoot封裝MinIO內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- SpringBoot整合MinIO實(shí)現(xiàn)文件上傳的方法詳解
- springboot整合minio實(shí)現(xiàn)文件上傳與下載且支持鏈接永久訪問
- 可能是全網(wǎng)最詳細(xì)的springboot整合minio教程
- SpringBoot整合Minio實(shí)現(xiàn)文件上傳和讀取功能
- springboot整合minio的超詳細(xì)教程
- SpringBoot整合Minio實(shí)現(xiàn)圖片上傳功能
- 手把手教你SpringBoot輕松整合Minio
- SpringBoot使用minio及配置代碼
- SpringBoot項(xiàng)目集成MinIO全過程
相關(guān)文章
JAVA8 STREAM COLLECT GROUPBY分組實(shí)例解析
這篇文章主要介紹了JAVA8 STREAM COLLECT GROUPBY分組實(shí)例解析,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-01-01
Springmvc發(fā)送json數(shù)據(jù)轉(zhuǎn)Java對象接收
這篇文章主要介紹了Springmvc發(fā)送json數(shù)據(jù)轉(zhuǎn)Java對象接收,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-10-10
java基于odbc連接oracle的實(shí)現(xiàn)方法
這篇文章主要介紹了java基于odbc連接oracle的實(shí)現(xiàn)方法,結(jié)合實(shí)例形式分析了連接操作的具體步驟與相關(guān)實(shí)現(xiàn)技巧,需要的朋友可以參考下2016-09-09
RocketMQ4.5.2 修改mqnamesrv 和 mqbroker的日志路徑操作
這篇文章主要介紹了RocketMQ 4.5.2 修改mqnamesrv 和 mqbroker的日志路徑操作,具有很好的參考價(jià)值,希望對大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-07-07
springboot程序啟動慢-未配置hostname的解決
這篇文章主要介紹了springboot程序啟動慢-未配置hostname的解決方案,具有很好的參考價(jià)值,希望對大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-08-08
Java實(shí)體類實(shí)現(xiàn)鏈?zhǔn)讲僮鲗?shí)例解析
這篇文章主要介紹了Java實(shí)體類實(shí)現(xiàn)鏈?zhǔn)讲僮鲗?shí)例解析,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2019-12-12
JAVA利用接口實(shí)現(xiàn)多繼承問題的代碼實(shí)操演示
Java語言并不支持多繼承,這是由于多繼承會帶來許多復(fù)雜的問題,例如"菱形問題"等,下面這篇文章主要給大家介紹了關(guān)于JAVA利用接口實(shí)現(xiàn)多繼承問題的相關(guān)資料,需要的朋友可以參考下2024-03-03
MyBatis自定義typeHandler的完整實(shí)例
這篇文章主要給大家介紹了關(guān)于MyBatis自定義typeHandler的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),對大家學(xué)習(xí)或者使用MyBatis具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面來一起學(xué)習(xí)學(xué)習(xí)吧2019-04-04
Mybatis-Plus進(jìn)階分頁與樂觀鎖插件及通用枚舉和多數(shù)據(jù)源詳解
這篇文章主要介紹了Mybatis-Plus的分頁插件與樂觀鎖插件還有通用枚舉和多數(shù)據(jù)源的相關(guān)介紹,文中代碼附有詳細(xì)的注釋,感興趣的朋友來看看吧2022-03-03

