专栏:《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);
}
}
这段代码有几个关键点:
-
用 Redis Set 存黑白名单,而不是写死在配置文件里。运营在后台一键封禁,网关实时生效,不需要重启。
-
X-Forwarded-For取第一个IP ,因为经过多层代理后这个头是客户端IP, 代理1IP, 代理2IP的链式结构。但注意------这个头可以被伪造,生产环境如果 Nginx 在前,要在 Nginx 层覆写这个头。 -
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
replenishRate 和 burstCapacity 的关系:令牌桶以 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<String, String> MODEL_SERVICE_MAP = Map.of(
"gpt", "ai-gpt-service",
"deepseek", "ai-deepseek-service",
"qwen", "ai-qwen-service"
);
@Override
public Mono<Void> 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<Void> 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(() -&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 微服务认证授权------从授权码模式到网关统一鉴权,把微服务的"门禁系统"搭起来。