SpringCloud 核心组件解析之服务网关
技术栈:Spring Boot 3.2.0 + Spring Cloud 2023.0.0 + Spring Cloud Gateway
已不维护:Netflix Zuul → 替代用 Gateway
前置网关:Nginx(简要介绍,通常作为 Gateway 的前置入口)
6.1 是什么 — 网关的核心概念
6.1.1 生活化类比:小区门卫
┌──────────┐
访客 ───→ │ 小区门卫 │ ───→ 各栋楼(微服务)
│ │
│ 职责: │
│ ① 查身份 │ ← 鉴权/认证
│ ② 指路 │ ← 路由转发
│ ③ 登记 │ ← 日志/监控
│ ④ 限流 │ ← 防止太多人同时涌入
└──────────┘网关 = 系统的统一入口,所有外部请求先到网关,再由网关转发到内部微服务。
6.1.2 网关 vs 直连
没有网关:用户 → http://192.168.1.10:8001/pay/get/1 ❌ 暴露内部 IP
有了网关:用户 → http://gateway.company.com/pay/get/1 ✅ 统一入口
│
▼
Gateway :9527 → Provider :8001/:80026.2 为什么 — 从 Nginx 到 Zuul 到 Gateway
6.2.1 Nginx(了解即可)
Nginx 是高性能反向代理,通常作为 Gateway 的前置层:
用户 → Nginx(SSL终结/静态资源) → Gateway(动态路由/鉴权) → 微服务
| Nginx | Spring Cloud Gateway | |
|---|---|---|
| 实现 | C 语言 | Java(Netty) |
| 动态路由 | 需 reload | 实时生效 ✅ |
| 与注册中心集成 | 需 Lua 扩展 | 原生支持 ✅ |
6.2.2 Netflix Zuul(已停更,了解即可)
| Zuul 1.x | Gateway | |
|---|---|---|
| 架构 | 同步 Servlet | 异步 Netty |
| 线程模型 | 一个请求一个线程 | 事件驱动 |
| Spring Cloud 集成 | 停止维护 | 主推 ✅ |
6.3 怎么做 — Gateway 完整实战
6.3.0 小 Demo:先暴露痛点
假设系统有 10 个微服务,各自不同端口。前端要记住 10 个 IP:Port → 维护噩梦;每个服务都要自己处理跨域、鉴权 → 代码重复;无法统一限流 → 安全风险。
引入 Gateway 后:一个端口(9527)→ 统一入口 → 路径路由 → 自动转发。
6.3.1 三板斧核心模型
Route(路由) ├── id:唯一标识 ├── uri:目标地址(lb://服务名) ├── predicates:断言(匹配规则) └── filters:过滤器(请求修改)
6.3.2 步骤 ①:引入依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-consul-discovery</artifactId>
</dependency>⚠️ Gateway 基于 WebFlux,不能引入
spring-boot-starter-web,两者冲突!
6.3.3 步骤 ②:YAML 配置路由
server:
port: 9527
spring:
application:
name: cloud-gateway
cloud:
consul:
host: localhost
port: 8500
discovery:
prefer-ip-address: true
service-name: ${spring.application.name}
gateway:
routes:
# 路由1:基础路由 + 自定义断言
- id: pay_routh1
uri: lb://cloud-payment-service
predicates:
- Path=/pay/gateway/get/**
- MyRoutePredicatedFactory="3"
# 路由2:另一个路径
- id: pay_routh2
uri: lb://cloud-payment-service
predicates:
- Path=/pay/gateway/info/**
# 路由3:带过滤器
- id: pay_routh3
uri: lb://cloud-payment-service
predicates:
- Path=/pay/gateway/filter/**
filters:
- My=atguigu关键配置解读:
| 配置项 | 说明 |
|---|---|
lb://cloud-payment-service | lb:// 是 Gateway 内置的负载均衡协议 |
Path=/pay/gateway/get/** | 通配符匹配路径(Ant 风格) |
filters | 只对当前路由生效的局部过滤器 |
6.3.4 步骤 ③:11 种内置断言
| 断言 | 示例 | 说明 |
|---|---|---|
| After | - After=2025-01-01T00:00:00+08:00 | 在此时间之后的请求 |
| Before | - Before=2025-12-31T23:59:59+08:00 | 在此时间之前的请求 |
| Between | - Between=...,... | 两个时间之间的请求 |
| Cookie | - Cookie=username,zzyy | Cookie 包含 username=zzyy |
| Header | - Header=X-Request-Id, \\d+ | Header 包含 X-Request-Id 且值为数字 |
| Host | - Host=**.atguigu.com | Host 以 .atguigu.com 结尾 |
| Method | - Method=GET | 指定 HTTP 方法 |
| Path | - Path=/api/v1/** | 按路径匹配(最常用) |
| Query | - Query=username, \\w+ | 查询参数包含 username 且值为字母 |
| RemoteAddr | - RemoteAddr=192.168.1.1/24 | 按来源 IP 匹配 |
| Weight | - Weight=group1, 8 | 按权重分流(灰度发布) |
6.3.5 步骤 ④:自定义断言 — 会员等级限制
// cloud-gateway-9527/.../mygateway/MyRoutePredicatedFactory.java
@Component
public class MyRoutePredicatedFactory
extends AbstractRoutePredicateFactory<MyRoutePredicatedFactory.Config> {
public MyRoutePredicatedFactory() {
super(Config.class);
}
@Override
public Predicate<ServerWebExchange> apply(Config config) {
return exchange -> {
String usertype = exchange.getRequest()
.getQueryParams().getFirst("userType");
return "3".equals(usertype); // 只有三级会员能访问
};
}
@Validated
public static class Config {
@Getter @Setter @NotEmpty
private String usertype;
}
@Override
public List<String> shortcutFieldOrder() {
return Collections.singletonList("userType");
}
}YAML 中:
- MyRoutePredicatedFactory="3"→ 请求必须带?userType=3。
6.3.6 步骤 ⑤:自定义局部过滤器 — 参数校验
// cloud-gateway-9527/.../mygateway/MyGatewayFilterFactory.java
@Component
public class MyGatewayFilterFactory
extends AbstractGatewayFilterFactory<MyGatewayFilterFactory.Config> {
public MyGatewayFilterFactory() { super(Config.class); }
@Override
public GatewayFilter apply(Config config) {
return (exchange, chain) -> {
// 请求必须包含参数 atguigu
if (exchange.getRequest().getQueryParams().containsKey("atguigu")) {
return chain.filter(exchange); // ✅ 放行
} else {
exchange.getResponse().setStatusCode(HttpStatus.BAD_REQUEST);
return exchange.getResponse().setComplete(); // ❌ 拦截
}
};
}
public static class Config {
@Setter @Getter
private String status;
}
@Override
public List<String> shortcutFieldOrder() {
return Arrays.asList("status");
}
}6.3.7 步骤 ⑥:全局过滤器 — 请求日志
// cloud-gateway-9527/.../mygateway/MyGlobalFilter.java
@Component
@Slf4j
public class MyGlobalFilter implements GlobalFilter, Ordered {
private static final String BEGIN_VISIT_TIME = "begin_visit_time";
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// Pre-filter:记录进入时间
exchange.getAttributes().put(BEGIN_VISIT_TIME, System.currentTimeMillis());
// Post-filter:打印日志
return chain.filter(exchange).then(Mono.fromRunnable(() -> {
Long beginTime = exchange.getAttribute(BEGIN_VISIT_TIME);
if (beginTime != null) {
log.info("访问接口: {}:{}{} 参数: {} 耗时: {}ms",
exchange.getRequest().getURI().getHost(),
exchange.getRequest().getURI().getPort(),
exchange.getRequest().getURI().getPath(),
exchange.getRequest().getURI().getRawQuery(),
System.currentTimeMillis() - beginTime);
}
}));
}
@Override
public int getOrder() { return 0; }
}Pre Filter vs Post Filter:
请求 → [Pre Filter: 鉴权/日志] → 业务处理 → [Post Filter: 修改响应/记日志] → 响应
chain.filter(exchange).then(...) 实现了 Post Filter 的效果。
6.3.8 Provider 端网关专用 Controller
// cloud-provider-payment8001/.../controller/PayGateWayController.java
@RestController
public class PayGateWayController {
@Resource
private PayService payService;
@GetMapping("/pay/gateway/get/{id}")
public ResultData<Pay> getById(@PathVariable("id") Integer id) {
return ResultData.success(payService.getById(id));
}
@GetMapping("/pay/gateway/get/info")
public ResultData<String> getInfo() {
return ResultData.success("gateway info test " + IdUtil.simpleUUID());
}
// 测试过滤器效果
@GetMapping("/pay/gateway/filter")
public ResultData<String> getFilter(HttpServletRequest request) {
String result = "";
Enumeration<String> names = request.getHeaderNames();
while (names.hasMoreElements()) {
String name = names.nextElement();
String value = request.getHeader(name);
if (name.equalsIgnoreCase("X-Request-atguigu1")
|| name.equalsIgnoreCase("X-Request-atguigu2")) {
result += name + "\t" + value + " ";
}
}
return ResultData.success("gateWayFilter 过滤器test:" + result
+ "\t" + DateUtil.now());
}
}6.4 深入原理 — Gateway 内部架构
┌──────────────────────────────────────────┐ │ Spring Cloud Gateway │ │ ┌─────────────────────────────────────┐ │ │ │ RoutePredicateHandlerMapping │ │ ← 匹配路由(遍历所有路由规则) │ └──────────────┬──────────────────────┘ │ │ ▼ │ │ ┌─────────────────────────────────────┐ │ │ │ FilteringWebHandler │ │ ← 执行过滤器链 │ │ (Pre Filter → Proxy → Post) │ │ │ └──────────────┬──────────────────────┘ │ │ ▼ │ │ ┌─────────────────────────────────────┐ │ │ │ NettyRoutingFilter │ │ ← 基于 Netty 异步非阻塞 │ └─────────────────────────────────────┘ │ └──────────────────────────────────────────┘
6.5 内置过滤器速查
| 过滤器 | 示例 | 说明 |
|---|---|---|
| PrefixPath | - PrefixPath=/pay | 给路径加前缀 |
| SetPath | - SetPath=/pay/gateway/{segment} | 重写路径 |
| RedirectTo | - RedirectTo=302, https://baidu.com | 重定向 |
| AddRequestHeader | - AddRequestHeader=X-Custom, value1 | 添加请求头 |
| RemoveRequestHeader | - RemoveRequestHeader=sec-fetch-site | 删除请求头 |
| AddRequestParameter | - AddRequestParameter=foo, bar | 添加查询参数 |
| AddResponseHeader | - AddResponseHeader=X-Response, Blue | 添加响应头 |
| RemoveResponseHeader | - RemoveResponseHeader=Content-Type | 删除响应头 |
| RequestRateLimiter | 配合 Redis 限流 | 请求限流 |
6.6 面试题
Q1:Gateway 和 Nginx 有什么区别?如何配合使用?
答:
- Nginx 是 C 语言高性能反向代理,适合静态资源、SSL 卸载、最外层入口
- Gateway 是 Java 实现,适合动态路由、鉴权、与注册中心原生集成
- 配合方式:
用户 → Nginx(SSL, 静态资源) → Gateway(动态路由, 鉴权) → 微服务
Q2:Gateway 的 Pre Filter 和 Post Filter 有什么区别?
答:Pre Filter 在转发下游前执行(鉴权、限流、加请求头);Post Filter 在下游返回响应后执行(加响应头、记日志)。Post Filter 通过 chain.filter(exchange).then(...) 实现。
Q3:如何实现网关层面的灰度发布?
答:使用 Gateway 的 Weight 路由,同一个路径按权重分流:
- id: service_v1 uri: lb://service predicates: [Path=/api/**, Weight=group1, 80] # 80% 流量 - id: service_v2 uri: lb://service-v2 predicates: [Path=/api/**, Weight=group1, 20] # 20% 流量
6.7 踩坑指南
| 坑 | 现象 | 原因 | 解决 |
|---|---|---|---|
| 🔴 Gateway 与 Web 冲突 | 启动报错 | Gateway 基于 WebFlux,不能引入 spring-boot-starter-web | 移除 web starter |
| 🔴 路由不生效 | 请求 404 | predicates 路径与请求路径不匹配 | 检查 /** vs /* 的区别 |
| 🔴 lb:// 不生效 | UnknownHostException | 未注册到 Consul 或服务名拼写错误 | 检查 Provider 注册状态 |
| 🔴 自定义过滤器不生效 | return 了 Mono.empty() | 正确的拦截姿势是 exchange.getResponse().setComplete() | 参考标准写法 |
| 🔴 Spring Boot 3.x | NoClassDefFoundError: javax | Jakarta 迁移 | 所有 javax.* 改为 jakarta.* |
6.8 章节总结
| 要点 | 说明 |
|---|---|
| 三板斧 | Route(路由规则)+ Predicate(匹配条件)+ Filter(请求处理) |
| 核心配置 | uri: lb://服务名 + predicates: Path=/xxx/** |
| 11 种内置断言 | After/Before/Cookie/Header/Host/Method/Path/Query/RemoteAddr/Weight 等 |
| 自定义组件 | 继承 AbstractRoutePredicateFactory(断言)/AbstractGatewayFilterFactory(局部过滤器)/ 实现 GlobalFilter(全局过滤器) |
| Pre/Post Filter | Pre 在转发前执行,Post 用 chain.filter().then() 实现 |
| Zuul | 已停更,Gateway 是官方替代(异步非阻塞 Netty) |
到此这篇关于SpringCloud 核心组件解析之服务网关的文章就介绍到这了,更多相关SpringCloud 服务网关内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!
相关文章
解决swaggerUI页面没有显示Controller方法的坑
这篇文章主要介绍了解决swaggerUI页面没有显示Controller方法的坑,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教2021-06-06
mybatis双重foreach如何实现遍历map中的两个list数组
本文介绍了如何解析前端传递的JSON字符串并在Java后台动态构建SQL查询条件,首先,通过JSONArray.fromObject()将JSON字符串转化为JSONArray对象,遍历JSONArray,从中提取name和infos,构建成Map对象用于Mybatis SQL映射2024-09-09


最新评论