Spring Cloud Gateway - 从 0 到 1 构建一个网关

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)。

  • 非阻塞转发:

    • 路由匹配成功后,通过一个特殊的过滤器(NettyRoutingFilter)使用 HttpClient(同样是 Reactor Netty 的非阻塞客户端)将请求异步转发给下游服务。

    • 在等待下游服务返回的过程中,当前线程不会被阻塞,而是立刻去处理其他进入网关的请求。

  • 响应回传:当下游服务返回数据时,触发回调,通过响应流(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 {

// 创建非阻塞的 WebClient
private final WebClient webClient = WebClient.create();

@Bean
public WebFilter myGatewayFilter() {
return (exchange, chain) -> {
ServerHttpRequest request = exchange.getRequest();
ServerHttpResponse response = exchange.getResponse();

// 1. 获取请求路径和参数
String path = request.getPath().value();
String rawQuery = request.getURI().getRawQuery();
String querySuffix = (rawQuery != null) ? "?" + rawQuery : "";

// 2. 拼接目标下游 URL
String targetUrl = "http://httpbin.org" + path + querySuffix;
System.out.println("[Mini-Gateway] 正在异步转发请求到: " + targetUrl);

// 3. 核心转发逻辑
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; // 配置的路径模式,例如 "/user/**"

public PathRoutePredicate(String pattern) {
this.pattern = pattern;
}

@Override
public boolean test(ServerWebExchange exchange) {
// 获取当前请求的实际路径
String path = exchange.getRequest().getPath().value();
// 运用 Ant 规则进行匹配
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() {
// 在这里,我们先手动模拟硬编码配置两条路由规则

// 路由1:凡是访问 /get 的请求,都路由到 httpbin.org
routes.add(new MyRoute(
"httpbin-get-route",
URI.create("http://httpbin.org"),
new PathRoutePredicate("/get")
));

// 路由2:凡是访问 /baidu/** 的请求,都路由到 baidu.com
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) -> {
// 1. 在路由池中,寻找第一个能够匹配当前请求断言的路由
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 : "";

// 2. 动态获取路由中配置的 Target URI (不再硬编码)
String targetUrl = route.getUri().toString() + path + querySuffix;
System.out.println("[Gateway] 路由匹配成功 [" + route.getId() + "], 正在转发至: " + targetUrl);

// 3. 异步非阻塞转发
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));
});
})
// 4. 如果匹配不到任何路由,说明网关不认这个路径,直接返回 404 Not Found
.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
# 正常返回 httpbin 的数据。
$ curl http://localhost:8080/get?name=koohub

# 页面直接显示 404。
$ 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) {
// 如果已经执行完了最后一个过滤器,直接返回空 Mono,结束链条
if (this.index >= filters.size()) {
return Mono.empty();
}

// 获取当前过滤器
MyGatewayFilter filter = filters.get(this.index);
// 创建下一个链条节点(索引 + 1)
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 阶段)");

// 修改 Request,暗中加上一个网关特有的 Header
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();

// 先调用 chain.filter 放行,然后利用 then() 编排后置逻辑
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() {
// 路由1:凡是访问 /get 的请求,都路由到 httpbin.org
routes.add(new MyRoute(
"httpbin-get-route",
URI.create("http://httpbin.org"),
new PathRoutePredicate("/get"),
filterList
));

// 路由2:凡是访问 /baidu/** 的请求,都路由到 baidu.com
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 -> {
// 1. 获取该路由自带的过滤器
List<MyGatewayFilter> gatewayFilters = new ArrayList<>(route.getFilters());

// 2. 核心:把我们的 “网络转发引擎” 作为最后一个过滤器追加到链条末尾!
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));
});
});

// 3. 构造过滤器链执行器,并启动执行
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;
}

/**
* 动态刷新/初始化路由的接口
* 测试路径:http://localhost:8080/admin/refresh
*/
@GetMapping("/admin/refresh")
public String refresh() {
routeLocator.clearRoutes();

// 动态注入一条新路由:把 /get 转发到 httpbin.org
routeLocator.addRoute(new MyRoute(
"dynamic-httpbin-route",
URI.create("http://httpbin.org"),
new PathRoutePredicate("/get"),
List.of(new LogTimerFilter(), new AddHeaderFilter())
));
return "路由热更新成功!";
}

/**
* 甚至可以通过参数动态指定转发到哪里
* 测试路径:http://localhost:8080/admin/add?path=/baidu/**&target=https://www.baidu.com
*/
@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 {
// 没拿到令牌,属于非法激增流量,直接拒绝响应,非阻塞返回 429
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();

// 加上一个严厉的限流过滤器:容量2,每秒恢复0.5个令牌
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();

// 如果是管理后台的接口,直接调用 chain.filter(exchange) 放行,这样请求就会漏给后方的 RouteManageController!
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
# 启动网关后,直接访问 http://localhost:8080/get。此时路由池是空的,网关会抛出 404。
$ curl http://localhost:8080/get

激活热更新:

1
2
# 访问 http://localhost:8080/admin/refresh。页面提示“路由热更新成功!”。
$ curl http://localhost:8080/admin/refresh

再次测试路由:

1
2
# 不需要重启,网关已经实时认得了这个路径并成功转发
$ curl http://localhost:8080/get?name=koohub

疯狂刷新破坏限流器:

1
2
3
4
# 鼠标连续狂点刷新 http://localhost:8080/get?name=koohub。
# 前两次可以正常访问,当点到第三四次时,页面会瞬间卡死并显示 429 Too Many Requests(或者空白):
# 控制台打印 [RateLimit] -> 警告:流量超限,触发限流保护!
$ curl http://localhost:8080/get?name=koohub

稍微等两秒,让令牌桶恢复一下,再次刷新,又可以正常访问了!