Day46—Spring Cloud Gateway:路由/过滤器/限流全攻略

专栏:《Java高级进阶》90天进阶系列,从 CRUD 到 AI 工程师的完整跃迁路径

一次没网关的生产事故

曾经做过一个电商平台。当时微服务搭建完后,八个服务直接通过 Nginx 暴露给前端。有个实习生写了个商品搜索接口,没加任何防护,上线第三天被爬虫公司盯上了------QPS 从平时的 200 飙到 8000。

事后复盘,核心问题就一个:没有一个统一的网关层。Nginx 能做负载均衡,但做不了细粒度的限流、鉴权、灰度路由,更做不了请求日志和链路追踪。


一、网关到底干嘛的?别把它当Nginx用

很多人以为网关就是个"高级转发器",这认知差了十万八千里。Spring Cloud Gateway 的本质是一条过滤器链(Filter Chain),请求进来后依次经过若干过滤器,最终路由到后端服务。它的核心能力有四层:

能力层 做什么 典型场景
路由层 把请求转发到哪个服务 按路径/Header/Host分发
过滤层 请求前/后做加工 鉴权、日志、参数清洗
治理层 限流、熔断、降级 保护后端不被打挂
扩展层 业务定制 AI模型路由、Token统计

Spring Cloud Gateway 基于 WebFlux + Netty 构建,天生非阻塞,单机吞吐能到几万 QPS,比 Zuul 1.x 快一个量级。这也是为什么 Spring Cloud 官方直接放弃了 Zuul,力推 Gateway。


二、路由配置:谓词工厂(Predicate)

Gateway 路由的核心是 Route,一个 Route 由三部分组成:ID + Predicate(谓词)+ Filter(过滤器)。谓词决定"匹配哪些请求",过滤器决定"对匹配的请求做什么"。

Gateway 内置了十几种谓词工厂,我挑最常用的几个讲。

2.1 YAML 配置方式(生产推荐)

java 复制代码
spring:
  cloud:
    gateway:
      routes:
        # 商品服务:按路径路由
        - id: product-service
          uri: lb://product-service  # lb:// 表示从注册中心负载均衡
          predicates:
            - Path=/api/product/**
            - Method=GET,POST
            - Header=X-Api-Version, v[1-2]  # 只允许 v1 和 v2 版本
          filters:
            - StripPrefix=1  # 去掉第一层路径前缀,/api/product/123 → /product/123
​
        # 用户服务:按 Host 路由(多租户场景)
        - id: user-service-tenant
          uri: lb://user-service
          predicates:
            - Host={subdomain}.mall.com  # 匹配 *.mall.com
            - Path=/api/user/**
​
        # AI对话服务:按查询参数路由
        - id: ai-chat-service
          uri: lb://ai-chat-service
          predicates:
            - Path=/api/ai/**
            - Query=model, (gpt|deepseek|qwen)  # 必须带 model 参数且值匹配正则
          filters:
            - StripPrefix=1

2.2 Java DSL 配置方式(需要动态路由时用)

这里有个坑很多人踩过:StripPrefix=1 是去掉路径的第一段,配置不当会导致后端服务收到的路径多一层或少一层。我的经验是统一在网关层做路径改写,后端服务只认干净的业务路径

下面这张图是请求从进入网关到路由到后端服务的完整链路:

复制代码
客户端请求
  │
  ▼
┌─────────────────────────────────┐
│  Gateway Handler Mapping        │  ← 谓词匹配,找到对应的 Route
│  (Predicate匹配)                 │
└──────────────┬──────────────────┘
               │ 匹配成功
               ▼
┌─────────────────────────────────┐
│  Gateway Filter Chain (前置)     │  ← GlobalFilter + GatewayFilter 按Order排序
│  ├─ 全局鉴权过滤器               │
│  ├─ IP黑白名单过滤器             │
│  ├─ 限流过滤器                   │
│  └─ 日志过滤器                   │
└──────────────┬──────────────────┘
               │
               ▼
┌─────────────────────────────────┐
│  代理转发 (Proxy Exchange)       │  ← 通过 Netty 非阻塞转发到后端服务
└──────────────┬──────────────────┘
               │ 后端响应
               ▼
┌─────────────────────────────────┐
│  Gateway Filter Chain (后置)     │  ← 响应头加工、响应日志、耗时统计
└──────────────┬──────────────────┘
               │
               ▼
          返回客户端

三、过滤器链:GatewayFilter vs GlobalFilter

这是 Gateway 最核心也最容易混淆的概念。先说结论:

  • GatewayFilter :作用范围是单个路由 ,通过 filters 配置绑定到具体 Route,只对该路由生效。

  • GlobalFilter :作用范围是全局所有路由 ,实现 GlobalFilter 接口即可,不需要配置。

两者都实现同一个接口 org.springframework.cloud.gateway.filter.GatewayFilter,在执行时会合并到同一条链路里,按 getOrder() 返回值排序(值小的先执行)。

3.1 自研IP黑白名单过滤器(GlobalFilter)

这是生产环境最常用的自定义过滤器之一。我直接给完整代码:

java 复制代码
/**
 * IP黑白名单过滤器
 * 优先级设为 -100,确保在鉴权和限流之前执行
 */
@Component
public class IpBlackListFilter implements GlobalFilter, Ordered {
​
    // 黑名单:从Redis动态加载,支持运营实时封禁
    private static final String BLACK_LIST_KEY = "gateway:ip:blacklist";
    // 白名单:内网IP、健康检查IP等
    private static final String WHITE_LIST_KEY = "gateway:ip:whitelist";
​
    private final StringRedisTemplate redisTemplate;
​
    public IpBlackListFilter(StringRedisTemplate redisTemplate) {
        this.redisTemplate = redisTemplate;
    }
​
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        String clientIp = getClientIp(exchange);
​
        // 1. 白名单直接放行(运维、监控、内网健康检查)
        if (isInList(WHITE_LIST_KEY, clientIp)) {
            return chain.filter(exchange);
        }
​
        // 2. 黑名单直接拒绝
        if (isInList(BLACK_LIST_KEY, clientIp)) {
            exchange.getResponse().setStatusCode(HttpStatus.FORBIDDEN);
            exchange.getResponse().getHeaders().setContentType(MediaType.APPLICATION_JSON);
            String body = "{\"code\":403,\"message\":\"IP已被封禁,如有疑问请联系管理员\"}";
            DataBuffer buffer = exchange.getResponse().bufferFactory()
                .wrap(body.getBytes(StandardCharsets.UTF_8));
            return exchange.getResponse().writeWith(Mono.just(buffer));
        }
​
        // 3. 正常放行,并在请求头中传递真实IP
        ServerHttpRequest request = exchange.getRequest().mutate()
            .header("X-Real-IP", clientIp)
            .build();
        return chain.filter(exchange.mutate().request(request).build());
    }
​
    @Override
    public int getOrder() {
        return -100;  // 最高优先级,最先执行
    }
​
    /**
     * 获取真实客户端IP,处理多层代理场景
     */
    private String getClientIp(ServerWebExchange exchange) {
        String ip = exchange.getRequest().getHeaders().getFirst("X-Forwarded-For");
        if (StringUtils.hasText(ip) && !"unknown".equalsIgnoreCase(ip)) {
            // X-Forwarded-For 可能是逗号分隔的IP链,取第一个(最原始的客户端IP)
            ip = ip.split(",")[0].trim();
        }
        if (!StringUtils.hasText(ip) || "unknown".equalsIgnoreCase(ip)) {
            ip = exchange.getRequest().getHeaders().getFirst("X-Real-IP");
        }
        if (!StringUtils.hasText(ip) || "unknown".equalsIgnoreCase(ip)) {
            ip = exchange.getRequest().getRemoteAddress() != null
                ? exchange.getRequest().getRemoteAddress().getAddress().getHostAddress()
                : "unknown";
        }
        return ip;
    }
​
    private boolean isInList(String key, String ip) {
        Boolean isMember = redisTemplate.opsForSet().isMember(key, ip);
        return Boolean.TRUE.equals(isMember);
    }
}

这段代码有几个关键点:

  1. 用 Redis Set 存黑白名单,而不是写死在配置文件里。运营在后台一键封禁,网关实时生效,不需要重启。

  2. X-Forwarded-For 取第一个IP ,因为经过多层代理后这个头是 客户端IP, 代理1IP, 代理2IP 的链式结构。但注意------这个头可以被伪造,生产环境如果 Nginx 在前,要在 Nginx 层覆写这个头。

  3. getOrder() 返回 -100,确保 IP 过滤在所有其他过滤器之前执行,别等限流都执行完了才发现是黑名单IP。

3.2 GatewayFilter:路由级响应头加工

GatewayFilter 用于"只对某个路由生效"的逻辑。比如 AI 服务需要加 CORS 头和超时控制:

java 复制代码
spring:
  cloud:
    gateway:
      routes:
        - id: ai-chat-service
          uri: lb://ai-chat-service
          predicates:
            - Path=/api/ai/**
          filters:
            - StripPrefix=1
            - AddResponseHeader=X-AI-Service, spring-ai-gateway
            - AddResponseHeader=Access-Control-Allow-Origin, "*"
            - name: RequestSize  # 限制请求体大小,AI接口防止超大Prompt
              args:
                maxSize: 10MB
            - name: Retry  # AI服务超时自动重试
              args:
                retries: 2
                statuses: BAD_GATEWAY, GATEWAY_TIMEOUT
                backoff:
                  firstBackoff: 1s
                  maxBackoff: 3s
          metadata:
            response-timeout: 30s  # AI接口超时放宽到30秒
            connect-timeout: 3s

四、限流:两种方案,按场景选

限流是网关最核心的治理能力。Gateway 提供了两种限流方式,各有适用场景。

4.1 方案一:内置 RequestRateLimiter(基于Redis + Lua令牌桶)

这是 Gateway 自带的限流过滤器,底层是 Redis + Lua 脚本实现的令牌桶算法,适合中小规模快速接入。

java 复制代码
/**
 * 自定义限流Key解析器
 * 按用户ID限流(已登录),未登录按IP限流
 */
@Bean
public KeyResolver userKeyResolver() {
    return exchange -> {
        String userId = exchange.getRequest().getHeaders().getFirst("X-User-Id");
        if (StringUtils.hasText(userId)) {
            return Mono.just(userId);  // 登录用户按ID限流
        }
        // 未登录用户按IP限流
        String ip = exchange.getRequest().getRemoteAddress()
            .getAddress().getHostAddress();
        return Mono.just("ip:" + ip);
    };
}
spring:
  cloud:
    gateway:
      routes:
        - id: ai-chat-service
          uri: lb://ai-chat-service
          predicates:
            - Path=/api/ai/**
          filters:
            - StripPrefix=1
            - name: RequestRateLimiter
              args:
                redis-rate-limiter.replenishRate: 10   # 令牌填充速率:10个/秒
                redis-rate-limiter.burstCapacity: 20   # 令牌桶容量:20(允许瞬时突发)
                key-resolver: "#{@userKeyResolver}"    # 引用上面的Bean

replenishRateburstCapacity 的关系:令牌桶以 replenishRate 的速度填充,桶满 burstCapacity 个令牌。每个请求消耗一个令牌,桶空了就返回 429。我的经验值是 burstCapacity = replenishRate × 2,既允许短暂突发,又不至于失控。

4.2 方案二:集成Sentinel(生产首选)

内置限流器简单但功能有限,不支持热点参数、熔断降级、系统自适应保护。生产环境我强烈推荐配合 Sentinel 使用------上一篇讲 Sentinel 时已经搭好了,这里直接在网关接入。

Maven 依赖:

java 复制代码
<!-- Spring Cloud Gateway 整合 Sentinel -->
<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-sentinel-gateway</artifactId>
    <version>2023.0.3.2</version>
</dependency>
java 复制代码
spring:
  cloud:
    sentinel:
      transport:
        dashboard: localhost:8080  # Sentinel Dashboard地址
      scg:
        fallback:
          mode: response
          response-status: 429
          response-body: '{"code":429,"message":"请求过于频繁,请稍后再试"}'
java 复制代码
/**
 * 自定义 Sentinel 网关限流分组
 * 按API分组限流,不同业务独立限流互不影响
 */
@PostConstruct
public void initGatewayRules() {
    Set<GatewayFlowRule> rules = new HashSet<>();
// AI对话接口:每秒50 QPS(AI模型并发有限)
rules.add(new GatewayFlowRule("ai_chat_group")
    .setResourceMode(SentinelGatewayConstants.RESOURCE_MODE_CUSTOM_API_NAME)
    .setCount(50)
    .setIntervalSec(1));
// 商品查询接口:每秒500 QPS(高频读接口)
rules.add(new GatewayFlowRule("product_query_group")
.setResourceMode(SentinelGatewayConstants.RESOURCE_MODE_CUSTOM_API_NAME)
.setCount(500)
.setIntervalSec(1));
// 订单创建接口:每秒100 QPS + 热点参数限流(按userId限流,防恶意刷单)
rules.add(new GatewayFlowRule("order_create_group")
.setResourceMode(SentinelGatewayConstants.RESOURCE_MODE_CUSTOM_API_NAME)
.setCount(100)
.setIntervalSec(1)
.setParamItem(new GatewayParamFlowItem()
.setParseStrategy(SentinelGatewayConstants.PARAM_PARSE_PARAM)
.setFieldName("userId")));
GatewayRuleManager.loadRules(rules);
}

Sentinel 接入网关后,可以在 Dashboard 上动态调整限流规则、实时查看 QPS 和拒绝数,比内置方案灵活得多。


五、AI网关扩展:按模型路由 + Token统计

作为 AI 工程师系列,我们得把 Gateway 和 AI 场景结合起来。一个常见的 AI 网关需求是:同一个对话接口,根据参数路由到不同的大模型服务

java 复制代码
/**
 * AI模型路由过滤器
 * 根据请求中的 model 参数,动态路由到不同的AI微服务
 * 比如:gpt → OpenAI代理服务,deepseek → DeepSeek服务,qwen → 通义千问服务
 */
@Component
public class AiModelRoutingFilter implements GlobalFilter, Ordered {
private static final Map&lt;String, String&gt; MODEL_SERVICE_MAP = Map.of(
    "gpt", "ai-gpt-service",
    "deepseek", "ai-deepseek-service",
    "qwen", "ai-qwen-service"
);
@Override
public Mono&lt;Void&gt; filter(ServerWebExchange exchange, GatewayFilterChain chain) {
String path = exchange.getRequest().getURI().getPath();
if (!path.startsWith("/api/ai/chat")) {
return chain.filter(exchange);  // 非AI接口直接放行
}
// 从查询参数或请求体中提取model
String model = exchange.getRequest().getQueryParams().getFirst("model");
if (model == null || !MODEL_SERVICE_MAP.containsKey(model)) {
    model = "deepseek";  // 默认降级到最便宜的模型
}

String targetService = MODEL_SERVICE_MAP.get(model);
// 动态修改路由URI,实现按模型路由
URI uri = URI.create("lb://" + targetService);
ServerHttpRequest request = exchange.getRequest().mutate()
    .header("X-Target-Model", model)  // 传递模型标识给后端
    .build();
ServerWebExchange mutatedExchange = exchange.mutate()
    .request(request)
    .build();
// 覆盖属性中的目标URI
mutatedExchange.getAttributes().put(ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR, uri);

return chain.filter(mutatedExchange);
}
@Override
public int getOrder() {
return -50;  // 在鉴权之后、限流之前
}
}

配合一个 Token 统计过滤器(后置),就能在网关层完成 AI 用量监控:

java 复制代码
/**
 * AI Token用量统计过滤器(后置执行)
 * 从响应头读取Token消耗,写入Redis做用量统计
 */
@Component
public class TokenUsageFilter implements GlobalFilter, Ordered {
private final StringRedisTemplate redisTemplate;
public TokenUsageFilter(StringRedisTemplate redisTemplate) {
this.redisTemplate = redisTemplate;
}
@Override
public Mono&lt;Void&gt; filter(ServerWebExchange exchange, GatewayFilterChain chain) {
String path = exchange.getRequest().getURI().getPath();
if (!path.startsWith("/api/ai/")) {
return chain.filter(exchange);
}
String userId = exchange.getRequest().getHeaders().getFirst("X-User-Id");
if (userId == null) userId = "anonymous";
long startTime = System.currentTimeMillis();

return chain.filter(exchange).doAfterTerminate(() -&amp;gt; {
    // 从响应头读取AI服务返回的Token消耗
    String tokenStr = exchange.getResponse().getHeaders().getFirst("X-Total-Tokens");
    if (tokenStr != null) {
        long tokens = Long.parseLong(tokenStr);
        long cost = System.currentTimeMillis() - startTime;
        String dateKey = LocalDate.now().toString();
        // 写入Redis:用户维度日Token用量 + 用户维度日调用耗时
        redisTemplate.opsForHash().increment("ai:tokens:" + dateKey, userId, tokens);
        redisTemplate.opsForHash().increment("ai:latency:" + dateKey, userId, cost);
    }
});
}
@Override
public int getOrder() {
return -40;  // 最低优先级,最后执行(后置统计)
}
}

这样后端 AI 服务只需要在响应头中返回 X-Total-Tokens,网关层就自动完成用量统计,业务服务完全无感知。


建议:

1. 路由配置用 YAML,动态路由用数据库 静态路由用 YAML 配置足够了,但生产环境经常需要"不改代码动态加路由"。方案是把路由规则存到 MySQL/Redis,自定义 RouteDefinitionRepository 从数据源加载,配合 RefreshRoutesEvent 动态刷新。不要把路由硬编码在 Java 里,改一次发一次版太蠢了。

2. 网关层只做横切逻辑,不写业务逻辑 网关的职责是路由、鉴权、限流、日志、监控------这些都是横切关注点。如果你发现自己在网关里写订单创建逻辑,那架构边界已经糊了。网关越薄越好,越薄越稳定。AI 路由和 Token 统计也属于横切逻辑,放在网关层是合理的。

3. 过滤器一定要排序,一定要处理异常 getOrder() 返回值决定执行顺序,生产环境务必画一张过滤器执行顺序图,贴在团队Wiki里。另外,自定义过滤器里如果抛异常,Gateway 默认会返回 500 但不给你详细信息。建议统一加一个全局异常处理的 ErrorWebExceptionHandler,把异常信息格式化成标准 JSON 返回,别让前端看到一堆 Netty 堆栈。


下一篇我们继续微服务安全话题:OAuth2.0 + JWT 微服务认证授权------从授权码模式到网关统一鉴权,把微服务的"门禁系统"搭起来。

相关推荐
浪客川2 小时前
使用 IDEA 生成API文档
java·ide·intellij-idea
上下求索,莫负韶华3 小时前
笔记-数据库事务和java事务和切面
java·数据库·笔记
@小匠3 小时前
Spring Boot Nacos绑定 Map 时中文 key 导致启动失败:一次从复现到源码的排查实录
java·开发语言·前端
Knight_AL3 小时前
Lombok @Builder 踩坑:build() 前后对象类型不一样
android·java·开发语言
SamChan904 小时前
PDF翻译中的并发控制:用Semaphore防止API限流与资源耗尽
java·jvm·pdf
大黄说说4 小时前
Java 与 Kotlin 混合开发避坑指南:老项目平滑迁移 Kotlin 实操手册
java·开发语言·kotlin
snow@li4 小时前
SpringBoot:AOP日志切面全景梳理/原理+流程+实战+避坑
java·开发语言·spring boot
ma_king5 小时前
Spring Boot 接入飞书自定义机器人
java·后端
2401_894915535 小时前
部署 GEO 优化源码常见报错排查:端口、伪静态、缓存问题解决
java·运维·服务器·后端·缓存·开源
七牛开发者5 小时前
Codex 实践系列 Vol.04:用 Goal 和 Plan 管住一个长任务
java·数据库·人工智能·github·copilot