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

3,213 阅读4分钟

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

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

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

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就可以了)

继续阅读代码:

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的配置类。

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

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

/**
 * 日志过滤器工厂类
 * @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;
      }
   }
}

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

/**
* 日志网关过滤器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);
    }
}

过滤器配置示例:

spring:
  application:
    name: gateway
  cloud:
    gateway:
      routes:
      - id: found
        uri: http://localhost:8081
        predicates:
        - Path=/user/**
        filters:
        - LogInfo

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

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