2026/10/19 4:09:06

Spring Cloud Gateway 微服务协作开发:如何精准路由到本地或指定 IP 实例

Spring Cloud Gateway 微服务协作开发:如何精准路由到本地或指定 IP 实例 有人可能会觉得Spring Cloud 微服务协作开发嘛只要把服务注册到注册中心网关按服务名转发就行了有什么好纠结的。但真的进入多人联调阶段你就会发现事情没那么简单。最典型的一幕是你在本地把用户服务调通了网关却把请求转发到了同事机器上正在启动的那个旧版本实例接口返回的数据跟你本地完全对不上排错排到怀疑人生。这篇内容就围绕一个很具体的场景展开Spring Cloud 微服务合作开发时如何让 Spring Cloud Gateway 这类网关只访问本地或指定 IP 下的微服务。我会讲清楚底层的路由逻辑、三种可落地的方案、完整的配置示例以及我在实际调试中踩过的坑。适合正在做微服务协作开发、团队内多人共用注册中心、又被网关路由搞到头大的 Java 后端工程师参考。1. 为什么协作开发中网关会“乱跑”问题场景与根因分析1.1 最典型的协作开发现场先说个我经常遇到的场景。某团队在做一套电商后端拆了用户服务、订单服务、商品服务、支付服务几个独立模块注册中心用的是 Nacos网关用的是 Spring Cloud Gateway。每个人在自己电脑上把服务跑起来注册到同一个测试环境的 Nacos 上。按道理服务名是唯一的网关通过lb://user-service就能负载均衡到所有的 user-service 实例上。但问题就出在这个“所有”上。团队里四五个人都在本地起了 user-service大家的代码版本还不一样A 同学刚改了登录逻辑B 同学还在调旧代码结果网关一转发随机命中一个实例有时候请求落在自己本地有时候落在别人机器上。这种“随机漂移”在协作开发里非常致命。联调时前端同学说调登录接口报错了你查了半天发现请求根本没到你本地是跑到同事那个没改完的实例上去了。更麻烦的是有些服务本地跑不起来依赖的数据库、中间件只有某一台开发机上有你必须让网关能精确地把请求转发到那一台机器。1.2 根因服务发现与目标路由的默认逻辑要解决这个问题先得弄清楚 Spring Cloud Gateway 默认是怎么工作的。当你在配置里写了spring: cloud: gateway: routes: - id: user-service uri: lb://user-service predicates: - Path/api/user/**这里的lb://user-service是个关键。网关会拿到这个服务名然后通过 DiscoveryClient 去注册中心查询所有名为 user-service 的实例再借助默认的负载均衡器从实例列表里选一个目标地址进行转发。问题就出在这个“从实例列表里选一个”。默认的负载均衡策略是轮询或随机它根本不知道你现在只想调到本地或者只想调到某一台机器。注册中心把所有人的本地实例都视为等价的可用节点网关自然就会“乱跑”。而且还有一个很容易被忽略的细节本地启动的服务注册到 Nacos 时默认使用的是本机内网 IP。所以当你看到 Nacos 控制台里 user-service 有七八个实例IP 各不相同那基本就是团队成员各自的电脑。网关转发时完全可能从这七八个 IP 里随机挑一个。1.3 解决思路的取舍核心矛盾一句话就能说清注册中心负责“找到所有实例”但协作开发时我们常常只需要“找其中一个实例”。所以思路无非两条路一是绕过注册中心用静态路由直接指定目标地址比如http://192.168.1.101:8080或http://localhost:8080。这种做法简单粗暴但每次换人联调都要改配置。二是在动态发现的基础上做筛选通过实例元数据、IP 过滤或自定义负载均衡策略让网关只选择指定 IP 的实例。这种做法灵活但实现成本更高。两条路没有绝对的好坏取决于你团队的使用场景。下面我分别把三种常用方案讲透你对照自己的情况选就行。2. 方案一写死本地与指定 IP 的静态路由配置2.1 通过 URI 直接指定目标服务地址最直接的做法就是把网关路由的uri从lb://user-service改成具体的地址spring: cloud: gateway: routes: - id: user-service-local uri: http://192.168.1.101:8080 predicates: - Path/api/user/**这样配置后所有/api/user/**的请求都会被打到192.168.1.101:8080完全不走注册中心也不存在随机选择的问题。想访问本地服务就改成http://localhost:8080或者http://127.0.0.1:8080。这个方案优点显而易见配置简单逻辑清晰任何人一看就明白当前网关指向哪里。缺点也明显换目标机器时得手动改配置并重启网关而且一旦服务注册中心里没有这个实例路由照样转发不会帮你做任何健康检查。2.2 多环境配置管理Profile 切换既然静态路由要经常改地址那就可以用 Spring Boot 的 Profile 机制把不同目标环境的配置拆开。我习惯的做法是在网关项目里建三个配置文件application.yml公共配置只放注册中心地址、端口等。application-local.yml本地联调配置路由全部指向localhost。application-dev.yml开发环境配置路由指向团队某台固定开发机的 IP。application-local.yml大致长这样spring: cloud: gateway: routes: - id: user-service uri: http://localhost:8081 predicates: - Path/api/user/** - id: order-service uri: http://localhost:8082 predicates: - Path/api/order/**启动时只要加一个参数java -jar gateway.jar --spring.profiles.activelocal切换到另一台机器联调时改一下application-dev.yml里的 IP重启网关即可。如果想不用重启就切换还可以把这些路由配置放到 Nacos 配置中心里修改后自动刷新。不过网关的路由刷新需要触发RefreshRoutesEvent在配置中心里改完要等待自动刷新实际效果还可以。2.3 为什么静态路由在协作模式可行有人可能会担心静态绕过了注册中心是不是把微服务“动态发现”的优点丢了确实丢了但在协作开发这个特定场景里这种“丢”反而是好事。协作开发的核心诉求是“确定性”。前端调你的接口你希望请求一定落在你本地这样才能配合抓包、看日志、断点调试。如果网关还在做负载均衡你根本无法确定请求是否真的到了你机器上。静态路由把目标地址写死从源头消除了不确定性联调效率反而更高。而且协作开发的量级很小不存在高并发负载均衡的需求。你只是想让网关把请求导到指定的那一台机器根本不需要考虑多实例分发。静态路由是“杀鸡用牛刀”里那把最顺手的刀。3. 方案二基于注册中心实例的下沉与过滤3.1 理解 Gateway 的 LB 负载均衡原理如果团队不希望完全绕开注册中心或者有些服务你本地并没有启动、必须通过注册中心来找实例那就得在动态发现的基础上做实例筛选。先理解一下lb://的完整处理链路。Spring Cloud Gateway 收到请求后RouteToRequestUrlFilter会把lb://user-service转换成ReactiveLoadBalancer的调用默认用的是ReactorLoadBalancer。它会调用DiscoveryClient拿到服务实例列表然后通过负载均衡策略选出一个实例最终把请求转发到实例的 IP 和端口上。所以想“只访问指定 IP 下的微服务”就是要干预这个选实例的过程。常见的干预手段有两种过滤实例列表或者自定义负载均衡策略。这一节先讲过滤。3.2 按 IP 过滤路由全局过滤器实现你可以在网关里写一个全局过滤器在请求转发之前把服务实例列表过滤到只剩指定 IP 的实例。思路是这样的用一个自定义的 Header 或者通过配置指定目标 IP在过滤器里修改 ServerWebExchange 的属性让后面的负载均衡器只从过滤后的列表里选。不过这种做法的侵入性比较强而且 Spring Cloud Gateway 的过滤器和负载均衡器的交互链路比较绕。更简洁的做法是直接自定义一个ServiceInstanceListSupplier它负责提供候选实例列表你可以在里面做 IP 过滤。3.3 借助元数据标识特定开发机实例更工程化的方案是在服务注册时给实例打上元数据标签然后网关根据标签过滤实例。比如在某个服务的application.yml里这样注册spring: cloud: nacos: discovery: metadata: developer: zhangsan env: local然后你在网关里自定义ServiceInstanceListSupplier只保留metadata.developer zhangsan的实例。这样即使所有人都在注册中心里网关也只挑指定开发者的实例。Nacos 的 Java 客户端提供了NamingService.getAllInstances方法你可以直接在自定义 Supplier 里调用并按元数据做筛选。大致逻辑如下public class MetadataFilterInstanceListSupplier implements ServiceInstanceListSupplier { private final ServiceInstanceListSupplier delegate; private final String developer; Override public FluxListServiceInstance get() { return delegate.get().map(instances - instances.stream() .filter(i - developer.equals(i.getMetadata().get(developer))) .collect(Collectors.toList()) ); } }然后在配置里声明spring: cloud: loadbalancer: configurations: metadata-filter这种方式的好处是服务注册信息里天然带了“这是谁的实例”这个维度网关按维度筛选非常贴合协作开发的场景。比写死 IP 更优雅因为开发者的机器 IP 可能会变但开发者标识基本不会变。4. 方案三自定义负载均衡策略锁定目标实例4.1 重写 ReactorLoadBalancer 的要点第三种方案更加“硬核”直接重写负载均衡策略让它在选实例的时候只认你指定的 IP。Spring Cloud 2020 版本之后的 API 变化比较大核心接口是ReactorServiceInstanceLoadBalancer我们需要实现它的choose方法返回MonoResponseServiceInstance。一个简化的自定义实现思路是内部持有目标 IP 集合每次choose时从DiscoveryClient拿到的实例列表里过滤掉不在目标 IP 集合中的实例然后再从剩余实例中用轮询或随机策略选一个。这里有个关键点你拿到的是ServiceInstance列表每个实例有getHost()和getPort()。在 Nacos 的场景下getHost()通常就是注册时上报的内网 IP。你可以把这个 IP 和目标 IP 对比public class FixedIpLoadBalancer implements ReactorServiceInstanceLoadBalancer { private final String serviceId; private final DiscoveryClient discoveryClient; private final String targetIp; Override public MonoResponseServiceInstance choose(Request request) { ListServiceInstance instances discoveryClient.getInstances(serviceId); ListServiceInstance candidates instances.stream() .filter(instance - targetIp.equals(instance.getHost())) .collect(Collectors.toList()); if (candidates.isEmpty()) { return Mono.just(new EmptyResponse()); } return Mono.just(new DefaultResponse(candidates.get(0))); } }然后在配置类里把ReactorServiceInstanceLoadBalancer绑定到指定服务上。这种方案适合那种“我就是要把 user-service 打到 192.168.1.101其他服务走默认负载均衡”的复合场景。4.2 结合请求头路由到指定开发者环境自定义负载均衡还有一种进阶玩法根据请求头动态决定目标实例。比如前端联调时在请求头里带一个X-Developer: zhangsan网关读到这个 Header就筛选出zhangsan的实例没带这个 Header 就走默认负载均衡。这种玩法在团队协作里非常好用。每个人本地都起服务注册到同一个 Nacos前端联调时只要在调试工具或代码里加一个 Header请求就到指定的人的机器上。不需要网关重启不需要改配置非常灵活。实现也不复杂在自定义choose方法里从Request里拿exchange再从ServerHttpRequest里取 Header 值然后过滤实例列表即可。不过要留意Request在负载均衡接口里是一个泛型你需要强转成RequestServerWebExchange才能拿到请求上下文。4.3 三种方案对比与选型建议我做了个对比表方便你快速决策方案实现成本灵活性适用场景静态路由写死 IP低低改目标要改配置单点联调、本地调试、环境固定实例元数据过滤中中按标签筛选多人共用注册中心、需要区分开发者自定义负载均衡 Header高高动态切换多人频繁交叉联调、需要按请求路由我的建议是如果只是自己开发调试用方案一就足够了。如果是团队协作、多人共用注册中心优先上方案三里的 Header 路由体验最好。方案二适合不想改网关代码的团队但需要业务服务的注册端配合加元数据。5. 实操演示从零配置一个“指定 IP”网关5.1 项目基础依赖我们现在动手搭一个最小可运行的网关实现“只访问指定 IP 下的微服务”。为了方便演示我假设网关端口是8080目标服务是一个用户服务运行在192.168.1.101:8081服务名是user-service。在网关项目的pom.xml里最核心的依赖是这几个dependency groupIdorg.springframework.cloud/groupId artifactIdspring-cloud-starter-gateway/artifactId /dependency dependency groupIdcom.alibaba.cloud/groupId artifactIdspring-cloud-starter-alibaba-nacos-discovery/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-webflux/artifactId /dependency注意Spring Cloud Gateway 基于 WebFlux不要引入spring-boot-starter-web否则启动时会因为 MVC 和 WebFlux 冲突直接报错。这是我见过最多人犯的错。5.2 核心配置与代码我们把 Gateway 的路由配置拆成两个部分基础配置和路由规则。基础配置里注册到 Nacos路由规则里通过lb://方式指向服务。先看application.ymlserver: port: 8080 spring: application: name: gateway-server cloud: nacos: discovery: server-addr: 127.0.0.1:8848 gateway: routes: - id: user-service uri: lb://user-service predicates: - Path/api/user/**然后定义负载均衡配置类固定路由到192.168.1.101Configuration public class GatewayLoadBalancerConfig { Bean LoadBalancerClient(name user-service, configuration UserServiceLoadBalancerConfig.class) public ReactorLoadBalancerServiceInstance userServiceLoadBalancer( ObjectProviderServiceInstanceListSupplier serviceInstanceListSupplierProvider, Environment environment) { return new RandomLoadBalancer( serviceInstanceListSupplierProvider.getIfAvailable(), environment.getProperty(loadbalancer.client.name)); } }当然更直接的方式是在配置类里指定固定 IP完整实现如下public class FixedIpServiceInstanceListSupplier extends DelegatingServiceInstanceListSupplier { private final String fixedIp; public FixedIpServiceInstanceListSupplier(ServiceInstanceListSupplier delegate, String fixedIp) { super(delegate); this.fixedIp fixedIp; } Override public FluxListServiceInstance get() { return getDelegate().get().map(instances - instances.stream() .filter(instance - fixedIp.equals(instance.getHost())) .collect(Collectors.toList()) ); } }把这个 Supplier 通过配置类装配进去Configuration public class FixedIpLoadBalancerConfig { Bean public ServiceInstanceListSupplier serviceInstanceListSupplier(ConfigurableApplicationContext context) { return new FixedIpServiceInstanceListSupplier( new DiscoveryClientServiceInstanceListSupplier( user-service, new NacosDiscoveryClient(user-service)), 192.168.1.101 ); } }不过这里要提醒一下不同版本的 Spring Cloud 对DelegatingServiceInstanceListSupplier和DiscoveryClientServiceInstanceListSupplier的构造函数要求不同。你的 Spring Cloud 版本是 2020.0.x 的话构造函数一般需要传一个ConfigurableApplicationContext。如果编译报错先看依赖版本别急着改代码。5.3 启动与验证流程配置写完后启动顺序很重要。一定要先把 Nacos 起起来再启动网关和你本地的 user-service确保服务注册成功后再用网关测试。验证步骤我一般这样走在 Nacos 控制台里确认 user-service 的实例列表记下目标机器 IP 是否在列表里。启动网关看日志里有没有报“No instances available”如果有说明注册中心里没有可用实例先排查服务注册。用命令行或 Postman 请求网关接口curl http://localhost:8080/api/user/123在目标机器上看 user-service 的日志确认请求是否真的进来了。我实测过的一个典型现象是请求偶尔到目标机器偶尔到其他机器这说明你仍然在用默认的lb://负载均衡过滤代码没有生效。重点排查ServiceInstanceListSupplier有没有被正确装配以及 Nacos 返回的实例host是不是预期的 IP。6. 常见问题与排查技巧实录6.1 问题速查表现象可能原因排查方向网关报 503 Service Unavailable注册中心里没有可用的服务实例确认目标服务是否注册成功请求打到其他机器使用了默认负载均衡过滤未生效检查自定义 Supplier 是否装配启动报 MVC 和 WebFlux 冲突网关引入了 web 依赖移除 spring-boot-starter-web自定义 Supplier 构造报错Spring Cloud 版本不匹配核对类签名和构造方法目标机器收不到请求防火墙或服务绑定地址不对检查目标服务是否监听 0.0.0.0IP 过滤后实例为空Nacos 上报的是容器 IP 或虚拟 IP在 Nacos 控制台确认实际注册 IP6.2 排查方法遇到网关转发异常我一般按这个顺序排查第一步看网关日志。Spring Cloud Gateway 在 DEBUG 级别下会打印请求的路由匹配信息包括命中了哪个 Route、转发到了哪个 URI。这一步能快速判断是路由没匹配上还是转发阶段出了问题。第二步看 Nacos 控制台。打开服务列表找到 user-service点开详情看实例 IP 和端口是否符合预期。如果本机起的服务注册的 IP 是127.0.0.1那其他机器永远访问不了它这是注册配置里network-interface没配好导致的。第三步在目标机器上抓包或用日志确认。我经常在目标机器上直接看访问日志确认网关发来的请求头里有没有带上原始的 Host 信息。第四步确认端口绑定。目标服务如果在 Linux 上跑注意监听地址是不是0.0.0.0如果只监听了127.0.0.1其他机器的请求怎么都进不来。6.3 团队协作的避坑心得最后分享几个团队协作模式下的实操心得都是踩过坑之后总结出来的。第一不要用localhost写死在公共配置里。localhost在网关所在机器上指向的是网关自己的回环地址一旦网关部署在服务器上、目标服务在另一台机器上这个配置就是无效的。本地调试用localhost没毛病但要放在单独的 Profile 里不要让团队成员共用。第二注册中心里的实例最好带上开发者标识。我所在的团队在服务注册时统一加了一个developer元数据这看起来是小事但排查问题的时候帮了大忙。任何人看到 Nacos 里的实例列表一眼就知道这个实例是谁起的、能不能碰。第三固定 IP 路由的方案要留个口子。我见过有同学把 IP 写成很深的硬编码后来换机器找了一下午。建议把目标 IP 放到配置中心或环境变量里比如target: user-service-ip: ${USER_SERVICE_IP:192.168.1.101}这样换人联调的时候只改环境变量就行不用动代码。第四网关的并发问题在协作开发中不会成为瓶颈但要注意网关的 Filter 不要写得太重。有些团队为了做“指定 IP”路由在过滤器里做了大量同步 IO 操作导致网关吞吐量明显下降。记住网关是响应式模型过滤器和负载均衡里的逻辑尽量用异步方式避免在事件循环线程里做阻塞调用。我个人在实际操作中体会最深的一点是网关路由配置越“显式”协作开发的痛苦越少。宁可牺牲一点所谓的“自动化”也要让每个请求的去向足够明确。不然前端问一句“请求到你那了吗”你都得登录注册中心查半天联调效率根本提不起来。如果你现在正被网关随机转发搞得焦头烂额先别急着上复杂方案从静态配置或 Header 路由开始大概率能立刻缓解大部分痛苦。