做微服务,绕不开网关这一层。Spring Cloud Gateway 的路由规则,简单说是整个微服务入口处的一张“流量分发表”:外部请求进来,网关根据路由规则判断该把请求转发到哪个后端服务,同时还能做鉴权、限流、改写路径这些事。我最早系统整理 Gateway 路由规则,是在把旧 Zuul 网关迁到 Spring Cloud Gateway 的时候,当时最头疼的不是服务怎么注册,而是路由怎么写才能不出各种幺蛾子。这篇围绕“路由规则”本身,把核心概念、配置细节、动态路由、常见坑一次说清楚。适合刚上手 Spring Cloud Gateway 的开发者,也适合已经用了一阵子、但经常被 502、404 这类问题折腾的同学对照着排查。
1. 路由规则的核心概念与整体设计
1.1 路由、断言、过滤器三件事
Spring Cloud Gateway 的路由规则,核心是三个概念:Route(路由)、Predicate(断言)、Filter(过滤器)。一条 Route 就是一条转发规则,由 id、uri、一组 predicate 和一组 filter 组成。你可以把它想象成快递柜:id 是柜口编号,uri 是目的地,predicate 是“取件码是否正确”的校验,filter 是取件过程中改包裹、贴标签的动作。请求进入网关后,会按顺序拿每条 Route 的 predicate 去匹配当前请求,命中了就进入这条 Route,执行它挂载的 filters,最后把请求转发到 uri 指向的后端地址。
这里有个容易忽略的点:predicate 和 filter 的定义顺序、命名方式,看起来只是配置,实际会直接影响 Spring Cloud Gateway 的解析逻辑。比如 predicate 写成 Path=/api/** 和 Path=/api/*,匹配范围完全不同;filter 配置成 StripPrefix=2 会删除前两段路径,配置成 1 又是一种效果。所以理解了这三件套,后续的路由规则才不是“背配置”。
1.2 为什么说路由规则是网关的“分诊台”
在微服务架构里,网关是唯一的流量入口,路由规则决定了四件事:谁能进(断言匹配)、进到哪个服务(uri)、进入前做什么(过滤器),以及失败时怎么办(超时、重试、降级)。路由规则写不好,轻则请求被转发错服务,重则直接把后端服务打挂。
举个例子,一个对外接口 /api/order/** 本应转发到 order-service,如果把 predicate 误写成 /api/**,那么所有 /api/user、/api/product 的请求也都会被送到订单服务,结果就是后端收到一堆不属于自己的请求,引发 404 甚至数据错乱。更隐蔽的是路由顺序问题,Spring Cloud Gateway 默认按路由注册顺序匹配,先到先得。如果有一条比较“贪心”的规则写在前面,后面的精确规则就永远没有机会生效。这些都属于路由规则设计层面需要提前考虑的问题。
1.3 路由配置方式怎么选
Spring Cloud Gateway 支持两种主流的配置方式:基于 YAML 的声明式配置,和基于 Java DSL 的编码式配置。YAML 适合静态、全局统一管理的场景,改起来直观,适合运维同学维护;Java DSL 适合路由规则比较复杂、需要动态构建逻辑的场景,比如根据配置中心数据生成路由,或者启动时从数据库加载路由。
现实项目里往往不会只用一种方式。我的建议是:简单项目完全用 YAML,复杂项目用 Java DSL,但要保证同一类路由放在同一个地方管理。如果团队规模大了,最好把路由规则抽到一个独立的配置中心,不要让每个人都在自己的服务里写路由,否则改一个规则要发布一次网关,代价很高。
2. 路由规则配置的实操细节
2.1 基于 YAML 的静态路由配置
最常用的路由规则配置还是 YAML。下面这个例子就是一条非常典型的路由:
spring: cloud: gateway: routes: - id: user-route uri: lb://user-service predicates: - Path=/api/user/** filters: - StripPrefix=1这里有几个字段需要解释清楚。id 是路由的唯一标识,不要求全局唯一,但建议语义清晰;uri 是转发目标地址,可以直接写 http://192.168.1.10:8080 这种固定地址,也可以写 lb://user-service 这种通过注册中心解析的服务名;predicates 是一组断言配置,下面可以用多个断言,比如 Path 加 Method 同时生效;filters 是网关过滤器快捷配置,StripPrefix=1 表示转发前把匹配路径的前一段去掉。
很多初学者以为 Path=/api/user/** 匹配到之后,后端收到的还是 /api/user/xxx,但实际上默认转发时会保留完整路径。如果后端接口是 /user/xxx,就必须通过 StripPrefix=1 把 /api 这一段剥掉。这个细节是路由配置里最容易出错的地方之一。
2.2 基于 Java DSL 的编码式路由配置
除了 YAML,Spring Cloud Gateway 还支持通过 RouteLocatorBuilder 在代码里定义路由。比如:
@Bean public RouteLocator customRouteLocator(RouteLocatorBuilder builder) { return builder.routes() .route("user-route", r -> r.path("/api/user/**") .filters(f -> f.stripPrefix(1)) .uri("lb://user-service")) .route("write-route", r -> r.path("/api/write/**") .and() .method(HttpMethod.POST) .filters(f -> f.rewritePath("/api/write/(?<segment>.*)", "/${segment}")) .uri("http://192.168.1.20:8080")) .build(); }Java DSL 的好处是可以写逻辑。比如某条路由只在新增数据时生效,就可以用 method(HttpMethod.POST) 把请求方法也作为匹配条件;想改路径,可以用 rewritePath 配合正则,把 /api/write/123 改写成 /123。这种方式比 YAML 更灵活,但可读性不如 YAML,适合封装成内部框架时使用。
两种方式可以混用,Spring Cloud Gateway 不会强制你二选一。我自己的经验是:基础路由用 YAML,需要做条件分支的复杂路由用 Java DSL,能少写很多靠字符串拼接的配置。
2.3 内置断言工厂盘点
Spring Cloud Gateway 内置了十几种断言工厂,也就是前面说的 predicate 的具体实现。我挑几个最常用的列出来:
| 断言工厂 | 作用 | 配置示例 |
|---|---|---|
| Path | 按请求路径匹配 | Path=/api/,/admin/ |
| Method | 按 HTTP 方法匹配 | Method=GET,POST |
| Header | 按请求头匹配 | Header=X-Request-Id, \d+ |
| Query | 按查询参数匹配 | Query=pageSize, \d{1,3} |
| Host | 按 Host 头匹配 | Host=**.example.com |
| Cookie | 按 Cookie 匹配 | Cookie=token,\w+ |
| RemoteAddr | 按来源 IP 匹配 | RemoteAddr=192.168.1.0/24 |
| Weight | 按权重分配流量 | Weight=service1, 80 |
路径匹配是最常用的,这里要特别注意 Path 里的通配符。/api/**可以匹配 /api/user、/api/user/1,但/api/*只匹配 /api/user,不匹配 /api/user/1。**是跨路径的,*不跨路径。Header、Query、Cookie 这类断言的 value 部分是支持正则表达式的,所以写的时候要考虑转义问题,比如\d+在 YAML 里要写成字符串,不能写成纯数字。
2.4 自定义断言与过滤器的扩展点
内置断言不够用的时候,Spring Cloud Gateway 允许自定义断言工厂。命名上有个硬性规则:类名必须以 RoutePredicateFactory 结尾,比如 TokenRoutePredicateFactory,这样在 YAML 里配置时对应 key 就是 Token。实现方式一般继承 AbstractRoutePredicateFactory,重写谓词匹配逻辑。
@Component public class TokenRoutePredicateFactory extends AbstractRoutePredicateFactory<TokenRoutePredicateFactory.Config> { public TokenRoutePredicateFactory() { super(Config.class); } @Override public Predicate<ServerWebExchange> apply(Config config) { return exchange -> { String token = exchange.getRequest().getHeaders().getFirst("X-Token"); return config.getExpected().equals(token); }; } @Data public static class Config { private String expected; } }配好之后,路由里就能直接写:
predicates: - Token=abc123过滤器同样可以自定义,继承 AbstractGatewayFilterFactory 即可。实际项目中我常用自定义过滤器做登录校验、灰度标记、请求签名验签。这些业务规则写在网关里,前端无需感知,后端的重复代码也可以大幅减少。
3. 路由转发、负载均衡与动态路由的落地
3.1 通过服务发现实现负载均衡路由
如果路由规则里把 uri 写成 lb://user-service,就相当于告诉网关:目标地址不是固定 IP,而是注册中心里的服务名,请通过负载均衡选择一个实例。Spring Cloud Gateway 收到这种 uri 后,会由内部负载均衡过滤器解析服务名,拿到实例列表后再做轮询或随机选择。
这里有个关键依赖:必须先引入 spring-cloud-starter-loadbalancer,否则 lb:// 解析不了。配合 Nacos 注册中心时,服务注册到 Nacos 后,网关才能发现实例。
spring: cloud: nacos: discovery: server-addr: 127.0.0.1:8848 gateway: routes: - id: user-route uri: lb://user-service predicates: - Path=/api/user/**这种方式的优势是自动感知后端实例上下线。后端一个实例挂了,注册中心摘除后,网关不会再往这个实例转发,比固定 URL 方式省心得多。
3.2 固定 URL 地址转发怎么配
也有些系统还没接入注册中心,或者要对接第三方固定地址,此时路由规则里的 uri 直接写目标地址就行:
routes: - id: legacy-route uri: http://192.168.10.15:8081 predicates: - Path=/legacy/api/** filters: - RewritePath=/legacy/api/(?<segment>.*), /${segment}这样 /legacy/api/user/123 会被转发到 http://192.168.10.15:8081/user/123。注意,如果没有 RewritePath,后端会收到 /legacy/api/user/123,这可能不是你要的效果。固定 URL 转发适合与老系统对接,配合 service mesh 的阶段也多见,简单直接,但没有负载均衡能力,需要自己在 DNS 或负载设备层面处理高可用。
3.3 动态路由:从配置中心刷新路由
生产环境的路由规则不可能一成不变。上线的服务多了之后,改一次路由就重启一次网关是不可接受的。Spring Cloud Gateway 提供了 RouteDefinitionRepository 接口,路由数据可以放到数据库、Redis、Nacos 配置中心等地方,通过事件驱动的方式动态刷新。
最常见的做法是监听配置中心变化,重新加载路由定义后发布 RefreshRoutesEvent 事件。核心代码可以抽象成这样:
@Service public class NacosRouteRefresher { @Autowired private RouteDefinitionWriter routeDefinitionWriter; @Autowired private ApplicationEventPublisher publisher; public void updateRoutes(List<RouteDefinition> definitions) { // 先清空旧路由,再写入新路由 routeDefinitionWriter.delete(Mono.just("user-route")).subscribe(); definitions.forEach(def -> routeDefinitionWriter.save(Mono.just(def)).subscribe()); // 刷新路由表 publisher.publishEvent(new RefreshRoutesEvent(this)); } }这里有几个坑。routeDefinitionWriter 的 delete 和 save 都是响应式操作,如果不同步处理,可能会在路由刷新过程中出现旧路由还没删除、新路由已经写入的中间状态。所以生产代码里要加同步逻辑,或者先清空再批量保存。另外,RefreshRoutesEvent 事件发布后,路由不会立即百分百刷新完毕,上游调用方可能短暂拿到旧路由,配合网关缓存配置一起考虑才能做到无缝切换。
3.4 多语言服务如何被纳入网关统一路由
Spring Cloud Alibaba 体系最常见的疑问之一,就是“非 Java 服务怎么进网关”。这里要说清楚:Spring Cloud Gateway 本身不关心后端服务用什么语言实现,只要这个服务能注册到 Nacos,并且暴露健康检查,网关就能用 lb:// 方式路由过去。
比如一个 Python 应用,用 Flask 提供 HTTP 接口,再通过 nacos-sdk-python 注册服务:
from flask import Flask import nacos app = Flask(__name__) @app.route("/health") def health(): return "ok" @app.route("/hello") def hello(): return "hello from python" if __name__ == "__main__": client = nacos.NacosClient("127.0.0.1:8848", namespace="public") client.add_naming_instance("python-service", "127.0.0.1", 5000) app.run(host="0.0.0.0", port=5000)然后网关里配置:
spring: cloud: gateway: routes: - id: python-route uri: lb://python-service predicates: - Path=/api/python/** filters: - StripPrefix=1这样 /api/python/hello 就会被路由到 Python 服务的 /hello 接口。多语言服务接入网关的关键不是语言,而是服务注册的元数据是否正确、健康检查路径是否对得上。这也是 Gateway 在多语言微服务体系里比较重要的价值:统一入口,大家互不关心实现细节。
3.5 Spring AI Agent 开发中网关路由能分担什么
最近不少团队在尝试 spring cloud 结合 spring ai 开发 Agent 应用,网关在其中的角色常被忽略。Agent 服务往往要对接多个模型供应商,不同模型、不同业务线的流量需要走不同通道。通过网关路由规则,可以把请求按路径或请求头分流。
比如 /agent/openai/** 转发到专门封装 OpenAI 接口的服务,/agent/ollama/** 转发到本地模型服务,甚至同一个 Agent 服务可以通过 Header 里的租户标识做路由,实现多租户隔离。网关的限流过滤器在这种场景下也很有用,模型接口的费用通常不低,按路由维度做限流比在后端服务里各自做更容易统一治理。
4. 路由规则实战中常见的坑与排查实录
4.1 502 Bad Gateway:转发成功了但后端没接住
网关相关报错中出现频率最高的,就是 502 Bad Gateway。这个错误从网关角度来看,意思是网关已经尝试把请求转发给后端,但后端没能在预期时间内完成响应,或者连接建立过程就失败了。常见原因有这么几类:后端服务没有启动;端口配置错误;注册中心里服务实例为空;后端接收请求后崩溃或主动断开连接;网关响应超时时间设置过短。
遇到 502,第一步不要盯着网关配置,而是先直接用 curl 请求后端地址,确认后端本身能通。后端如果没问题,再检查网关路由里的 uri 是否写错,或者 lb:// 服务名是否真的在注册中心存在。排查命令一般是:
curl http://127.0.0.1:8081/health如果这一步失败,说明问题出在后端服务或网络连通性;如果成功,接着看网关日志里有没有 ConnectException、Connection refused、Read timed out 这类关键词。日志会直接告诉你是连不上、被拒绝,还是读超时。
还有一个容易忽略的场景:后端接口本身返回了错误状态码,比如上游业务网关返回了 JSON 格式的 502,这时候 Gateway 会把上游的 502 原样带回来。看起来网关报了 502,实际根因在下游服务内部逻辑,必须结合链路追踪把调用链拉出来看。
4.2 路由不生效:404、断言和 StripPrefix 的配合
路由不生效的典型表现是请求打回到网关直接 404。404 意味着没有任何一条路由匹配上这个请求,或者匹配到了,但转发过去后后端也不认识这个路径。前者查断言,后者查路径改写。
先说断言。我见过不少案例,Path 写成 /api/**,但实际请求是 /api/v1/user/xxx,看起来能匹配,结果因为请求里带了额外的环境前缀,导致匹配失败。这时候可以通过 /actuator/gateway/routes 查看当前路由表,辅助定位。
再看路径改写。假设路由是:
- id: demo-route uri: http://127.0.0.1:8081 predicates: - Path=/demo/**请求 /demo/user/list 转发到后端时,默认路径还是 /demo/user/list。如果后端接口只暴露 /user/list,就必须加 StripPrefix=1。如果后端接口依旧是 /demo/user/list,加了 StripPrefix=1 反而会把路径改错,引起 404。所以配置 StripPrefix 之前,先想清楚后端到底期望收到什么路径。
4.3 断言匹配的性能与正则陷阱
路由规则里如果写了太多复杂正则,性能影响在低流量下不明显,高并发时却可能成为瓶颈。Spring Cloud Gateway 的谓词匹配是基于 ServerWebExchange 的,Path 匹配走的是 PathPatternParser,效率较高,但 Header、Query、Cookie 的值匹配会走正则引擎。正则写得不好,比如存在大量回溯,请求量上来之后网关线程会卡住。
所以路由规则里能用 Path 解决的事情不要绕到 Header 正则里去判断。正则如果需要匹配.等特殊字符,记得转义。YAML 里写正则还容易踩另一个坑:反斜杠需要写成双反斜杠,比如\d+在 YAML 里经常要写成\\d+,否则解析后可能丢失反斜杠,导致规则静默失效。
4.4 调试与观测技巧
排查路由问题时,我习惯先打开网关的 DEBUG 日志:
logging: level: org.springframework.cloud.gateway: DEBUG org.springframework.http.server.reactive: DEBUG然后通过 Actuator 暴露网关端点:
management: endpoints: web: exposure: include: gateway,health,info启动后用 /actuator/gateway/routes 看当前生效的路由表,用 /actuator/gateway/globalfilters 看全局过滤器链路。这些端点返回内容非常直观,可以看到每条路由的 id、uri、断言和过滤器状态。
下面是一张排查速查表,把我在项目里遇到的高频问题列出来:
| 现象 | 可能原因 | 处理方案 |
|---|---|---|
| 网关返回 404 | 没有路由匹配或 StripPrefix 配置错误 | 检查断言和过滤器配置 |
| 网关返回 502 | 后端不可达、连接超时、上游崩溃 | 先排查后端健康状态和注册中心 |
| 网关返回 504 | 上游响应超时 | 调整 httpclient 的 response-timeout |
| 路由变更不生效 | 配置中心变化未触发路由刷新 | 发布 RefreshRoutesEvent 事件 |
| 请求被转到错误服务 | 路由匹配顺序不对 | 调整路由顺序或细化断言 |
| 转发路径多了一段 | StripPrefix 没配或配错 | 确认后端期望的路径格式 |
网关的日志是排查问题的第一手资料,但很多团队会忽略它的价值。Spring Cloud Gateway 的请求日志和异常堆栈包含了路由 id、断言匹配结果、转发目标地址这些信息,线上出问题时,先把这一步日志收集好,能省一半排查时间。
最后分享一个我自己的习惯。我现在维护的路由规则基本都放在配置中心,不再写死在代码里。每次改路由之前,我会先看两样东西:当前生效的路由表,以及后端服务的健康状态。把这两个确认清楚,绝大多数路由问题都能快速缩小范围。希望这篇关于 Spring Cloud Gateway 路由规则的总结,能帮你在自己项目里少踩几个坑。