概述
上一篇把 gateway 跑起来了,路由能转发,但很多人对 predicates 这一行只是照抄。这篇就把断言讲透:它到底在判断什么、有哪些内置断言工厂、多个断言写在一起是"与"还是"或"、断言和过滤器各管哪一段。
纲要
- 断言(Predicate)与断言工厂(Predicate Factory):一个负责"规则"、一个负责"解析"
- 请求进来后怎么被逐条路由筛选:流程图 + 时序图
- 十一类内置断言工厂速查表(讲义口径,含配置示例)
- 本系列基线工程
cloud-demo的真实routes配置 - 最常用的四种断言写法:
Path/Method/Query/Header - 两个高频误解:Ant 通配符怎么写、多个断言之间是 AND
- 时间断言的时区格式(写错直接启动失败)
Weight权重断言与灰度分流- 断言 vs 过滤器:职责、时机、典型实现对照
- 一次可复现的验证:
curl实测 GET 与 POST - 断言全不匹配为什么是 404,以及怎么开日志看匹配过程
断言是什么:路由的匹配条件
先说结论:断言就是路由的匹配条件。
Gateway 里配置的每一条 route 不是"默认生效",而是要过一道门。请求进来后,Gateway 会按 routes 列表的顺序,挨个判断这条路由的断言是否满足。只有全部满足,才走这条路由;否则跳到下一条继续判;所有路由都不匹配,返回 404。
讲义里对断言的定义就一句:predicates 是判断路由的规则,例如 Path=/user/** 表示只要请求路径以 /user/ 开头就算符合。
那"断言工厂"又是什么?配置文件里写的 Path=/user/** 本质上只是一个字符串。字符串得有人解析、翻译成真正的判断逻辑,干这件事的就是断言工厂。讲义原文:
我们在配置文件中写的断言规则只是字符串,这些字符串会被 Predicate Factory 读取并处理,转变为路由判断的条件。
以 Path=/user/** 为例,它对应的实现类是:
text
org.springframework.cloud.gateway.handler.predicate.PathRoutePredicateFactory
命名规律很好记:断言名 + RoutePredicateFactory。配置里写 Method=,背后就是 MethodRoutePredicateFactory;写 After=,背后就是 AfterRoutePredicateFactory。Spring Cloud Gateway 内置了十几个,下面这张表就是完整清单。记住一句就行:不要背,用到哪个去官方文档抄示例。
请求是怎么被逐条路由筛掉的
官方文档把路由匹配画成一个链条,理解这个链条,后面所有坑都好解释。
#mermaid-svg-sU4L8g6jiJEW5UJR{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-sU4L8g6jiJEW5UJR .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-sU4L8g6jiJEW5UJR .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-sU4L8g6jiJEW5UJR .error-icon{fill:#552222;}#mermaid-svg-sU4L8g6jiJEW5UJR .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-sU4L8g6jiJEW5UJR .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-sU4L8g6jiJEW5UJR .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-sU4L8g6jiJEW5UJR .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-sU4L8g6jiJEW5UJR .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-sU4L8g6jiJEW5UJR .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-sU4L8g6jiJEW5UJR .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-sU4L8g6jiJEW5UJR .marker{fill:#333333;stroke:#333333;}#mermaid-svg-sU4L8g6jiJEW5UJR .marker.cross{stroke:#333333;}#mermaid-svg-sU4L8g6jiJEW5UJR svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-sU4L8g6jiJEW5UJR p{margin:0;}#mermaid-svg-sU4L8g6jiJEW5UJR .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-sU4L8g6jiJEW5UJR .cluster-label text{fill:#333;}#mermaid-svg-sU4L8g6jiJEW5UJR .cluster-label span{color:#333;}#mermaid-svg-sU4L8g6jiJEW5UJR .cluster-label span p{background-color:transparent;}#mermaid-svg-sU4L8g6jiJEW5UJR .label text,#mermaid-svg-sU4L8g6jiJEW5UJR span{fill:#333;color:#333;}#mermaid-svg-sU4L8g6jiJEW5UJR .node rect,#mermaid-svg-sU4L8g6jiJEW5UJR .node circle,#mermaid-svg-sU4L8g6jiJEW5UJR .node ellipse,#mermaid-svg-sU4L8g6jiJEW5UJR .node polygon,#mermaid-svg-sU4L8g6jiJEW5UJR .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-sU4L8g6jiJEW5UJR .rough-node .label text,#mermaid-svg-sU4L8g6jiJEW5UJR .node .label text,#mermaid-svg-sU4L8g6jiJEW5UJR .image-shape .label,#mermaid-svg-sU4L8g6jiJEW5UJR .icon-shape .label{text-anchor:middle;}#mermaid-svg-sU4L8g6jiJEW5UJR .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-sU4L8g6jiJEW5UJR .rough-node .label,#mermaid-svg-sU4L8g6jiJEW5UJR .node .label,#mermaid-svg-sU4L8g6jiJEW5UJR .image-shape .label,#mermaid-svg-sU4L8g6jiJEW5UJR .icon-shape .label{text-align:center;}#mermaid-svg-sU4L8g6jiJEW5UJR .node.clickable{cursor:pointer;}#mermaid-svg-sU4L8g6jiJEW5UJR .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-sU4L8g6jiJEW5UJR .arrowheadPath{fill:#333333;}#mermaid-svg-sU4L8g6jiJEW5UJR .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-sU4L8g6jiJEW5UJR .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-sU4L8g6jiJEW5UJR .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-sU4L8g6jiJEW5UJR .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-sU4L8g6jiJEW5UJR .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-sU4L8g6jiJEW5UJR .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-sU4L8g6jiJEW5UJR .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-sU4L8g6jiJEW5UJR .cluster text{fill:#333;}#mermaid-svg-sU4L8g6jiJEW5UJR .cluster span{color:#333;}#mermaid-svg-sU4L8g6jiJEW5UJR div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-sU4L8g6jiJEW5UJR .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-sU4L8g6jiJEW5UJR rect.text{fill:none;stroke-width:0;}#mermaid-svg-sU4L8g6jiJEW5UJR .icon-shape,#mermaid-svg-sU4L8g6jiJEW5UJR .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-sU4L8g6jiJEW5UJR .icon-shape p,#mermaid-svg-sU4L8g6jiJEW5UJR .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-sU4L8g6jiJEW5UJR .icon-shape .label rect,#mermaid-svg-sU4L8g6jiJEW5UJR .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-sU4L8g6jiJEW5UJR .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-sU4L8g6jiJEW5UJR .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-sU4L8g6jiJEW5UJR :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 满足
不满足
有
没有
请求进入 Gateway
按序遍历 routes 列表
当前路由的断言
是否全部满足
执行该路由的 filters
转发到 uri 指向的服务
后面还有路由吗
取下一路由
返回 404 Not Found
注意两个细节:一是顺序 ,routes 是列表,靠前的先判;二是短路,某条路由命中后就不再往下判了。所以路由顺序在有多条规则重叠时是有意义的。
十一类断言工厂速查表
讲义 3.3 节列出的清单(这是权威依据,按原口径整理):
| 断言 | 作用 | 配置示例 |
|---|---|---|
After |
某个时间点之后的请求才放行 | - After=2037-01-20T17:42:47.789-07:00[America/Denver] |
Before |
某个时间点之前的请求才放行 | - Before=2031-04-13T15:14:47.433+08:00[Asia/Shanghai] |
Between |
两个时间点之间的请求才放行 | - Between=2037-01-20T17:42:47.789-07:00[America/Denver], 2037-01-21T17:42:47.789-07:00[America/Denver] |
Cookie |
请求必须携带指定 Cookie(值可正则) | - Cookie=chocolate, ch.p |
Header |
请求必须携带指定请求头(值可正则) | - Header=X-Request-Id, \d+ |
Host |
请求访问的域名必须匹配 | - Host=**.somehost.org,**.anotherhost.org |
Method |
请求方法必须是指定方法 | - Method=GET,POST |
Path |
请求路径必须符合规则(最常用) | - Path=/red/{segment},/blue/** |
Query |
请求参数必须包含指定参数(值可正则) | - Query=name, Jack 或 - Query=name |
RemoteAddr |
请求来源 IP 必须在指定网段内 | - RemoteAddr=192.168.1.1/24 |
Weight |
权重处理,用于按比例分流 | 见下文灰度小节 |
几个用法上的差异要区分开:
Method=GET,POST是枚举语义,逗号分隔取"或",GET 或 POST 都算通过。Path=/red/{segment},/blue/**也是逗号分隔,多个路径规则任一符合即通过。Query=name, Jack的第二个值是正则 ,要求请求参数name的值匹配Jack;只写- Query=name则只要求"存在这个参数"。RemoteAddr=192.168.1.1/24是 CIDR 网段写法,/24表示前 24 位固定,即 192.168.1.0 ~ 192.168.1.255。这个断言在实际场景里就是"限制某些来源 IP",比如只允许内网网段访问管理接口。After/Before/Between是时间窗口,After常用于"活动开始后才放行",Before相反。
基线工程的真实 routes 配置
本系列用的是 cloud-demo,第二天终态(SpringCloud02)的 gateway 模块配置如下。注意模块路径多一层嵌套:cloud-demo/cloud-demo/gateway。
tree
cloud-demo
├── eureka-server
├── feign-api
├── gateway # 本篇文章的主角
│ └── src/main/resources/application.yml
├── user-service
└── order-service
真实的 application.yml(取自 SpringCloud02/代码/cloud-demo/cloud-demo/gateway/src/main/resources/application.yml,第二天的终态已经把注册中心切到 Nacos):
yaml
server:
port: 10010
logging:
level:
cn.itcast: debug
pattern:
dateformat: MM-dd HH:mm:ss:SSS
spring:
application:
name: gateway
cloud:
nacos:
server-addr: nacos:8848 # nacos地址
gateway:
routes:
- id: user-service # 路由标示,必须唯一
uri: lb://userservice # 路由的目标地址
predicates: # 路由断言,判断请求是否符合规则
- Path=/user/** # 路径断言,判断路径是否是以/user开头,如果是则符合
- id: order-service
uri: lb://orderservice
predicates:
- Path=/order/**
default-filters:
- AddRequestHeader=Truth,Itcast is freaking awesome!
这份配置就是后续所有改造的基线:两条路由、两条 Path 断言、uri 统一用 lb://(走注册中心负载均衡)。端口固定 10010,user-service 是 8081,order-service 第二天终态是 8088。下面加断言都在这个文件上改。
最常用的四种断言写法
Path 之外,Method / Query / Header 是最常配的三个。都能直接整段粘到 predicates 下。
Path:路径匹配
yaml
spring:
cloud:
gateway:
routes:
- id: user-service
uri: lb://userservice
predicates:
- Path=/user/**
Ant 风格通配符只有两个规则,别记混:
| 写法 | 含义 | 能匹配 | 不能匹配 |
|---|---|---|---|
/user/* |
* 只匹配一级路径 |
/user/1、/user/abc |
/user/1/order |
/user/** |
** 匹配任意多级路径 |
/user/1、/user/1/order、/user/1/order/2 |
/order/1 |
所以 /user/* 这种写法几乎没人用------真实接口大多是多级的,/user/1/order 就漏了。日常直接写 /user/**。
另外 Path=/red/{segment} 里的 {segment} 是路径占位符,可以取到路径里的这一段参数。大多数场景用不上,写 /blue/** 就够了,需要拿到路径变量时再按官方文档用占位符。
Method:请求方法
yaml
spring:
cloud:
gateway:
routes:
- id: user-service
uri: lb://userservice
predicates:
- Path=/user/**
- Method=GET,POST # 只放行 GET 和 POST
Query:请求参数
yaml
spring:
cloud:
gateway:
routes:
- id: user-service
uri: lb://userservice
predicates:
- Path=/user/**
- Query=page # 必须带上 page 参数(只看有没有)
# - Query=page, \d+ # 加上正则:page 的值必须是纯数字
Header:请求头
yaml
spring:
cloud:
gateway:
routes:
- id: user-service
uri: lb://userservice
predicates:
- Path=/user/**
- Header=X-Request-Id, \d+ # 请求头 X-Request-Id 必须存在且值为数字
Header / Query / Cookie 的第二个参数都是正则,写 \d+ 就是"纯数字",写 ch.p 就是"ch 开头、中间任意一个字符、p 结尾"。正则和 yaml 的转义要小心,必要时用单引号包起来。
多个断言之间是"与",不是"或"
这是最容易误解的一点。上面 Path + Method 写在一起,含义是两个条件都满足才算命中这条路由,是逻辑与(AND)。
text
请求 POST /user/1
├─ Path=/user/** → 满足 ✓
└─ Method=GET,POST → 满足 ✓
结论:命中,转发
请求 PUT /user/1
├─ Path=/user/** → 满足 ✓
└─ Method=GET,POST → 不匹配 ✗
结论:整条路由不命中,继续匹配下一条
想让某个条件"满足其一",得分两条路由写,而不是在一条路由里堆断言。比如"GET /user/** 和 POST /user/** 走不同服务",就是两条 route,各写一个 Method。
时间断言必须带时区
After / Before / Between 的参数是 ZonedDateTime,格式是:
text
yyyy-MM-ddTHH:mm:ss.SSS±HH:mm[区域]
例:2031-04-13T15:14:47.433+08:00[Asia/Shanghai]
两个数字上的坑:
- 必须带
[时区],比如[Asia/Shanghai]。只写2031-04-13T15:14:47或漏掉方括号里的时区,应用启动阶段解析配置就会报错,Gateway 起不来。 - 时间点是写死 的。讲义里的示例是
2037、2031这类远期时间,只是演示。
yaml
spring:
cloud:
gateway:
routes:
- id: order-service
uri: lb://orderservice
predicates:
- Path=/order/**
- After=2031-04-13T15:14:47.433+08:00[Asia/Shanghai] # 2031 年之后才放行
用讲义里那个例子实测:现在远没到 2031 年,After 不满足,即便 /order/** 路径匹配成功,整条路由也不命中,访问 /order/1 返回 404。把 After 换成 Before=2031-04-13T15:14:47.433+08:00[Asia/Shanghai],条件变成"2031 年之前",当前时间满足,重启后 /order/1 就能正常转发。这一对比正好说明了"路径匹配 ≠ 路由命中"。
Weight 断言与灰度分流
Weight 不是判断"能不能走",而是决定"走哪一条"。典型用法是灰度:同一个服务部署了新老两个版本,用权重把流量按比例切过去,比如 90% 走老版本、10% 走新版本,先小流量验证。
这里只点一句:Weight 走的是网关层按比例分流这条路,和 Nacos 服务列表里给实例调权重是两条不同的路线------前者在网关入口切,后者在服务调用时切。具体配置放到灰度那篇再展开。
断言和过滤器:谁管匹配,谁管加工
路由配置里 predicates 和 filters 挨着写,很容易混。一句话切开:
- 断言决定"这条路要不要走"------匹配。
- 过滤器决定"走过去时要不要加工请求/响应"------处理。
| 维度 | 断言(predicates) | 过滤器(filters) |
|---|---|---|
| 职责 | 判断请求是否符合本路由 | 对请求或响应做加工 |
| 执行时机 | 路由匹配阶段,转发之前 | 匹配命中之后,转发前后都会经过 |
| 常见用途 | 限路径 / 限方法 / 限时间 / 限 IP / 限域名 | 加请求头 / 改路径 / 限流 / 鉴权 / 记录日志 |
| 典型实现 | PathRoutePredicateFactory 等 |
AddRequestHeader、StripPrefix、自定义 GlobalFilter |
| 不满足的结果 | 该路由不命中,继续匹配下一条,最后 404 | 请求已被放行,在过滤器里被改写或拦截 |
基线配置里的 default-filters: AddRequestHeader=... 就是过滤器,它不参与"走哪条路由"的判断,只是在转发时给请求塞一个头。后面三篇(过滤器配置、全局过滤器、过滤器链顺序)都建立在这个分工上。
验证:只接受 GET,且路径为 /user/**
拿"把基线工程改造成只接受 GET 的 user 路由"来试一次。
yaml
spring:
cloud:
gateway:
routes:
- id: user-service
uri: lb://userservice
predicates:
- Path=/user/**
- Method=GET # 只放行 GET
启动 gateway(10010),用 curl 分别打 GET 和 POST:
bash
# 1) GET /user/1 ------ 两个断言都满足,命中路由
curl -i http://localhost:10010/user/1
# 预期:HTTP/1.1 200,返回 user-service 的数据
# 2) POST /user/1 ------ Path 满足但 Method 不满足,路由不命中
curl -i -X POST http://localhost:10010/user/1
# 预期:HTTP/1.1 404 Not Found
第二个请求返回 404 不是"接口不存在",而是网关没找到能匹配的路由。整个判断过程如下:
userservice Gateway :10010 curl userservice Gateway :10010 curl #mermaid-svg-0dRqgY4lpjetRZun{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-0dRqgY4lpjetRZun .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-0dRqgY4lpjetRZun .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-0dRqgY4lpjetRZun .error-icon{fill:#552222;}#mermaid-svg-0dRqgY4lpjetRZun .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-0dRqgY4lpjetRZun .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-0dRqgY4lpjetRZun .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-0dRqgY4lpjetRZun .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-0dRqgY4lpjetRZun .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-0dRqgY4lpjetRZun .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-0dRqgY4lpjetRZun .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-0dRqgY4lpjetRZun .marker{fill:#333333;stroke:#333333;}#mermaid-svg-0dRqgY4lpjetRZun .marker.cross{stroke:#333333;}#mermaid-svg-0dRqgY4lpjetRZun svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-0dRqgY4lpjetRZun p{margin:0;}#mermaid-svg-0dRqgY4lpjetRZun .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-0dRqgY4lpjetRZun text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-0dRqgY4lpjetRZun .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-0dRqgY4lpjetRZun .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-0dRqgY4lpjetRZun .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-0dRqgY4lpjetRZun .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-0dRqgY4lpjetRZun #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-0dRqgY4lpjetRZun .sequenceNumber{fill:white;}#mermaid-svg-0dRqgY4lpjetRZun #sequencenumber{fill:#333;}#mermaid-svg-0dRqgY4lpjetRZun #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-0dRqgY4lpjetRZun .messageText{fill:#333;stroke:none;}#mermaid-svg-0dRqgY4lpjetRZun .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-0dRqgY4lpjetRZun .labelText,#mermaid-svg-0dRqgY4lpjetRZun .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-0dRqgY4lpjetRZun .loopText,#mermaid-svg-0dRqgY4lpjetRZun .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-0dRqgY4lpjetRZun .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-0dRqgY4lpjetRZun .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-0dRqgY4lpjetRZun .noteText,#mermaid-svg-0dRqgY4lpjetRZun .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-0dRqgY4lpjetRZun .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-0dRqgY4lpjetRZun .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-0dRqgY4lpjetRZun .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-0dRqgY4lpjetRZun .actorPopupMenu{position:absolute;}#mermaid-svg-0dRqgY4lpjetRZun .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-0dRqgY4lpjetRZun .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-0dRqgY4lpjetRZun .actor-man circle,#mermaid-svg-0dRqgY4lpjetRZun line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-0dRqgY4lpjetRZun :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} GET /user/1 Path=/user/** 满足, Method=GET 满足 转发 200 OK POST /user/1 Path 满足, Method=GET 不匹配 404 Not Found(无路由命中)
排查:断言不匹配是 404,不是配置错误
配错了断言,最容易卡在"到底是哪条路由没走"。几个能少走弯路的点:
- 不匹配就是 404 。断言全不满足不会报配置错误,只会在请求时返回 404。如果以为是接口写错了,会查很久。开 Gateway 的匹配日志能直接看到判断过程:把
logging.level.org.springframework.cloud.gateway: debug(或项目自带的cn.itcast: debug)打开,日志里会打印每条路由断言的计算结果。 - 缩进和
-不能少 。predicates下面是 YAML 列表,每一行必须以-开头,缩进对齐。写成Path=/user/**(丢-)会变成非法结构,启动就报错。 - 通配符别写小 。
/user/*只匹配一级,/user/1/order匹配不上,表现就是 404。不确定就写/user/**。 - 时间断言缺时区 。
After/Before/Between的值必须带[时区],否则应用启动失败,报的是配置解析异常。 - 多断言误当成"或"。写到一条路由里的断言是 AND,需要"或"就拆成多条路由。
官方文档
- Spring Cloud Gateway - Route Predicate Factories
- Spring Cloud Gateway - The Weight Route Predicate Factory
- Spring Cloud Gateway - The Path Route Predicate Factory
总结
- 断言是路由的匹配条件;断言工厂负责把配置里的字符串解析成真正的判断逻辑,
Path对应PathRoutePredicateFactory,类名规律是"断言名 +RoutePredicateFactory"。 - 一条路由的多个断言之间是 AND ,全部满足才命中;要"或"就拆多条路由。路径通配符
*只匹配一级、**匹配多级,日常写/user/**。 - 断言不匹配不会有配置报错,只在请求时返回 404;排查时开 Gateway 的 debug 日志看匹配过程最省事。
- 时间断言
After/Before/Between的值是ZonedDateTime,必须带[时区],漏了会启动失败。 - 断言管"走不走",过滤器管"走过去怎么加工",这条分工是后面过滤器系列的地基。