Skip to content

路由断言工厂


Spring Cloud 中的断言工厂是用于 定义路由匹配条件 的一种机制。简单来说,断言工厂决定某个请求是否匹配某个路由,就像 if 判断条件一样,只有满足了断言,才会触发这个路由后续的逻辑(比如过滤器链、转发到目标服务等)。

断言工厂和过滤器工厂的区别:断言工厂决定这个路由是否能用、能不能进来,而过滤器工厂决定用了这个路由进来之后要做什么。

当一个客户端请求到达 Gateway 时,请求首先被 Gateway 的主入口(DispatcherHandler)捕获。此时,Gateway 会从配置中加载所有的路由信息,每一条路由都包含若干个由断言工厂生成的断言条件(即 Predicate 对象)。这些断言是在应用启动时,根据 YAML 配置通过相应的 RoutePredicateFactory 解析出来的。比如,如果配置了 Path=/api/**Gateway 就会调用 PathRoutePredicateFactory 来构建一个匹配请求路径的断言函数。

接着,请求会被传入路由匹配逻辑中。Gateway 会依次遍历每一条路由,调用该路由中所有断言的 test() 方法,判断这些断言是否都为 true。只有当一个路由的所有断言都通过(即所有 Predicate 返回 true),这条路由才会被视为匹配成功。此时,Gateway 会将请求交给该路由定义的过滤器链以及目标服务 URI 继续处理;如果没有任何路由匹配成功,则请求将被拒绝或者转向 fallback 逻辑。

整个断言处理流程具有高度可扩展性。Gateway 支持用户自定义断言工厂,只需实现 RoutePredicateFactory 接口或继承 AbstractRoutePredicateFactory,即可创建新的匹配逻辑。这种机制使得 Gateway 在路由选择上非常灵活,可以根据各种业务需求(如 IP 限制、设备类型判断、用户身份标识等)做出精细化的流量分发。

简单来说,断言工厂就是用来判断一个 HTTP 请求是否符合某个路由规则的条件。网关接收到请求后,会遍历配置的所有路由规则,并使用每条路由规则上配置的断言工厂集合来检查当前请求。只有当请求满足某个路由规则上配置的 所有 断言条件时,该路由规则才会被匹配,然后网关才会应用该路由上配置的过滤器,并将请求转发到该路由指定的目标 URI

通过以上介绍可知,断言工厂用于匹配路径,匹配成功则允许请求继续处理。但这与直接请求目标服务有何不同呢?看似只是多了一层校验,实际上作用远不止如此。

  1. 集中管理入口 我们系统中往往包含多个服务,如用户服务、订单服务、支付服务等。如果客户端直接请求这些服务,就需要知道每个服务的地址;一旦地址变更,客户端也要随之修改。而通过网关与断言工厂,所有请求统一先进入网关,再由断言判断并转发到对应服务。这样即使后续服务地址发生变化,也只需调整网关配置,客户端无需改动。
  2. 灵活的路由配置 断言工厂支持按路径、HostHeaderMethod、时间段、权重等多种条件进行组合匹配。例如 /api/v1/** 走新版本服务,/api/v2/** 走老版本服务;只有在工作时间内才允许访问某些接口;某些路径只允许 POST 请求。如果不通过网关断言,这些规则就需要在每个服务中单独实现,既重复又不易维护。
  3. 与过滤器配合实现统一治理 断言工厂匹配成功后,过滤器工厂才能执行,从而实现按路由精确控制。例如仅在特定条件下限流、鉴权,或为特定路径添加请求头。通过这种方式,可以在全局统一治理的同时保持灵活性。
  4. 提高安全性 断言工厂还能作为第一道安全防线,例如限制来源 IP 白名单、要求携带特定 Header、限制访问时间等。即使有人知道后端服务的真实地址,没有通过网关断言的请求也无法进入,从而有效提升系统安全性。

首先,需要实现 AbstractRoutePredicateFactory 接口来定义自定义的断言工厂。apply 方法用来定义断言工厂的主要逻辑,用来返回一个新的 Predicate,即路由匹配规则。假设要创建一个基于请求的 User-Agent 头部的自定义断言工厂(例如,如果 User-Agent 包含某些关键字,则匹配该路由)。

import org.springframework.cloud.gateway.handler.predicate.AbstractRoutePredicateFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import java.util.List;
import java.util.function.Predicate;
@Component
public class UserAgentRoutePredicateFactory extends AbstractRoutePredicateFactory<UserAgentRoutePredicateFactory.Config> {
public UserAgentRoutePredicateFactory() {
super(Config.class);
}
@Override
public Predicate<ServerWebExchange> apply(Config config) {
return exchange -> {
String userAgent = exchange.getRequest().getHeaders().getFirst("User-Agent");
if (userAgent == null) {
return false;
}
// 判断是否包含任意一个关键词
return config.getKeywords().stream().anyMatch(userAgent::contains);
};
}
@Override
public List<String> shortcutFieldOrder() {
return List.of("keywords"); // 支持 YAML 中简写
}
public static class Config {
private List<String> keywords;
public List<String> getKeywords() {
return keywords;
}
public void setKeywords(List<String> keywords) {
this.keywords = keywords;
}
}
}
spring:
cloud:
gateway:
routes:
- id: mobile_user_route
uri: https://example.com
predicates:
- UserAgent=Android,iPhone

Config 类字段是断言工厂的配置信息载体,字段根据断言功能不同而不同。shortcutFieldOrder() 返回的字段顺序要和 Config 中字段定义一一对应,YAML 中的参数会根据 shortcutFieldOrder 顺序绑定到 Config 对应字段。

  • Config 的设计原则 Config 类应仅包含断言逻辑真正需要的参数。如果断言只需一个参数,则定义一个字段;如果需要多个条件,则定义多个字段,避免包含无关字段。字段名必须与 application.yml长格式写法args 部分的键名严格对应(区分大小写,遵循 Java Bean 命名规范),Spring 会通过对应的 setter 方法注入参数值。若未来可能增加参数,可以提前预留可选字段,但不建议一次性添加大量无用字段。
  • shortcutFieldOrder 的作用 该方法用于告诉 Gatewayapplication.yml 使用简写格式时,字段的顺序映射关系。举例来说,简写配置为 - MyQuery=type,abcGateway 会按照 shortcutFieldOrder() 返回的字段顺序,依次将 "type""abc" 注入到对应的第一个和第二个字段。
  1. 只需要一个参数(比如判断某个 header 是否存在)
public static class Config {
private String headerName;
public String getHeaderName() { return headerName; }
public void setHeaderName(String headerName) { this.headerName = headerName; }
}
@Override
public List<String> shortcutFieldOrder() {
return List.of("headerName");
}
predicates:
- MyHeader=X-Token
  1. 需要两个参数(比如判断某个 query 参数是否等于某个值)
public static class Config {
private String param;
private String value;
// getter / setter
}
@Override
public List<String> shortcutFieldOrder() {
return List.of("param", "value");
}
predicates:
- MyQuery=type,abc
  1. 参数多而且可选(比如根据请求时间范围判断)
public static class Config {
private String startTime;
private String endTime;
private boolean inclusive = true; // 可选,默认包含边界
// getter / setter
}
@Override
public List<String> shortcutFieldOrder() {
return List.of("startTime", "endTime", "inclusive");
}
predicates:
- MyTime=08:00,18:00,true
public static class Config {
private String startTime;
private String endTime;
private Boolean inclusive;
public String getStartTime() {
return startTime;
}
public void setStartTime(String startTime) {
this.startTime = startTime;
}
public String getEndTime() {
return endTime;
}
public void setEndTime(String endTime) {
this.endTime = endTime;
}
public Boolean getInclusive() {
return inclusive;
}
public void setInclusive(Boolean inclusive) {
this.inclusive = inclusive;
}
}
predicates:
- name: MyTime
args:
startTime: "08:00"
endTime: "18:00"
inclusive: true