4
0

4 Коммиты b565440ddf ... 35c06cf598

Автор SHA1 Сообщение Дата
  baiying 35c06cf598 Merge branch 'master' into feat-20260603-gray支持跨group服务 2 недель назад
  baiying 98511faabb 删除 target 2 недель назад
  baiying c83793cfcd gray支持跨group服务 2 недель назад
  baiying 4a9ce6ec19 添加gitignore 2 недель назад

+ 1 - 1
.gitignore

@@ -66,4 +66,4 @@ node_modules
 ### oneself ###
 .idea/
 .vscode/
-
+.claude/

+ 190 - 0
yyc-common-gray/README.md

@@ -0,0 +1,190 @@
+# yyc-common-gray
+
+基于 Nacos 的**版本灰度**与**跨 Group 服务发现**公共组件,供 Feign + Ribbon 消费者与 Spring Cloud Gateway 复用。
+
+## 能力概览
+
+| 能力 | 场景 | 核心类 |
+|------|------|--------|
+| 版本灰度(Ribbon) | 微服务通过 Feign 调用同 Group 服务 | `GrayRibbonLoadBalancerRule`、`GrayFeignRequestInterceptor` |
+| 跨 Group 发现(Ribbon) | 消费者与提供方不在同一 Nacos Group | `NacosCrossGroupServerList` |
+| 跨 Group 工具(Gateway) | 路由 URI 编解码、直接查 Nacos 选实例 | `NacosGroupServiceIdCodec`、`NacosCrossGroupInstanceSelector` |
+
+引入依赖后通过 `META-INF/spring.factories` 自动装配,无需手动 `@Import`。
+
+## 依赖引入
+
+```xml
+<dependency>
+    <groupId>net.yyc.common</groupId>
+    <artifactId>yyc-common-gray</artifactId>
+    <version>${yyc-common.version}</version>
+</dependency>
+```
+
+传递依赖:`yyc-common-core`、`spring-cloud-starter-alibaba-nacos-discovery`、`feign-core`。
+
+适用栈:Spring Cloud Alibaba **2.2.x** + Netflix Ribbon(`@FeignClient` + `ribbon` 负载均衡)。
+
+## 版本灰度(Feign / Ribbon)
+
+### 开关
+
+```yaml
+gray:
+  rule:
+    enabled: true
+```
+
+开启后注册:
+
+- `ribbonLoadBalancerRule` → `GrayRibbonLoadBalancerRule`:按请求头 `VERSION` 与实例元数据 `VERSION` 匹配;无头或未匹配时随机选实例。
+- `grayFeignRequestInterceptor`:将当前 HTTP 请求的 `VERSION` 头透传到下游 Feign 调用。
+
+### 约定
+
+- 请求头 / 元数据键名:`VERSION`(见 `CommonConstants.VERSION`)。
+- 实例需在 Nacos 注册元数据中携带对应 `VERSION` 字段。
+
+### 注意
+
+- 非 Web 场景(定时任务、消息消费等)无 `HttpServletRequest` 时,规则会退化为随机选实例,与改造前行为一致。
+- **不要**再配置 `NFLoadBalancerRuleClassName`(如 `NacosRule`),会与 `ribbonLoadBalancerRule` Bean 冲突。
+
+## 跨 Nacos Group(Feign / Ribbon)
+
+官方 `NacosServerList` 固定使用消费者自身的 `spring.cloud.nacos.discovery.group`,无法通过 `{serviceId}.ribbon.nacos.group` 指定目标 Group。本模块用 `NacosCrossGroupServerList` 覆盖实例拉取逻辑。
+
+### 配置
+
+为每个 `@FeignClient` 的 `value`(即 Ribbon `clientName` / `serviceId`)指定目标 Group:
+
+```yaml
+yyc:
+  nacos:
+    cross-group:
+      gulop-gig-biz: cso_gig
+```
+
+未配置时 fallback 到 `spring.cloud.nacos.discovery.group`,与同 Group 服务行为一致。
+
+### 示例
+
+消费者 `hnqz-upms-biz`(Group `cso_hnqz`)调用 `gulop-gig-biz`(Group `cso_gig`):
+
+```java
+@FeignClient(value = "gulop-gig-biz")
+public interface GigUserApiClient { ... }
+```
+
+```yaml
+spring:
+  cloud:
+    nacos:
+      discovery:
+        group: cso_hnqz
+
+yyc:
+  nacos:
+    cross-group:
+      gulop-gig-biz: cso_gig
+
+gray:
+  rule:
+    enabled: true
+```
+
+### 自动配置说明
+
+| 组件 | 作用 |
+|------|------|
+| `NacosCrossGroupEnvironmentPostProcessor` | 启动早期排除 `RibbonNacosAutoConfiguration`,避免与自定义 `@RibbonClients` 重复注册 |
+| `NacosCrossGroupRibbonAutoConfiguration` | `@RibbonClients(defaultConfiguration = NacosCrossGroupRibbonClientConfiguration.class)` |
+| `NacosCrossGroupRibbonClientConfiguration` | 注册 `NacosCrossGroupServerList` 替代默认 `NacosServerList` |
+
+激活条件:`@ConditionalOnRibbonNacos`(项目启用 Ribbon + Nacos 时)。
+
+## 跨 Nacos Group(Gateway 共享工具)
+
+网关侧不经过 Ribbon,使用 `net.yyc.common.gray.nacos` 包中的工具类(`yyc-gateway-biz` 等业务模块引用本 jar 即可)。
+
+### 服务名编解码
+
+Java `URI` 的 host 不允许 `@`、`_` 等字符,跨 Group 路由使用:
+
+| 形态 | 示例 |
+|------|------|
+| Nacos serviceId | `cso_gig@@gulop-gig-biz` |
+| 路由 URI host(存储) | `cso-gig--gulop-gig-biz`(group 中 `_` → `-`) |
+
+`NacosGroupServiceIdCodec` 提供 `encodeLbUri`、`parseFromUriHost`、`parseFromNacosServiceId` 等方法,并处理 `%40%40` 被 URI 解析器编码的情况。
+
+### 实例选择
+
+存在 `org.springframework.cloud.gateway.filter.GlobalFilter` 时,自动注册 `NacosCrossGroupInstanceSelector` Bean,按 **group + serviceName** 调用 Nacos `selectInstances`,并支持 `VERSION` 头灰度(与 Ribbon 规则语义一致)。
+
+业务侧(如 `NacosGroupGrayLoadBalancer`)注入该 Bean 并传入版本头即可。
+
+## 架构关系
+
+```mermaid
+flowchart TB
+  subgraph feign [Feign 消费者]
+    REQ[HTTP 请求 VERSION]
+    INT[GrayFeignRequestInterceptor]
+    SL[NacosCrossGroupServerList]
+    RULE[GrayRibbonLoadBalancerRule]
+    REQ --> INT
+    INT --> RULE
+    SL --> RULE
+    SL -->|"yyc.nacos.cross-group.{serviceId}"| Nacos[(Nacos)]
+  end
+
+  subgraph gw [Gateway]
+    CODEC[NacosGroupServiceIdCodec]
+    SEL[NacosCrossGroupInstanceSelector]
+    CODEC --> SEL
+    SEL --> Nacos
+  end
+```
+
+## 包结构
+
+```
+net.yyc.common.gray
+├── GrayRibbonLoadBalancerConfiguration    # gray.rule.enabled 灰度规则与 Feign 拦截器
+├── feign/
+│   └── GrayFeignRequestInterceptor
+├── rule/
+│   └── GrayRibbonLoadBalancerRule
+├── ribbon/
+│   ├── NacosCrossGroupEnvironmentPostProcessor
+│   ├── NacosCrossGroupRibbonAutoConfiguration
+│   ├── NacosCrossGroupRibbonClientConfiguration
+│   └── NacosCrossGroupServerList
+└── nacos/
+    ├── NacosGroupService
+    ├── NacosGroupServiceIdCodec
+    ├── NacosCrossGroupInstanceSelector
+    └── NacosCrossGroupGatewayAutoConfiguration
+```
+
+## 常见问题
+
+**Q: 配置了 `yyc.nacos.cross-group.gulop-gig-biz` 仍拉不到实例?**  
+A: 确认已引入本模块且未排除 `NacosCrossGroupRibbonAutoConfiguration`;查看日志中 `Nacos cross-group server list` 的 `targetGroup` 是否为预期值。
+
+**Q: Gateway 需要单独配 `yyc.nacos.cross-group` 吗?**  
+A: 不需要。网关跨 Group 在路由 URI 中编码 `group--service`,由 `NacosCrossGroupInstanceSelector` 解析后查询;与 Feign 的 YAML 键是两套机制。
+
+**Q: 与 `yyc-common-gateway` 的关系?**  
+A: `yyc-common-gateway` 提供 `GrayLoadBalancer`、`VersionGrayLoadBalancer` 等 Gateway 灰度接口;本模块提供 Nacos 跨 Group 底层能力与 Ribbon 侧完整链路。网关业务模块通常同时依赖两者。
+
+## 构建
+
+```bash
+cd yyc-common-parent
+mvn -pl yyc-common-gray -am install
+```
+
+发布新版本后,通过 `yyc-common-bom` 统一升级各服务的 `yyc-common-gray` 坐标。

+ 42 - 0
yyc-common-gray/src/main/java/net/yyc/common/gray/nacos/NacosCrossGroupGatewayAutoConfiguration.java

@@ -0,0 +1,42 @@
+/*
+ *    Copyright (c) 2018-2025, hnqz All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * Redistributions of source code must retain the above copyright notice,
+ * this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ * Neither the name of the pig4cloud.com developer nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ * Author: hnqz
+ */
+
+package net.yyc.common.gray.nacos;
+
+import com.alibaba.cloud.nacos.NacosDiscoveryProperties;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+/**
+ * 网关场景注册 {@link NacosCrossGroupInstanceSelector},供跨 Group 负载均衡使用。
+ */
+@Configuration(proxyBeanMethods = false)
+@ConditionalOnClass(name = "org.springframework.cloud.gateway.filter.GlobalFilter")
+@ConditionalOnBean(NacosDiscoveryProperties.class)
+public class NacosCrossGroupGatewayAutoConfiguration {
+
+	@Bean
+	@ConditionalOnMissingBean
+	public NacosCrossGroupInstanceSelector nacosCrossGroupInstanceSelector(
+			NacosDiscoveryProperties nacosDiscoveryProperties) {
+		return new NacosCrossGroupInstanceSelector(nacosDiscoveryProperties);
+	}
+
+}

+ 79 - 0
yyc-common-gray/src/main/java/net/yyc/common/gray/nacos/NacosCrossGroupInstanceSelector.java

@@ -0,0 +1,79 @@
+/*
+ *    Copyright (c) 2018-2025, hnqz All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * Redistributions of source code must retain the above copyright notice,
+ * this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ * Neither the name of the pig4cloud.com developer nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ * Author: hnqz
+ */
+
+package net.yyc.common.gray.nacos;
+
+import com.alibaba.cloud.nacos.NacosDiscoveryProperties;
+import com.alibaba.cloud.nacos.discovery.NacosServiceDiscovery;
+import com.alibaba.cloud.nacos.ribbon.ExtendBalancer;
+import com.alibaba.nacos.api.naming.pojo.Instance;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import net.yyc.common.core.constant.CommonConstants;
+import org.springframework.cloud.client.ServiceInstance;
+import org.springframework.util.CollectionUtils;
+import org.springframework.util.StringUtils;
+
+import java.util.List;
+
+/**
+ * 跨 Nacos Group 实例选择:按目标 group + serviceName 查询,绕过仅使用消费者自身
+ * {@code discovery.group} 的实例发现限制。
+ */
+@Slf4j
+@RequiredArgsConstructor
+public class NacosCrossGroupInstanceSelector {
+
+	private final NacosDiscoveryProperties nacosDiscoveryProperties;
+
+	/**
+	 * @param version 请求头 {@link CommonConstants#VERSION},无则按权重随机
+	 */
+	public ServiceInstance choose(NacosGroupService groupService, String version) {
+		try {
+			List<Instance> instances = nacosDiscoveryProperties.namingServiceInstance()
+				.selectInstances(groupService.getServiceName(), groupService.getGroup(), true);
+			if (CollectionUtils.isEmpty(instances)) {
+				log.warn("[跨Group路由] Nacos 无可用实例, group={}, service={}", groupService.getGroup(),
+						groupService.getServiceName());
+				return null;
+			}
+			Instance instance = selectInstance(instances, version);
+			log.debug("[跨Group路由] 选中实例 group={}, service={}, target={}:{}", groupService.getGroup(),
+					groupService.getServiceName(), instance.getIp(), instance.getPort());
+			return NacosServiceDiscovery.hostToServiceInstance(instance, groupService.getServiceName());
+		}
+		catch (Exception e) {
+			log.error("[跨Group路由] Nacos 查询失败, group={}, service={}", groupService.getGroup(),
+					groupService.getServiceName(), e);
+			return null;
+		}
+	}
+
+	private Instance selectInstance(List<Instance> instances, String version) {
+		if (StringUtils.hasText(version)) {
+			for (Instance instance : instances) {
+				String targetVersion = instance.getMetadata().get(CommonConstants.VERSION);
+				if (version.equalsIgnoreCase(targetVersion)) {
+					return instance;
+				}
+			}
+		}
+		return ExtendBalancer.getHostByRandomWeight2(instances);
+	}
+
+}

+ 34 - 0
yyc-common-gray/src/main/java/net/yyc/common/gray/nacos/NacosGroupService.java

@@ -0,0 +1,34 @@
+/*
+ *    Copyright (c) 2018-2025, hnqz All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * Redistributions of source code must retain the above copyright notice,
+ * this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ * Neither the name of the pig4cloud.com developer nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ * Author: hnqz
+ */
+
+package net.yyc.common.gray.nacos;
+
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+
+/**
+ * Nacos 跨 Group 路由的 group 与 serviceName。
+ */
+@Getter
+@RequiredArgsConstructor
+public class NacosGroupService {
+
+	private final String group;
+
+	private final String serviceName;
+
+}

+ 111 - 0
yyc-common-gray/src/main/java/net/yyc/common/gray/nacos/NacosGroupServiceIdCodec.java

@@ -0,0 +1,111 @@
+/*
+ *    Copyright (c) 2018-2025, hnqz All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * Redistributions of source code must retain the above copyright notice,
+ * this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ * Neither the name of the pig4cloud.com developer nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ * Author: hnqz
+ */
+
+package net.yyc.common.gray.nacos;
+
+import java.util.Optional;
+
+import lombok.extern.slf4j.Slf4j;
+
+/**
+ * Nacos 跨 Group 服务名 {@code group@@service} 与 URI 合法 host 之间的编解码。
+ * <p>
+ * Java {@link java.net.URI} 的 host 不允许 {@code @}、{@code _} 等字符,网关路由 URI 使用
+ * {@code group--service}(group 中下划线转为连字符)存储,负载均衡前再还原为 Nacos serviceId。
+ * <p>
+ * 当路由通过 Spring 标准 {@code PropertiesRouteDefinitionLocator} 加载时,{@code @@} 会被
+ * Java URI 解析器 URL 编码为 {@code %40%40},本类同时处理这两种格式。
+ */
+@Slf4j
+public final class NacosGroupServiceIdCodec {
+
+	private static final String NACOS_GROUP_SERVICE_SEPARATOR = "@@";
+
+	private static final String URI_GROUP_SERVICE_SEPARATOR = "--";
+
+	private static final String URL_ENCODED_SEPARATOR = "%40%40";
+
+	private NacosGroupServiceIdCodec() {
+	}
+
+	/**
+	 * 将 Nacos 跨 group URI 编码为 URI 合法格式。
+	 * <ul>
+	 *   <li>{@code lb://cso_hnqz@@hnqz-auth} → {@code lb://cso-hnqz--hnqz-auth}</li>
+	 *   <li>{@code lb://cso_hnqz%40%40hnqz-auth} → {@code lb://cso-hnqz--hnqz-auth}</li>
+	 * </ul>
+	 */
+	public static String encodeLbUri(String uri) {
+		if (uri == null || !uri.startsWith("lb://")) {
+			return uri;
+		}
+		String hostPart = uri.substring("lb://".length());
+		if (hostPart.isEmpty()) {
+			return uri;
+		}
+
+		String working = hostPart;
+		if (working.contains(URL_ENCODED_SEPARATOR)) {
+			working = working.replace(URL_ENCODED_SEPARATOR, NACOS_GROUP_SERVICE_SEPARATOR);
+		}
+
+		if (!working.contains(NACOS_GROUP_SERVICE_SEPARATOR)) {
+			return uri;
+		}
+
+		int sep = working.indexOf(NACOS_GROUP_SERVICE_SEPARATOR);
+		String group = working.substring(0, sep).replace('_', '-');
+		String service = working.substring(sep + NACOS_GROUP_SERVICE_SEPARATOR.length());
+		String encoded = "lb://" + group + URI_GROUP_SERVICE_SEPARATOR + service;
+		log.debug("[跨Group编解码] URI 编码: {} → {}", uri, encoded);
+		return encoded;
+	}
+
+	/**
+	 * {@code cso-hnqz--hnqz-auth} → {@code cso_hnqz@@hnqz-auth}
+	 */
+	public static String decodeServiceId(String uriHost) {
+		return parseFromUriHost(uriHost).map(gs -> gs.getGroup() + NACOS_GROUP_SERVICE_SEPARATOR + gs.getServiceName())
+			.orElse(uriHost);
+	}
+
+	/**
+	 * 从 URI host({@code cso-hnqz--hnqz-auth})解析 group 与 serviceName。
+	 */
+	public static Optional<NacosGroupService> parseFromUriHost(String uriHost) {
+		if (uriHost == null || !uriHost.contains(URI_GROUP_SERVICE_SEPARATOR)) {
+			return Optional.empty();
+		}
+		int sep = uriHost.indexOf(URI_GROUP_SERVICE_SEPARATOR);
+		String group = uriHost.substring(0, sep).replace('-', '_');
+		String serviceName = uriHost.substring(sep + URI_GROUP_SERVICE_SEPARATOR.length());
+		return Optional.of(new NacosGroupService(group, serviceName));
+	}
+
+	/**
+	 * 从 Nacos serviceId({@code cso_hnqz@@hnqz-auth})解析 group 与 serviceName。
+	 */
+	public static Optional<NacosGroupService> parseFromNacosServiceId(String nacosServiceId) {
+		if (nacosServiceId == null || !nacosServiceId.contains(NACOS_GROUP_SERVICE_SEPARATOR)) {
+			return Optional.empty();
+		}
+		int sep = nacosServiceId.indexOf(NACOS_GROUP_SERVICE_SEPARATOR);
+		return Optional.of(new NacosGroupService(nacosServiceId.substring(0, sep),
+				nacosServiceId.substring(sep + NACOS_GROUP_SERVICE_SEPARATOR.length())));
+	}
+
+}

+ 72 - 0
yyc-common-gray/src/main/java/net/yyc/common/gray/ribbon/NacosCrossGroupEnvironmentPostProcessor.java

@@ -0,0 +1,72 @@
+/*
+ *    Copyright (c) 2018-2025, hnqz All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * Redistributions of source code must retain the above copyright notice,
+ * this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ * Neither the name of the pig4cloud.com developer nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ * Author: hnqz
+ */
+
+package net.yyc.common.gray.ribbon;
+
+import com.alibaba.cloud.nacos.ribbon.RibbonNacosAutoConfiguration;
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.env.EnvironmentPostProcessor;
+import org.springframework.core.Ordered;
+import org.springframework.core.env.ConfigurableEnvironment;
+import org.springframework.core.env.MapPropertySource;
+import org.springframework.util.StringUtils;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * 排除官方 {@link RibbonNacosAutoConfiguration},避免 Ribbon 子上下文重复注册
+ * {@code NacosRibbonClientConfiguration} 导致跨 Group ServerList 无法生效。
+ *
+ * @author hnqz
+ */
+public class NacosCrossGroupEnvironmentPostProcessor implements EnvironmentPostProcessor, Ordered {
+
+	private static final String EXCLUDE_KEY = "spring.autoconfigure.exclude";
+
+	private static final String EXCLUDE_TARGET = RibbonNacosAutoConfiguration.class.getName();
+
+	@Override
+	public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) {
+		String existing = environment.getProperty(EXCLUDE_KEY, "");
+		if (containsExclude(existing, EXCLUDE_TARGET)) {
+			return;
+		}
+		String merged = StringUtils.hasText(existing) ? existing + "," + EXCLUDE_TARGET : EXCLUDE_TARGET;
+		Map<String, Object> source = new HashMap<>(1);
+		source.put(EXCLUDE_KEY, merged);
+		environment.getPropertySources().addFirst(new MapPropertySource("nacosCrossGroupAutoConfigureExclude", source));
+	}
+
+	private boolean containsExclude(String existing, String target) {
+		if (!StringUtils.hasText(existing)) {
+			return false;
+		}
+		for (String item : existing.split(",")) {
+			if (target.equals(StringUtils.trimWhitespace(item))) {
+				return true;
+			}
+		}
+		return false;
+	}
+
+	@Override
+	public int getOrder() {
+		return Ordered.HIGHEST_PRECEDENCE;
+	}
+
+}

+ 34 - 0
yyc-common-gray/src/main/java/net/yyc/common/gray/ribbon/NacosCrossGroupRibbonAutoConfiguration.java

@@ -0,0 +1,34 @@
+/*
+ *    Copyright (c) 2018-2025, hnqz All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * Redistributions of source code must retain the above copyright notice,
+ * this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ * Neither the name of the pig4cloud.com developer nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ * Author: hnqz
+ */
+
+package net.yyc.common.gray.ribbon;
+
+import com.alibaba.cloud.nacos.ribbon.ConditionalOnRibbonNacos;
+import org.springframework.cloud.netflix.ribbon.RibbonClients;
+import org.springframework.context.annotation.Configuration;
+
+/**
+ * 注册跨 Group Ribbon 默认配置,替代 {@link com.alibaba.cloud.nacos.ribbon.RibbonNacosAutoConfiguration}。
+ *
+ * @author hnqz
+ */
+@Configuration(proxyBeanMethods = false)
+@ConditionalOnRibbonNacos
+@RibbonClients(defaultConfiguration = NacosCrossGroupRibbonClientConfiguration.class)
+public class NacosCrossGroupRibbonAutoConfiguration {
+
+}

+ 56 - 0
yyc-common-gray/src/main/java/net/yyc/common/gray/ribbon/NacosCrossGroupRibbonClientConfiguration.java

@@ -0,0 +1,56 @@
+/*
+ *    Copyright (c) 2018-2025, hnqz All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * Redistributions of source code must retain the above copyright notice,
+ * this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ * Neither the name of the pig4cloud.com developer nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ * Author: hnqz
+ */
+
+package net.yyc.common.gray.ribbon;
+
+import com.alibaba.cloud.nacos.NacosDiscoveryProperties;
+import com.alibaba.cloud.nacos.ribbon.ConditionalOnRibbonNacos;
+import com.alibaba.cloud.nacos.ribbon.NacosRibbonClientConfiguration;
+import com.netflix.client.config.IClientConfig;
+import com.netflix.loadbalancer.ServerList;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.cloud.netflix.ribbon.PropertiesFactory;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.core.env.Environment;
+
+/**
+ * 覆盖默认 {@link com.alibaba.cloud.nacos.ribbon.NacosServerList},支持跨 Group 发现。
+ * 仅通过 {@link NacosCrossGroupRibbonAutoConfiguration} 的 {@code @RibbonClients} 导入 Ribbon 子上下文。
+ *
+ * @author hnqz
+ */
+@Configuration(proxyBeanMethods = false)
+@ConditionalOnRibbonNacos
+public class NacosCrossGroupRibbonClientConfiguration extends NacosRibbonClientConfiguration {
+
+	@Autowired
+	private PropertiesFactory propertiesFactory;
+
+	@Bean
+	@SuppressWarnings("rawtypes")
+	public ServerList ribbonServerList(IClientConfig config, NacosDiscoveryProperties nacosDiscoveryProperties,
+			Environment environment) {
+		if (this.propertiesFactory.isSet(ServerList.class, config.getClientName())) {
+			return this.propertiesFactory.get(ServerList.class, config, config.getClientName());
+		}
+		NacosCrossGroupServerList serverList = new NacosCrossGroupServerList(nacosDiscoveryProperties, environment);
+		serverList.initWithNiwsConfig(config);
+		return serverList;
+	}
+
+}

+ 107 - 0
yyc-common-gray/src/main/java/net/yyc/common/gray/ribbon/NacosCrossGroupServerList.java

@@ -0,0 +1,107 @@
+/*
+ *    Copyright (c) 2018-2025, hnqz All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * Redistributions of source code must retain the above copyright notice,
+ * this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ * Neither the name of the pig4cloud.com developer nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ * Author: hnqz
+ */
+
+package net.yyc.common.gray.ribbon;
+
+import com.alibaba.cloud.nacos.NacosDiscoveryProperties;
+import com.alibaba.cloud.nacos.ribbon.NacosServer;
+import com.alibaba.cloud.nacos.ribbon.NacosServerList;
+import com.alibaba.nacos.api.naming.pojo.Instance;
+import com.alibaba.nacos.client.naming.utils.CollectionUtils;
+import cn.hutool.core.util.StrUtil;
+import com.netflix.client.config.IClientConfig;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.core.env.Environment;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * 支持按 {@code yyc.nacos.cross-group.{serviceId}} 跨 Nacos Group 拉取实例。
+ *
+ * @author hnqz
+ */
+@Slf4j
+public class NacosCrossGroupServerList extends NacosServerList {
+
+	private static final String CROSS_GROUP_PREFIX = "yyc.nacos.cross-group.";
+
+	private final NacosDiscoveryProperties discoveryProperties;
+
+	private final Environment environment;
+
+	private String serviceId;
+
+	public NacosCrossGroupServerList(NacosDiscoveryProperties discoveryProperties, Environment environment) {
+		super(discoveryProperties);
+		this.discoveryProperties = discoveryProperties;
+		this.environment = environment;
+	}
+
+	@Override
+	public void initWithNiwsConfig(IClientConfig iClientConfig) {
+		super.initWithNiwsConfig(iClientConfig);
+		this.serviceId = iClientConfig.getClientName();
+	}
+
+	@Override
+	public List<NacosServer> getInitialListOfServers() {
+		return resolveServers();
+	}
+
+	@Override
+	public List<NacosServer> getUpdatedListOfServers() {
+		return resolveServers();
+	}
+
+	private List<NacosServer> resolveServers() {
+		String resolvedServiceId = serviceId != null ? serviceId : getServiceId();
+		String targetGroup = resolveTargetGroup(resolvedServiceId);
+		try {
+			List<Instance> instances = discoveryProperties.namingServiceInstance()
+				.selectInstances(resolvedServiceId, targetGroup, true);
+			List<NacosServer> servers = instancesToServerList(instances);
+			log.debug("Nacos cross-group server list: serviceId={}, targetGroup={}, count={}", resolvedServiceId,
+					targetGroup, servers.size());
+			return servers;
+		}
+		catch (Exception e) {
+			throw new IllegalStateException("Can not get service instances from nacos, serviceId=" + resolvedServiceId
+					+ ", group=" + targetGroup, e);
+		}
+	}
+
+	private String resolveTargetGroup(String resolvedServiceId) {
+		String targetGroup = environment.getProperty(CROSS_GROUP_PREFIX + resolvedServiceId);
+		if (StrUtil.isNotBlank(targetGroup)) {
+			return targetGroup;
+		}
+		return discoveryProperties.getGroup();
+	}
+
+	private List<NacosServer> instancesToServerList(List<Instance> instances) {
+		List<NacosServer> result = new ArrayList<>();
+		if (CollectionUtils.isEmpty(instances)) {
+			return result;
+		}
+		for (Instance instance : instances) {
+			result.add(new NacosServer(instance));
+		}
+		return result;
+	}
+
+}

+ 5 - 1
yyc-common-gray/src/main/resources/META-INF/spring.factories

@@ -1,2 +1,6 @@
 org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
-  net.yyc.common.gray.GrayRibbonLoadBalancerConfiguration
+  net.yyc.common.gray.GrayRibbonLoadBalancerConfiguration,\
+  net.yyc.common.gray.ribbon.NacosCrossGroupRibbonAutoConfiguration,\
+  net.yyc.common.gray.nacos.NacosCrossGroupGatewayAutoConfiguration
+org.springframework.boot.env.EnvironmentPostProcessor=\
+  net.yyc.common.gray.ribbon.NacosCrossGroupEnvironmentPostProcessor