3. 缓存请求body引起的gateway filter重复执行

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的方案和解决问题的过程。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值