第一部分:基础概念
1.1 什么是断言(Predicate)?
断言是一个函数式编程概念,在Gateway中的作用是:判断请求是否满足某个条件。
- 本质 :
Predicate<T>是一个函数式接口,接收一个参数,返回boolean(true或false) - 在Gateway中:用来判断请求是否应该被转发到某个目标服务
- 执行顺序 :一个路由可以配置多个断言,所有断言都必须返回true,请求才能被转发
java
// 函数式接口的定义
@FunctionalInterface
public interface Predicate<T> {
boolean test(T t); // 只有一个方法:test
}
1.2 断言在Gateway中的执行流程
请求到达
↓
遍历所有配置的路由
↓
对每个路由,检查所有断言
↓
断言1 && 断言2 && 断言3 ...
↓
都是true? → 转发请求到uri
都是true? → 继续检查下一个路由
有一个false? → 检查下一个路由
都不匹配? → 返回404或默认路由
1.3 你的配置文件结构分析
bash
spring:
cloud:
gateway:
routes:
- id: bing-route # 路由ID,唯一标识
uri: https://cn.bing.com/ # 目标地址
predicates: # 断言列表(所有都要满足)
- name: Path
args: {patterns: /search}
- name: Query
args: {param: q, regexp: haha}
- name: Vip
args: {param: user, value: wangsi}
这个配置的含义:
- 如果请求的路径是
/search并且 查询参数q等于haha 并且 参数user等于wangsi - 那么就把请求转发到
https://cn.bing.com/
第二部分:Gateway内置断言详解
2.1 Path断言 - 路径匹配
配置方式:
predicates:
- name: Path
args:
patterns: /search
工作原理:
- 提取请求的URI路径部分
- 与配置的patterns进行匹配(支持通配符和正则)
- 匹配成功返回true
例子中:
bash
- name: Path
args:
patterns: /search
- ✅ 请求
/search→ 通过 - ✅ 请求
/search/→ 通过 - ❌ 请求
/api/search→ 不通过 - ❌ 请求
/Search(大小写不同)→ 不通过
高级用法:
bash
# 支持多个路径
patterns: /search,/query
# 支持通配符
patterns: /api/**
# 支持正则表达式
patterns: /api/[0-9]+
实现代码参考:
java
// Gateway内部的Path断言实现大概是这样的
public Predicate<ServerWebExchange> apply(Config config) {
return exchange -> {
String path = exchange.getRequest().getPath().value();
return pathMatcher.match(config.patterns, path);
};
}
2.2 Query断言 - 查询参数匹配
配置方式:
bash
predicates:
- name: Query
args:
param: q # 参数名
regexp: haha # 参数值(支持正则)
工作原理:
- 提取URL中的查询参数部分(?后面的内容)
- 检查是否存在名为
param的参数 - 将参数值与
regexp进行正则匹配 - 都满足返回true
例子中:
- name: Query
args:
param: q
regexp: haha
- ✅ 请求
/search?q=haha→ q参数存在且值为haha → 通过 - ✅ 请求
/search?q=haha123→ q参数值包含haha → 通过(正则匹配) - ✅ 请求
/search?q=haha&name=test→ 有其他参数不影响 → 通过 - ❌ 请求
/search?q=java→ q参数不匹配haha → 不通过 - ❌ 请求
/search→ 没有q参数 → 不通过
高级用法:
bash
# 简单的相等匹配(没有regexp时,就是完全相等)
args:
param: user_type
regexp: vip
# 正则表达式匹配
args:
param: age
regexp: \d+ # 匹配任何数字
# 复杂的正则
args:
param: email
regexp: ^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$
2.3 Header断言 - 请求头匹配
配置方式:
predicates:
- name: Header
args:
header: X-User-Type
regexp: vip
工作原理:检查HTTP请求头中的值
例子:
- ✅ 请求头
X-User-Type: vip→ 通过 - ❌ 请求头
X-User-Type: normal→ 不通过 - ❌ 没有
X-User-Type请求头 → 不通过
2.4 Method断言 - HTTP方法匹配
配置方式:
predicates:
- name: Method
args:
methods: GET,POST
例子:
- ✅ GET请求 → 通过
- ✅ POST请求 → 通过
- ❌ DELETE请求 → 不通过
2.5 Host断言 - 主机名匹配
配置方式:
bash
predicates:
- name: Host
args:
patterns: "**.example.com"
例子:
- ✅ 请求来自
api.example.com→ 通过 - ✅ 请求来自
www.example.com→ 通过 - ❌ 请求来自
example.org→ 不通过
2.6 RemoteAddr断言 - 远程地址匹配
配置方式:
predicates:
- name: RemoteAddr
args:
sources: 192.168.1.0/24,127.0.0.1
例子:
- ✅ 请求来自
192.168.1.100→ 通过 - ✅ 请求来自
127.0.0.1(本机)→ 通过 - ❌ 请求来自
192.168.2.100→ 不通过
第三部分:自定义断言详解(重点!)
3.1 为什么需要自定义断言?
内置的断言可能无法满足业务需求。比如你需要:
- 检查用户是否是VIP会员
- 根据请求中的特定参数进行复杂逻辑判断
- 整合数据库查询或缓存检查
3.2 自定义断言的完整步骤
第一步:继承AbstractRoutePredicateFactory
java
@Component // ⚠️ 关键!必须注册为Spring Bean
public class VipRoutePredicateFactory
extends AbstractRoutePredicateFactory<VipRoutePredicateFactory.Config> {
public VipRoutePredicateFactory(){
super(Config.class); // 传入配置类
}
为什么要继承这个类?
- 这是Gateway提供的抽象类
- 继承它能让Gateway自动识别你的断言
- 命名规则:
XxxRoutePredicateFactory(Xxx是你的断言名, 后期yml配置的时候,name就是xxx)
第二步:创建内部配置类
java
@Validated // 启用参数验证
public static class Config {
@NotEmpty // 验证不能为空
private String param;
@NotEmpty
private String value;
// getter和setter必须有
public String getParam() {
return param;
}
public void setParam(String param) {
this.param = param;
}
public String getValue() {
return value;
}
public void setValue(String value) {
this.value = value;
}
}
配置类的作用:
- 接收YAML配置文件中args部分的参数
- 通过Spring的参数绑定机制自动注入
- 使用@Validated和@NotEmpty进行参数校验
第三步:实现apply方法(核心逻辑)
java
@Override
public Predicate<ServerWebExchange> apply(Config config) {
// 返回一个GatewayPredicate
return new GatewayPredicate(){
@Override
public boolean test(ServerWebExchange serverWebExchange) {
// 核心业务逻辑写在这里
ServerHttpRequest request = serverWebExchange.getRequest();
// 第1步:获取查询参数
String param = request.getQueryParams().getFirst(config.param);
// 第2步:判断逻辑
return StringUtils.hasText(param) && param.equals(config.value);
// true → 请求通过,被转发
// false → 请求不通过,尝试下一个路由
}
};
}
apply方法的执行时序:
- Gateway启动时,读取配置文件
- 发现
name: Vip时,创建VipRoutePredicateFactory实例 - 读取args中的参数,创建Config对象
- 调用apply(config)方法
- apply方法返回一个Predicate对象
- 请求到达时,调用Predicate的test方法进行判断
第四步:定义参数顺序
java
public List<String> shortcutFieldOrder() {
return Arrays.asList("param", "value");
}
这个方法的作用:
- 允许使用简化的配置语法
- 定义参数的顺序
不同的配置写法都可以工作:
bash
# 标准写法
- name: Vip
args:
param: user
value: wangsi
# 简化写法(如果定义了shortcutFieldOrder)
- Vip=user,wangsi
3.3 完整代码详解
java
package org.example.predicates;
import jakarta.validation.constraints.NotEmpty;
import org.springframework.cloud.gateway.handler.predicate.AbstractRoutePredicateFactory;
import org.springframework.cloud.gateway.handler.predicate.GatewayPredicate;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.server.ServerWebExchange;
import java.util.Arrays;
import java.util.List;
import java.util.function.Predicate;
@Component // 注册为Spring Bean,这样Gateway启动时能自动扫描
public class VipRoutePredicateFactory
extends AbstractRoutePredicateFactory<VipRoutePredicateFactory.Config> {
// 构造函数
public VipRoutePredicateFactory(){
super(Config.class); // 告诉父类,配置参数类是Config
}
// 最重要的方法:实现断言逻辑
@Override
public Predicate<ServerWebExchange> apply(Config config) {
// ServerWebExchange 包含了整个HTTP请求/响应的信息
// config 是从YAML配置文件中解析出来的参数
return new GatewayPredicate(){
@Override
public boolean test(ServerWebExchange serverWebExchange) {
// 获取HTTP请求对象
ServerHttpRequest request = serverWebExchange.getRequest();
// getQueryParams() → 获取所有查询参数,返回MultiValueMap
// getFirst(key) → 获取指定参数的第一个值(如果有多个值)
String param = request.getQueryParams().getFirst(config.param);
// StringUtils.hasText(param) → 检查字符串是否有实际内容
// param.equals(config.value) → 检查值是否相等
// && → 两个条件都要满足
return StringUtils.hasText(param) && param.equals(config.value);
}
};
}
// 配置参数的顺序(支持简化写法)
public List<String> shortcutFieldOrder() {
return Arrays.asList("param", "value");
}
// 配置类:承载YAML中的args参数
@Validated // 启用JSR-303参数验证
public static class Config {
@NotEmpty // 不能为null或空字符串
private String param; // 参数名,比如"user"
@NotEmpty
private String value; // 参数值,比如"wangsi"
public String getParam() {
return param;
}
public void setParam(String param) {
this.param = param;
}
public String getValue() {
return value;
}
public void setValue(String value) {
this.value = value;
}
}
}
3.4 请求的完整处理流程
假设收到这样的请求:
GET https://localhost:8080/search?q=haha&user=wangsi&age=20
Gateway的处理流程:
1. 请求到达Gateway
↓
2. 遍历routes配置,找到id=bing-route的路由
↓
3. 执行第一个断言:Path
- 提取路径:/search
- 检查是否匹配 /search
- 结果:✅ true
↓
4. 执行第二个断言:Query
- 检查查询参数 q 是否存在 ✅
- 检查 q 的值是否匹配 haha ✅
- 结果:✅ true
↓
5. 执行第三个断言:Vip
- 调用VipRoutePredicateFactory的apply方法
- 进入test方法
- 获取user参数值:wangsi
- StringUtils.hasText("wangsi") ✅
- "wangsi".equals("wangsi") ✅
- 结果:✅ true
↓
6. 所有断言都是true
↓
7. 转发请求到 https://cn.bing.com/search?q=haha&user=wangsi&age=20
第四部分:多个断言的关系
4.1 多个断言是AND关系
- id: bing-route
uri: https://cn.bing.com/
predicates:
- name: Path
args:
patterns: /search
- name: Query
args:
param: q
regexp: haha
- name: Vip
args:
param: user
value: wangsi
逻辑 :Path AND Query AND Vip
- ✅
/search?q=haha&user=wangsi→ 通过(三个都满足) - ❌
/search?q=java&user=wangsi→ 不通过(Query不满足) - ❌
/api?q=haha&user=wangsi→ 不通过(Path不满足) - ❌
/search?q=haha&user=zhangsan→ 不通过(Vip不满足)
4.2 多个路由是OR关系
routes:
- id: route1
uri: http://service1
predicates:
- Path=/api/user/**
- id: route2
uri: http://service2
predicates:
- Path=/api/order/**
逻辑 :(route1的断言) OR (route2的断言)
- ✅
/api/user/list→ 匹配route1 - ✅
/api/order/list→ 匹配route2 - ❌
/api/product/list→ 都不匹配 → 404
第五部分:常见问题
5.1 断言的大小写是否敏感?
Path和Query : 都是大小写敏感的
bash
# 这两个是不同的
- Path=/Search
- Path=/search
5.2 查询参数中有多个相同名称的参数怎么办?
GET /search?q=haha&q=java
使用 getFirst("q") 获取第一个值 → haha 使用 getAll("q") 获取所有值 → [haha, java]
5.3 参数不存在时会发生什么?
java
String param = request.getQueryParams().getFirst(config.param);
// 如果参数不存在,param = null
StringUtils.hasText(param) // null或空字符串 → false
// 所以断言返回false,请求不通过
5.4 如何进行复杂的业务逻辑判断?
你可以在test方法中加入更复杂的逻辑:
java
@Override
public boolean test(ServerWebExchange serverWebExchange) {
ServerHttpRequest request = serverWebExchange.getRequest();
String userParam = request.getQueryParams().getFirst("user");
// 1. 检查参数存在
if (!StringUtils.hasText(userParam)) {
return false;
}
// 2. 检查是否是VIP用户(从数据库查询)
boolean isVip = checkUserIsVip(userParam); // 自己的业务方法
// 3. 检查是否在允许的时间段
boolean inTimeWindow = isInAllowedTimeWindow();
// 4. 综合判断
return isVip && inTimeWindow;
}
5.5 如何在自定义断言中使用Spring Bean?
java
@Component
public class VipRoutePredicateFactory
extends AbstractRoutePredicateFactory<VipRoutePredicateFactory.Config> {
// 注入其他Spring Bean
private final UserService userService;
public VipRoutePredicateFactory(UserService userService){
super(Config.class);
this.userService = userService;
}
@Override
public Predicate<ServerWebExchange> apply(Config config) {
return new GatewayPredicate(){
@Override
public boolean test(ServerWebExchange serverWebExchange) {
String username = serverWebExchange.getRequest()
.getQueryParams().getFirst("user");
// 使用注入的Bean
User user = userService.getUserByName(username);
return user != null && user.isVip();
}
};
}
}
第六部分:配置完整解析
bash
spring:
cloud:
gateway:
routes:
- id: order-route
uri: lb://server-order
predicates:
- Path=/api/order/**
# 解释:只要路径是/api/order/开头,就转发到server-order服务
- id: product-route
uri: lb://server-product
predicates:
- Path=/api/product/**
# 解释:只要路径是/api/product/开头,就转发到server-product服务
- id: bing-route
uri: https://cn.bing.com/
predicates:
# 断言1:路径必须是/search
- name: Path
args:
patterns: /search
# 断言2:查询参数q必须存在且值为haha
- name: Query
args:
param: q
regexp: haha
# 断言3:查询参数user必须存在且值为wangsi(你自己写的)
- name: Vip
args:
param: user
value: wangsi
# 综合条件:
# Path=/search AND Query(q=haha) AND Vip(user=wangsi)
# 满足以上条件,转发请求到 https://cn.bing.com/search?q=haha&user=wangsi&...
完整请求示例
请求1:全部通过
GET http://localhost:8080/search?q=haha&user=wangsi
✅ Path匹配 /search
✅ Query参数 q=haha
✅ Vip参数 user=wangsi
→ 转发到 https://cn.bing.com/search?q=haha&user=wangsi
请求2:Path不匹配
GET http://localhost:8080/api/search?q=haha&user=wangsi
❌ Path不匹配 /search(实际是/api/search)
→ 不转发,尝试下一个路由
请求3:Query参数不匹配
GET http://localhost:8080/search?q=java&user=wangsi
✅ Path匹配 /search
❌ Query参数 q=java(不等于haha)
→ 不转发
请求4:Vip参数不匹配
GET http://localhost:8080/search?q=haha&user=zhangsan
✅ Path匹配 /search
✅ Query参数 q=haha
❌ Vip参数 user=zhangsan(不等于wangsi)
→ 不转发
总结
| 知识点 | 要点 |
|---|---|
| 断言本质 | 返回boolean的判断条件 |
| 多个断言关系 | AND(全部都要true) |
| 多个路由关系 | OR(其中一个路由的断言true就用该路由) |
| 内置断言 | Path、Query、Header、Method、Host等 |
| 自定义断言 | 继承AbstractRoutePredicateFactory实现apply方法 |
| 配置类 | 用@Validated和@NotEmpty进行参数验证 |
| 必要注解 | @Component(让Gateway能找到你的断言) |
| 关键对象 | ServerWebExchange(包含请求的所有信息) |