Spring Cloud Gateway 设置全局接口访问日志

虽然网关只做转发,但是对于每个转发的请求,我们都希望能够在日志中打印出请求的信息,网上版本很多,踩了很多坑,目前没找到完美的解决方案,最后我这个应该是大成版。希望对大家有用。

先贴代码,再说遇到什么坑吧。

/**
 * @author chenzhangx
 * @date 2021/11/30 15:09
 */
@Component
public class AccessFilter extends AbstractFilter implements GlobalFilter, Ordered {

    private static Logger logger = LoggerFactory.getLogger(AccessFilter.class.getSimpleName());

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {

        return 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() {
        return -4;
    }

    //---------------------------------------------- private ---------------------------------------------------------

    private void logInfo(ServerHttpRequest request, String body) {

        String uri = request.getPath().value();
        String params = request.getQueryParams().toString();
        String method = request.getMethodValue();
        String ip = request.getRemoteAddress().toString();
        String headers = request.getHeaders().entrySet()
                .stream()
                .map(entry -> entry.getKey() + ": [" + String.join(";", entry.getValue()) + "]")
                .collect(Collectors.joining("\n"));
        long accessDate = System.currentTimeMillis();

        StringBuilder sb = new StringBuilder();
        sb.append("\n==================================[API_CALL]==================================\n");
        sb.append("uri : " + uri + "\n");
        sb.append("method : " + method + "\n");
        sb.append("ip : " + ip + "\n");
        sb.append("params : " + params + "\n");
        sb.append("body : " + body + "\n");
        sb.append("accessDate : " + accessDate + "\n");
        sb.append("headers : { \n" + headers + "  }\n");
        sb.append("==============================================================================\n");
        logger.info(String.valueOf(sb));
    }

    private Mono<Void> cacheRequestBody(ServerWebExchange exchange, Function<ServerHttpRequest, Mono<Void>> function) {
        ServerHttpResponse response = exchange.getResponse();
        ServerHttpRequest request = exchange.getRequest();
        NettyDataBufferFactory factory = (NettyDataBufferFactory) response.bufferFactory();
        // Join all the DataBuffers so we have a single DataBuffer for the body
        return DataBufferUtils.join(exchange.getRequest().getBody())
                .defaultIfEmpty(factory.wrap(new EmptyByteBuf(factory.getByteBufAllocator())))
                .map((dataBuffer) -> {
                    byte[] bytes = new byte[dataBuffer.readableByteCount()];
                    dataBuffer.read(bytes);
                    String bodyString = new String(bytes, StandardCharsets.UTF_8);
                    logInfo(request, bodyString);
                    // 这里下面的代码我原先没写,后续的转发直接失效,因为body数据被拿出来了
                    Flux<DataBuffer> cachedFlux = Flux.defer(() -> {
                        DataBuffer buffer = exchange.getResponse().bufferFactory()
                                .wrap(bytes);
                        return Mono.just(buffer);
                    });

                    return (ServerHttpRequest) new ServerHttpRequestDecorator(
                            exchange.getRequest()) {
                        @Override
                        public Flux<DataBuffer> getBody() {
                            return cachedFlux;
                        }
                    };
                }).switchIfEmpty(Mono.just(exchange.getRequest())).flatMap(function);
    }

}

思路就是定义一个全局Filter,因为是接入日志的打印,所以order小点。

代码就这些,复制即用,说下遇到的坑吧

  • 1、日志打印完后,body消失转发失败 这个是因为流传输中,你把buffer里面的数据拿出来了,但是不再塞进去,他就没了哦,所以需要再自己包装一个request
  • 2、如果是post请求,但是没有请求体,就会出现过滤器不通过的情况(如果不知道我说啥忽略 这个是真的坑,看网上的方法是判断是否是post或者header中的content length来判断是否有请求体,但是post可以没有请求体,contentLength也可以不传)
  • 3、如果将获取body的过程提取为方法,可能是webflux异步的原因(我猜的,我太菜了,如果知道的大佬评论说一下),会先return,然后body信息就拿不到为null,所以网上的方法都是将提取body的代码写在return中的。

希望有帮到大家!