文章目录
1. 问题
今天我在看gateway源码,debug gateway的过程中,无意中发现项目中gateway filter链会执行两次。
2. 问题复现
项目中filter链如下图所示
发起请求时filter链第一次执行完毕,执行到 Mono.empty()
断点放行后,调用链第二次执行。
第二次执行时,必要的参数并未获取到,返回Mono.error异常,filter链停止执行。
由于第一次执行时,结果已经返回给了调用方,调用方并不会感知异常,导致问题被隐藏起来。
3. 问题思考
问题的现象是调用链重复执行了,既然这样,在filter中应该有重复调用chain.filter(exchange)的地方。为什么filter从1开始执行,而不是从0开始执行呢?说明重复调用chain.filter(exchange)的代码就在filter 0中。
4. 猜测验证
为了验证以上猜测,我把filter 0调整了一下位置,调整成第2个。
再次执行调用,filter链第一次执行完毕
debug通过后,果然从filter 3开始重复执行了。
5. 盘盘filter 0
问题出现在filter 0中,那就要仔细盘盘filter 0了。我使用gateway做开放平台的网关,需要获取body中的数据进行业务操作,之后在把body传给业务服务。gateway基于webflux,但webflux的body数据只能获取一次,获取后数据源头就没有数据了,因此需要把body数据缓存起来,filter 0 就是用来缓存body的。通过百度,我参考了如下代码:
return DataBufferUtils
.join(exchange.getRequest().getBody())
.flatMap(dataBuffer -> {
DataBufferUtils.retain(dataBuffer);
Flux<DataBuffer> cachedFlux = Flux
.defer(() -> Flux.just(dataBuffer.slice(0, dataBuffer.readableByteCount())));
ServerHttpRequest mutatedRequest = new ServerHttpRequestDecorator(exchange.getRequest()) {
@Override
public Flux<DataBuffer> getBody() {
return cachedFlux;
}
};
return chain.filter(exchange.mutate().request(mutatedRequest).build());
}).switchIfEmpty(chain.filter(exchange)); //注意,此行是我后加的
但此代码有一个问题,就是body有可能为null(比如用户调用的时候,没传body),此时不能正常执行filter链,所以我在以上代码的最后加了一行switchIfEmpty(chain.filter(exchange));问题就出现在这一行代码上,去掉此行代码测试后发现filter 链正常执行。
那缓存body应该如何做呢,body为空时应该怎么处理呢?但就以上代码来说switchIfEmpty不应该加载最后,应该加载body为空的位置,也就是join的后边,类似这样
但我没有采用这种方式,因为有两个filter的名字引起了我的注意,RemoveCacheBodyFilter和AdaptCacheBodyGlobalFilter,从名字上看,他们两个在做body缓存和清除body缓存的事情。
6. 问题解决
6.1AdaptCacheBodyGlobalFilter缓存body逻辑
通过研究源码,我发现正如名字所暗示的,它两个确实在做body缓存和清除body缓存的事情,源码以后有机会再细分享,此处我主要分享跟怎么使用有关的部分。
AdaptCacheBodyGlobalFilter实现了ApplicationListener接口,监听EnableBodyCachingEvent事件,在onApplicationEvent方法中填充routesToCache Map的值
public class AdaptCachedBodyGlobalFilter
implements GlobalFilter, Ordered, ApplicationListener<EnableBodyCachingEvent> {
private ConcurrentMap<String, Boolean> routesToCache = new ConcurrentHashMap<>();
@Override
public void onApplicationEvent(EnableBodyCachingEvent event) {
this.routesToCache.putIfAbsent(event.getRouteId(), true);
}
在filter方法中,routesToCache map用来判断route的id是否在map中,如果不在就跳过,如果在就缓存body。
if (body != null || !this.routesToCache.containsKey(route.getId())) {
return chain.filter(exchange);
}
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());
});
6.2 AdaptCacheBodyGlobalFilter缓存逻辑触发
AdaptCacheBodyGlobalFilter缓存逻辑主要通过gateway读取路由配置时触发。
我项目中路由配置是存放在nacos中的,我的触发逻辑写在初始化或者更新nacos配置时,代码如下所示,dynamicRouteService.updateList(routeDefinitions)是初始化或者更新时使用的逻辑
ConfigService configService = NacosFactory.createConfigService(properties);
configService.addListener(dataId, group, new Listener() {
@Override
public void receiveConfigInfo(String configInfo) {
List<RouteDefinition> routeDefinitions = JSON.parseArray(configInfo, RouteDefinition.class);
dynamicRouteService.updateList(routeDefinitions);//
}
@Override
public Executor getExecutor() {
return null;
}
});
String configInfo = configService.getConfig(dataId, group, 5000);
if (configInfo != null) {
List<RouteDefinition> routeDefinitions = JSON.parseArray(configInfo, RouteDefinition.class);
dynamicRouteService.updateList(routeDefinitions);//
}
updateList在迭代每一个route的过程中发布了enableBodyCachingEvent事件,代码如下所示:
public String updateList(List<RouteDefinition> routeDefinitions) {
routeDefinitions.forEach(this::update);
routeDefinitions.forEach(item ->{
EnableBodyCachingEvent enableBodyCachingEvent = new EnableBodyCachingEvent(new Object(), item.getId());
//发布事件
applicationContext.publishEvent(enableBodyCachingEvent);
});
return "update done";
}
如果是通过配置文件的方式,可以引入GatewayProperties,通过GatewayProperties中的getRoutes方法迭代路由。
@Autowired
private GatewayProperties gatewayProperties;
@PostConstruct
public void init() {
gatewayProperties.getRoutes().forEach(routeDefinition -> {
EnableBodyCachingEvent enableBodyCachingEvent = new EnableBodyCachingEvent(new Object(), routeDefinition.getId());
adaptCachedBodyGlobalFilter.onApplicationEvent(enableBodyCachingEvent);
});
}
亲测问题解决。
7. 总结
本文分享了百度到的缓存请求body的方法和此方法的缺陷以及在改进缺陷的过程中引入的filter 链重复执行的问题,分享了gateway官方缓存body的方案和解决问题的过程。