SpringCloud GateWay网关通过全局拦截器GlobalFilter实现API日志

SpringCloud GateWay+RocketMQ实现API访问日志收集

需求背景

产品经理突然找到我说,咱们这个产品貌似没有实现之前旧的系统平台操作日志了;希望我尽快实现这个需求,以应对一些检查;因为时间关系再加上人员问题,跟我原先规划得有些背道而驰
在这里插入图片描述

草拟方案

1.写一个AOP日志Starter,再需要的模块中引入,对应方法去标记注解,工程量比较大,目前所有的模块的都得逐步去添加,个人比较懒,因此该方案备选
2. 在网关层通过全局拦截器Filter拦截所有请求,通过MQ记录日志,再通过监听MQ实现日志入库,因为原先的架构已经有MQ了,所以觉得这种方案更快捷,因为撸起袖子往下干

具体实现(推荐使用方式1)

之前一直在看如何去获取请求体;各种区分MediaType跟Method对应不同的读取方式,解析重新构建请求往下游传递,中间出现了各种问题;没有解决的一个情况是有几个接口都是base64图片传参的,早前的通过BodyInserter 去重新构建请求体跟获取响应体,遇到这几个接口都会出现报错

private Mono<Void> writeBodyLog(ServerWebExchange exchange, GatewayFilterChain chain, GatewayLog gatewayLog) {
        ServerRequest serverRequest = ServerRequest.create(exchange, messageReaders);
        Mono<String> modifiedBody = serverRequest.bodyToMono(String.class)
                .flatMap(body -> {
                    gatewayLog.setRequestBody(body);
                    return Mono.just(body);
                });
        // 通过 BodyInserter 插入 body, 避免 request body 只能获取一次
        BodyInserter bodyInserter = BodyInserters.fromPublisher(modifiedBody, String.class);
        HttpHeaders headers = new HttpHeaders();
        headers.putAll(exchange.getRequest().getHeaders());
        // the new content type will be computed by bodyInserter
        // and then set in the request decorator
        headers.remove(HttpHeaders.CONTENT_LENGTH);
        CachedBodyOutputMessage outputMessage = new CachedBodyOutputMessage(exchange, headers);
 return bodyInserter.insert(outputMessage, new BodyInserterContext()).then(Mono.defer(() -> {
 	// 重新封装请求
	ServerHttpRequest decoratedRequest = requestDecorate(exchange,headers,outputMessage);
	// 处理响应日志
	ServerHttpResponseDecorator decoratedResponse =recordResponse(exchange,gatewayLog);
	return chain.filter(exchange.mutate().request(decoratedRequest).response(decoratedResponse).build()).then(Mono.fromRunnable(() -> { writeAccessLog(gatewayLog);}));}));
}

报错IllegalReferenceCountException异常(io.netty.util.IllegalReferenceCountException: refCnt: 0, decrement: 1)。具体如下

[TID:N/A] 2023-03-27 11:38:34.767 ERROR 30056 --- [ctor-http-nio-4] r.n.channel.ChannelOperationsHandler     : [id: 0x53e73793, L:/192.168.1.53:6868 ! R:/192.168.1.62:56218] Error was received while reading the incoming data. The connection will be closed.
io.netty.util.IllegalReferenceCountException: refCnt: 0, decrement: 1
 at io.netty.util.internal.ReferenceCountUpdater.toLiveRealRefCnt(ReferenceCountUpdater.java:74) ~[netty-common-4.1.51.Final.jar:4.1.51.Final]
 at io.netty.util.internal.ReferenceCountUpdater.release(ReferenceCountUpdater.java:138) ~[netty-common-4.1.51.Final.jar:4.1.51.Final]
 at io.netty.buffer.AbstractReferenceCountedByteBuf.release(AbstractReferenceCountedByteBuf.java:100) ~[netty-buffer-4.1.51.Final.jar:4.1.51.Final]
 at io.netty.handler.codec.http.DefaultHttpContent.release(DefaultHttpContent.java:92) ~[netty-codec-http-4.1.51.Final.jar:4.1.51.Final]
 at io.netty.util.ReferenceCountUtil.release(ReferenceCountUtil.java:88) ~[netty-common-4.1.51.Final.jar:4.1.51.Final]
 at reactor.netty.channel.FluxReceive.onInboundNext(FluxReceive.java:344) ~[reactor-netty-0.9.10.RELEASE.jar:0.9.10.RELEASE]
 at reactor.netty.channel.ChannelOperations.onInboundNext(ChannelOperations.java:358) ~[reactor-netty-0.9.10.RELEASE.jar:0.9.10.RELEASE]
 at reactor.netty.http.server.HttpServerOperations.onInboundNext(HttpServerOperations.java:487) ~[reactor-netty-0.9.10.RELEASE.jar:0.9.10.RELEASE]
 at reactor.netty.channel.ChannelOperationsHandler.channelRead(ChannelOperationsHandler.java:96) ~[reactor-netty-0.9.10.RELEASE.jar:0.9.10.RELEASE]
 at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:379) [netty-transport-4.1.51.Final.jar:4.1.51.Final]
 at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:365) [netty-transport-4.1.51.Final.jar:4.1.51.Final]
 at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:357) [netty-transport-4.1.51.Final.jar:4.1.51.Fina

造成这个问题的点有一种说法:DataBufferUtils.release(buffer)在低版本spring-core下是有问题,详见:https://github.com/spring-projects/spring-framework/issues/26060;如果依赖的spring-cloud-starter-gateway版本较低,可以单独升spring-core的版本spring-core升级为5.2.13.RELEASE及以上【本人尝试后还是报错但不是上面的错误了,没再去定位】

<dependency>
        	<groupId>org.springframework.cloud</groupId>
        	<artifactId>spring-cloud-starter-gateway</artifactId>
			<exclusions>
				<exclusion>
					<groupId>org.springframework</groupId>
					<artifactId>spring-core</artifactId>
				</exclusion>
			</exclusions>
    	</dependency>
		<dependency>
			<groupId>org.springframework</groupId>
			<artifactId>spring-core</artifactId>
			<version>5.2.13.RELEASE</version>
		</dependency>

后来更改了requeset.getBody()的方式也可以完成日志实现,完整代码如下:

@Component
@Slf4j
@RequiredArgsConstructor
public class GatewayLogFilterBak230329 implements GlobalFilter, Ordered {
    private final ApplicationEventPublisher applicationEventPublisher;
    private static final String CONTENT_TYPE = "application/json";

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        ServerHttpRequest request = exchange.getRequest();
        // 请求路径
        String requestPath = request.getPath().pathWithinApplication().value();
        // 获取路由信息
        Route route = getGatewayRoute(exchange);
        String ipAddress = IpUtils.getIp(request);
        GatewayLog gatewayLog = new GatewayLog();
        gatewayLog.setDevice(IpUtils.getServerDevices(request));
        gatewayLog.setProtocol(request.getURI().getScheme());
        gatewayLog.setRequestMethod(request.getMethodValue());
        gatewayLog.setRequestPath(requestPath);
        gatewayLog.setTargetServer(route.getUri().toString());
        gatewayLog.setStartTime(new Date().getTime());
        gatewayLog.setIp(ipAddress);
        Map<String, Object> headers = new HashMap<>();
        for (String key : request.getHeaders().keySet()) {
            headers.put(key, request.getHeaders().getFirst(key));
        }
        gatewayLog.setHeaders(JSON.toJSONString(headers));
        MediaType mediaType = request.getHeaders().getContentType();
        if (request.getHeaders().getContentType() != null) {
            gatewayLog.setRequestContentType(request.getHeaders().getContentType().toString());
        }

        if (mediaType != null && (MediaType.APPLICATION_FORM_URLENCODED.isCompatibleWith(mediaType) || MediaType.APPLICATION_JSON.isCompatibleWith(mediaType))) {
            return writeBodyLog(exchange, chain, gatewayLog);
        } else {
            return writeBasicLog(exchange, chain, gatewayLog);
        }
    }

    @Override
    public int getOrder() {
        // 过滤器链路上的排序要在NettyWriteResponseFilter(这个拦截器默认是-1)之前
        return -2;
    }

    /**
     * 获取路由信息
     *
     * @param exchange
     * @return
     */
    private Route getGatewayRoute(ServerWebExchange exchange) {
        return exchange.getAttribute(ServerWebExchangeUtils.GATEWAY_ROUTE_ATTR);
    }

    private Mono<Void> writeBasicLog(ServerWebExchange exchange, GatewayFilterChain chain, GatewayLog gatewayLog) {
        MultiValueMap<String, String> queryParams = exchange.getRequest().getQueryParams();
        gatewayLog.setRequestBody(getUrlParamsByMap(queryParams));

        //获取响应体
        ServerHttpResponseDecorator decoratedResponse = recordResponseLog(exchange, gatewayLog);

        return chain.filter(exchange.mutate().response(decoratedResponse).build())
                .then(Mono.fromRunnable(() -> {
                    // 打印日志
                    writeAccessLog(gatewayLog);
                }));
    }


    /**
     * 解决 request body 只能读取一次问题,
     * 参考: org.springframework.cloud.gateway.filter.factory.rewrite.ModifyRequestBodyGatewayFilterFactory
     *
     * @param exchange
     * @param chain
     * @param gatewayLog
     * @return
     */
    private Mono<Void> writeBodyLog(ServerWebExchange exchange, GatewayFilterChain chain, GatewayLog gatewayLog) {
        ServerHttpRequest request = exchange.getRequest();
        return DataBufferUtils.join(request.getBody())
                .flatMap(d -> Mono.just(Optional.of(d))).defaultIfEmpty(Optional.empty())
                .flatMap(optional -> {
                    try {
                        URI uri = request.getURI();
                        HttpHeaders headers = new HttpHeaders();
                        headers.putAll(exchange.getRequest().getHeaders());

                        byte[] bodyBytes = null;
                        if (optional.isPresent()) {
                            byte[] oldBytes = new byte[optional.get().readableByteCount()];
                            optional.get().read(oldBytes);
                            bodyBytes = oldBytes;
                        }
                        // 无Body请求重写
                        if (ArrayUtils.isEmpty(bodyBytes)) {
                            return chain.filter(exchange.mutate().request(new ServerHttpRequestDecorator(request.mutate().uri(uri).build()) {
                                @Override
                                public HttpHeaders getHeaders() {
                                    return headers;
                                }
                            }).response(recordResponseLog(exchange, gatewayLog)).build());
                        }
                        String body = new String(bodyBytes, StandardCharsets.UTF_8);
                        gatewayLog.setRequestBody(body);
                        final byte[] finalBodyBytes = bodyBytes;
                        return chain.filter(exchange.mutate().request(new ServerHttpRequestDecorator(request.mutate().uri(uri).build()) {
                            @Override
                            public Flux<DataBuffer> getBody() {
                                DataBuffer buffer = exchange.getResponse().bufferFactory().wrap(finalBodyBytes);
                                DataBufferUtils.retain(buffer);

                                return Flux.just(buffer);
                            }

                            @Override
                            public HttpHeaders getHeaders() {
                                return headers;
                            }
                        }).response(recordResponseLog(exchange, gatewayLog)).build()).then(Mono.fromRunnable(() -> {
                            writeAccessLog(gatewayLog);
                        }));
                    } catch (Exception ex) {
                        return chain.filter(exchange);
                    } finally {
                        if (optional.isPresent()) {
                            DataBufferUtils.release(optional.get());
                        }
                    }
                });
    }


    /**
     * 打印日志
     *
     * @param gatewayLog 网关日志
     */
    private void writeAccessLog(GatewayLog gatewayLog) {
        applicationEventPublisher.publishEvent(new GatewayLogEvent(this, gatewayLog));
    }

    /**
     * 记录响应日志
     * 通过 DataBufferFactory 解决响应体分段传输问题。
     */
    private ServerHttpResponseDecorator recordResponseLog(ServerWebExchange exchange, GatewayLog gatewayLog) {
        ServerHttpResponse response = exchange.getResponse();
        DataBufferFactory bufferFactory = response.bufferFactory();

        return new ServerHttpResponseDecorator(response) {
            @Override
            public Mono<Void> writeWith(Publisher<? extends DataBuffer> body) {
                if (body instanceof Flux) {
                    Date responseTime = new Date();
                    gatewayLog.setEndTime(responseTime.getTime());
                    // 计算执行时间
                    long executeTime = (responseTime.getTime() - gatewayLog.getStartTime());
                    gatewayLog.setExecuteTime(executeTime);
                    gatewayLog.setStatus(response.getStatusCode().value() == 200 ? "成功" : "失败");
                    // 获取响应类型,如果是 json 就打印
                    String originalResponseContentType = exchange.getAttribute(ServerWebExchangeUtils.ORIGINAL_RESPONSE_CONTENT_TYPE_ATTR);

                    if (Objects.equals(this.getStatusCode(), HttpStatus.OK)
                            && StringUtils.isNotBlank(originalResponseContentType)
                            && originalResponseContentType.contains(CONTENT_TYPE)) {

                        Flux<? extends DataBuffer> fluxBody = Flux.from(body);
                        return super.writeWith(fluxBody.buffer().map(dataBuffers -> {
                            // 合并多个流集合,解决返回体分段传输
                            DataBufferFactory dataBufferFactory = new DefaultDataBufferFactory();
                            DataBuffer join = dataBufferFactory.join(dataBuffers);
                            byte[] content = new byte[join.readableByteCount()];
                            join.read(content);
                            // 释放掉内存
                            DataBufferUtils.release(join);
                            String responseResult = new String(content, StandardCharsets.UTF_8);
                            gatewayLog.setResponseData(responseResult);
                            return bufferFactory.wrap(content);
                        }));
                    }
                }
                // if body is not a flux. never got there.
                return super.writeWith(body);
            }
        };
    }


    /**
     * 将map参数转换成url参数
     *
     * @param map
     * @return
     */
    private String getUrlParamsByMap(MultiValueMap<String, String> map) {
        if (ObjectUtils.isEmpty(map)) {
            return "";
        }
        StringBuilder sb = new StringBuilder();
        for (Map.Entry<String, List<String>> entry : map.entrySet()) {
            sb.append(entry.getKey()).append("=").append(entry.getValue().get(0));
            sb.append("&");
        }
        String s = sb.toString();
        if (s.endsWith("&")) {
            s = StringUtils.substringBeforeLast(s, "&");
        }
        return s;
    }
}

后来也参考了蛮多大牛文章,自己也去找了很久gateway源代码,终于在这个工具类ServerWebExchangeUtils中发现有更好实现的点,cacheRequestBody()这个方法,英文注释大概是说可以缓存请求正文到这个属性中;后续可以通过获取属性的方式获取到请求正文;拿着这个方法去百度果然有人也是这么解决请求体的问题,直接上代码。

方式一:通过两个拦截器终于实现了日志记录

@Slf4j
@Component
// 顶级过滤器用来缓存请求正文
public class CacheGlobalRequestBodyFilter implements Ordered, GatewayFilter, GlobalFilter {

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        return ServerWebExchangeUtils
                .cacheRequestBody(
                        exchange,
                        (request) -> chain.filter(
                                exchange.mutate().request(request).build()));
    }
    @Override
    public int getOrder() {
        return Ordered.HIGHEST_PRECEDENCE;
    }
}
@Slf4j
@Component
@RequiredArgsConstructor
public class GatewayLogFilter implements GlobalFilter, Ordered {
    private final ApplicationEventPublisher applicationEventPublisher;

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        ServerHttpRequest request = exchange.getRequest();
        // 请求路径
        String requestPath = request.getPath().pathWithinApplication().value();
        // 获取路由信息
        Route route = getGatewayRoute(exchange);
        String ipAddress = IpUtils.getIp(request);
        GatewayLog gatewayLog = new GatewayLog();
        gatewayLog.setDevice(IpUtils.getServerDevices(request));
        gatewayLog.setProtocol(request.getURI().getScheme());
        gatewayLog.setRequestMethod(request.getMethodValue());
        gatewayLog.setRequestPath(requestPath);
        gatewayLog.setTargetServer(route.getUri().toString());
        gatewayLog.setStartTime(new Date().getTime());
        gatewayLog.setIp(ipAddress);
        Map<String, Object> headers = new HashMap<>();
        for (String key : request.getHeaders().keySet()) {
            headers.put(key, request.getHeaders().getFirst(key));
        }
        gatewayLog.setHeaders(JSON.toJSONString(headers));
        if (request.getHeaders().getContentType() != null) {
            gatewayLog.setRequestContentType(request.getHeaders().getContentType().toString());
        }
        // GatewayUtil.getRequestBodyContent(exchange)这里实际上就是一个获取 exchange.getAttribute(ServerWebExchangeUtils.CACHED_REQUEST_BODY_ATTR)属性而已
        gatewayLog.setRequestBody(GatewayUtil.getRequestBodyContent(exchange));
        //获取响应体
        ServerHttpResponseDecorator decoratedResponse = recordResponseLog(exchange, gatewayLog);
        return chain.filter(exchange.mutate().response(decoratedResponse).build())
                .then(Mono.fromRunnable(() -> {
                    // 打印日志
                    writeAccessLog(gatewayLog);
                }));
    }

    @Override
    public int getOrder() {
        return 0;
    }

    /**
     * 获取路由信息
     *
     * @param exchange
     * @return
     */
    private Route getGatewayRoute(ServerWebExchange exchange) {
        return exchange.getAttribute(ServerWebExchangeUtils.GATEWAY_ROUTE_ATTR);
    }

    /**
     * 打印日志
     *
     * @param gatewayLog 网关日志
     */
    private void writeAccessLog(GatewayLog gatewayLog) {
        applicationEventPublisher.publishEvent(new GatewayLogEvent(this, gatewayLog));
    }

    /**
     * 记录响应日志
     * 通过 DataBufferFactory 解决响应体分段传输问题。
     */
    private ServerHttpResponseDecorator recordResponseLog(ServerWebExchange exchange, GatewayLog gatewayLog) {
        ServerHttpResponse response = exchange.getResponse();
        DataBufferFactory bufferFactory = response.bufferFactory();

        return new ServerHttpResponseDecorator(response) {
            @Override
            public Mono<Void> writeWith(Publisher<? extends DataBuffer> body) {
                if (body instanceof Flux) {
                    Date responseTime = new Date();
                    gatewayLog.setEndTime(responseTime.getTime());
                    // 执行时间
                    long executeTime = (responseTime.getTime() - gatewayLog.getStartTime());
                    gatewayLog.setExecuteTime(executeTime);
                    gatewayLog.setStatus(response.getStatusCode().value() == 200 ? "成功" : "失败");
                    // 获取响应类型json
                    String originalResponseContentType = exchange.getAttribute(ServerWebExchangeUtils.ORIGINAL_RESPONSE_CONTENT_TYPE_ATTR);

                    if (Objects.equals(this.getStatusCode(), HttpStatus.OK) && StringUtils.isNotBlank(originalResponseContentType)
                            && originalResponseContentType.contains(MediaType.APPLICATION_JSON_VALUE)) {
                        Flux<? extends DataBuffer> fluxBody = Flux.from(body);
                        return super.writeWith(fluxBody.buffer().map(dataBuffers -> {
                            // 合并多个流集合,解决返回体分段传输
                            DataBufferFactory dataBufferFactory = new DefaultDataBufferFactory();
                            DataBuffer join = dataBufferFactory.join(dataBuffers);
                            byte[] content = new byte[join.readableByteCount()];
                            join.read(content);
                            // 释放掉内存
                            DataBufferUtils.release(join);
                            String responseResult = new String(content, StandardCharsets.UTF_8);
                            gatewayLog.setResponseData(responseResult);
                            return bufferFactory.wrap(content);
                        }));
                    }
                }
                // if body is not a flux. never got there.
                return super.writeWith(body);
            }
        };
    }
}

方式二 通过AdaptCachedBodyGlobalFilter实现请求体缓存

@Component
@RequiredArgsConstructor
public class GatewayCommonConfig{ 
    private final GatewayProperties gatewayProperties;
    private final ApplicationContext applicationContext;

    @PostConstruct
    public void init(){
        //发布对应路由的EnableBodyCachingEvent事件
        gatewayProperties.getRoutes().forEach(e->{
            EnableBodyCachingEvent enableBodyCachingEvent = new EnableBodyCachingEvent(new Object(), e.getId());
            //发布事件
            applicationContext.publishEvent(enableBodyCachingEvent);
        });
    }
}

然后 就可以在自定义的拦截器中通过request.getBody()获取请求体了

方式一参考链接:

方式一参考链接:

方式二参考链接:

  • 1
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
对于Spring Cloud Gateway全局拦截,你可以通过实现GlobalFilter接口来实现GlobalFilterSpring Cloud Gateway提供的全局过滤器接口,可以在请求进入网关之前或者响应离开网关之前进行一些统一的处理。 你可以创建一个类实现GlobalFilter接口,并在类上添加@Component注解,将其注册为一个Spring Bean。在这个类中,你可以实现pre和post方法来定义请求进入和响应离开网关时的处理逻辑。 在pre方法中,你可以获取到请求的信息,可以对请求进行一些校验、修改或者记录日志等操作。在post方法中,你可以获取到响应的信息,可以对响应进行一些处理,比如修改响应内容、设置响应头等。 以下是一个简单的示例: ```java import org.springframework.cloud.gateway.filter.GatewayFilterChain; import org.springframework.cloud.gateway.filter.GlobalFilter; import import org.springframework.core.Ordered; import org.springframework.stereotype.Component; import org.springframework.web.server.ServerWebExchange; import reactor.core.publisher.Mono; @Component public class MyGlobalFilter implements GlobalFilter, Ordered { @Override public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { // 在pre方法中进行请求处理逻辑 // ... return chain.filter(exchange).then(Mono.fromRunnable(() -> { // 在post方法中进行响应处理逻辑 // ... })); } @Override public int getOrder() { // 设置过滤器的执行顺序 return Ordered.H
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值