update 更新 完善新的S3客户端相关配置

This commit is contained in:
秋辞未寒
2026-03-21 17:08:29 +08:00
parent aa38a7b98a
commit 2dc289979f
9 changed files with 219 additions and 56 deletions

View File

@@ -1,5 +1,8 @@
package org.dromara.common.oss.s3.client;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.util.IdUtil;
import org.dromara.common.core.utils.StringUtils;
import org.dromara.common.oss.s3.config.S3StorageClientConfig;
import org.dromara.common.oss.s3.domain.GetObjectResult;
import org.dromara.common.oss.s3.domain.HandleAsyncResult;
@@ -45,6 +48,13 @@ public abstract class AbstractS3StorageClientImpl implements S3StorageClient {
private final AtomicBoolean initialized = new AtomicBoolean(false);
/**
* S3 存储客户端ID
*
* 用于标识客户端,初始化后不允许更改
*/
protected final String clientId;
/**
* S3 存储客户端配置。
*/
@@ -70,15 +80,34 @@ public abstract class AbstractS3StorageClientImpl implements S3StorageClient {
*/
protected ExecutorService asyncExecutor;
public AbstractS3StorageClientImpl(S3StorageClientConfig config) {
public AbstractS3StorageClientImpl(String clientId, S3StorageClientConfig config) {
Assert.notNull(config, () -> S3StorageException.form("S3StorageClientConfig must not be null"));
// 如果没有设置存储客户端ID则随机生成一个
this.clientId = StringUtils.isBlank(clientId)? IdUtil.fastSimpleUUID() : clientId;
this.config = config;
this.initialize();
}
@Override
public String clientId() {
return this.clientId;
}
@Override
public S3StorageClientConfig config() {
// 仅返回copy副本防篡改
return this.config.copy();
}
@Override
public boolean isInitialized() {
return initialized.get();
}
@Override
public void initialize() {
// 如果已经是初始化状态,则直接返回
if (initialized.get()) {
if (isInitialized()) {
return;
}
try {
@@ -118,12 +147,13 @@ public abstract class AbstractS3StorageClientImpl implements S3StorageClient {
@Override
public boolean verifyConfig(Function<S3StorageClientConfig, Boolean> verifyConfigAction) {
return Boolean.TRUE.equals(verifyConfigAction.apply(config.copy()));
S3StorageClientConfig config = config();
return Boolean.TRUE.equals(verifyConfigAction.apply(config));
}
@Override
public boolean verifyConfig(S3StorageClientConfig verifyConfig) {
return verifyConfig(config -> Objects.equals(config, verifyConfig));
return verifyConfig((config) -> Objects.equals(config, verifyConfig));
}
@Override
@@ -456,5 +486,7 @@ public abstract class AbstractS3StorageClientImpl implements S3StorageClient {
if (asyncExecutor != null) {
asyncExecutor.close();
}
// 重置初始化状态为 false
initialized.compareAndSet(true, false);
}
}

View File

@@ -23,8 +23,8 @@ import java.util.concurrent.Executors;
*/
public class DefaultS3StorageClientImpl extends AbstractS3StorageClientImpl {
public DefaultS3StorageClientImpl(S3StorageClientConfig config) {
super(config);
public DefaultS3StorageClientImpl(String clientId, S3StorageClientConfig config) {
super(clientId, config);
}
@Override

View File

@@ -1,5 +1,6 @@
package org.dromara.common.oss.s3.client;
import cn.hutool.core.util.IdUtil;
import org.dromara.common.oss.s3.config.S3StorageClientConfig;
import org.dromara.common.oss.s3.domain.GetObjectResult;
import org.dromara.common.oss.s3.domain.HandleAsyncResult;
@@ -40,6 +41,27 @@ import java.util.function.Function;
*/
public interface S3StorageClient extends AutoCloseable {
/**
* S3 存储客户端ID
*
* 用于标识客户端,初始化后不允许更改
*
* @return S3 存储客户端ID
*/
default String clientId(){
return IdUtil.fastSimpleUUID();
}
/**
* 获取客户端配置copy副本
*/
S3StorageClientConfig config();
/**
* 是否已经初始化
*/
boolean isInitialized();
/**
* 初始化客户端
*/

View File

@@ -1,6 +1,6 @@
package org.dromara.common.oss.s3.config;
import lombok.*;
import lombok.Builder;
import org.dromara.common.oss.s3.enums.AccessPolicy;
import org.jspecify.annotations.NonNull;
@@ -11,35 +11,31 @@ import java.util.Optional;
/**
* S3 ACL访问策略配置
*
* @param enabled 是否启用ACL
* @param accessPolicy 访问策略
* @author 秋辞未寒
*/
@Data
@Builder
@EqualsAndHashCode
public class S3AccessControlPolicyConfig implements Config<S3AccessControlPolicyConfig,S3AccessControlPolicyConfig.S3AccessControlPolicyConfigBuilder>,Serializable {
public record S3AccessControlPolicyConfig(
boolean enabled
, AccessPolicy accessPolicy
) implements Config<S3AccessControlPolicyConfig, S3AccessControlPolicyConfig.S3AccessControlPolicyConfigBuilder>, Serializable {
@Serial
private static final long serialVersionUID = 1L;
public static final S3AccessControlPolicyConfig DEFAULT = S3AccessControlPolicyConfig.builder().build();
/**
* 是否启用ACL
* 默认访问策略配置
*/
private boolean enabled;
/**
* 访问策略
*/
private AccessPolicy accessPolicy;
public boolean enabled() {
return enabled;
}
public static final S3AccessControlPolicyConfig DEFAULT = S3AccessControlPolicyConfig.builder()
.enabled(false)
.accessPolicy(AccessPolicy.PUBLIC_READ_WRITE)
.build();
@Override
public @NonNull AccessPolicy accessPolicy() {
return Optional.ofNullable(accessPolicy)
.orElse(AccessPolicy.PRIVATE);
.orElse(AccessPolicy.PUBLIC_READ_WRITE);
}
@Override

View File

@@ -1,8 +1,6 @@
package org.dromara.common.oss.s3.config;
import lombok.Builder;
import lombok.EqualsAndHashCode;
import lombok.RequiredArgsConstructor;
import java.io.Serial;
import java.io.Serializable;
@@ -10,12 +8,17 @@ import java.io.Serializable;
/**
* S3 异步执行器配置
*
* @param enabledVirtualThread 是否启用虚拟线程
* @param corePoolSize 核心线程数
* <p>
* 默认为当前CPU核心数该配置项在配置了虚拟线程后会失效
* @author 秋辞未寒
*/
@RequiredArgsConstructor
@Builder
@EqualsAndHashCode
public class S3AsyncExecutorConfig implements Config<S3AsyncExecutorConfig,S3AsyncExecutorConfig.S3AsyncExecutorConfigBuilder>,Serializable {
public record S3AsyncExecutorConfig(
boolean enabledVirtualThread
, int corePoolSize
) implements Config<S3AsyncExecutorConfig, S3AsyncExecutorConfig.S3AsyncExecutorConfigBuilder>, Serializable {
@Serial
private static final long serialVersionUID = 1L;
@@ -25,23 +28,18 @@ public class S3AsyncExecutorConfig implements Config<S3AsyncExecutorConfig,S3Asy
*/
public static final int DEFAULT_CORE_POOL_SIZE = Runtime.getRuntime().availableProcessors();
public static final S3AsyncExecutorConfig DEFAULT = S3AsyncExecutorConfig.builder().build();
/**
* 是否启用虚拟线程
*/
private boolean enabledVirtualThread = false;
/**
* 核心线程数
*
* 默认为当前CPU核心数该配置项在配置了虚拟线程后会失效
*/
private int corePoolSize = DEFAULT_CORE_POOL_SIZE;
/**
* 默认异步执行器配置
*/
public static final S3AsyncExecutorConfig DEFAULT = S3AsyncExecutorConfig.builder()
.enabledVirtualThread(false)
.corePoolSize(DEFAULT_CORE_POOL_SIZE)
.build();
/**
* 是否启用虚拟线程
*/
@Override
public boolean enabledVirtualThread() {
return enabledVirtualThread;
}
@@ -49,6 +47,7 @@ public class S3AsyncExecutorConfig implements Config<S3AsyncExecutorConfig,S3Asy
/**
* 核心线程数
*/
@Override
public int corePoolSize() {
return corePoolSize;
}

View File

@@ -4,6 +4,10 @@ import cn.hutool.http.HttpUtil;
import lombok.Builder;
import lombok.EqualsAndHashCode;
import lombok.RequiredArgsConstructor;
import org.dromara.common.core.constant.SystemConstants;
import org.dromara.common.core.utils.StringUtils;
import org.dromara.common.oss.constant.OssConstant;
import org.dromara.common.oss.properties.OssProperties;
import org.dromara.common.oss.s3.exception.S3StorageException;
import org.dromara.common.oss.s3.util.BucketUrlUtil;
import org.jspecify.annotations.NonNull;
@@ -236,4 +240,41 @@ public class S3StorageClientConfig implements Config<S3StorageClientConfig, S3St
.accessControlPolicyConfig(accessControlPolicyConfig().copy())
.asyncExecutorConfig(asyncExecutorConfig().copy());
}
public static S3StorageClientConfig formProperties(OssProperties properties){
return formPropertiesBuilder(properties).build();
}
public static S3StorageClientConfigBuilder formPropertiesBuilder(OssProperties properties){
String regionString = properties.getRegion();
Region region = Region.US_EAST_1;
if (StringUtils.isNotBlank(regionString)) {
region = Region.of(regionString);
}
// 是否使用路径风格应当由使用者明确去配置,此处的配置只是为了适配旧的配置项
// MinIO 使用 HTTPS 限制使用域名访问,站点填域名。需要启用路径样式访问
boolean usePathStyleAccess = !StringUtils.containsAny(properties.getEndpoint(), OssConstant.CLOUD_SERVICE);
// // 目前自定义实现的 Client 中并没有实际使用到ACL相关配置只是作为一个扩展点保留有需要ACL的自行实现调用逻辑
// String accessPolicyString = properties.getAccessPolicy();
// // 绝大多数的云厂商都是不允许操作ACL的所以此处的默认配置也是禁用ACL的
// S3AccessControlPolicyConfig accessControlPolicyConfig = S3AccessControlPolicyConfig.DEFAULT;
// if (StringUtils.isNotBlank(accessPolicyString)) {
// accessControlPolicyConfig = S3AccessControlPolicyConfig.builder()
// .enabled(true)
// .accessPolicy(AccessPolicy.formType(accessPolicyString))
// .build();
// }
return builder()
.endpoint(properties.getEndpoint())
.domain(properties.getDomainUrl())
.accessKey(properties.getAccessKey())
.secretKey(properties.getSecretKey())
.bucket(properties.getBucketName())
.region(region)
.useHttps(SystemConstants.YES.equals(properties.getIsHttps()))
.usePathStyleAccess(usePathStyleAccess);
// .accessControlPolicyConfig(accessControlPolicyConfig);
}
}

View File

@@ -2,9 +2,12 @@ package org.dromara.common.oss.s3.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
import org.dromara.common.oss.s3.exception.S3StorageException;
import software.amazon.awssdk.services.s3.model.BucketCannedACL;
import software.amazon.awssdk.services.s3.model.ObjectCannedACL;
import java.util.Arrays;
/**
* 访问策略
*
@@ -17,17 +20,22 @@ public enum AccessPolicy {
/**
* 私有
*/
PRIVATE(BucketCannedACL.PRIVATE, ObjectCannedACL.PRIVATE),
PRIVATE(0,BucketCannedACL.PRIVATE, ObjectCannedACL.PRIVATE),
/**
* 公有读写
*/
PUBLIC_READ_WRITE(BucketCannedACL.PUBLIC_READ_WRITE, ObjectCannedACL.PUBLIC_READ_WRITE),
PUBLIC_READ_WRITE(1,BucketCannedACL.PUBLIC_READ_WRITE, ObjectCannedACL.PUBLIC_READ_WRITE),
/**
* 公有只读
*/
PUBLIC_READ(BucketCannedACL.PUBLIC_READ, ObjectCannedACL.PUBLIC_READ);
PUBLIC_READ(2,BucketCannedACL.PUBLIC_READ, ObjectCannedACL.PUBLIC_READ);
/**
* 访问策略类型
*/
private final Integer type;
/**
* 桶权限
@@ -39,4 +47,10 @@ public enum AccessPolicy {
*/
private final ObjectCannedACL objectCannedACL;
public static AccessPolicy formType(String type) {
return Arrays.stream(values())
.filter(policy -> policy.getType().toString().equals(type))
.findFirst()
.orElseThrow(() -> S3StorageException.form("'type' not found By " + type));
}
}

View File

@@ -5,6 +5,7 @@ import org.dromara.common.core.constant.CacheNames;
import org.dromara.common.core.utils.StringUtils;
import org.dromara.common.json.utils.JsonUtils;
import org.dromara.common.oss.constant.OssConstant;
import org.dromara.common.oss.properties.OssProperties;
import org.dromara.common.oss.s3.client.DefaultS3StorageClientImpl;
import org.dromara.common.oss.s3.client.S3StorageClient;
import org.dromara.common.oss.s3.config.S3StorageClientConfig;
@@ -14,6 +15,7 @@ import org.dromara.common.redis.utils.RedisUtils;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.locks.ReentrantLock;
/**
* S3存储客户端工厂
@@ -24,6 +26,7 @@ import java.util.concurrent.ConcurrentHashMap;
public class S3StorageClientFactory {
private static final Map<String, S3StorageClient> CLIENT_CACHE = new ConcurrentHashMap<>();
private static final ReentrantLock LOCK = new ReentrantLock();
/**
* 获取默认实例
@@ -41,20 +44,30 @@ public class S3StorageClientFactory {
* 根据类型获取实例
*/
public static S3StorageClient instance(String configKey) {
// 使用租户标识避免多个租户相同key实例覆盖
return CLIENT_CACHE.computeIfAbsent(configKey, S3StorageClientFactory::instanceCache);
}
/**
* 使用缓存实例化
*/
private static S3StorageClient instanceCache(String configKey) {
String json = CacheUtils.get(CacheNames.SYS_OSS_CONFIG, configKey);
if (json == null) {
throw S3StorageException.form("系统异常, '" + configKey + "'配置信息不存在!");
}
S3StorageClientConfig config = JsonUtils.parseObject(json, S3StorageClientConfig.class);
return new DefaultS3StorageClientImpl(config);
OssProperties properties = JsonUtils.parseObject(json, OssProperties.class);
S3StorageClientConfig config = S3StorageClientConfig.formProperties(properties);
LOCK.lock();
try {
// 如果已经存在,则校验配置一致性
if (CLIENT_CACHE.containsKey(configKey)) {
S3StorageClient client = CLIENT_CACHE.get(configKey);
if (!client.verifyConfig(config)) {
// 配置不一致,刷新配置
client.refresh(config);
CLIENT_CACHE.put(configKey, client);
}
return client;
}
DefaultS3StorageClientImpl client = new DefaultS3StorageClientImpl(configKey,config);
CLIENT_CACHE.put(configKey, client);
return client;
} finally {
LOCK.lock();
}
}
/**
@@ -68,7 +81,7 @@ public class S3StorageClientFactory {
try {
client.close();
} catch (Exception e) {
log.warn("S3存储客户端关闭异常错误信息: {}",e.getMessage(),e);
log.warn("S3存储客户端关闭异常错误信息: {}", e.getMessage(), e);
}
return true;
}

View File

@@ -0,0 +1,46 @@
package org.dromara.common.oss.s3.util;
import cn.hutool.core.util.IdUtil;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import org.dromara.common.core.utils.DateUtils;
import org.dromara.common.core.utils.StringUtils;
/**
* S3文件对象工具类
*
* @author 秋辞未寒
*/
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class S3ObjectUtil {
/**
* 生成一个 【自定义前缀 + 日期路径 + SimpleUUID.文件后缀】 的对象Key 示例: images/20260321/019d0f89c9b1130a48c90dbca0475a.jpg
*
* @param prefix 前缀
* @param withSuffixFileName 带后缀的文件名
* @return 文件路径对象Key
*/
public static String buildPathKey(String prefix, String withSuffixFileName) {
// 获取后缀
String suffix = StringUtils.substring(withSuffixFileName, withSuffixFileName.lastIndexOf("."), withSuffixFileName.length());
// 生成日期路径
String datePath = DateUtils.datePath();
// 生成uuid
String uuid = IdUtil.fastSimpleUUID();
// 拼接路径
String path = StringUtils.isNotEmpty(prefix) ? prefix + StringUtils.SLASH + datePath + StringUtils.SLASH + uuid : datePath + StringUtils.SLASH + uuid;
return path + suffix;
}
/**
* 生成一个 【日期路径 + SimpleUUID.文件后缀】 的对象Key 示例: 20260321/019d0f89c9b1130a48c90dbca0475a.jpg
*
* @param withSuffixFileName 带后缀的文件名
* @return 文件路径对象Key
*/
public static String buildPathKey(String withSuffixFileName) {
return buildPathKey("", withSuffixFileName);
}
}