Spring Cloud Gateway 的底层
首先声明,以下提到的 “网关”,都特指微服务网关。它是微服务架构的 “唯一入口安全门卫兼交通指挥官”,它的本职工作是负责请求的 路由转发、统一鉴权与流量控制。
传统的微服务网关 zuul1.x (Zuul 2.x 基于 Netty 重构)基于同步阻塞 I/O 模型构建,严格遵循 “一请求一线程” 架构,每个请求占用一个独立线程,在等待后端 I/O 时线程处于阻塞状态,高并发下易导致线程资源耗尽 。目前 Spring Cloud 生态中 Zuul 1.x 已进入维护模式,而 Zuul 2.x 因复杂度较高且未完全融入 Spring Cloud 体系,实际落地较少,后续演进多由 Spring Cloud Gateway(基于 WebFlux/Netty 的非阻塞模型)承接 。
核心流程
Spring Cloud Gateway (以下称 SCG)的底层是 Spring WebFlux(基于 Reactor 和 Netty),它是反应式、非阻塞的架构模型,核心运行流程如下:
Netty 接收请求:底层由 Reactor Netty 启动一个高性能的非阻塞服务器。
HttpHandler 映射:请求进入 WebFlux 的 HttpHandler,随后被分发给 FilteringWebHandler。
责任链模式(过滤器链):这是网关的核心。SCG 将所有的 GatewayFilter 组装成一个链条(DefaultGatewayFilterChain)。
非阻塞转发:
响应回传:当下游服务返回数据时,触发回调,通过响应流(Reactive Stream)将数据异步写回客户端。
核心组件
为了实现这个流程,SCG 抽象出了三个核心概念,它们在 WebFlux 中都有对应的落地方案:
- Route(路由):网关的基本构建模块。包含 ID、目标 URI、断言(Predicate)集合和过滤器(Filter)集合。
- Predicate(断言):WebFlux 的 ServerWebExchange 匹配器。决定了请求是否走这条路由(例如:路径是不是 /api/v1/)。
- Filter(过滤器):标准的 GatewayFilter,利用 WebFlux 的 Mono 实现请求前置处理(Pre)和响应后置处理(Post)。
Spring Cloud Gateway 的使用
这部分内容可以参照以下文档:
使用 WebFlux 手撕一个网关
任务拆分
为了做到 “从简单到复杂”,我们将目标拆分为四个阶段。在此我们将完全脱离 Spring Cloud Gateway 的源码包,只基于 webflux 从零开始手写。
- 阶段 1:极简内核(实现非阻塞转发)。目标是用最少代码,让 WebFlux 服务接收请求,并异步转发到另一个网址。
- 阶段 2:动态路由与断言机制(Route & Predicate)。让网关支持配置多条路由,并能根据请求特征(路径、Header等)自动选择下游。
- 阶段 3:过滤器链(Filter Chain 责任链模式)。实现类似 SCG 的 Filter 机制,支持在转发前后修改请求和响应。
- 阶段 4:高级特性(性能与生产级优化)。让我们的网关具备动态配置、限流等高级能力。
阶段1 - 实现最简易网关
为了做到彻底拆解,我们必须明白:一个最基础的网关,本质上就是一个 “特殊的 Web 服务器”。它接收客户端的 HTTP 请求,自己不处理业务,而是用一个 HTTP 客户端异步转发给下游,拿到响应后再异步写回。既然底层是 WebFlux,我们将直接使用 WebFlux 提供的核心组件:
- HttpHandler / WebHandler:WebFlux 的请求总入口,负责接收并处理所有请求。
- WebClient:WebFlux 自带的、基于 Reactor Netty 的非阻塞响应式 HTTP 客户端,用来做异步转发。
项目依赖
1 2 3 4 5 6 7 8 9
| <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-webflux</artifactId> <version>3.3.0</version> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </dependency>
|
核心代码
我们常说 Spring Cloud Gateway 性能高、异步非阻塞,下面的代码就是这一特性的最直观体现。请注意两个核心点:
- 没有线程等待:当执行到 webClient.exchangeToMono(…) 时,网关并没有在原地死等 httpbin.org(一个专门用来测试 HTTP 请求的公网网站) 的响应。相反,它只是注册了一个回调通知,随后当前线程立刻被释放,去接收下一个用户的请求了。
- 基于流的写回:response.writeWith(clientResponse.bodyToFlux(…))。这意味着当下游服务开始返回数据时,数据是以一块块的 DataBuffer(数据缓冲区)流式传回来的。网关收到一块,就立刻往客户端写一块,不需要在网关内存中积压整个响应体,内存占用极小!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45
| import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.http.server.reactive.ServerHttpResponse; import org.springframework.web.reactive.function.client.WebClient; import org.springframework.web.server.WebFilter;
@Configuration public class MiniGatewayConfiguration {
private final WebClient webClient = WebClient.create();
@Bean public WebFilter myGatewayFilter() { return (exchange, chain) -> { ServerHttpRequest request = exchange.getRequest(); ServerHttpResponse response = exchange.getResponse();
String path = request.getPath().value(); String rawQuery = request.getURI().getRawQuery(); String querySuffix = (rawQuery != null) ? "?" + rawQuery : "";
String targetUrl = "http://httpbin.org" + path + querySuffix; System.out.println("[Mini-Gateway] 正在异步转发请求到: " + targetUrl);
return webClient .method(request.getMethod()) .uri(targetUrl) .headers(headers -> headers.addAll(request.getHeaders())) .body(request.getBody(), DataBuffer.class) .exchangeToMono(clientResponse -> { response.setStatusCode(clientResponse.statusCode()); response.getHeaders().addAll(clientResponse.headers().asHttpHeaders()); return response.writeWith(clientResponse.bodyToFlux(DataBuffer.class)); }); }; } }
|
启动类
1 2 3 4 5 6
| @SpringBootApplication public class GatewayApp { public static void main(String[] args) { SpringApplication.run(GatewayApp.class, args); } }
|
测试验证
1
| $ curl http://localhost:8080/get?name=koohub
|
你会发现浏览器成功展示了 httpbin.org 返回的 JSON 数据。
阶段 2:动态路由与断言
核心概念抽象
在阶段 1 中,我们把所有的流量都一股脑抛给了 httpbin.org。但在实际微服务中,网关面对的是成百上千个不同的服务。请求 /user/xxx 应该转发到 用户服务 (user-service),请求 /order/xxx 应该转发到订单服务 (order-service)。为了实现这种分流,SCG 抽象出了两个核心概念:
- Predicate(断言):一段判断逻辑(比如:判断请求路径是否匹配 /user/)。如果满足,就说明“匹配成功”。
- Route(路由):一个完整的转发规则。它包含:
- id:路由的唯一标识。
- uri:目标下游服务的地址。
- predicate:决定此路由是否生效的断言。
代码实现
下面我们就要把这两个概念用代码抽象出来。
第一步:定义断言接口(Predicate)。在 WebFlux 的世界里,所有的请求上下文都封装在 ServerWebExchange 中。因此,我们的断言本质上就是一个接收 ServerWebExchange 并返回 boolean 的函数。
1 2 3 4 5 6 7
|
@FunctionalInterface public interface MyRoutePredicate { boolean test(ServerWebExchange exchange); }
|
紧接着,我们实现一个最常用的路径匹配断言。比如配置了 /user/,只要请求路径匹配,就返回 true。这里我们借助 Spring 自带的 AntPathMatcher 来实现通配符匹配。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
|
public class PathRoutePredicate implements MyRoutePredicate {
private final AntPathMatcher pathMatcher = new AntPathMatcher(); private final String pattern;
public PathRoutePredicate(String pattern) { this.pattern = pattern; }
@Override public boolean test(ServerWebExchange exchange) { String path = exchange.getRequest().getPath().value(); return pathMatcher.match(pattern, path); } }
|
第二步:定义路由实体(Route)。有了断言,我们就可以组合出一条完整的路由规则了。
新建类 MyRoute:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
|
@Data public class MyRoute { private final String id; private final URI uri; private final MyRoutePredicate predicate;
public MyRoute(String id, URI uri, MyRoutePredicate predicate) { this.id = id; this.uri = uri; this.predicate = predicate; } }
|
第三步:实现路由定位器(RouteLocator)。网关里可能会有很多条路由,我们需要一个管理器来存储和查找它们。在 SCG 中这个组件叫 RouteLocator。为了让以后能实现“动态热更新”,我们用一个 Flux 流来返回所有的路由。新建类 MyRouteLocator:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| @Component public class MyRouteLocator { private final List<MyRoute> routes = new ArrayList<>();
public MyRouteLocator() {
routes.add(new MyRoute( "httpbin-get-route", URI.create("http://httpbin.org"), new PathRoutePredicate("/get") ));
routes.add(new MyRoute( "baidu-route", URI.create("https://www.baidu.com"), new PathRoutePredicate("/baidu/**") )); }
public Flux<MyRoute> getRoutes() { return Flux.fromIterable(routes); } }
|
第四步:组装升级我们的网关内核。现在我们回到最开始的 MiniGatewayConfiguration,升级我们的 WebFilter。它的逻辑将变为:
- 请求进来,遍历 MyRouteLocator 中所有的路由。
- 哪个路由的 Predicate.test(exchange) 返回 true,就选哪个路由。
- 如果找到了,就提取该路由的 uri 进行动态拼接和转发;如果找不到,说明没有配置此路由,直接返回 404。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46
| @Configuration public class MiniGatewayConfiguration { private final WebClient webClient = WebClient.create();
@Bean public WebFilter myGatewayFilter(MyRouteLocator routeLocator) { return (exchange, chain) -> { return routeLocator.getRoutes() .filter(route -> route.getPredicate().test(exchange)) .next() .flatMap(route -> { ServerHttpRequest request = exchange.getRequest(); ServerHttpResponse response = exchange.getResponse();
String path = request.getPath().value(); String rawQuery = request.getURI().getRawQuery(); String querySuffix = (rawQuery != null) ? "?" + rawQuery : "";
String targetUrl = route.getUri().toString() + path + querySuffix; System.out.println("[Gateway] 路由匹配成功 [" + route.getId() + "], 正在转发至: " + targetUrl);
return webClient .method(request.getMethod()) .uri(targetUrl) .headers(headers -> headers.addAll(request.getHeaders())) .body(request.getBody(), DataBuffer.class) .exchangeToMono(clientResponse -> { response.setStatusCode(clientResponse.statusCode()); response.getHeaders().addAll(clientResponse.headers().asHttpHeaders()); return response.writeWith(clientResponse.bodyToFlux(DataBuffer.class)); }); }) .switchIfEmpty(Mono.defer(() -> { ServerHttpResponse response = exchange.getResponse(); response.setStatusCode(HttpStatus.NOT_FOUND); System.out.println("[Gateway] 警告:没有找到匹配的路由规则!路径: " + exchange.getRequest().getPath().value()); return response.setComplete(); })); }; } }
|
测试验证
1 2 3 4 5
| $ curl http://localhost:8080/get?name=koohub
$ curl http://localhost:8080/hello
|
我们现在不仅能转发,还能根据业务规则实现分流了。
阶段 3:过滤器责任链
在 Spring Cloud Gateway 中,过滤器是灵魂所在。鉴权、日志、限流、修改请求头等所有核心业务,全部是由一个个过滤器组合而成的。传统 Servlet 的 Filter 责任链是用循环或递归阻塞执行的,而 WebFlux 是非阻塞、异步的。我们需要用 Reactor 的 Mono<Void> 把一堆过滤器串成一条异步流式责任链。下面我们仿照 SCG 的底层设计,开始组装我们的过滤器框架。
核心概念抽象
为了搞定过滤器链,我们需要抽象出三个核心角色:
- MyGatewayFilter(过滤器接口):每个过滤器要实现的接口,负责执行具体的拦截逻辑。
- MyGatewayFilterChain(过滤器链条):负责控制过滤器一个接一个地往下走(类似于指针或计数器)。
- Pre/Post 机制:
- Pre(前置执行):在远程转发之前做的事(如:鉴权、加请求头)。
- Post(后置执行):在远程转发拿到响应后做的事(如:记录耗时、修改响应体)。
代码实现
定义过滤器链接口:网关过滤器链需要维持当前执行的状态,并驱动下一个过滤器。
1 2 3 4 5 6 7 8 9
|
public interface MyGatewayFilterChain {
Mono<Void> filter(ServerWebExchange exchange); }
|
定义过滤器接口:
1 2 3 4 5 6
|
public interface MyGatewayFilter { Mono<Void> filter(ServerWebExchange exchange, MyGatewayFilterChain chain); }
|
实现核心:异步责任链驱动器。这是最精妙的地方。如何不用 for 循环,而是用纯响应式代码让过滤器按顺序执行?我们需要实现一个 DefaultMyGatewayFilterChain,每次调用 filter 时,下标 +1,并实例化下一个链条对象。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
|
public class DefaultMyGatewayFilterChain implements MyGatewayFilterChain {
private final List<MyGatewayFilter> filters; private final int index;
public DefaultMyGatewayFilterChain(List<MyGatewayFilter> filters) { this.filters = filters; this.index = 0; }
private DefaultMyGatewayFilterChain(DefaultMyGatewayFilterChain parent, int index) { this.filters = parent.filters; this.index = index; }
@Override public Mono<Void> filter(ServerWebExchange exchange) { if (this.index >= filters.size()) { return Mono.empty(); }
MyGatewayFilter filter = filters.get(this.index); DefaultMyGatewayFilterChain nextChain = new DefaultMyGatewayFilterChain(this, this.index + 1);
return filter.filter(exchange, nextChain); } }
|
编写两个实际工作的过滤器(Pre 与 Post)。
前置过滤器:AddHeaderFilter
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| public class AddHeaderFilter implements MyGatewayFilter { @Override public Mono<Void> filter(ServerWebExchange exchange, MyGatewayFilterChain chain) { System.out.println("[Filter] -> 执行 AddHeaderFilter (Pre 阶段)");
ServerHttpRequest modifiedRequest = exchange.getRequest().mutate() .header("X-Gateway-From", "Koohub-Custom-Gateway") .build();
ServerWebExchange modifiedExchange = exchange.mutate().request(modifiedRequest).build();
return chain.filter(modifiedExchange); } }
|
后置过滤器:LogTimerFilter
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| public class LogTimerFilter implements MyGatewayFilter { @Override public Mono<Void> filter(ServerWebExchange exchange, MyGatewayFilterChain chain) { System.out.println("[Filter] -> 进入 LogTimerFilter (Pre 阶段)"); long startTime = System.currentTimeMillis();
return chain.filter(exchange).then(Mono.fromRunnable(() -> { long executeTime = System.currentTimeMillis() - startTime; System.out.println("[Filter] -> 离开 LogTimerFilter (Post 阶段),请求耗时: " + executeTime + "ms"); })); } }
|
现在我们需要修改 MyRoute 实体,让每一条路由都能携带自己的过滤器集合。然后修改 MiniGatewayConfiguration,在匹配到路由后,先组装并执行过滤器链,而最后的远程转发。
更新 MyRoute:为路由增加 filters 属性。
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| @Data public class MyRoute { private final String id; private final URI uri; private final MyRoutePredicate predicate; private final List<MyGatewayFilter> filters;
public MyRoute(String id, URI uri, MyRoutePredicate predicate, List<MyGatewayFilter> filters) { this.id = id; this.uri = uri; this.predicate = predicate; this.filters = filters; } }
|
更新 MyRouteLocator:为我们的测试路由塞入刚刚写好的过滤器。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| @Component public class MyRouteLocator { private final List<MyRoute> routes = new ArrayList<>(); List<MyGatewayFilter> filterList = List.of(new LogTimerFilter(), new AddHeaderFilter());
public MyRouteLocator() { routes.add(new MyRoute( "httpbin-get-route", URI.create("http://httpbin.org"), new PathRoutePredicate("/get"), filterList ));
routes.add(new MyRoute( "baidu-route", URI.create("https://www.baidu.com"), new PathRoutePredicate("/baidu/**"), filterList )); }
public Flux<MyRoute> getRoutes() { return Flux.fromIterable(routes); } }
|
最终重构 MiniGatewayConfiguration:现在,我们要把 “网络转发” 包装成一个特殊的、放在最末尾的全局过滤器(在 SCG 中叫 NettyRoutingFilter)。这样,整个网关的运行就完全变成了过滤器链的舞台!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44
| @Configuration public class MiniGatewayConfiguration { private final WebClient webClient = WebClient.create();
@Bean public WebFilter myGatewayFilter(MyRouteLocator routeLocator) { return (exchange, chain) -> routeLocator.getRoutes() .filter(route -> route.getPredicate().test(exchange)) .next() .flatMap(route -> { List<MyGatewayFilter> gatewayFilters = new ArrayList<>(route.getFilters());
gatewayFilters.add((currExchange, currChain) -> { ServerHttpRequest request = currExchange.getRequest(); ServerHttpResponse response = currExchange.getResponse(); String path = request.getPath().value(); String rawQuery = request.getURI().getRawQuery(); String querySuffix = (rawQuery != null) ? "?" + rawQuery : ""; String targetUrl = route.getUri().toString() + path + querySuffix;
return webClient .method(request.getMethod()) .uri(targetUrl) .headers(headers -> headers.addAll(request.getHeaders())) .body(request.getBody(), DataBuffer.class) .exchangeToMono(clientResponse -> { response.setStatusCode(clientResponse.statusCode()); response.getHeaders().addAll(clientResponse.headers().asHttpHeaders()); return response.writeWith(clientResponse.bodyToFlux(DataBuffer.class)); }); });
DefaultMyGatewayFilterChain filterChain = new DefaultMyGatewayFilterChain(gatewayFilters); return filterChain.filter(exchange); }) .switchIfEmpty(Mono.defer(() -> { exchange.getResponse().setStatusCode(HttpStatus.NOT_FOUND); return exchange.getResponse().setComplete(); })); } }
|
测试验证
重启网关,再次访问。看浏览器响应:你会发现 httpbin.org 返回的 JSON 中,headers 里面多了一项:
“X-Gateway-From”: “Koohub-Custom-Gateway”。说明我们的 AddHeaderFilter 修改请求成功!
1
| $ curl http://localhost:8080/get?name=koohub
|
看控制台打印:就是一个标准的洋葱模型!LogTimerFilter 准确捕捉到了后续所有过滤器加网络请求的总耗时。
1 2 3
| [Filter] -> 进入 LogTimerFilter (Pre 阶段) [Filter] -> 执行 AddHeaderFilter (Pre 阶段) [Filter] -> 离开 LogTimerFilter (Post 阶段),请求耗时: 1921ms
|
阶段4:动态配置与限流
实现路由热更新
在前面的阶段中,我们的 MyRouteLocator 是在构造方法里硬编码死路由的。现在我们要把它改造成一个支持动态发布事件的响应式路由中心。
改写 MyRouteLocator。用以下代码完全覆盖 MyRouteLocator。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| import java.util.List; import java.util.concurrent.CopyOnWriteArrayList;
@Component public class MyRouteLocator {
private final List<MyRoute> routes = new CopyOnWriteArrayList<>();
public void addRoute(MyRoute route) { this.routes.add(route); System.out.println("[Route-Center] 成功动态添加路由: " + route.getId()); }
public void clearRoutes() { this.routes.clear(); System.out.println("[Route-Center] 路由已全部清空"); }
public Flux<MyRoute> getRoutes() { return Flux.fromIterable(routes); } }
|
为了测试热更新,我们写一个标准的 WebFlux Controller,暴露出一个通过 API 动态添加路由的入口。新建类 RouteManageController。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41
| @RestController public class RouteManageController { private final MyRouteLocator routeLocator;
public RouteManageController(MyRouteLocator routeLocator) { this.routeLocator = routeLocator; }
@GetMapping("/admin/refresh") public String refresh() { routeLocator.clearRoutes();
routeLocator.addRoute(new MyRoute( "dynamic-httpbin-route", URI.create("http://httpbin.org"), new PathRoutePredicate("/get"), List.of(new LogTimerFilter(), new AddHeaderFilter()) )); return "路由热更新成功!"; }
@GetMapping("/admin/add") public String addCustomRoute(@RequestParam String path, @RequestParam String target) { routeLocator.addRoute(new MyRoute( "custom-route-" + System.currentTimeMillis(), URI.create(target), new PathRoutePredicate(path), List.of(new LogTimerFilter()) )); return "成功动态添加路径 " + path + " 到 " + target + " 的路由!"; } }
|
手写限流过滤器
网关作为微服务的总闸门,必须能挡住突发流量。我们来手写一个简易的令牌桶(Token Bucket)限流过滤器。
- 桶中最多装 $N$ 个令牌。
- 每一个请求过来,消耗一个令牌。如果桶空了,直接拒绝请求(返回 429 Too Many Requests)。
- 每秒钟以固定速率往桶里补水(令牌)。
新建过滤器类 RateLimitFilter:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46
| public class RateLimitFilter implements MyGatewayFilter { private final long capacity; private final double refillRate;
private double tokens; private long lastRefillTime;
public RateLimitFilter(long capacity, double refillRate) { this.capacity = capacity; this.refillRate = refillRate; this.tokens = capacity; this.lastRefillTime = System.currentTimeMillis(); }
private synchronized boolean tryConsume() { long now = System.currentTimeMillis(); double tokensToAdd = ((now - lastRefillTime) / 1000.0) * refillRate;
this.tokens = Math.min(capacity, this.tokens + tokensToAdd); this.lastRefillTime = now;
if (this.tokens >= 1.0) { this.tokens -= 1.0; return true; } return false; }
@Override public Mono<Void> filter(ServerWebExchange exchange, MyGatewayFilterChain chain) { if (tryConsume()) { return chain.filter(exchange); } else { System.out.println("[RateLimit] -> 警告:流量超限,触发限流保护!"); exchange.getResponse().setStatusCode(HttpStatus.TOO_MANY_REQUESTS); return exchange.getResponse().setComplete(); } } }
|
修改 Controller 引入限流器:我们修改刚刚写好的 RouteManageControlle 中的 refresh 方法,把限流过滤器也塞进路由里。我们将限流器设置容量 2,每秒只生成 0.5 个令牌(这样我们疯狂刷新时就能轻松触发限流)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| @GetMapping("/admin/refresh") public String refresh() { routeLocator.clearRoutes();
RateLimitFilter rateLimitFilter = new RateLimitFilter(2, 0.5); routeLocator.addRoute(new MyRoute( "dynamic-httpbin-route", URI.create("http://httpbin.org"), new PathRoutePredicate("/get"), List.of(new LogTimerFilter(), rateLimitFilter, new AddHeaderFilter()) )); return "路由热更新成功!"; }
|
另外我们需要在网关的全局过滤器中,把管理后台的接口(比如 /admin/)给特批放行,不让它们走网关的转发逻辑,而是继续往下传递,交给 Spring WebFlux 的 Controller 自己处理。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53
| @Configuration public class MiniGatewayConfiguration { private final WebClient webClient = WebClient.create(); private final AntPathMatcher pathMatcher = new AntPathMatcher();
@Bean public WebFilter myGatewayFilter(MyRouteLocator routeLocator) { return (exchange, chain) -> { String path = exchange.getRequest().getPath().value();
if (pathMatcher.match("/admin/**", path)) { return chain.filter(exchange); }
return routeLocator.getRoutes() .filter(route -> route.getPredicate().test(exchange)) .next() .flatMap(route -> { List<MyGatewayFilter> gatewayFilters = new ArrayList<>(route.getFilters());
gatewayFilters.add((currExchange, currChain) -> { ServerHttpRequest request = currExchange.getRequest(); ServerHttpResponse response = currExchange.getResponse(); String rawQuery = request.getURI().getRawQuery(); String querySuffix = (rawQuery != null) ? "?" + rawQuery : ""; String targetUrl = route.getUri().toString() + path + querySuffix;
return webClient .method(request.getMethod()) .uri(targetUrl) .headers(headers -> headers.addAll(request.getHeaders())) .body(request.getBody(), DataBuffer.class) .exchangeToMono(clientResponse -> { response.setStatusCode(clientResponse.statusCode()); response.getHeaders().addAll(clientResponse.headers().asHttpHeaders()); return response.writeWith(clientResponse.bodyToFlux(DataBuffer.class)); }); });
DefaultMyGatewayFilterChain filterChain = new DefaultMyGatewayFilterChain(gatewayFilters); return filterChain.filter(exchange); }) .switchIfEmpty(Mono.defer(() -> { System.out.println("[Gateway] 警告:没有找到匹配的路由规则!路径: " + path); exchange.getResponse().setStatusCode(HttpStatus.NOT_FOUND); return exchange.getResponse().setComplete(); })); }; } }
|
测试验证
测试热更新前的 404:
1 2
| $ curl http://localhost:8080/get
|
激活热更新:
1 2
| $ curl http://localhost:8080/admin/refresh
|
再次测试路由:
1 2
| $ curl http://localhost:8080/get?name=koohub
|
疯狂刷新破坏限流器:
1 2 3 4
|
$ curl http://localhost:8080/get?name=koohub
|
稍微等两秒,让令牌桶恢复一下,再次刷新,又可以正常访问了!
标题:
Spring Cloud Gateway - 从 0 到 1 构建一个网关