refactor: 简化路由逻辑,直接使用 tenantId 匹配实例

问题:tenantGroup 是 tenantId 的简单转换,造成冗余
  tenantGroup = "TENANT_" + tenantId.toUpperCase()

解决方案:
1. 直接使用 tenantId 匹配实例
   - 移除 tenantGroup 概念
   - 负载均衡器直接匹配 metadata.tenant-id

2. 简化配置
   - JWT 只需 tenantId 一个字段
   - 实例元数据只有 tenant-id

3. 前端简化
   - 请求头只需 X-Tenant-Id
   - 不再需要 X-Tenant-Group

路由规则:
  共享实例: metadata.tenant-id = "" (空)
  VIP实例:  metadata.tenant-id = "VIP_001"
  匹配逻辑: 找到匹配实例 → VIP专属,找不到 → 共享实例
This commit is contained in:
zhangjf 2026-02-19 21:33:51 +08:00
parent e52e2ba801
commit 5a2154c1a1
17 changed files with 238 additions and 196 deletions

View File

@ -184,8 +184,8 @@ services:
REDIS_HOST: redis
REDIS_PORT: 6379
JAVA_OPTS: -Xms256m -Xmx512m
# 租户 - 共享实例(空值,所有租户可用)
TENANT_GROUP: ""
# 租户 ID - 共享实例(空值,所有租户可用)
TENANT_ID: ""
ports:
- "8100:8100"
depends_on:
@ -227,8 +227,8 @@ services:
REDIS_HOST: redis
REDIS_PORT: 6379
JAVA_OPTS: -Xms256m -Xmx512m
# 租户 - VIP_001 专属实例
TENANT_GROUP: "TENANT_VIP_001"
# 租户 ID - VIP_001 专属实例
TENANT_ID: "VIP_001"
ports:
- "8101:8101"
depends_on:
@ -270,8 +270,8 @@ services:
# REDIS_HOST: redis
# REDIS_PORT: 6379
# JAVA_OPTS: -Xms256m -Xmx512m
# # 租户 - VIP_002 专属实例
# TENANT_GROUP: "TENANT_VIP_002"
# # 租户 ID - VIP_002 专属实例
# TENANT_ID: "VIP_002"
# ports:
# - "8102:8102"
# depends_on:

View File

@ -9,17 +9,17 @@ import java.util.List;
/**
* 多租户路由配置属性
*
* <p>租户路由基于 Nacos 服务实例的 metadata.tenant-group 元数据进行匹配</p>
* <p>租户路由基于 Nacos 服务实例的 metadata.tenant-id 元数据进行匹配</p>
*
* <h3>工作原理</h3>
* <pre>
* 1. 服务实例注册到 Nacos metadata 中声明 tenant-group
* - 共享实例: tenant-group 为空或不存在
* - VIP 实例: tenant-group = "TENANT_VIP_001"
* 1. 服务实例注册到 Nacos metadata 中声明 tenant-id
* - 共享实例: tenant-id 为空或不存在
* - VIP 实例: tenant-id = "VIP_001"
*
* 2. Gateway 从请求中提取租户组负载均衡器匹配实例元数据
* - 请求 tenantGroup = "TENANT_VIP_001" 路由到匹配的 VIP 实例
* - 无匹配实例 回退到共享实例
* 2. 负载均衡器根据请求中的 tenantId 匹配实例
* - 找到匹配的 tenant-id VIP 专属实例
* - 找不到 回退到共享实例
* </pre>
*
* <h3>配置示例</h3>
@ -28,11 +28,7 @@ import java.util.List;
* routing:
* enabled: true
* tenant-header: X-Tenant-Id
* default-tenant-id: "1"
* fallback-to-shared: true
* shared-services:
* - fund-gateway
* - fund-report
* </pre>
*/
@Component
@ -45,12 +41,6 @@ public class TenantRoutingProperties {
/** 租户 ID 请求头 */
private String tenantHeader = "X-Tenant-Id";
/** 租户组请求头 */
private String tenantGroupHeader = "X-Tenant-Group";
/** 服务组分隔符 */
private String groupSeparator = "TENANT_";
/** 默认租户 ID当未指定时使用 */
private String defaultTenantId = "1";
@ -80,22 +70,6 @@ public class TenantRoutingProperties {
this.tenantHeader = tenantHeader;
}
public String getTenantGroupHeader() {
return tenantGroupHeader;
}
public void setTenantGroupHeader(String tenantGroupHeader) {
this.tenantGroupHeader = tenantGroupHeader;
}
public String getGroupSeparator() {
return groupSeparator;
}
public void setGroupSeparator(String groupSeparator) {
this.groupSeparator = groupSeparator;
}
public String getDefaultTenantId() {
return defaultTenantId;
}
@ -120,24 +94,8 @@ public class TenantRoutingProperties {
this.fallbackToShared = fallbackToShared;
}
/**
* 构建租户组名称
*
* @param tenantId 租户 ID
* @return 租户组名称 "TENANT_VIP_001"
*/
public String buildTenantGroup(String tenantId) {
if (tenantId == null || tenantId.isEmpty()) {
return "DEFAULT";
}
return getGroupSeparator() + tenantId.toUpperCase();
}
/**
* 判断是否为共享服务
*
* @param serviceName 服务名
* @return 是否为共享服务
*/
public boolean isSharedService(String serviceName) {
return sharedServices != null && sharedServices.contains(serviceName);

View File

@ -24,17 +24,21 @@ import java.util.stream.Collectors;
*
* <p>根据租户 ID 进行服务实例路由支持</p>
* <ul>
* <li>租户专属实例优先</li>
* <li>共享实例回退</li>
* <li>轮询负载均衡</li>
* <li>混合模式VIP 租户专属 + 普通租户共享</li>
* <li>租户专属实例优先metadata.tenant-id 匹配</li>
* <li>共享实例回退metadata.tenant-id 为空</li>
* <li>随机负载均衡</li>
* </ul>
*
* <h3>使用场景</h3>
* <h3>路由规则</h3>
* <pre>
* 混合模式部署
* - VIP 客户独立部署服务实例 tenant-group 标签
* - 普通客户共享服务实例 tenant-group 标签
* 1. 查找 metadata.tenant-id == 请求.tenantId 的实例 VIP 专属实例
* 2. 找不到 回退到共享实例metadata.tenant-id 为空
* </pre>
*
* <h3>实例配置</h3>
* <pre>
* 共享实例: metadata.tenant-id = "" ()
* VIP 实例: metadata.tenant-id = "VIP_001"
* </pre>
*/
public class TenantAwareLoadBalancer implements ReactorServiceInstanceLoadBalancer {
@ -61,9 +65,8 @@ public class TenantAwareLoadBalancer implements ReactorServiceInstanceLoadBalanc
return getDefaultResponse();
}
// 从请求上下文获取租户信息
// 从请求上下文获取租户 ID
String tenantId = getTenantIdFromRequest(request);
String tenantGroup = buildTenantGroup(tenantId);
if (supplierProvider == null) {
logger.warn("[TenantLB] ServiceInstanceListSupplier 未提供");
@ -77,7 +80,7 @@ public class TenantAwareLoadBalancer implements ReactorServiceInstanceLoadBalanc
}
return supplier.get().next()
.map(instances -> filterByTenantGroup(instances, tenantGroup))
.map(instances -> filterByTenantId(instances, tenantId))
.map(this::getInstanceResponse);
}
@ -123,34 +126,20 @@ public class TenantAwareLoadBalancer implements ReactorServiceInstanceLoadBalanc
}
/**
* 构建租户组名称
*/
String buildTenantGroup(String tenantId) {
if (routingProperties != null) {
return routingProperties.buildTenantGroup(tenantId);
}
// 默认逻辑
if (tenantId == null || tenantId.isEmpty()) {
return "DEFAULT";
}
return "TENANT_" + tenantId.toUpperCase();
}
/**
* 根据租户组过滤服务实例
* 根据租户 ID 过滤服务实例
*
* <p>路由策略</p>
* <ol>
* <li>优先选择租户专属实例metadata.tenant-group 匹配</li>
* <li>回退到共享实例 tenant-group 标签</li>
* <li>优先选择租户专属实例metadata.tenant-id 匹配</li>
* <li>回退到共享实例metadata.tenant-id 为空或不存在</li>
* </ol>
*/
List<ServiceInstance> filterByTenantGroup(List<ServiceInstance> instances, String tenantGroup) {
List<ServiceInstance> filterByTenantId(List<ServiceInstance> instances, String tenantId) {
if (instances == null || instances.isEmpty()) {
return instances;
}
logger.debug("[TenantLB] 租户组:{},候选实例数:{}", tenantGroup, instances.size());
logger.debug("[TenantLB] 租户 ID{},候选实例数:{}", tenantId, instances.size());
// 检查是否为共享服务不需要租户路由
if (routingProperties != null && routingProperties.isSharedService(serviceId)) {
@ -159,23 +148,25 @@ public class TenantAwareLoadBalancer implements ReactorServiceInstanceLoadBalanc
}
// 优先选择租户专属实例
if (tenantId != null && !tenantId.isEmpty()) {
List<ServiceInstance> tenantInstances = instances.stream()
.filter(inst -> {
Map<String, String> metadata = inst.getMetadata();
if (metadata == null) return false;
String instanceGroup = metadata.get("tenant-group");
boolean match = tenantGroup.equals(instanceGroup);
String instanceTenantId = metadata.get("tenant-id");
boolean match = tenantId.equals(instanceTenantId);
if (match) {
logger.debug("[TenantLB] 匹配租户实例:{}:{}", inst.getHost(), inst.getPort());
logger.debug("[TenantLB] 匹配租户专属实例:{}:{}", inst.getHost(), inst.getPort());
}
return match;
})
.collect(Collectors.toList());
if (!tenantInstances.isEmpty()) {
logger.info("[TenantLB] 找到 {} 个租户专属实例,租户{}", tenantInstances.size(), tenantGroup);
logger.info("[TenantLB] 找到 {} 个租户专属实例,租户 ID{}", tenantInstances.size(), tenantId);
return tenantInstances;
}
}
// 检查是否启用回退到共享实例
boolean fallbackEnabled = true;
@ -188,11 +179,11 @@ public class TenantAwareLoadBalancer implements ReactorServiceInstanceLoadBalanc
return List.of();
}
// 回退到共享实例 tenant-group 标签
// 回退到共享实例metadata.tenant-id 为空或不存在
List<ServiceInstance> sharedInstances = instances.stream()
.filter(inst -> {
Map<String, String> metadata = inst.getMetadata();
return metadata == null || !metadata.containsKey("tenant-group");
return metadata == null || metadata.get("tenant-id") == null || metadata.get("tenant-id").isEmpty();
})
.collect(Collectors.toList());
@ -209,10 +200,9 @@ public class TenantAwareLoadBalancer implements ReactorServiceInstanceLoadBalanc
return new EmptyResponse();
}
// 随机选择
int index = ThreadLocalRandom.current().nextInt(instances.size());
ServiceInstance chosen = instances.get(index);
logger.info("[TenantLB] 选择实例:{}:{} (index={})", chosen.getHost(), chosen.getPort(), index);
logger.info("[TenantLB] 选择实例:{}:{}", chosen.getHost(), chosen.getPort());
return new DefaultResponse(chosen);
}

View File

@ -13,21 +13,21 @@ import java.util.Map;
/**
* Nacos 服务注册元数据配置
*
* <p>服务启动时自动注册租户组标签 Nacos支持租户感知的负载均衡</p>
* <p>服务启动时自动注册租户 ID Nacos支持租户感知的负载均衡</p>
*
* <h3>租户配置规则</h3>
* <h3>租户 ID 配置规则</h3>
* <pre>
* 共享实例: tenant-group 为空或不配置 所有租户都可使用
* VIP实例: tenant-group = "TENANT_VIP_001" 仅该租户可用
* 共享实例: tenant-id 为空或不配置 所有租户都可使用
* VIP实例: tenant-id = "VIP_001" 仅该租户可用
* </pre>
*
* <h3>配置方式</h3>
* <pre>
* # 方式1: 环境变量Docker/K8s 推荐
* TENANT_GROUP=TENANT_VIP_001
* TENANT_ID=VIP_001
*
* # 方式2: 配置文件
* spring.cloud.nacos.discovery.metadata.tenant-group=TENANT_VIP_001
* spring.cloud.nacos.discovery.metadata.tenant-id=VIP_001
* </pre>
*/
@Configuration
@ -40,11 +40,11 @@ public class NacosMetadataConfig {
private String applicationName;
/**
* 租户优先级环境变量 > 配置文件
* 租户 ID优先级环境变量 > 配置文件
* 为空表示共享实例供所有租户使用
*/
@Value("${TENANT_GROUP:${spring.cloud.nacos.discovery.metadata.tenant-group:}}")
private String tenantGroup;
@Value("${TENANT_ID:${spring.cloud.nacos.discovery.metadata.tenant-id:}}")
private String tenantId;
private final Registration registration;
@ -57,21 +57,21 @@ public class NacosMetadataConfig {
if (registration != null && registration.getMetadata() != null) {
Map<String, String> metadata = registration.getMetadata();
// 添加租户元数据
if (tenantGroup != null && !tenantGroup.isEmpty()) {
metadata.put("tenant-group", tenantGroup);
logger.info("[Nacos] {} 注册为 VIP 专属实例,租户组:{}", applicationName, tenantGroup);
// 添加租户 ID 元数据
if (tenantId != null && !tenantId.isEmpty()) {
metadata.put("tenant-id", tenantId);
logger.info("[Nacos] {} 注册为 VIP 专属实例,租户 ID{}", applicationName, tenantId);
} else {
logger.info("[Nacos] {} 注册为共享实例,供所有租户使用", applicationName);
}
}
}
public String getTenantGroup() {
return tenantGroup;
public String getTenantId() {
return tenantId;
}
public boolean isVipInstance() {
return tenantGroup != null && !tenantGroup.isEmpty();
return tenantId != null && !tenantId.isEmpty();
}
}

View File

@ -4,13 +4,10 @@ import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.cloud.client.DefaultServiceInstance;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.loadbalancer.Request;
import org.springframework.cloud.client.loadbalancer.RequestData;
import java.util.*;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;
/**
* 租户负载均衡器测试
@ -25,73 +22,70 @@ class TenantAwareLoadBalancerTest {
}
@Test
void testBuildTenantGroup() {
// 测试租户组名称构建
String group1 = invokeBuildTenantGroup(loadBalancer, "1");
assertEquals("TENANT_1", group1);
String group2 = invokeBuildTenantGroup(loadBalancer, "tenant_001");
assertEquals("TENANT_TENANT_001", group2);
String group3 = invokeBuildTenantGroup(loadBalancer, null);
assertEquals("DEFAULT", group3);
String group4 = invokeBuildTenantGroup(loadBalancer, "");
assertEquals("DEFAULT", group4);
}
@Test
void testFilterByTenantGroup() {
void testFilterByTenantId() {
// 创建测试实例
List<ServiceInstance> instances = Arrays.asList(
createInstance("fund-sys", "192.168.1.1", 8100, Map.of("tenant-group", "TENANT_1")),
createInstance("fund-sys", "192.168.1.2", 8101, Map.of("tenant-group", "TENANT_1")),
createInstance("fund-sys", "192.168.1.3", 8102, Map.of("tenant-group", "TENANT_2")),
createInstance("fund-sys", "192.168.1.1", 8100, Map.of("tenant-id", "VIP_001")),
createInstance("fund-sys", "192.168.1.2", 8101, Map.of("tenant-id", "VIP_001")),
createInstance("fund-sys", "192.168.1.3", 8102, Map.of("tenant-id", "VIP_002")),
createInstance("fund-sys", "192.168.1.4", 8103, Collections.emptyMap()) // 共享实例
);
// 测试租户 1 过滤
List<ServiceInstance> tenant1Instances = invokeFilterByTenantGroup(loadBalancer, instances, "TENANT_1");
assertEquals(2, tenant1Instances.size());
assertTrue(tenant1Instances.stream().allMatch(i -> "TENANT_1".equals(i.getMetadata().get("tenant-group"))));
// 测试 VIP_001 过滤
List<ServiceInstance> vip1Instances = invokeFilterByTenantId(loadBalancer, instances, "VIP_001");
assertEquals(2, vip1Instances.size());
assertTrue(vip1Instances.stream().allMatch(i -> "VIP_001".equals(i.getMetadata().get("tenant-id"))));
// 测试租户 2 过滤
List<ServiceInstance> tenant2Instances = invokeFilterByTenantGroup(loadBalancer, instances, "TENANT_2");
assertEquals(1, tenant2Instances.size());
// 测试 VIP_002 过滤
List<ServiceInstance> vip2Instances = invokeFilterByTenantId(loadBalancer, instances, "VIP_002");
assertEquals(1, vip2Instances.size());
// 测试未知租户回退到共享实例
List<ServiceInstance> unknownTenantInstances = invokeFilterByTenantGroup(loadBalancer, instances, "TENANT_UNKNOWN");
assertEquals(1, unknownTenantInstances.size());
assertFalse(unknownTenantInstances.get(0).getMetadata().containsKey("tenant-group"));
List<ServiceInstance> unknownInstances = invokeFilterByTenantId(loadBalancer, instances, "VIP_999");
assertEquals(1, unknownInstances.size());
assertFalse(unknownInstances.get(0).getMetadata().containsKey("tenant-id"));
}
@Test
void testMixedMode() {
// 混合模式VIP 客户有专属实例普通客户使用共享实例
List<ServiceInstance> instances = Arrays.asList(
// VIP 租户 001 的专属实例
createInstance("fund-sys", "192.168.1.1", 8100, Map.of("tenant-group", "TENANT_VIP_001")),
createInstance("fund-sys", "192.168.1.2", 8101, Map.of("tenant-group", "TENANT_VIP_001")),
// VIP 租户 002 的专属实例
createInstance("fund-sys", "192.168.1.3", 8102, Map.of("tenant-group", "TENANT_VIP_002")),
// VIP_001 的专属实例
createInstance("fund-sys", "192.168.1.1", 8100, Map.of("tenant-id", "VIP_001")),
createInstance("fund-sys", "192.168.1.2", 8101, Map.of("tenant-id", "VIP_001")),
// VIP_002 的专属实例
createInstance("fund-sys", "192.168.1.3", 8102, Map.of("tenant-id", "VIP_002")),
// 共享实例普通租户使用
createInstance("fund-sys", "192.168.1.10", 8110, Collections.emptyMap()),
createInstance("fund-sys", "192.168.1.11", 8111, Collections.emptyMap())
);
// VIP 租户 001 应该路由到专属实例
List<ServiceInstance> vip1Instances = invokeFilterByTenantGroup(loadBalancer, instances, "TENANT_VIP_001");
// VIP_001 应该路由到专属实例
List<ServiceInstance> vip1Instances = invokeFilterByTenantId(loadBalancer, instances, "VIP_001");
assertEquals(2, vip1Instances.size());
// VIP 租户 002 应该路由到专属实例
List<ServiceInstance> vip2Instances = invokeFilterByTenantGroup(loadBalancer, instances, "TENANT_VIP_002");
// VIP_002 应该路由到专属实例
List<ServiceInstance> vip2Instances = invokeFilterByTenantId(loadBalancer, instances, "VIP_002");
assertEquals(1, vip2Instances.size());
// 普通租户应该路由到共享实例
List<ServiceInstance> normalInstances = invokeFilterByTenantGroup(loadBalancer, instances, "TENANT_NORMAL");
List<ServiceInstance> normalInstances = invokeFilterByTenantId(loadBalancer, instances, "NORMAL_001");
assertEquals(2, normalInstances.size());
}
@Test
void testNullTenantId() {
// 测试空租户 ID应该回退到共享实例
List<ServiceInstance> instances = Arrays.asList(
createInstance("fund-sys", "192.168.1.1", 8100, Map.of("tenant-id", "VIP_001")),
createInstance("fund-sys", "192.168.1.2", 8101, Collections.emptyMap())
);
List<ServiceInstance> result = invokeFilterByTenantId(loadBalancer, instances, null);
assertEquals(1, result.size());
assertFalse(result.get(0).getMetadata().containsKey("tenant-id"));
}
// 辅助方法创建服务实例
private ServiceInstance createInstance(String serviceId, String host, int port, Map<String, String> metadata) {
return new DefaultServiceInstance(
@ -104,24 +98,13 @@ class TenantAwareLoadBalancerTest {
);
}
// 辅助方法调用私有方法 buildTenantGroup
private String invokeBuildTenantGroup(TenantAwareLoadBalancer lb, String tenantId) {
try {
var method = TenantAwareLoadBalancer.class.getDeclaredMethod("buildTenantGroup", String.class);
method.setAccessible(true);
return (String) method.invoke(lb, tenantId);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
// 辅助方法调用私有方法 filterByTenantGroup
// 辅助方法调用私有方法 filterByTenantId
@SuppressWarnings("unchecked")
private List<ServiceInstance> invokeFilterByTenantGroup(TenantAwareLoadBalancer lb, List<ServiceInstance> instances, String tenantGroup) {
private List<ServiceInstance> invokeFilterByTenantId(TenantAwareLoadBalancer lb, List<ServiceInstance> instances, String tenantId) {
try {
var method = TenantAwareLoadBalancer.class.getDeclaredMethod("filterByTenantGroup", List.class, String.class);
var method = TenantAwareLoadBalancer.class.getDeclaredMethod("filterByTenantId", List.class, String.class);
method.setAccessible(true);
return (List<ServiceInstance>) method.invoke(lb, instances, tenantGroup);
return (List<ServiceInstance>) method.invoke(lb, instances, tenantId);
} catch (Exception e) {
throw new RuntimeException(e);
}

View File

@ -1,4 +1,4 @@
-------------------------------------------------------------------------------
Test set: com.fundplatform.common.loadbalancer.TenantAwareLoadBalancerTest
-------------------------------------------------------------------------------
Tests run: 3, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.310 s -- in com.fundplatform.common.loadbalancer.TenantAwareLoadBalancerTest
Tests run: 3, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.206 s -- in com.fundplatform.common.loadbalancer.TenantAwareLoadBalancerTest

View File

@ -14,9 +14,9 @@ spring:
username: nacos
password: nacos
# 租户路由元数据
# tenant-group: 空值=共享实例,有值=VIP专属实例
# tenant-id: 空值=共享实例,有值=VIP专属实例
metadata:
tenant-group: ${TENANT_GROUP:}
tenant-id: ${TENANT_ID:}
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver

View File

@ -14,9 +14,9 @@ spring:
username: nacos
password: nacos
# 租户路由元数据
# tenant-group: 空值=共享实例,有值=VIP专属实例
# tenant-id: 空值=共享实例,有值=VIP专属实例
metadata:
tenant-group: ${TENANT_GROUP:}
tenant-id: ${TENANT_ID:}
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver

View File

@ -1138,3 +1138,39 @@ java.lang.NullPointerException: Cannot invoke "com.zaxxer.hikari.HikariPoolMXBea
at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1144)
at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:642)
at java.base/java.lang.Thread.run(Thread.java:1583)
2026-02-19 21:26:06.988 [scheduling-1] [] INFO com.fundplatform.sys.config.HikariMonitorConfig - === HikariCP 连接池状态 ===
2026-02-19 21:26:06.988 [scheduling-1] [] INFO com.fundplatform.sys.config.HikariMonitorConfig - 连接池名称: FundSysHikariPool
2026-02-19 21:26:06.989 [scheduling-1] [] ERROR o.s.s.support.TaskUtils$LoggingErrorHandler - Unexpected error occurred in scheduled task
java.lang.NullPointerException: Cannot invoke "com.zaxxer.hikari.HikariPoolMXBean.getActiveConnections()" because the return value of "com.zaxxer.hikari.HikariDataSource.getHikariPoolMXBean()" is null
at com.fundplatform.sys.config.HikariMonitorConfig.monitorHikariPool(HikariMonitorConfig.java:38)
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:103)
at java.base/java.lang.reflect.Method.invoke(Method.java:580)
at org.springframework.scheduling.support.ScheduledMethodRunnable.runInternal(ScheduledMethodRunnable.java:130)
at org.springframework.scheduling.support.ScheduledMethodRunnable.lambda$run$2(ScheduledMethodRunnable.java:124)
at io.micrometer.observation.Observation.observe(Observation.java:499)
at org.springframework.scheduling.support.ScheduledMethodRunnable.run(ScheduledMethodRunnable.java:124)
at org.springframework.scheduling.support.DelegatingErrorHandlingRunnable.run(DelegatingErrorHandlingRunnable.java:54)
at java.base/java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:572)
at java.base/java.util.concurrent.FutureTask.runAndReset(FutureTask.java:358)
at java.base/java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:305)
at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1144)
at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:642)
at java.base/java.lang.Thread.run(Thread.java:1583)
2026-02-19 21:31:06.988 [scheduling-1] [] INFO com.fundplatform.sys.config.HikariMonitorConfig - === HikariCP 连接池状态 ===
2026-02-19 21:31:06.988 [scheduling-1] [] INFO com.fundplatform.sys.config.HikariMonitorConfig - 连接池名称: FundSysHikariPool
2026-02-19 21:31:06.988 [scheduling-1] [] ERROR o.s.s.support.TaskUtils$LoggingErrorHandler - Unexpected error occurred in scheduled task
java.lang.NullPointerException: Cannot invoke "com.zaxxer.hikari.HikariPoolMXBean.getActiveConnections()" because the return value of "com.zaxxer.hikari.HikariDataSource.getHikariPoolMXBean()" is null
at com.fundplatform.sys.config.HikariMonitorConfig.monitorHikariPool(HikariMonitorConfig.java:38)
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:103)
at java.base/java.lang.reflect.Method.invoke(Method.java:580)
at org.springframework.scheduling.support.ScheduledMethodRunnable.runInternal(ScheduledMethodRunnable.java:130)
at org.springframework.scheduling.support.ScheduledMethodRunnable.lambda$run$2(ScheduledMethodRunnable.java:124)
at io.micrometer.observation.Observation.observe(Observation.java:499)
at org.springframework.scheduling.support.ScheduledMethodRunnable.run(ScheduledMethodRunnable.java:124)
at org.springframework.scheduling.support.DelegatingErrorHandlingRunnable.run(DelegatingErrorHandlingRunnable.java:54)
at java.base/java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:572)
at java.base/java.util.concurrent.FutureTask.runAndReset(FutureTask.java:358)
at java.base/java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:305)
at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1144)
at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:642)
at java.base/java.lang.Thread.run(Thread.java:1583)

View File

@ -1120,3 +1120,21 @@ java.lang.NullPointerException: Cannot invoke "com.zaxxer.hikari.HikariPoolMXBea
at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1144)
at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:642)
at java.base/java.lang.Thread.run(Thread.java:1583)
2026-02-19 21:29:07.541 [scheduling-1] [] INFO com.fundplatform.sys.config.HikariMonitorConfig - === HikariCP 连接池状态 ===
2026-02-19 21:29:07.541 [scheduling-1] [] INFO com.fundplatform.sys.config.HikariMonitorConfig - 连接池名称: FundSysHikariPool
2026-02-19 21:29:07.541 [scheduling-1] [] ERROR o.s.s.support.TaskUtils$LoggingErrorHandler - Unexpected error occurred in scheduled task
java.lang.NullPointerException: Cannot invoke "com.zaxxer.hikari.HikariPoolMXBean.getActiveConnections()" because the return value of "com.zaxxer.hikari.HikariDataSource.getHikariPoolMXBean()" is null
at com.fundplatform.sys.config.HikariMonitorConfig.monitorHikariPool(HikariMonitorConfig.java:38)
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:103)
at java.base/java.lang.reflect.Method.invoke(Method.java:580)
at org.springframework.scheduling.support.ScheduledMethodRunnable.runInternal(ScheduledMethodRunnable.java:130)
at org.springframework.scheduling.support.ScheduledMethodRunnable.lambda$run$2(ScheduledMethodRunnable.java:124)
at io.micrometer.observation.Observation.observe(Observation.java:499)
at org.springframework.scheduling.support.ScheduledMethodRunnable.run(ScheduledMethodRunnable.java:124)
at org.springframework.scheduling.support.DelegatingErrorHandlingRunnable.run(DelegatingErrorHandlingRunnable.java:54)
at java.base/java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:572)
at java.base/java.util.concurrent.FutureTask.runAndReset(FutureTask.java:358)
at java.base/java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:305)
at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1144)
at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:642)
at java.base/java.lang.Thread.run(Thread.java:1583)

View File

@ -1518,3 +1518,51 @@ java.lang.NullPointerException: Cannot invoke "com.zaxxer.hikari.HikariPoolMXBea
at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1144)
at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:642)
at java.base/java.lang.Thread.run(Thread.java:1583)
2026-02-19 21:26:06.989 [scheduling-1] [] ERROR o.s.s.support.TaskUtils$LoggingErrorHandler - Unexpected error occurred in scheduled task
java.lang.NullPointerException: Cannot invoke "com.zaxxer.hikari.HikariPoolMXBean.getActiveConnections()" because the return value of "com.zaxxer.hikari.HikariDataSource.getHikariPoolMXBean()" is null
at com.fundplatform.sys.config.HikariMonitorConfig.monitorHikariPool(HikariMonitorConfig.java:38)
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:103)
at java.base/java.lang.reflect.Method.invoke(Method.java:580)
at org.springframework.scheduling.support.ScheduledMethodRunnable.runInternal(ScheduledMethodRunnable.java:130)
at org.springframework.scheduling.support.ScheduledMethodRunnable.lambda$run$2(ScheduledMethodRunnable.java:124)
at io.micrometer.observation.Observation.observe(Observation.java:499)
at org.springframework.scheduling.support.ScheduledMethodRunnable.run(ScheduledMethodRunnable.java:124)
at org.springframework.scheduling.support.DelegatingErrorHandlingRunnable.run(DelegatingErrorHandlingRunnable.java:54)
at java.base/java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:572)
at java.base/java.util.concurrent.FutureTask.runAndReset(FutureTask.java:358)
at java.base/java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:305)
at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1144)
at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:642)
at java.base/java.lang.Thread.run(Thread.java:1583)
2026-02-19 21:29:07.541 [scheduling-1] [] ERROR o.s.s.support.TaskUtils$LoggingErrorHandler - Unexpected error occurred in scheduled task
java.lang.NullPointerException: Cannot invoke "com.zaxxer.hikari.HikariPoolMXBean.getActiveConnections()" because the return value of "com.zaxxer.hikari.HikariDataSource.getHikariPoolMXBean()" is null
at com.fundplatform.sys.config.HikariMonitorConfig.monitorHikariPool(HikariMonitorConfig.java:38)
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:103)
at java.base/java.lang.reflect.Method.invoke(Method.java:580)
at org.springframework.scheduling.support.ScheduledMethodRunnable.runInternal(ScheduledMethodRunnable.java:130)
at org.springframework.scheduling.support.ScheduledMethodRunnable.lambda$run$2(ScheduledMethodRunnable.java:124)
at io.micrometer.observation.Observation.observe(Observation.java:499)
at org.springframework.scheduling.support.ScheduledMethodRunnable.run(ScheduledMethodRunnable.java:124)
at org.springframework.scheduling.support.DelegatingErrorHandlingRunnable.run(DelegatingErrorHandlingRunnable.java:54)
at java.base/java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:572)
at java.base/java.util.concurrent.FutureTask.runAndReset(FutureTask.java:358)
at java.base/java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:305)
at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1144)
at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:642)
at java.base/java.lang.Thread.run(Thread.java:1583)
2026-02-19 21:31:06.988 [scheduling-1] [] ERROR o.s.s.support.TaskUtils$LoggingErrorHandler - Unexpected error occurred in scheduled task
java.lang.NullPointerException: Cannot invoke "com.zaxxer.hikari.HikariPoolMXBean.getActiveConnections()" because the return value of "com.zaxxer.hikari.HikariDataSource.getHikariPoolMXBean()" is null
at com.fundplatform.sys.config.HikariMonitorConfig.monitorHikariPool(HikariMonitorConfig.java:38)
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:103)
at java.base/java.lang.reflect.Method.invoke(Method.java:580)
at org.springframework.scheduling.support.ScheduledMethodRunnable.runInternal(ScheduledMethodRunnable.java:130)
at org.springframework.scheduling.support.ScheduledMethodRunnable.lambda$run$2(ScheduledMethodRunnable.java:124)
at io.micrometer.observation.Observation.observe(Observation.java:499)
at org.springframework.scheduling.support.ScheduledMethodRunnable.run(ScheduledMethodRunnable.java:124)
at org.springframework.scheduling.support.DelegatingErrorHandlingRunnable.run(DelegatingErrorHandlingRunnable.java:54)
at java.base/java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:572)
at java.base/java.util.concurrent.FutureTask.runAndReset(FutureTask.java:358)
at java.base/java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:305)
at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1144)
at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:642)
at java.base/java.lang.Thread.run(Thread.java:1583)

View File

@ -334,3 +334,9 @@
2026-02-19 21:21:06.988 [scheduling-1] [] INFO com.fundplatform.sys.config.HikariMonitorConfig - 连接池名称: FundSysHikariPool
2026-02-19 21:24:07.541 [scheduling-1] [] INFO com.fundplatform.sys.config.HikariMonitorConfig - === HikariCP 连接池状态 ===
2026-02-19 21:24:07.541 [scheduling-1] [] INFO com.fundplatform.sys.config.HikariMonitorConfig - 连接池名称: FundSysHikariPool
2026-02-19 21:26:06.988 [scheduling-1] [] INFO com.fundplatform.sys.config.HikariMonitorConfig - === HikariCP 连接池状态 ===
2026-02-19 21:26:06.988 [scheduling-1] [] INFO com.fundplatform.sys.config.HikariMonitorConfig - 连接池名称: FundSysHikariPool
2026-02-19 21:29:07.541 [scheduling-1] [] INFO com.fundplatform.sys.config.HikariMonitorConfig - === HikariCP 连接池状态 ===
2026-02-19 21:29:07.541 [scheduling-1] [] INFO com.fundplatform.sys.config.HikariMonitorConfig - 连接池名称: FundSysHikariPool
2026-02-19 21:31:06.988 [scheduling-1] [] INFO com.fundplatform.sys.config.HikariMonitorConfig - === HikariCP 连接池状态 ===
2026-02-19 21:31:06.988 [scheduling-1] [] INFO com.fundplatform.sys.config.HikariMonitorConfig - 连接池名称: FundSysHikariPool