【SpringCloudGateway】自定义日志过滤器:获

在接触了SpringCloudGateway时,需要给路由添加一个日志记录的过滤器,难点出现在获取RequestBody上。

在阅读源码时发现了全局过滤器AdaptCachedBodyGlobalFilter,可以将RequestBody缓存到exchange中。

它的执行优先级非常高(注释编号:A),利用这一点,我们可以让这个全局网关过滤器工作,缓存requestBody,然后在自定义过滤器中读取requestBody,进行日志记录:
先来看AdaptCachedBodyGlobalFilter的源码(版本:3.0.3.RELEASE):

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
java复制代码public class AdaptCachedBodyGlobalFilter implements GlobalFilter, Ordered, ApplicationListener<EnableBodyCachingEvent> {
private ConcurrentMap<String, Boolean> routesToCache = new ConcurrentHashMap<>();
@Override
public void onApplicationEvent(EnableBodyCachingEvent event) {
//编号C. 在接收到EnableBodyCachingEvent事件时 记录需要缓存requestBody的路由。
this.routesToCache.putIfAbsent(event.getRouteId(), true);
}

@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// the cached ServerHttpRequest is used when the ServerWebExchange can not be
// mutated, for example, during a predicate where the body is read, but still
// needs to be cached.
// 如果在断言中requestBody已经被读过了,为什么cachedRequest不等于null时,也跳过了下面的缓存方法呢,
//可以看下ServerWebExchangeUtils的cacheRequestBody方法,发现只要能读取到cachedRequest,说明至少已经缓存过RequestBody了。
ServerHttpRequest cachedRequest =exchange.getAttributeOrDefault(CACHED_SERVER_HTTP_REQUEST_DECORATOR_ATTR,
null);
if (cachedRequest != null) {
exchange.getAttributes().remove(CACHED_SERVER_HTTP_REQUEST_DECORATOR_ATTR);
return chain.filter(exchange.mutate().request(cachedRequest).build());
}
// 获取exchange缓存中的 requestBody
DataBuffer body = exchange.getAttributeOrDefault(CACHED_REQUEST_BODY_ATTR, null);
Route route = exchange.getAttribute(GATEWAY_ROUTE_ATTR);
// 编号B. 如果当前的路由id没有被保存到routesToCache话,则不做缓存
if (body != null || !this.routesToCache.containsKey(route.getId())) {
return chain.filter(exchange);
}
// 如果requestBody没有被缓存,并且路由被标记为需要缓存缓存的话,去缓存requestBody
return ServerWebExchangeUtils.cacheRequestBody(exchange, (serverHttpRequest) -> {
// don't mutate and build if same request object
if (serverHttpRequest == exchange.getRequest()) {
return chain.filter(exchange);
}
return chain.filter(exchange.mutate().request(serverHttpRequest).build());
});
}

@Override
public int getOrder() {
//编号A. Order.HIGHEST_PRECEDENCE = Integer.MIN_VALUE,说明这个过滤器的执行优先级非常高。
return Ordered.HIGHEST_PRECEDENCE + 1000;
}
}

我们看到(注释编号:B)想要ServerWebExchangeUtils.cacheRequestBody(exchange,functions)方法执行(也就是缓存requestBody)的必要条件有两个:

  1. Databuffer没有被缓存到exchange的attributes对象中。
  2. 路由被标记为需要缓存,也就是this.routesToCache.containsKey(rouceId)方法必须返回true。

第一个条件不必多说,那么如何满足第二个条件呢?看到代码(注释编号:C),当本过滤器接收到事件EnableBodyCachingEvent时,会将路由ID,保存到this.routesToCache中。因此,只需要我们的自定义过滤器LogInfoGatewayFilter发送出EnableBodyCachingEvent事件,框架就会自动为我们缓存requestBody。因而在本过滤器被执行的时候,就不再需要自建ServerHttpRquest的装饰类了。

下一个问题:如何发送EnableBodyCachingEvent事件?

搜索源码发现,只有RetryGatewayFilterFactory重试过滤器工厂这个类发送了事件,其实也很好理解,当路由中设置了重试过滤器,最简便的方式就是事先缓存好请求数据。(这里不会深入RetryGatewayFilterFactory的源码 ,只需要我们从中了解如何发送RetryGatewayFilterFactory就可以了)

继续阅读代码:

1
2
3
4
5
6
7
8
9
java复制代码public GatewayFilter apply(String routeId, Repeat<ServerWebExchange> repeat, Retry<ServerWebExchange> retry) {
if (routeId != null && getPublisher() != null) {
// 发送事件,使缓存生效
getPublisher().publishEvent(new EnableBodyCachingEvent(this, routeId));
}
return (exchange, chain) -> {
trace("Entering retry-filter");
....
}

发现所有继承了AbstractGatewayFilterFactory<C>抽象过滤器网关的类都会一起继承方法getPublisher(),来获取事件推送器publier,用来发送EnableBodyCachingEvent事件。

接下来我们如何获取routeId?

同样也是参照RetryGatewayFilterFactory类,发现他的配置类RetryConfig实现了HasRouteId接口,当在RouteDefinitionRouteLocator在加载过滤器的时候,会将路由的id赋值到RetryConfig中。因此,可以模仿RetryConfig编写一个filter的配置类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码public static class LogConfig implements HasRouteId {

private String routeId;

@Override
public String getRouteId() {
return routeId;
}

@Override
public void setRouteId(String routeId) {
this.routeId = routeId;
}
}

做好了以上的准备,自定义日志过滤器LogInfoGatewayFilterFactory在被加载时,就会将开启缓存的事件发送给AdaptCachedBodyGlobalFilter,将routeId缓存起来。AdaptCachedBodyGlobalFilter在网关接收到网络请求的时候,将requestBody缓存到exchange中。

日志过滤器工厂类的代码如下:

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
java复制代码/**
* 日志过滤器工厂类
* @author ZongZi
* @date 2021/8/3 3:35 下午
*/
@Component
public class LogInfoGatewayFilterFactory extends AbstractGatewayFilterFactory<LogInfoGatewayFilterFactory.LogConfig> {

private static final Log log = LogFactory.getLog(LogInfoGatewayFilterFactory.class);
public LogInfoGatewayFilterFactory() {
super(LogConfig.class);
}
@Override
public GatewayFilter apply(LogConfig logConfig) {
String routeId = logConfig.getRouteId();
if (routeId != null && getPublisher() != null) {
// 将routeId上报,这样AdaptCachedBodyGlobalFilter就可以缓存requestBody
getPublisher().publishEvent(new EnableBodyCachingEvent(this, routeId));
}
return new LogInfoGatewayFilter();
}

// 实现HasRouteId,让框架传递路由ID进来。
public static class LogConfig implements HasRouteId {
private String routeId;

@Override
public String getRouteId() {
return routeId;
}

@Override
public void setRouteId(String routeId) {
this.routeId = routeId;
}
}
}

日志网关过滤的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
java复制代码/**
* 日志网关过滤器v1
* @author ZongZi
* @date 2021/8/2 2:17 下午
*/
public class LogInfoGatewayFilter implements GatewayFilter {

private static final Logger log = LoggerFactory.getLogger(LogInfoGatewayFilter.class);

private static final String CACHE_REQUEST_BODY_OBJECT_KEY = "cachedRequestBody";

private List<HttpMessageReader<?>> messageReaders = HandlerStrategies.withDefaults().messageReaders();

@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
// 获取被全局网关过滤器缓存起来的 requestBody
DataBuffer cachedRequestBody = exchange.getAttribute(CACHE_REQUEST_BODY_OBJECT_KEY);
CharBuffer charBuffer = StandardCharsets.UTF_8.decode(cachedRequestBody.asByteBuffer());
String s = charBuffer.toString();
System.out.println(s)
return chain.filter(exchange);
}
}

过滤器配置示例:

1
2
3
4
5
6
7
8
9
10
11
12
yml复制代码spring:
application:
name: gateway
cloud:
gateway:
routes:
- id: found
uri: http://localhost:8081
predicates:
- Path=/user/**
filters:
- LogInfo

添加LogInfo的过滤器,在启动的时候,就可以输出RquestBody啦

注:以上的代码还只在本地环境中测试,至于在正式环境中表现如何,还需要经过实践考验。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

0%