Java框架 SpringCloud 快速入门: 路由断言工厂

概述

上一篇把 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,需要"或"就拆成多条路由。

官方文档

总结

  • 断言是路由的匹配条件;断言工厂负责把配置里的字符串解析成真正的判断逻辑,Path 对应 PathRoutePredicateFactory,类名规律是"断言名 + RoutePredicateFactory"。
  • 一条路由的多个断言之间是 AND ,全部满足才命中;要"或"就拆多条路由。路径通配符 * 只匹配一级、** 匹配多级,日常写 /user/**。
  • 断言不匹配不会有配置报错,只在请求时返回 404;排查时开 Gateway 的 debug 日志看匹配过程最省事。
  • 时间断言 After / Before / Between 的值是 ZonedDateTime,必须带 [时区],漏了会启动失败。
  • 断言管"走不走",过滤器管"走过去怎么加工",这条分工是后面过滤器系列的地基。
相关推荐
ym hyd 1111 小时前
门诊挂号系统源码 Java+SpringBoot+Vue3 前后分离
java·vue.js·spring boot·毕设
杨丰玮4181 小时前
C语言核心语法
java·c语言·开发语言·学习方法
海宇AI1 小时前
Java数据工程:利用海宇婚恋风险报告优化高端婚恋实名与涉诉核验合规体验
java·人工智能
CEZ1 小时前
中小企业上 WMS 该先上哪几块:JeeWMS 开源 Java 仓库管理系统的分批上线清单
java·开源
Wang's Blog1 小时前
Java框架 SpringCloud 快速入门: Gateway 路由的过滤器配置
java·spring cloud·gateway
迅猛龙办公室2 小时前
python实现简单进度条
java·前端·python
斯内普吖2 小时前
(开源)宠物饲养系统实战指南 基于 Java + SSM + Vue + MySQL
java·vue.js·mysql·开源·宠物
知守观2 小时前
Spring Boot 2.1 → 3.5 迁移推演:这个 2018 年的项目会炸在哪
java·spring boot
Flynt2 小时前
800KB 的 JSON 吃掉 8.5 秒 CPU:jackson 这波修复里,最容易被漏掉的洞藏在字符串里
java·安全