路由断言工厂
Spring Cloud 中的断言工厂是用于 定义路由匹配条件 的一种机制。简单来说,断言工厂决定某个请求是否匹配某个路由,就像 if 判断条件一样,只有满足了断言,才会触发这个路由后续的逻辑(比如过滤器链、转发到目标服务等)。
断言工厂和过滤器工厂的区别:断言工厂决定这个路由是否能用、能不能进来,而过滤器工厂决定用了这个路由进来之后要做什么。
1. 断言工厂的处理流程
Section titled “1. 断言工厂的处理流程”当一个客户端请求到达 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。
2. 断言工厂的意义
Section titled “2. 断言工厂的意义”通过以上介绍可知,断言工厂用于匹配路径,匹配成功则允许请求继续处理。但这与直接请求目标服务有何不同呢?看似只是多了一层校验,实际上作用远不止如此。
- 集中管理入口 我们系统中往往包含多个服务,如用户服务、订单服务、支付服务等。如果客户端直接请求这些服务,就需要知道每个服务的地址;一旦地址变更,客户端也要随之修改。而通过网关与断言工厂,所有请求统一先进入网关,再由断言判断并转发到对应服务。这样即使后续服务地址发生变化,也只需调整网关配置,客户端无需改动。
- 灵活的路由配置
断言工厂支持按路径、
Host、Header、Method、时间段、权重等多种条件进行组合匹配。例如/api/v1/**走新版本服务,/api/v2/**走老版本服务;只有在工作时间内才允许访问某些接口;某些路径只允许POST请求。如果不通过网关断言,这些规则就需要在每个服务中单独实现,既重复又不易维护。 - 与过滤器配合实现统一治理 断言工厂匹配成功后,过滤器工厂才能执行,从而实现按路由精确控制。例如仅在特定条件下限流、鉴权,或为特定路径添加请求头。通过这种方式,可以在全局统一治理的同时保持灵活性。
- 提高安全性
断言工厂还能作为第一道安全防线,例如限制来源
IP白名单、要求携带特定Header、限制访问时间等。即使有人知道后端服务的真实地址,没有通过网关断言的请求也无法进入,从而有效提升系统安全性。
2. 自定义断言工厂
Section titled “2. 自定义断言工厂”2.1. 创建自定义断言工厂类
Section titled “2.1. 创建自定义断言工厂类”首先,需要实现 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;
@Componentpublic 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,iPhone3. Config 配置承载类
Section titled “3. Config 配置承载类”Config 类字段是断言工厂的配置信息载体,字段根据断言功能不同而不同。shortcutFieldOrder() 返回的字段顺序要和 Config 中字段定义一一对应,YAML 中的参数会根据 shortcutFieldOrder 顺序绑定到 Config 对应字段。
Config的设计原则Config类应仅包含断言逻辑真正需要的参数。如果断言只需一个参数,则定义一个字段;如果需要多个条件,则定义多个字段,避免包含无关字段。字段名必须与application.yml中长格式写法下args部分的键名严格对应(区分大小写,遵循 Java Bean 命名规范),Spring 会通过对应的 setter 方法注入参数值。若未来可能增加参数,可以提前预留可选字段,但不建议一次性添加大量无用字段。shortcutFieldOrder的作用 该方法用于告诉Gateway在application.yml使用简写格式时,字段的顺序映射关系。举例来说,简写配置为- MyQuery=type,abc,Gateway会按照shortcutFieldOrder()返回的字段顺序,依次将"type"和"abc"注入到对应的第一个和第二个字段。
4. 不同场景下的写法示例
Section titled “4. 不同场景下的写法示例”- 只需要一个参数(比如判断某个
header是否存在)
public static class Config { private String headerName; public String getHeaderName() { return headerName; } public void setHeaderName(String headerName) { this.headerName = headerName; }}
@Overridepublic List<String> shortcutFieldOrder() { return List.of("headerName");}predicates: - MyHeader=X-Token- 需要两个参数(比如判断某个
query参数是否等于某个值)
public static class Config { private String param; private String value; // getter / setter}
@Overridepublic List<String> shortcutFieldOrder() { return List.of("param", "value");}predicates: - MyQuery=type,abc- 参数多而且可选(比如根据请求时间范围判断)
public static class Config { private String startTime; private String endTime; private boolean inclusive = true; // 可选,默认包含边界 // getter / setter}
@Overridepublic List<String> shortcutFieldOrder() { return List.of("startTime", "endTime", "inclusive");}predicates: - MyTime=08:00,18:00,truepublic 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