This commit is contained in:
dengqichen 2026-01-22 10:54:40 +08:00
parent 60395e46ed
commit 2fbd77b74e
6 changed files with 398 additions and 324 deletions

View File

@ -241,15 +241,6 @@ public interface IK8sServiceIntegration extends IExternalSystemIntegration {
} }
} }
/**
* 获取K8S ApiClient带缓存
* 用于直接调用Kubernetes Java Client API
*
* @param system K8S系统配置
* @return ApiClient实例
*/
io.kubernetes.client.openapi.ApiClient getApiClient(ExternalSystem system);
/** /**
* 获取用于日志流的K8S ApiClient长超时 * 获取用于日志流的K8S ApiClient长超时
* 日志流是长连接需要更长的读取超时时间 * 日志流是长连接需要更长的读取超时时间

View File

@ -6,23 +6,252 @@ import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;
/** /**
* 外部系统集成基类 * 外部系统集成基类
* 提供统一的密码解密功能 * 提供统一的凭证缓存管理和敏感数据解密功能
* *
* <p>子类通过泛型指定凭证类型</p>
* <ul>
* <li>Git: HttpHeaders包含PRIVATE-TOKEN</li>
* <li>K8S: ApiClient包含kubeconfig配置</li>
* <li>Jenkins: JenkinsCrumbIssuerResponse包含Crumb和Cookie</li>
* </ul>
*
* @param <C> 凭证类型Credential Type
* @author qqchen * @author qqchen
* @since 2025-11-11 * @since 2025-11-11
*/ */
@Slf4j @Slf4j
public abstract class BaseExternalSystemIntegration { public abstract class BaseExternalSystemIntegration<C> {
@Resource @Resource
protected SensitiveDataEncryptor encryptor; protected SensitiveDataEncryptor encryptor;
/**
* 凭证缓存 - 按系统ID存储
*/
private final Map<Long, CredentialCache<C>> credentialCacheMap = new ConcurrentHashMap<>();
/**
* 缓存锁 - 避免synchronized无限等待
*/
private final ReentrantLock cacheLock = new ReentrantLock();
/**
* 锁超时时间
*/
private static final long LOCK_TIMEOUT_SECONDS = 3;
// ==================== 凭证缓存结构 ====================
/**
* 凭证缓存包装类
* 统一存储凭证对象解密后的系统信息和过期时间
*/
protected static class CredentialCache<C> {
private final C credential;
private final ExternalSystem decryptedSystem;
private final long expireTime;
public CredentialCache(C credential, ExternalSystem decryptedSystem, long expireTimeMs) {
this.credential = credential;
this.decryptedSystem = decryptedSystem;
this.expireTime = System.currentTimeMillis() + expireTimeMs;
}
public C getCredential() {
return credential;
}
public ExternalSystem getDecryptedSystem() {
return decryptedSystem;
}
public boolean isExpired() {
return System.currentTimeMillis() > expireTime;
}
public long getExpireTime() {
return expireTime;
}
}
// ==================== 抽象方法 - 子类必须实现 ====================
/**
* 创建凭证对象
* 子类负责基于已解密的系统信息创建特定类型的凭证
*
* @param decryptedSystem 已解密的系统配置密码/Token已是明文
* @return 凭证对象
*/
protected abstract C createCredential(ExternalSystem decryptedSystem);
/**
* 获取凭证缓存过期时间毫秒
* 不同系统可以有不同的过期策略
*
* @return 过期时间毫秒
*/
protected abstract long getCredentialExpireTimeMs();
// ==================== 可选覆盖方法 ====================
/**
* 凭证过期时的清理操作
* 子类可覆盖此方法进行资源释放如关闭连接池
*
* @param credential 过期的凭证对象
*/
protected void onCredentialExpired(C credential) {
// 默认空实现子类按需覆盖
}
// ==================== 通用凭证获取方法 ====================
/**
* 获取凭证带缓存降级策略
*
* <p>实现逻辑</p>
* <ol>
* <li>快速路径无锁检查缓存是否有效</li>
* <li>尝试获取锁最多等待3秒</li>
* <li>双重检查获取锁后再次检查缓存</li>
* <li>统一解密 + 创建新凭证并缓存</li>
* <li>降级策略获取锁超时时返回过期缓存</li>
* </ol>
*
* @param system 系统配置包含加密数据
* @return 凭证对象
*/
protected final C getCredential(ExternalSystem system) {
Long systemId = system.getId();
// 1. 快速路径无锁检查缓存
CredentialCache<C> cache = credentialCacheMap.get(systemId);
if (cache != null && !cache.isExpired()) {
return cache.getCredential();
}
// 2. 缓存不存在或已过期尝试获取锁来更新
boolean lockAcquired = false;
try {
lockAcquired = cacheLock.tryLock(LOCK_TIMEOUT_SECONDS, TimeUnit.SECONDS);
if (lockAcquired) {
// 3. 双重检查其他线程可能已更新缓存
cache = credentialCacheMap.get(systemId);
if (cache == null || cache.isExpired()) {
log.debug("凭证缓存失效,重新创建: systemId={}, systemType={}",
systemId, getClass().getSimpleName());
// 关闭旧凭证如K8S需要关闭连接池
if (cache != null) {
try {
onCredentialExpired(cache.getCredential());
} catch (Exception e) {
log.warn("关闭过期凭证时发生异常: systemId={}, error={}", systemId, e.getMessage());
}
}
// 4. 统一解密 + 创建新凭证并缓存
ExternalSystem decryptedSystem = decryptSystem(system);
C newCredential = createCredential(decryptedSystem);
cache = new CredentialCache<>(newCredential, decryptedSystem, getCredentialExpireTimeMs());
credentialCacheMap.put(systemId, cache);
log.debug("凭证缓存已更新: systemId={}, expireTime={}", systemId, cache.getExpireTime());
}
return cache.getCredential();
} else {
// 5. 获取锁超时尝试降级策略
log.warn("获取凭证缓存锁超时({}秒): systemId={}, systemType={}",
LOCK_TIMEOUT_SECONDS, systemId, getClass().getSimpleName());
cache = credentialCacheMap.get(systemId);
if (cache != null) {
log.warn("使用过期凭证作为降级: systemId={}", systemId);
return cache.getCredential();
}
// 无缓存可用同步创建最后保底
log.warn("无可用凭证缓存,同步创建: systemId={}", systemId);
ExternalSystem decryptedSystem = decryptSystem(system);
C newCredential = createCredential(decryptedSystem);
cache = new CredentialCache<>(newCredential, decryptedSystem, getCredentialExpireTimeMs());
credentialCacheMap.put(systemId, cache);
return newCredential;
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
log.warn("获取凭证缓存锁被中断: systemId={}", systemId);
// 被中断时尝试返回现有缓存
cache = credentialCacheMap.get(systemId);
if (cache != null) {
return cache.getCredential();
}
// 无缓存可用同步创建最后保底
log.warn("无可用凭证缓存,同步创建: systemId={}", systemId);
ExternalSystem decryptedSystem = decryptSystem(system);
C newCredential = createCredential(decryptedSystem);
cache = new CredentialCache<>(newCredential, decryptedSystem, getCredentialExpireTimeMs());
credentialCacheMap.put(systemId, cache);
return newCredential;
} finally {
if (lockAcquired) {
cacheLock.unlock();
}
}
}
/**
* 获取缓存包含凭证和解密后的系统信息
* 用于需要同时访问凭证和解密系统信息的场景如Jenkins需要用decryptedSystem构建HttpHeaders
*
* @param system 系统配置
* @return 缓存对象
*/
protected final CredentialCache<C> getCredentialCache(ExternalSystem system) {
Long systemId = system.getId();
// 先调用 getCredential 确保缓存已初始化
getCredential(system);
// 返回缓存
return credentialCacheMap.get(systemId);
}
/**
* 清除指定系统的凭证缓存
* 用于连接失败后强制下次重新创建
*
* @param systemId 系统ID
*/
protected final void clearCredentialCache(Long systemId) {
CredentialCache<C> removed = credentialCacheMap.remove(systemId);
if (removed != null) {
log.debug("已清除凭证缓存: systemId={}", systemId);
try {
onCredentialExpired(removed.getCredential());
} catch (Exception e) {
log.warn("清除缓存时关闭凭证发生异常: systemId={}, error={}", systemId, e.getMessage());
}
}
}
// ==================== 解密工具方法 ====================
/** /**
* 解密外部系统的敏感数据密码和Token * 解密外部系统的敏感数据密码和Token
* 创建一个新对象不影响原对象 * 创建一个新对象不影响原对象
* *
* @param system 原始的外部系统对象包含加密的密码 * @param system 原始的外部系统对象包含加密的密码
* @return 解密后的外部系统对象 * @return 解密后的外部系统对象
*/ */
@ -33,7 +262,7 @@ public abstract class BaseExternalSystemIntegration {
// 创建新对象避免修改原对象 // 创建新对象避免修改原对象
ExternalSystem decrypted = new ExternalSystem(); ExternalSystem decrypted = new ExternalSystem();
// 复制所有属性 // 复制所有属性
decrypted.setId(system.getId()); decrypted.setId(system.getId());
decrypted.setName(system.getName()); decrypted.setName(system.getName());
@ -45,7 +274,7 @@ public abstract class BaseExternalSystemIntegration {
decrypted.setConfig(system.getConfig()); decrypted.setConfig(system.getConfig());
decrypted.setRemark(system.getRemark()); decrypted.setRemark(system.getRemark());
decrypted.setSort(system.getSort()); decrypted.setSort(system.getSort());
// 解密密码 // 解密密码
if (StringUtils.isNotBlank(system.getPassword())) { if (StringUtils.isNotBlank(system.getPassword())) {
try { try {
@ -57,7 +286,7 @@ public abstract class BaseExternalSystemIntegration {
decrypted.setPassword(system.getPassword()); decrypted.setPassword(system.getPassword());
} }
} }
// 解密Token // 解密Token
if (StringUtils.isNotBlank(system.getToken())) { if (StringUtils.isNotBlank(system.getToken())) {
try { try {
@ -69,7 +298,7 @@ public abstract class BaseExternalSystemIntegration {
decrypted.setToken(system.getToken()); decrypted.setToken(system.getToken());
} }
} }
return decrypted; return decrypted;
} }
} }

View File

@ -17,7 +17,6 @@ import org.springframework.web.util.UriComponentsBuilder;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatter;
import java.util.*; import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors; import java.util.stream.Collectors;
/** /**
@ -26,13 +25,16 @@ import java.util.stream.Collectors;
*/ */
@Slf4j @Slf4j
@Service @Service
public class GitServiceIntegrationImpl extends BaseExternalSystemIntegration implements IGitServiceIntegration { public class GitServiceIntegrationImpl extends BaseExternalSystemIntegration<HttpHeaders> implements IGitServiceIntegration {
private final RestTemplate restTemplate; private final RestTemplate restTemplate;
// Git系统解密缓存 - 线程安全 /** 缓存过期时间30分钟 */
private static final Map<Long, GitSystemCache> SYSTEM_CACHE = new ConcurrentHashMap<>(); private static final long CACHE_EXPIRE_TIME = 30 * 60 * 1000;
private static final long CACHE_EXPIRE_TIME = 30 * 60 * 1000; // 30分钟过期
// GitLab API 分页配置
private static final int PER_PAGE = 100; // GitLab API 最大值
private static final int MAX_PAGES = 100; // 防止无限循环最多获取 10000
public GitServiceIntegrationImpl() { public GitServiceIntegrationImpl() {
// 配置RestTemplate超时防止网络故障时长时间阻塞 // 配置RestTemplate超时防止网络故障时长时间阻塞
@ -42,58 +44,52 @@ public class GitServiceIntegrationImpl extends BaseExternalSystemIntegration imp
this.restTemplate = new RestTemplate(factory); this.restTemplate = new RestTemplate(factory);
} }
// GitLab API 分页配置 // ==================== 实现基类抽象方法 ====================
private static final int PER_PAGE = 100; // GitLab API 最大值
private static final int MAX_PAGES = 100; // 防止无限循环最多获取 10000
/** /**
* Git系统缓存内部类 * 创建Git认证凭证HttpHeaders
* 基于已解密的系统信息构建认证头
*
* @param decryptedSystem 已解密的系统配置基类统一解密
*/ */
private static class GitSystemCache { @Override
final ExternalSystem decryptedSystem; protected HttpHeaders createCredential(ExternalSystem decryptedSystem) {
final long expireTime; HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
GitSystemCache(ExternalSystem system) { // 根据认证类型设置认证信息
this.decryptedSystem = system; switch (decryptedSystem.getAuthType()) {
this.expireTime = System.currentTimeMillis() + CACHE_EXPIRE_TIME; case BASIC -> {
String auth = decryptedSystem.getUsername() + ":" + decryptedSystem.getPassword();
byte[] encodedAuth = Base64.getEncoder().encode(auth.getBytes());
headers.set("Authorization", "Basic " + new String(encodedAuth));
}
case TOKEN -> {
// Token认证 - GitLab使用Private Token
headers.set("PRIVATE-TOKEN", decryptedSystem.getToken());
}
case OAUTH -> {
// OAuth认证
headers.set("Authorization", "Bearer " + decryptedSystem.getToken());
}
default -> throw new RuntimeException("Unsupported authentication type: " + decryptedSystem.getAuthType());
} }
boolean isExpired() { log.debug("Git认证凭证已创建: systemId={}, authType={}", decryptedSystem.getId(), decryptedSystem.getAuthType());
return System.currentTimeMillis() > expireTime; return headers;
}
} }
/** @Override
* 线程安全地获取Git系统缓存 protected long getCredentialExpireTimeMs() {
* 如果缓存不存在或已过期会重新解密 return CACHE_EXPIRE_TIME;
*/
private synchronized GitSystemCache getSystemCache(ExternalSystem system) {
Long systemId = system.getId();
GitSystemCache cache = SYSTEM_CACHE.get(systemId);
if (cache == null || cache.isExpired()) {
log.debug("Git系统缓存失效重新解密: systemId={}", systemId);
// 解密系统信息只在这里解密一次
ExternalSystem decryptedSystem = decryptSystem(system);
// 创建新缓存
cache = new GitSystemCache(decryptedSystem);
SYSTEM_CACHE.put(systemId, cache);
log.debug("Git系统缓存已更新: systemId={}, expireTime={}", systemId, cache.expireTime);
}
return cache;
} }
@Override @Override
public boolean testConnection(ExternalSystem system) { public boolean testConnection(ExternalSystem system) {
try { try {
// 直接使用原始系统信息构建URLURL不需要解密
String url = system.getUrl() + "/api/v4/version"; String url = system.getUrl() + "/api/v4/version";
// 创建请求头内部自动处理解密 // 使用基类统一的凭证获取方法
HttpHeaders headers = createHeaders(system); HttpHeaders headers = getCredential(system);
HttpEntity<String> entity = new HttpEntity<>(headers); HttpEntity<String> entity = new HttpEntity<>(headers);
ResponseEntity<String> response = restTemplate.exchange( ResponseEntity<String> response = restTemplate.exchange(
@ -106,6 +102,8 @@ public class GitServiceIntegrationImpl extends BaseExternalSystemIntegration imp
return response.getStatusCode() == HttpStatus.OK; return response.getStatusCode() == HttpStatus.OK;
} catch (Exception e) { } catch (Exception e) {
log.error("Git connection test failed for system: {}", system.getName(), e); log.error("Git connection test failed for system: {}", system.getName(), e);
// 连接失败时清除缓存下次重新创建
clearCredentialCache(system.getId());
return false; return false;
} }
} }
@ -113,7 +111,7 @@ public class GitServiceIntegrationImpl extends BaseExternalSystemIntegration imp
@Override @Override
public List<GitGroupResponse> groups(ExternalSystem system) { public List<GitGroupResponse> groups(ExternalSystem system) {
List<GitGroupResponse> allGroups = new ArrayList<>(); List<GitGroupResponse> allGroups = new ArrayList<>();
HttpHeaders headers = createHeaders(system); HttpHeaders headers = getCredential(system);
HttpEntity<String> entity = new HttpEntity<>(headers); HttpEntity<String> entity = new HttpEntity<>(headers);
try { try {
@ -155,10 +153,8 @@ public class GitServiceIntegrationImpl extends BaseExternalSystemIntegration imp
@Override @Override
public List<GitProjectResponse> projects(ExternalSystem system) { public List<GitProjectResponse> projects(ExternalSystem system) {
try { try {
// 直接使用原始系统信息构建URLURL不需要解密
String url = String.format("%s/api/v4/projects", system.getUrl()); String url = String.format("%s/api/v4/projects", system.getUrl());
// 创建请求头内部自动处理解密 HttpHeaders headers = getCredential(system);
HttpHeaders headers = createHeaders(system);
HttpEntity<String> entity = new HttpEntity<>(headers); HttpEntity<String> entity = new HttpEntity<>(headers);
ResponseEntity<List<GitProjectResponse>> response = restTemplate.exchange( ResponseEntity<List<GitProjectResponse>> response = restTemplate.exchange(
@ -179,7 +175,7 @@ public class GitServiceIntegrationImpl extends BaseExternalSystemIntegration imp
@Override @Override
public List<GitProjectResponse> projectsByGroup(ExternalSystem system, Long groupId) { public List<GitProjectResponse> projectsByGroup(ExternalSystem system, Long groupId) {
List<GitProjectResponse> allProjects = new ArrayList<>(); List<GitProjectResponse> allProjects = new ArrayList<>();
HttpHeaders headers = createHeaders(system); HttpHeaders headers = getCredential(system);
HttpEntity<String> entity = new HttpEntity<>(headers); HttpEntity<String> entity = new HttpEntity<>(headers);
try { try {
@ -221,7 +217,7 @@ public class GitServiceIntegrationImpl extends BaseExternalSystemIntegration imp
@Override @Override
public List<GitBranchResponse> branches(ExternalSystem system, Long projectId) { public List<GitBranchResponse> branches(ExternalSystem system, Long projectId) {
List<GitBranchResponse> allBranches = new ArrayList<>(); List<GitBranchResponse> allBranches = new ArrayList<>();
HttpHeaders headers = createHeaders(system); HttpHeaders headers = getCredential(system);
HttpEntity<String> entity = new HttpEntity<>(headers); HttpEntity<String> entity = new HttpEntity<>(headers);
try { try {
@ -266,41 +262,6 @@ public class GitServiceIntegrationImpl extends BaseExternalSystemIntegration imp
} }
} }
/**
* 创建包含认证信息的完整请求头
* 内部自动处理系统解密和缓存
*/
private HttpHeaders createHeaders(ExternalSystem system) {
// 获取缓存的解密系统信息
GitSystemCache cache = getSystemCache(system);
ExternalSystem decryptedSystem = cache.decryptedSystem;
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
// 根据认证类型设置认证信息使用解密后的系统信息
switch (decryptedSystem.getAuthType()) {
case BASIC -> {
// Basic认证
String auth = decryptedSystem.getUsername() + ":" + decryptedSystem.getPassword();
byte[] encodedAuth = Base64.getEncoder().encode(auth.getBytes());
String authHeader = "Basic " + new String(encodedAuth);
headers.set("Authorization", authHeader);
}
case TOKEN -> {
// Token认证 - GitLab使用Private Token
headers.set("PRIVATE-TOKEN", decryptedSystem.getToken());
}
case OAUTH -> {
// OAuth认证
headers.set("Authorization", "Bearer " + decryptedSystem.getToken());
}
default -> throw new RuntimeException("Unsupported authentication type: " + decryptedSystem.getAuthType());
}
return headers;
}
@Override @Override
public GitBranchResponse getBranch(ExternalSystem system, Long projectId, String branchName) { public GitBranchResponse getBranch(ExternalSystem system, Long projectId, String branchName) {
try { try {
@ -308,7 +269,7 @@ public class GitServiceIntegrationImpl extends BaseExternalSystemIntegration imp
String url = String.format("%s/api/v4/projects/%d/repository/branches/%s", String url = String.format("%s/api/v4/projects/%d/repository/branches/%s",
system.getUrl(), projectId, branchName); system.getUrl(), projectId, branchName);
HttpHeaders headers = createHeaders(system); HttpHeaders headers = getCredential(system);
HttpEntity<String> entity = new HttpEntity<>(headers); HttpEntity<String> entity = new HttpEntity<>(headers);
ResponseEntity<GitBranchResponse> response = restTemplate.exchange( ResponseEntity<GitBranchResponse> response = restTemplate.exchange(
@ -337,7 +298,7 @@ public class GitServiceIntegrationImpl extends BaseExternalSystemIntegration imp
String url = String.format("%s/api/v4/projects/%d/repository/commits?ref_name=%s&per_page=%d", String url = String.format("%s/api/v4/projects/%d/repository/commits?ref_name=%s&per_page=%d",
system.getUrl(), projectId, branchName, limit); system.getUrl(), projectId, branchName, limit);
HttpHeaders headers = createHeaders(system); HttpHeaders headers = getCredential(system);
HttpEntity<String> entity = new HttpEntity<>(headers); HttpEntity<String> entity = new HttpEntity<>(headers);
ResponseEntity<List<com.qqchen.deploy.backend.deploy.integration.response.GitCommitResponse>> response = ResponseEntity<List<com.qqchen.deploy.backend.deploy.integration.response.GitCommitResponse>> response =

View File

@ -56,29 +56,21 @@ import java.util.Base64;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;
import org.springframework.http.client.SimpleClientHttpRequestFactory; import org.springframework.http.client.SimpleClientHttpRequestFactory;
@Slf4j @Slf4j
@Service @Service
public class JenkinsServiceIntegrationImpl extends BaseExternalSystemIntegration implements IJenkinsServiceIntegration { public class JenkinsServiceIntegrationImpl extends BaseExternalSystemIntegration<JenkinsCrumbIssuerResponse> implements IJenkinsServiceIntegration {
@Resource @Resource
private IExternalSystemRepository systemRepository; private IExternalSystemRepository systemRepository;
private final RestTemplate restTemplate; private final RestTemplate restTemplate;
// Jenkins Crumb缓存 - 线程安全 /** Crumb缓存过期时间25分钟 */
private static final Map<Long, JenkinsCrumbCache> CRUMB_CACHE = new ConcurrentHashMap<>(); private static final long CRUMB_EXPIRE_TIME = 25 * 60 * 1000;
// 每个Jenkins系统独立的锁 - 避免不同系统间互相阻塞
private static final Map<Long, ReentrantLock> SYSTEM_LOCKS = new ConcurrentHashMap<>();
private static final long CRUMB_EXPIRE_TIME = 25 * 60 * 1000; // 25分钟过期
private static final long LOCK_TIMEOUT_SECONDS = 3; // 锁超时时间3秒
public JenkinsServiceIntegrationImpl() { public JenkinsServiceIntegrationImpl() {
// 配置RestTemplate超时防止网络故障时长时间阻塞 // 配置RestTemplate超时防止网络故障时长时间阻塞
SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory(); SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
@ -87,106 +79,30 @@ public class JenkinsServiceIntegrationImpl extends BaseExternalSystemIntegration
this.restTemplate = new RestTemplate(factory); this.restTemplate = new RestTemplate(factory);
} }
/** // ==================== 实现基类抽象方法 ====================
* Jenkins Crumb缓存内部类
*/
private static class JenkinsCrumbCache {
final JenkinsCrumbIssuerResponse crumb;
final ExternalSystem decryptedSystem;
final long expireTime;
JenkinsCrumbCache(JenkinsCrumbIssuerResponse crumb, ExternalSystem system) {
this.crumb = crumb;
this.decryptedSystem = system;
this.expireTime = System.currentTimeMillis() + CRUMB_EXPIRE_TIME;
}
boolean isExpired() {
return System.currentTimeMillis() > expireTime;
}
}
/** /**
* 线程安全地获取Jenkins Crumb缓存 * 创建Jenkins Crumb凭证
* 使用ReentrantLock + tryLock实现带超时的锁机制避免死锁 * 通过HTTP请求从Jenkins服务器获取Crumb
* 如果缓存不存在或已过期会重新获取和解密 *
* @param decryptedSystem 已解密的系统配置基类统一解密
*/ */
private JenkinsCrumbCache getCrumbCache(ExternalSystem system) { @Override
Long systemId = system.getId(); protected JenkinsCrumbIssuerResponse createCredential(ExternalSystem decryptedSystem) {
// 快速路径如果缓存有效直接返回
JenkinsCrumbCache cache = CRUMB_CACHE.get(systemId);
if (cache != null && !cache.isExpired()) {
return cache;
}
// 获取该Jenkins系统的独立锁公平锁避免线程饥饿
ReentrantLock lock = SYSTEM_LOCKS.computeIfAbsent(systemId, k -> new ReentrantLock(true));
try {
// 尝试获取锁最多等待3秒
if (lock.tryLock(LOCK_TIMEOUT_SECONDS, TimeUnit.SECONDS)) {
try {
// 双重检查获取锁后再次检查缓存
cache = CRUMB_CACHE.get(systemId);
if (cache != null && !cache.isExpired()) {
log.debug("Jenkins Crumb缓存命中双重检查: systemId={}", systemId);
return cache;
}
log.debug("Jenkins Crumb缓存失效重新获取: systemId={}", systemId);
// 解密系统信息只在这里解密一次
ExternalSystem decryptedSystem = decryptSystem(system);
// 获取新的crumb网络I/O但不持有synchronized锁
JenkinsCrumbIssuerResponse crumb = fetchCrumbFromJenkins(decryptedSystem);
// 创建新缓存
cache = new JenkinsCrumbCache(crumb, decryptedSystem);
CRUMB_CACHE.put(systemId, cache);
log.debug("Jenkins Crumb缓存已更新: systemId={}, expireTime={}", systemId, cache.expireTime);
return cache;
} finally {
lock.unlock();
}
} else {
// 获取锁超时使用降级策略
log.warn("获取Jenkins Crumb锁超时{}秒systemId={}", LOCK_TIMEOUT_SECONDS, systemId);
// 降级策略使用过期缓存容忍短暂的认证失败
cache = CRUMB_CACHE.get(systemId);
if (cache != null) {
log.warn("使用过期的Jenkins Crumb缓存作为降级: systemId={}, 过期时间差={}ms",
systemId, System.currentTimeMillis() - cache.expireTime);
return cache;
}
// 没有缓存可用抛出异常
throw new BusinessException(ResponseCode.JENKINS_API_ERROR,
new Object[]{"CRUMB_LOCK_TIMEOUT", "获取Jenkins认证信息超时请稍后重试"});
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
log.error("获取Jenkins Crumb锁被中断: systemId={}", systemId, e);
throw new BusinessException(ResponseCode.JENKINS_API_ERROR,
new Object[]{"CRUMB_LOCK_INTERRUPTED", "获取Jenkins认证信息被中断"});
}
}
/**
* 直接从Jenkins服务器获取Crumb不经过缓存
*/
private JenkinsCrumbIssuerResponse fetchCrumbFromJenkins(ExternalSystem decryptedSystem) {
String url = decryptedSystem.getUrl() + "/crumbIssuer/api/json"; String url = decryptedSystem.getUrl() + "/crumbIssuer/api/json";
HttpHeaders headers = createBasicHeaders(decryptedSystem); HttpHeaders headers = createBasicHeaders(decryptedSystem);
HttpEntity<String> entity = new HttpEntity<>(headers); HttpEntity<String> entity = new HttpEntity<>(headers);
ResponseEntity<String> response = restTemplate.exchange(url, HttpMethod.GET, entity, String.class); ResponseEntity<String> response = restTemplate.exchange(url, HttpMethod.GET, entity, String.class);
return convertResponse(response); JenkinsCrumbIssuerResponse crumb = convertResponse(response);
log.debug("Jenkins Crumb凭证已创建: systemId={}", decryptedSystem.getId());
return crumb;
}
@Override
protected long getCredentialExpireTimeMs() {
return CRUMB_EXPIRE_TIME;
} }
@Override @Override
@ -199,8 +115,7 @@ public class JenkinsServiceIntegrationImpl extends BaseExternalSystemIntegration
HttpHeaders headers = createHeaders(system); HttpHeaders headers = createHeaders(system);
// 打印实际发送的请求头 // 打印实际发送的请求头
log.info("Authorization头: {}", headers.getFirst("Authorization")); log.debug("Authorization头: {}", headers.getFirst("Authorization"));
log.info("===================================");
HttpEntity<String> entity = new HttpEntity<>(headers); HttpEntity<String> entity = new HttpEntity<>(headers);
@ -220,13 +135,13 @@ public class JenkinsServiceIntegrationImpl extends BaseExternalSystemIntegration
/** /**
* 创建包含认证信息和Jenkins Crumb的完整请求头 * 创建包含认证信息和Jenkins Crumb的完整请求头
* 内部自动处理缓存获取和解密 * 使用基类统一的凭证缓存机制
*/ */
private HttpHeaders createHeaders(ExternalSystem system) { private HttpHeaders createHeaders(ExternalSystem system) {
// 获取缓存的解密系统和crumb // 获取缓存包含已解密的系统信息和Crumb凭证
JenkinsCrumbCache cache = getCrumbCache(system); CredentialCache<JenkinsCrumbIssuerResponse> cache = getCredentialCache(system);
ExternalSystem decryptedSystem = cache.decryptedSystem; ExternalSystem decryptedSystem = cache.getDecryptedSystem();
JenkinsCrumbIssuerResponse crumb = cache.crumb; JenkinsCrumbIssuerResponse crumb = cache.getCredential();
// 创建基础认证头 // 创建基础认证头
HttpHeaders headers = createBasicHeaders(decryptedSystem); HttpHeaders headers = createBasicHeaders(decryptedSystem);
@ -519,7 +434,7 @@ public class JenkinsServiceIntegrationImpl extends BaseExternalSystemIntegration
(String) executable.get("url") (String) executable.get("url")
); );
} }
// 还在队列中等待 // 还在队列中等待
return null; return null;
} }

View File

@ -32,70 +32,74 @@ import java.time.ZoneId;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Arrays;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
/** /**
* K8S集成服务实现 * K8S集成服务实现
* 使用基类统一的凭证缓存管理机制
*/ */
@Slf4j @Slf4j
@Service @Service
public class K8sServiceIntegrationImpl extends BaseExternalSystemIntegration implements IK8sServiceIntegration { public class K8sServiceIntegrationImpl extends BaseExternalSystemIntegration<ApiClient> implements IK8sServiceIntegration {
// K8S ApiClient缓存 - 线程安全
private static final Map<Long, K8sApiClientCache> API_CLIENT_CACHE = new ConcurrentHashMap<>();
// 日志流专用ApiClient缓存长超时按k8sSystemId复用 // 日志流专用ApiClient缓存长超时按k8sSystemId复用
private static final Map<Long, ApiClient> LOG_STREAM_API_CLIENT_CACHE = new ConcurrentHashMap<>(); private static final Map<Long, ApiClient> LOG_STREAM_API_CLIENT_CACHE = new ConcurrentHashMap<>();
private static final long CACHE_EXPIRE_TIME = 10 * 60 * 1000; // 10分钟过期优化缩短过期时间避免持有失效连接 /** 缓存过期时间10分钟缩短过期时间避免持有失效连接 */
private static final long CACHE_EXPIRE_TIME = 10 * 60 * 1000;
// ==================== 实现基类抽象方法 ====================
/** /**
* K8S ApiClient缓存内部类 * 创建K8S ApiClient凭证
* 解析kubeconfig并强制使用HTTP/1.1协议
*
* @param decryptedSystem 已解密的系统配置基类统一解密
*/ */
private static class K8sApiClientCache { @Override
final ApiClient apiClient; protected ApiClient createCredential(ExternalSystem decryptedSystem) {
try {
String config = decryptedSystem.getConfig();
if (config == null || config.trim().isEmpty()) {
throw new BusinessException(ResponseCode.K8S_CONFIG_EMPTY);
}
final long expireTime; // 直接使用config作为kubeconfig内容
ApiClient client = Config.fromConfig(new StringReader(config));
K8sApiClientCache(ApiClient apiClient) { client.setConnectTimeout(15000); // 15秒连接超时
this.apiClient = apiClient; client.setReadTimeout(120000); // 120秒读取超时优化日志查询等耗时操作
this.expireTime = System.currentTimeMillis() + CACHE_EXPIRE_TIME;
} // 关键优化强制使用 HTTP/1.1避免 HTTP/2 连接复用问题
// HTTP/2 K8S API 高并发场景下容易出现 ConnectionShutdownExceptionBroken pipe 等问题
boolean isExpired() { okhttp3.OkHttpClient originalHttpClient = client.getHttpClient();
return System.currentTimeMillis() > expireTime; okhttp3.OkHttpClient httpClient = originalHttpClient.newBuilder()
.protocols(Arrays.asList(okhttp3.Protocol.HTTP_1_1)) // 强制 HTTP/1.1
.retryOnConnectionFailure(true) // 连接失败时重试
.build();
client.setHttpClient(httpClient);
log.debug("K8S ApiClient凭证已创建使用HTTP/1.1协议: systemId={}", decryptedSystem.getId());
return client;
} catch (BusinessException e) {
throw e;
} catch (Exception e) {
log.error("创建K8S ApiClient失败: systemId={}, error={}", decryptedSystem.getId(), e.getMessage(), e);
throw new BusinessException(ResponseCode.K8S_CONNECTION_FAILED);
} }
} }
@Override
protected long getCredentialExpireTimeMs() {
return CACHE_EXPIRE_TIME;
}
/** /**
* 线程安全地获取K8S ApiClient缓存 * 凭证过期时关闭ApiClient释放OkHttp资源
* 如果缓存不存在或已过期会重新创建
*/ */
private synchronized K8sApiClientCache getApiClientCache(ExternalSystem system) { @Override
Long systemId = system.getId(); protected void onCredentialExpired(ApiClient credential) {
K8sApiClientCache cache = API_CLIENT_CACHE.get(systemId); closeApiClient(credential);
if (cache == null || cache.isExpired()) {
log.debug("K8S ApiClient缓存失效重新创建: systemId={}", systemId);
// 关闭旧的ApiClient以释放OkHttp资源
if (cache != null && cache.isExpired()) {
log.debug("关闭过期的K8S ApiClient: systemId={}", systemId);
closeApiClient(cache.apiClient);
}
try {
ApiClient apiClient = createApiClientInternal(system);
cache = new K8sApiClientCache(apiClient);
API_CLIENT_CACHE.put(systemId, cache);
log.debug("K8S ApiClient缓存已更新: systemId={}, expireTime={}", systemId, cache.expireTime);
} catch (Exception e) {
log.error("创建K8S ApiClient失败: systemId={}", systemId, e);
throw new BusinessException(ResponseCode.K8S_CONNECTION_FAILED);
}
}
return cache;
} }
@Override @Override
@ -116,9 +120,9 @@ public class K8sServiceIntegrationImpl extends BaseExternalSystemIntegration imp
throw new BusinessException(ResponseCode.K8S_CONFIG_EMPTY); throw new BusinessException(ResponseCode.K8S_CONFIG_EMPTY);
} }
// 使用缓存的ApiClient进行连接测试优化复用连接减少资源开销 // 使用基类统一的凭证获取方法
K8sApiClientCache cache = getApiClientCache(system); ApiClient apiClient = getCredential(system);
VersionApi versionApi = new VersionApi(cache.apiClient); VersionApi versionApi = new VersionApi(apiClient);
VersionInfo version = versionApi.getCode(); VersionInfo version = versionApi.getCode();
log.info("K8S集群连接成功版本: {}", version.getGitVersion()); log.info("K8S集群连接成功版本: {}", version.getGitVersion());
@ -128,7 +132,7 @@ public class K8sServiceIntegrationImpl extends BaseExternalSystemIntegration imp
log.error("K8S连接测试失败集群: {}, 错误: {}", system.getName(), e.getMessage(), e); log.error("K8S连接测试失败集群: {}, 错误: {}", system.getName(), e.getMessage(), e);
// 连接失败时清除缓存下次重新创建 // 连接失败时清除缓存下次重新创建
API_CLIENT_CACHE.remove(system.getId()); clearCredentialCache(system.getId());
return false; return false;
} }
@ -142,8 +146,8 @@ public class K8sServiceIntegrationImpl extends BaseExternalSystemIntegration imp
log.info("查询K8S命名空间集群: {}", externalSystem.getName()); log.info("查询K8S命名空间集群: {}", externalSystem.getName());
try { try {
K8sApiClientCache cache = getApiClientCache(externalSystem); ApiClient apiClient = getCredential(externalSystem);
CoreV1Api api = new CoreV1Api(cache.apiClient); CoreV1Api api = new CoreV1Api(apiClient);
V1NamespaceList namespaceList = api.listNamespace( V1NamespaceList namespaceList = api.listNamespace(
null, null, null, null, null, null, null, null, null, null, null null, null, null, null, null, null, null, null, null, null, null
@ -196,8 +200,8 @@ public class K8sServiceIntegrationImpl extends BaseExternalSystemIntegration imp
log.info("查询K8S Deployment集群: {}, 命名空间: {}", externalSystem.getName(), namespace); log.info("查询K8S Deployment集群: {}, 命名空间: {}", externalSystem.getName(), namespace);
try { try {
K8sApiClientCache cache = getApiClientCache(externalSystem); ApiClient apiClient = getCredential(externalSystem);
AppsV1Api api = new AppsV1Api(cache.apiClient); AppsV1Api api = new AppsV1Api(apiClient);
V1DeploymentList deploymentList = api.listNamespacedDeployment( V1DeploymentList deploymentList = api.listNamespacedDeployment(
namespace, null, null, null, null, null, null, null, null, null, null, null namespace, null, null, null, null, null, null, null, null, null, null, null
@ -275,8 +279,8 @@ public class K8sServiceIntegrationImpl extends BaseExternalSystemIntegration imp
log.info("查询所有K8S Deployment集群: {}", externalSystem.getName()); log.info("查询所有K8S Deployment集群: {}", externalSystem.getName());
try { try {
K8sApiClientCache cache = getApiClientCache(externalSystem); ApiClient apiClient = getCredential(externalSystem);
AppsV1Api api = new AppsV1Api(cache.apiClient); AppsV1Api api = new AppsV1Api(apiClient);
V1DeploymentList deploymentList = api.listDeploymentForAllNamespaces( V1DeploymentList deploymentList = api.listDeploymentForAllNamespaces(
null, null, null, null, null, null, null, null, null, null, null null, null, null, null, null, null, null, null, null, null, null
@ -353,8 +357,8 @@ public class K8sServiceIntegrationImpl extends BaseExternalSystemIntegration imp
log.info("查询K8S Pod集群: {}, 命名空间: {}", externalSystem.getName(), namespace); log.info("查询K8S Pod集群: {}, 命名空间: {}", externalSystem.getName(), namespace);
try { try {
K8sApiClientCache cache = getApiClientCache(externalSystem); ApiClient apiClient = getCredential(externalSystem);
CoreV1Api api = new CoreV1Api(cache.apiClient); CoreV1Api api = new CoreV1Api(apiClient);
V1PodList podList = api.listNamespacedPod( V1PodList podList = api.listNamespacedPod(
namespace, null, null, null, null, null, null, null, null, null, null, null namespace, null, null, null, null, null, null, null, null, null, null, null
@ -384,10 +388,10 @@ public class K8sServiceIntegrationImpl extends BaseExternalSystemIntegration imp
externalSystem.getName(), namespace, deploymentName); externalSystem.getName(), namespace, deploymentName);
try { try {
K8sApiClientCache cache = getApiClientCache(externalSystem); ApiClient apiClient = getCredential(externalSystem);
// 1. 先查询Deployment获取selector // 1. 先查询Deployment获取selector
AppsV1Api appsApi = new AppsV1Api(cache.apiClient); AppsV1Api appsApi = new AppsV1Api(apiClient);
V1Deployment deployment = appsApi.readNamespacedDeployment(deploymentName, namespace, null); V1Deployment deployment = appsApi.readNamespacedDeployment(deploymentName, namespace, null);
if (deployment.getSpec() == null || deployment.getSpec().getSelector() == null if (deployment.getSpec() == null || deployment.getSpec().getSelector() == null
@ -396,7 +400,7 @@ public class K8sServiceIntegrationImpl extends BaseExternalSystemIntegration imp
return new ArrayList<>(); return new ArrayList<>();
} }
// 2. 构建label selector // 2. 构建Label selector
Map<String, String> matchLabels = deployment.getSpec().getSelector().getMatchLabels(); Map<String, String> matchLabels = deployment.getSpec().getSelector().getMatchLabels();
String labelSelector = matchLabels.entrySet().stream() String labelSelector = matchLabels.entrySet().stream()
.map(entry -> entry.getKey() + "=" + entry.getValue()) .map(entry -> entry.getKey() + "=" + entry.getValue())
@ -404,7 +408,7 @@ public class K8sServiceIntegrationImpl extends BaseExternalSystemIntegration imp
.orElse(""); .orElse("");
// 3. 使用label selector查询Pod // 3. 使用label selector查询Pod
CoreV1Api coreApi = new CoreV1Api(cache.apiClient); CoreV1Api coreApi = new CoreV1Api(apiClient);
V1PodList podList = coreApi.listNamespacedPod( V1PodList podList = coreApi.listNamespacedPod(
namespace, null, null, null, null, labelSelector, null, null, null, null, null, null namespace, null, null, null, null, labelSelector, null, null, null, null, null, null
); );
@ -441,8 +445,8 @@ public class K8sServiceIntegrationImpl extends BaseExternalSystemIntegration imp
externalSystem.getName(), namespace, podName); externalSystem.getName(), namespace, podName);
try { try {
K8sApiClientCache cache = getApiClientCache(externalSystem); ApiClient apiClient = getCredential(externalSystem);
CoreV1Api api = new CoreV1Api(cache.apiClient); CoreV1Api api = new CoreV1Api(apiClient);
V1Pod pod = api.readNamespacedPod(podName, namespace, null); V1Pod pod = api.readNamespacedPod(podName, namespace, null);
@ -599,8 +603,8 @@ public class K8sServiceIntegrationImpl extends BaseExternalSystemIntegration imp
externalSystem.getName(), namespace, podName, container, effectiveTail, effectiveSinceSeconds); externalSystem.getName(), namespace, podName, container, effectiveTail, effectiveSinceSeconds);
try { try {
K8sApiClientCache cache = getApiClientCache(externalSystem); ApiClient apiClient = getCredential(externalSystem);
CoreV1Api api = new CoreV1Api(cache.apiClient); CoreV1Api api = new CoreV1Api(apiClient);
// 日志大小限制10MB防止OOM // 日志大小限制10MB防止OOM
Integer limitBytes = 10 * 1024 * 1024; Integer limitBytes = 10 * 1024 * 1024;
@ -681,8 +685,8 @@ public class K8sServiceIntegrationImpl extends BaseExternalSystemIntegration imp
externalSystem.getName(), namespace, deploymentName); externalSystem.getName(), namespace, deploymentName);
try { try {
K8sApiClientCache cache = getApiClientCache(externalSystem); ApiClient apiClient = getCredential(externalSystem);
AppsV1Api api = new AppsV1Api(cache.apiClient); AppsV1Api api = new AppsV1Api(apiClient);
// 生成ISO 8601格式的时间戳作为重启标记 // 生成ISO 8601格式的时间戳作为重启标记
String timestamp = java.time.format.DateTimeFormatter.ISO_INSTANT String timestamp = java.time.format.DateTimeFormatter.ISO_INSTANT
@ -746,8 +750,8 @@ public class K8sServiceIntegrationImpl extends BaseExternalSystemIntegration imp
externalSystem.getName(), namespace, deploymentName, replicas); externalSystem.getName(), namespace, deploymentName, replicas);
try { try {
K8sApiClientCache cache = getApiClientCache(externalSystem); ApiClient apiClient = getCredential(externalSystem);
AppsV1Api api = new AppsV1Api(cache.apiClient); AppsV1Api api = new AppsV1Api(apiClient);
// 构建patch内容更新spec.replicas // 构建patch内容更新spec.replicas
String patchBody = String.format("{\"spec\":{\"replicas\":%d}}", replicas); String patchBody = String.format("{\"spec\":{\"replicas\":%d}}", replicas);
@ -1057,26 +1061,6 @@ public class K8sServiceIntegrationImpl extends BaseExternalSystemIntegration imp
return true; return true;
} }
/**
* 创建K8S ApiClient内部实现不使用缓存
*
* @param externalSystem K8S系统配置
* @return ApiClient
*/
private ApiClient createApiClientInternal(ExternalSystem externalSystem) throws Exception {
String config = externalSystem.getConfig();
if (config == null || config.trim().isEmpty()) {
throw new BusinessException(ResponseCode.K8S_CONFIG_EMPTY);
}
// 直接使用config作为kubeconfig内容
ApiClient client = Config.fromConfig(new StringReader(config));
client.setConnectTimeout(15000); // 15秒连接超时
client.setReadTimeout(120000); // 120秒读取超时优化日志查询等耗时操作
return client;
}
/** /**
* 提取Deployment中第一个容器的镜像 * 提取Deployment中第一个容器的镜像
* *
@ -1189,19 +1173,6 @@ public class K8sServiceIntegrationImpl extends BaseExternalSystemIntegration imp
} }
} }
/**
* 获取K8S ApiClient带缓存
* 复用内部缓存机制避免重复创建连接
*
* @param system K8S系统配置
* @return ApiClient实例
*/
@Override
public ApiClient getApiClient(ExternalSystem system) {
K8sApiClientCache cache = getApiClientCache(system);
return cache.apiClient;
}
/** /**
* 获取用于日志流的K8S ApiClient长超时按集群缓存复用 * 获取用于日志流的K8S ApiClient长超时按集群缓存复用
* *
@ -1221,6 +1192,7 @@ public class K8sServiceIntegrationImpl extends BaseExternalSystemIntegration imp
/** /**
* 创建日志流专用ApiClient内部方法 * 创建日志流专用ApiClient内部方法
* 日志流是长连接需要更长的读取超时同样强制使用HTTP/1.1
*/ */
private ApiClient createLogStreamApiClient(ExternalSystem system) { private ApiClient createLogStreamApiClient(ExternalSystem system) {
try { try {
@ -1232,8 +1204,16 @@ public class K8sServiceIntegrationImpl extends BaseExternalSystemIntegration imp
ApiClient client = Config.fromConfig(new StringReader(config)); ApiClient client = Config.fromConfig(new StringReader(config));
client.setConnectTimeout(15000); // 15秒连接超时 client.setConnectTimeout(15000); // 15秒连接超时
client.setReadTimeout(30 * 60 * 1000); // 30分钟读取超时日志流长连接 client.setReadTimeout(30 * 60 * 1000); // 30分钟读取超时日志流长连接
// 关键优化强制使用 HTTP/1.1避免 HTTP/2 连接复用问题
okhttp3.OkHttpClient originalHttpClient = client.getHttpClient();
okhttp3.OkHttpClient httpClient = originalHttpClient.newBuilder()
.protocols(Arrays.asList(okhttp3.Protocol.HTTP_1_1)) // 强制 HTTP/1.1
.retryOnConnectionFailure(true) // 连接失败时重试
.build();
client.setHttpClient(httpClient);
log.debug("日志流ApiClient创建完成readTimeout=30分钟"); log.debug("日志流ApiClient创建完成readTimeout=30分钟使用HTTP/1.1协议");
return client; return client;
} catch (BusinessException e) { } catch (BusinessException e) {
throw e; throw e;

View File

@ -97,9 +97,7 @@ public class ExternalSystemHealthCheckScheduler {
config = new ServerMonitorNotificationConfig(); config = new ServerMonitorNotificationConfig();
config.setNotificationChannelId(notificationChannelId); config.setNotificationChannelId(notificationChannelId);
config.setResourceAlertTemplateId(resourceAlertTemplateId); config.setResourceAlertTemplateId(resourceAlertTemplateId);
log.debug("开始检查外部系统健康状态: channelId={}, alertTemplateId={}", notificationChannelId, resourceAlertTemplateId);
log.debug("开始检查外部系统健康状态: channelId={}, alertTemplateId={}",
notificationChannelId, resourceAlertTemplateId);
} else { } else {
config = null; config = null;
log.debug("开始检查外部系统健康状态(不发送通知)"); log.debug("开始检查外部系统健康状态(不发送通知)");
@ -112,7 +110,7 @@ public class ExternalSystemHealthCheckScheduler {
List<ExternalSystem> systems = externalSystemRepository.findByDeletedFalseOrderBySort() List<ExternalSystem> systems = externalSystemRepository.findByDeletedFalseOrderBySort()
.stream() .stream()
.filter(system -> Boolean.TRUE.equals(system.getEnabled())) .filter(system -> Boolean.TRUE.equals(system.getEnabled()))
.collect(Collectors.toList()); .toList();
if (systems.isEmpty()) { if (systems.isEmpty()) {
log.debug("没有需要检查的外部系统,跳过健康检查"); log.debug("没有需要检查的外部系统,跳过健康检查");
@ -125,7 +123,7 @@ public class ExternalSystemHealthCheckScheduler {
List<CompletableFuture<Void>> futures = systems.stream() List<CompletableFuture<Void>> futures = systems.stream()
.map(system -> CompletableFuture.runAsync(() -> .map(system -> CompletableFuture.runAsync(() ->
checkSingleSystem(system, config), externalSystemHealthCheckExecutor)) checkSingleSystem(system, config), externalSystemHealthCheckExecutor))
.collect(Collectors.toList()); .toList();
// 3. 等待所有检测完成带超时控制防止线程永久阻塞 // 3. 等待所有检测完成带超时控制防止线程永久阻塞
CompletableFuture<Void> allFutures = CompletableFuture.allOf( CompletableFuture<Void> allFutures = CompletableFuture.allOf(