refactor(common-oss): 优化 OSS 客户端生命周期与配置构建

- 下载 InputStream 回调后自动关闭响应流,避免底层连接释放不明确
  - 统一解包异步调用异常,保留真实 S3 错误原因
  - OssFactory 改为按 configKey 粒度加锁,降低多配置初始化阻塞
  - OSS 配置更新和删除后主动移除本地旧客户端,释放相关资源
  - 收敛 OSS region、ACL、路径风格和访问 URL 构建逻辑
  - Options 新增 create 方法,保留 builder 兼容旧调用
This commit is contained in:
疯狂的狮子Li
2026-05-16 14:56:07 +08:00
parent ce764f08a7
commit 287cce6238
5 changed files with 123 additions and 94 deletions
@@ -37,7 +37,9 @@ import java.util.Collection;
import java.util.Map; import java.util.Map;
import java.util.Objects; import java.util.Objects;
import java.util.Optional; import java.util.Optional;
import java.util.concurrent.CompletionException;
import java.util.concurrent.ExecutorService; import java.util.concurrent.ExecutorService;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.BiFunction; import java.util.function.BiFunction;
import java.util.function.Consumer; import java.util.function.Consumer;
@@ -119,10 +121,7 @@ public abstract class AbstractOssClientImpl implements OssClient {
// 将状态转为已初始化 // 将状态转为已初始化
initialized.compareAndSet(false, true); initialized.compareAndSet(false, true);
} catch (Exception e) { } catch (Exception e) {
if (e instanceof S3StorageException) { throw toStorageException(e);
throw e;
}
throw S3StorageException.form(e);
} }
} }
@@ -168,10 +167,7 @@ public abstract class AbstractOssClientImpl implements OssClient {
.handleAsync(handleAsyncAction) .handleAsync(handleAsyncAction)
.join(); .join();
} catch (Exception e) { } catch (Exception e) {
if (e instanceof S3StorageException ex) { throw toStorageException(e);
throw ex;
}
throw S3StorageException.form(e);
} }
} }
@@ -229,10 +225,7 @@ public abstract class AbstractOssClientImpl implements OssClient {
options.setLength(file.length()); options.setLength(file.length());
return bucketUpload(bucket, key, file.getChannel(), -1L, options); return bucketUpload(bucket, key, file.getChannel(), -1L, options);
} catch (Exception e) { } catch (Exception e) {
if (e instanceof S3StorageException ex) { throw toStorageException(e);
throw ex;
}
throw S3StorageException.form(e);
} }
} }
@@ -253,10 +246,7 @@ public abstract class AbstractOssClientImpl implements OssClient {
} }
return bucketUpload(bucket, key, in, size, options); return bucketUpload(bucket, key, in, size, options);
} catch (Exception e) { } catch (Exception e) {
if (e instanceof S3StorageException ex) { throw toStorageException(e);
throw ex;
}
throw S3StorageException.form(e);
} }
} }
@@ -282,10 +272,7 @@ public abstract class AbstractOssClientImpl implements OssClient {
try (ByteArrayInputStream in = new ByteArrayInputStream(data)) { try (ByteArrayInputStream in = new ByteArrayInputStream(data)) {
return bucketUpload(bucket, key, in, data.length, options); return bucketUpload(bucket, key, in, data.length, options);
} catch (Exception e) { } catch (Exception e) {
if (e instanceof S3StorageException ex) { throw toStorageException(e);
throw ex;
}
throw S3StorageException.form(e);
} }
} }
@@ -341,10 +328,7 @@ public abstract class AbstractOssClientImpl implements OssClient {
.join() .join()
.result(); .result();
} catch (Exception e) { } catch (Exception e) {
if (e instanceof S3StorageException ex) { throw toStorageException(e);
throw ex;
}
throw S3StorageException.form(e);
} }
} }
@@ -356,25 +340,18 @@ public abstract class AbstractOssClientImpl implements OssClient {
publisher.subscribe(downloadSubscriber).join(); publisher.subscribe(downloadSubscriber).join();
return getObjectResult; return getObjectResult;
} catch (Exception e) { } catch (Exception e) {
if (e instanceof S3StorageException ex) { throw toStorageException(e);
throw ex;
}
throw S3StorageException.form(e);
} }
} }
@Override @Override
public <T> T bucketDownload(String bucket, String key, BiFunction<GetObjectResult, InputStream, T> downloadTransformer) { public <T> T bucketDownload(String bucket, String key, BiFunction<GetObjectResult, InputStream, T> downloadTransformer) {
try { try (ResponseInputStream<GetObjectResponse> responseInputStream = doCustomDownload(builder -> builder.bucket(bucket).key(key), AsyncResponseTransformer.toBlockingInputStream(), null)) {
ResponseInputStream<GetObjectResponse> responseInputStream = doCustomDownload(builder -> builder.bucket(bucket).key(key), AsyncResponseTransformer.toBlockingInputStream(), null);
GetObjectResponse response = responseInputStream.response(); GetObjectResponse response = responseInputStream.response();
GetObjectResult getObjectResult = buildGetObjectResult(key, response); GetObjectResult getObjectResult = buildGetObjectResult(key, response);
return downloadTransformer.apply(getObjectResult, responseInputStream); return downloadTransformer.apply(getObjectResult, responseInputStream);
} catch (Exception e) { } catch (Exception e) {
if (e instanceof S3StorageException ex) { throw toStorageException(e);
throw ex;
}
throw S3StorageException.form(e);
} }
} }
@@ -383,10 +360,7 @@ public abstract class AbstractOssClientImpl implements OssClient {
try (OutputStream out = Files.newOutputStream(path)) { try (OutputStream out = Files.newOutputStream(path)) {
return bucketDownload(bucket, key, out); return bucketDownload(bucket, key, out);
} catch (Exception e) { } catch (Exception e) {
if (e instanceof S3StorageException ex) { throw toStorageException(e);
throw ex;
}
throw S3StorageException.form(e);
} }
} }
@@ -395,10 +369,7 @@ public abstract class AbstractOssClientImpl implements OssClient {
try (FileOutputStream out = new FileOutputStream(file)) { try (FileOutputStream out = new FileOutputStream(file)) {
return bucketDownload(bucket, key, out); return bucketDownload(bucket, key, out);
} catch (Exception e) { } catch (Exception e) {
if (e instanceof S3StorageException ex) { throw toStorageException(e);
throw ex;
}
throw S3StorageException.form(e);
} }
} }
@@ -438,7 +409,7 @@ public abstract class AbstractOssClientImpl implements OssClient {
s3AsyncClient.deleteObject(builder -> builder.bucket(bucket).key(key)).join(); s3AsyncClient.deleteObject(builder -> builder.bucket(bucket).key(key)).join();
return true; return true;
} catch (Exception e) { } catch (Exception e) {
throw S3StorageException.form(e); throw toStorageException(e);
} }
} }
@@ -452,7 +423,7 @@ public abstract class AbstractOssClientImpl implements OssClient {
.url() .url()
.toExternalForm(); .toExternalForm();
} catch (Exception e) { } catch (Exception e) {
throw S3StorageException.form(e); throw toStorageException(e);
} }
} }
@@ -466,7 +437,7 @@ public abstract class AbstractOssClientImpl implements OssClient {
.url() .url()
.toExternalForm(); .toExternalForm();
} catch (Exception e) { } catch (Exception e) {
throw S3StorageException.form(e); throw toStorageException(e);
} }
} }
@@ -623,6 +594,22 @@ public abstract class AbstractOssClientImpl implements OssClient {
return fileName.substring(index); return fileName.substring(index);
} }
private S3StorageException toStorageException(Throwable e) {
Throwable cause = unwrapAsyncException(e);
if (cause instanceof S3StorageException ex) {
return ex;
}
return S3StorageException.form(cause);
}
private Throwable unwrapAsyncException(Throwable e) {
Throwable cause = e;
while ((cause instanceof CompletionException || cause instanceof ExecutionException) && cause.getCause() != null) {
cause = cause.getCause();
}
return cause;
}
@Override @Override
public void close() throws Exception { public void close() throws Exception {
if (s3TransferManager != null) { if (s3TransferManager != null) {
@@ -166,38 +166,17 @@ public class OssClientConfig implements Config<OssClientConfig, OssClientConfig.
* @return 客户端配置构造器 * @return 客户端配置构造器
*/ */
public static OssClientConfigBuilder formPropertiesBuilder(OssProperties properties) { public static OssClientConfigBuilder 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);
// 绝大多数的云厂商都是不允许操作ACL的,所以此处的默认配置也是禁用ACL的
AccessControlPolicyConfig accessControlPolicyConfig = AccessControlPolicyConfig.DEFAULT;
// 目前自定义实现的 Client 上传/下载/删除中并没有实际使用到ACL相关配置
// 仅有业务中的链接预签名使用到(SysOssServiceImpl#matchingUrl),更多只是作为一个扩展点保留,如有需要ACL的自行实现调用逻辑
String accessPolicyString = properties.getAccessPolicy();
if (StringUtils.isNotBlank(accessPolicyString)) {
accessControlPolicyConfig = AccessControlPolicyConfig.builder()
.enabled(true)
.accessPolicy(AccessPolicy.formType(accessPolicyString))
.build();
}
return builder() return builder()
.endpoint(properties.getEndpoint()) .endpoint(properties.getEndpoint())
.domain(properties.getDomainUrl()) .domain(properties.getDomainUrl())
.accessKey(properties.getAccessKey()) .accessKey(properties.getAccessKey())
.secretKey(properties.getSecretKey()) .secretKey(properties.getSecretKey())
.bucket(properties.getBucketName()) .bucket(properties.getBucketName())
.region(region) .region(parseRegion(properties.getRegion()))
.prefix(properties.getPrefix()) .prefix(properties.getPrefix())
.useHttps(SystemConstants.YES.equals(properties.getIsHttps())) .useHttps(SystemConstants.YES.equals(properties.getIsHttps()))
.usePathStyleAccess(usePathStyleAccess) .usePathStyleAccess(resolvePathStyleAccess(properties))
.accessControlPolicyConfig(accessControlPolicyConfig); .accessControlPolicyConfig(resolveAccessControlPolicy(properties.getAccessPolicy()));
} }
/** /**
@@ -206,10 +185,7 @@ public class OssClientConfig implements Config<OssClientConfig, OssClientConfig.
* @return 访问站点URL地址 * @return 访问站点URL地址
*/ */
public String getEndpointUrl() { public String getEndpointUrl() {
String endpoint = endpoint() return BucketUrlUtil.rebuildUrlHeader(useHttps, getEndpoint());
.filter(s -> !s.isBlank())
.orElseThrow(() -> S3StorageException.form("endpoint is not configured."));
return BucketUrlUtil.rebuildUrlHeader(useHttps, endpoint);
} }
/** /**
@@ -221,7 +197,7 @@ public class OssClientConfig implements Config<OssClientConfig, OssClientConfig.
return domain() return domain()
// 如果已经配置了自定义域名,则优先使用域名 // 如果已经配置了自定义域名,则优先使用域名
// 检查携带协议头 // 检查携带协议头
.filter(s -> HttpUtil.isHttp(s) || HttpUtil.isHttps(s)) .filter(OssClientConfig::hasHttpHeader)
// 否则使用站点 // 否则使用站点
.orElseGet(this::getEndpointUrl); .orElseGet(this::getEndpointUrl);
} }
@@ -245,20 +221,50 @@ public class OssClientConfig implements Config<OssClientConfig, OssClientConfig.
* @return 桶URL地址 * @return 桶URL地址
*/ */
public String getBucketUrl(String bucket) { public String getBucketUrl(String bucket) {
// 如果已经配置了自定义域名,则优先使用域名 String url = getAccessBaseUrl();
String url = domain()
// 检查携带协议头
.filter(s -> HttpUtil.isHttp(s) || HttpUtil.isHttps(s))
// 否则使用站点
.orElseGet(() ->
endpoint()
.filter(s -> !s.isBlank())
.orElseThrow(() -> S3StorageException.form("endpoint is not configured."))
);
// 根据是否使用路径风格配置项决定存储桶的URL风格 // 根据是否使用路径风格配置项决定存储桶的URL风格
return usePathStyleAccess ? BucketUrlUtil.getPathStyleBucketUrl(useHttps, url, bucket) : BucketUrlUtil.getSiteStyleBucketUrl(useHttps, url, bucket); return usePathStyleAccess ? BucketUrlUtil.getPathStyleBucketUrl(useHttps, url, bucket) : BucketUrlUtil.getSiteStyleBucketUrl(useHttps, url, bucket);
} }
private static Region parseRegion(String regionString) {
if (StringUtils.isBlank(regionString)) {
return Region.US_EAST_1;
}
return Region.of(regionString);
}
private static boolean resolvePathStyleAccess(OssProperties properties) {
// 旧配置没有显式路径风格字段,只能继续按内置云厂商 endpoint 做兼容推断。
return !StringUtils.containsAny(properties.getEndpoint(), OssConstant.CLOUD_SERVICE);
}
private static AccessControlPolicyConfig resolveAccessControlPolicy(String accessPolicyString) {
// 绝大多数云厂商不允许操作 ACL,默认禁用;当前业务只用访问策略判断是否生成预签名 URL。
if (StringUtils.isBlank(accessPolicyString)) {
return AccessControlPolicyConfig.DEFAULT;
}
return AccessControlPolicyConfig.builder()
.enabled(true)
.accessPolicy(AccessPolicy.formType(accessPolicyString))
.build();
}
private String getAccessBaseUrl() {
return domain()
.filter(OssClientConfig::hasHttpHeader)
.orElseGet(this::getEndpoint);
}
private String getEndpoint() {
return endpoint()
.filter(s -> !s.isBlank())
.orElseThrow(() -> S3StorageException.form("endpoint is not configured."));
}
private static boolean hasHttpHeader(String url) {
return HttpUtil.isHttp(url) || HttpUtil.isHttps(url);
}
/** /**
* ACL访问策略配置 * ACL访问策略配置
*/ */
@@ -26,7 +26,7 @@ import java.util.concurrent.locks.ReentrantLock;
public class OssFactory { public class OssFactory {
private static final Map<String, OssClient> CLIENT_CACHE = new ConcurrentHashMap<>(); private static final Map<String, OssClient> CLIENT_CACHE = new ConcurrentHashMap<>();
private static final ReentrantLock LOCK = new ReentrantLock(); private static final Map<String, ReentrantLock> CLIENT_LOCKS = new ConcurrentHashMap<>();
/** /**
* 获取默认实例 * 获取默认实例
@@ -44,13 +44,17 @@ public class OssFactory {
* 根据类型获取实例 * 根据类型获取实例
*/ */
public static OssClient instance(String configKey) { public static OssClient instance(String configKey) {
if (StringUtils.isBlank(configKey)) {
throw S3StorageException.form("文件存储服务类型无法找到!");
}
String json = CacheUtils.get(CacheNames.SYS_OSS_CONFIG, configKey); String json = CacheUtils.get(CacheNames.SYS_OSS_CONFIG, configKey);
if (json == null) { if (json == null) {
throw S3StorageException.form("系统异常, '" + configKey + "'配置信息不存在!"); throw S3StorageException.form("系统异常, '" + configKey + "'配置信息不存在!");
} }
OssProperties properties = JsonUtils.parseObject(json, OssProperties.class); OssProperties properties = JsonUtils.parseObject(json, OssProperties.class);
OssClientConfig config = OssClientConfig.formProperties(properties); OssClientConfig config = OssClientConfig.formProperties(properties);
LOCK.lock(); ReentrantLock lock = getClientLock(configKey);
lock.lock();
try { try {
OssClient client = CLIENT_CACHE.get(configKey); OssClient client = CLIENT_CACHE.get(configKey);
if (client != null) { if (client != null) {
@@ -63,7 +67,7 @@ public class OssFactory {
CLIENT_CACHE.put(configKey, newClient); CLIENT_CACHE.put(configKey, newClient);
return newClient; return newClient;
} finally { } finally {
LOCK.unlock(); lock.unlock();
} }
} }
@@ -71,12 +75,25 @@ public class OssFactory {
* 移除实例 * 移除实例
*/ */
public static boolean remove(String configKey) { public static boolean remove(String configKey) {
OssClient client = CLIENT_CACHE.remove(configKey); if (StringUtils.isBlank(configKey)) {
if (client == null) {
return false; return false;
} }
closeClient(configKey, client); ReentrantLock lock = getClientLock(configKey);
return true; lock.lock();
try {
OssClient client = CLIENT_CACHE.remove(configKey);
if (client == null) {
return false;
}
closeClient(configKey, client);
return true;
} finally {
lock.unlock();
}
}
private static ReentrantLock getClientLock(String configKey) {
return CLIENT_LOCKS.computeIfAbsent(configKey, key -> new ReentrantLock());
} }
private static void closeClient(String configKey, OssClient client) { private static void closeClient(String configKey, OssClient client) {
@@ -74,6 +74,13 @@ public class Options {
* 创建可选项对象 * 创建可选项对象
*/ */
public static Options builder() { public static Options builder() {
return create();
}
/**
* 创建可选项对象
*/
public static Options create() {
return new Options(); return new Options();
} }
} }
@@ -17,6 +17,7 @@ import org.dromara.common.json.utils.JsonUtils;
import org.dromara.common.mybatis.core.page.PageQuery; import org.dromara.common.mybatis.core.page.PageQuery;
import org.dromara.common.mybatis.core.query.QueryBuilder; import org.dromara.common.mybatis.core.query.QueryBuilder;
import org.dromara.common.oss.constant.OssConstant; import org.dromara.common.oss.constant.OssConstant;
import org.dromara.common.oss.factory.OssFactory;
import org.dromara.common.redis.utils.CacheUtils; import org.dromara.common.redis.utils.CacheUtils;
import org.dromara.common.redis.utils.RedisUtils; import org.dromara.common.redis.utils.RedisUtils;
import org.dromara.system.domain.SysOssConfig; import org.dromara.system.domain.SysOssConfig;
@@ -29,6 +30,7 @@ import org.springframework.transaction.annotation.Transactional;
import java.util.Collection; import java.util.Collection;
import java.util.List; import java.util.List;
import java.util.Objects;
/** /**
* 对象存储配置Service业务层处理 * 对象存储配置Service业务层处理
@@ -130,6 +132,7 @@ public class SysOssConfigServiceImpl implements ISysOssConfigService {
public Boolean updateByBo(SysOssConfigBo bo) { public Boolean updateByBo(SysOssConfigBo bo) {
SysOssConfig config = MapstructUtils.convert(bo, SysOssConfig.class); SysOssConfig config = MapstructUtils.convert(bo, SysOssConfig.class);
validEntityBeforeSave(config); validEntityBeforeSave(config);
SysOssConfig oldConfig = ossConfigMapper.selectById(config.getOssConfigId());
boolean flag = ossConfigMapper.lambda() boolean flag = ossConfigMapper.lambda()
.set(ObjectUtil.isNull(config.getPrefix()), SysOssConfig::getPrefix, "") .set(ObjectUtil.isNull(config.getPrefix()), SysOssConfig::getPrefix, "")
.set(ObjectUtil.isNull(config.getRegion()), SysOssConfig::getRegion, "") .set(ObjectUtil.isNull(config.getRegion()), SysOssConfig::getRegion, "")
@@ -140,7 +143,12 @@ public class SysOssConfigServiceImpl implements ISysOssConfigService {
if (flag) { if (flag) {
// 从数据库查询完整的数据做缓存 // 从数据库查询完整的数据做缓存
config = ossConfigMapper.selectById(config.getOssConfigId()); config = ossConfigMapper.selectById(config.getOssConfigId());
if (ObjectUtil.isNotNull(oldConfig) && !Objects.equals(oldConfig.getConfigKey(), config.getConfigKey())) {
CacheUtils.evict(CacheNames.SYS_OSS_CONFIG, oldConfig.getConfigKey());
OssFactory.remove(oldConfig.getConfigKey());
}
CacheUtils.put(CacheNames.SYS_OSS_CONFIG, config.getConfigKey(), JsonUtils.toJsonString(config)); CacheUtils.put(CacheNames.SYS_OSS_CONFIG, config.getConfigKey(), JsonUtils.toJsonString(config));
OssFactory.remove(config.getConfigKey());
} }
return flag; return flag;
} }
@@ -174,12 +182,16 @@ public class SysOssConfigServiceImpl implements ISysOssConfigService {
List<SysOssConfig> list = CollUtil.newArrayList(); List<SysOssConfig> list = CollUtil.newArrayList();
for (Long configId : ids) { for (Long configId : ids) {
SysOssConfig config = ossConfigMapper.selectById(configId); SysOssConfig config = ossConfigMapper.selectById(configId);
list.add(config); if (ObjectUtil.isNotNull(config)) {
list.add(config);
}
} }
boolean flag = ossConfigMapper.deleteByIds(ids) > 0; boolean flag = ossConfigMapper.deleteByIds(ids) > 0;
if (flag) { if (flag) {
list.forEach(sysOssConfig -> list.forEach(sysOssConfig -> {
CacheUtils.evict(CacheNames.SYS_OSS_CONFIG, sysOssConfig.getConfigKey())); CacheUtils.evict(CacheNames.SYS_OSS_CONFIG, sysOssConfig.getConfigKey());
OssFactory.remove(sysOssConfig.getConfigKey());
});
} }
return flag; return flag;
} }