Spring Cloud Gateway 断言完全讲解

第一部分:基础概念

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          # 参数值(支持正则)

工作原理

  1. 提取URL中的查询参数部分(?后面的内容)
  2. 检查是否存在名为param的参数
  3. 将参数值与regexp进行正则匹配
  4. 都满足返回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方法的执行时序

  1. Gateway启动时,读取配置文件
  2. 发现 name: Vip 时,创建VipRoutePredicateFactory实例
  3. 读取args中的参数,创建Config对象
  4. 调用apply(config)方法
  5. apply方法返回一个Predicate对象
  6. 请求到达时,调用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(包含请求的所有信息)
相关推荐
whltaoin4 小时前
【 Java微服务 】Spring Cloud Alibaba :Nacos 注册中心与配置中心全攻略(含服务发现、负载均衡与动态配置)
java·微服务·nacos·springcloud·注册中心·配置中心
灰小猿5 天前
分布式项目集成TLog实现轻量级日志链路追踪
java·分布式·springcloud·tlog·日志链路追踪
最后1116 天前
lamp-cloud 5.7.0 发布,新增缓存清理 + 修复优化全覆盖
java·后端·spring·缓存·springboot·springcloud
漂流幻境6 天前
Spring cloud gateway 跨域配置与碰到的问题
java·gateway·springcloud·跨域
小坏讲微服务7 天前
Docker Compose搭建Git仓库私服上传微服务
分布式·git·docker·微服务·容器·springcloud·springalibaba
小坏讲微服务8 天前
Spring Cloud Alibaba整合SkyWalking的监控完整使用
java·微服务·架构·springcloud·监控·skywalking·java微服务
小坏讲微服务10 天前
Spring Cloud Alibaba 2025.0.0 与 Nacos 3.1.0 集群整合
分布式·nacos·架构·springcloud·nacos集群·springalibaba
没有bug.的程序员10 天前
Spring Cloud Gateway 性能优化与限流设计
java·spring boot·spring·nacos·性能优化·gateway·springcloud
serendipity_hky16 天前
【微服务 - easy视频 | day01】准备工具+gateway网关及路由至内部服务
java·微服务·架构·gateway·springcloud