SpringCloud Gateway获取请求响应body大小

前提

本文获取请求、响应body大小方法的前提 : 网关只做转发逻辑,不修改请求、相应的body内容。

SpringCloud Gateway内部的机制类似下图,HttpServer(也就是NettyServer)接收外部的请求,在Gateway内部请求将会通过HttpClient(Netty实现的客户端)发送给后端应用。

本文的body获取方式,基于HttpClient端实现,通过获取HttpClient发送、接收后端的请求、响应body实现。如果SpringCloudGateway内部逻辑修改了body,那么本文方式获取的body大小将会存在歧义误差。

如果想要在HttpServer层获取到报文大小,可以尝试自定义实现Netty的ChannelDuplexHandler,尝试获取到报文大小。

SpringCloud Gateway底层基于异步模型Netty实现,调用时相关的body内容不直接加载到内存。如果使用简单的SpringCloud Gateway Filter读取报文,读取body大小,会大幅影响网关性能。因此需要考虑一种方法,在不影响网关性能的前提下,获取请求、响应body大小。

方式一、重写SpringCloudGateway Filter类

重写 NettyRoutingFilter 获取 Request Body

重写Gateway自带的org.springframework.cloud.gateway.filter.NettyRoutingFilter

修改类的filter内的代码,在底层获取请求body的大小,并在exchange保存。

java 复制代码
Flux<HttpClientResponse> responseFlux = getHttpClient(route, exchange)
      .headers(headers -> {
         ...
      }).request(method).uri(url).send((req, nettyOutbound) -> {
         ...
         return nettyOutbound.send(request.getBody().map(body -> {
            // 修改此处代码,获取请求body的大小,并将获取到的结果存入exchange内。
            int size = body.readableByteCount();
            exchange.getAttributes().put("gw-request-body-size", size);
            return getByteBuf(body);
         }));
      }).responseConnection((res, connection) -> {
        ...

重写 NettyWriteResponseFilter 获取 Response Body

重写Gateway自带的org.springframework.cloud.gateway.filter.NettyWriteResponseFilter

修改类filter内的代码,在底层获取响应body的大小,并在exchange保存。

java 复制代码
return chain.filter(exchange)
      .doOnError(throwable -> cleanup(exchange))
      .then(Mono.defer(() -> {
        ...
         // TODO: needed?
         final Flux<DataBuffer> body = connection
               .inbound()
               .receive()
               .retain()
               .map(byteBuf -> {
                  // 获取响应报文的长度,并将结果写入exchange内。
                  int respSize = byteBuf.readableBytes();
                  exchange.getAttributes().put("gw-response-body-size", respSize);
                  return wrap(byteBuf, response);
               });
      ...

自定义Filter打印报文大小

通过上述的2个方法,request、response body的大小已经写入exchange内,只需要实现一个自定义的Filter,就可以获取到报文的大小。假设自定义的Filter命名为BodySizeFilter,它的Order需要在NettyWriteResponseFilter之前。

在filter方法内,从exchange获取request、response body大小。

java 复制代码
@Slf4j
@Component
public class BodySizeFilter implements GlobalFilter, Ordered {
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        return chain.filter(exchange)
                .then(Mono.defer(() -> {
                    Integer exchangeReq = exchange.getAttribute("gw-request-body-size");
                    Integer exchangeResp = exchange.getAttribute("gw-response-body-size");
                    log.info("req from exchange: {}", exchangeReq);
                    log.info("resp from exchange: {}", exchangeResp);
                    return Mono.empty();
                }));
    }
    @Override
    public int getOrder() {
        return NettyWriteResponseFilter.WRITE_RESPONSE_FILTER_ORDER - 1;
    }
}

方式二、自定义Netty Handler

另一种方式是基于Netty的Hander,非重写SpringCloud Gateway类。本文构建的SpringCloudGateway版本为2.2.9.RELEASE

实现自定义的Netty ChannelDuplexHandler

重写2个方法 write、channelRead。

java 复制代码
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelDuplexHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelPromise;
import io.netty.util.Attribute;
import io.netty.util.AttributeKey;
import lombok.extern.slf4j.Slf4j;
import org.slf4j.MDC;

import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.UUID;

@Slf4j
public class HttpClientLoggingHandler extends ChannelDuplexHandler {
    private static final AttributeKey<Long> RESP_SIZE = AttributeKey.valueOf("resp-size");

    private static final AttributeKey<Long> REQ_SIZE = AttributeKey.valueOf("req-size");

    @Override
    public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
        if (msg instanceof ByteBuf) {
            final ByteBuf buf = (ByteBuf) msg;
            // 读取报文大小,一笔请求可能存在多个 msg,也就是一个请求报文,可能分多次经过write方法。
            int length = buf.readableBytes();
            long size;
            // 将结果以attribute形式保存在channel内,一笔完整的调用对应一个完整的context上下文。
            Attribute<Long> sizeAttr = ctx.channel().attr(REQ_SIZE);
            if (sizeAttr.get() == null) {
                size = 0L;
            } else {
                size = sizeAttr.get();
            }
            // 每次累加当前请求的报文大小。
            size += length;
            ctx.channel().attr(REQ_SIZE).set(size);
        }
        super.write(ctx, msg, promise);
    }

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        if (msg instanceof ByteBuf) {
            final ByteBuf buf = (ByteBuf) msg;
            // 获取响应body的大小,一笔响应可能存在多个 msg,也就是一个响应报文,可能分多次经过channelRead方法。
            int length = buf.readableBytes();
            long size;
            Attribute<Long> sizeAttr = ctx.channel().attr(RESP_SIZE);
            if (sizeAttr.get() == null) {
                size = 0L;
            } else {
                size = ctx.channel().attr(RESP_SIZE).get();
            }
            size += length;
            // 将结果以attribute形式保存在channel内,一笔完整的调用对应一个完整的context上下文。
            ctx.channel().attr(RESP_SIZE).set(size);
        }
        super.channelRead(ctx, msg);
    }
}

将自定义Handler配置到网关内。

java 复制代码
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.gateway.config.HttpClientCustomizer;
import org.springframework.context.annotation.Configuration;
import reactor.netty.channel.BootstrapHandlers;
import reactor.netty.http.client.HttpClient;

@Slf4j
@Configuration
public class GwHttpClientCustomizer implements HttpClientCustomizer {
    @Override
    public HttpClient customize(HttpClient client) {
        // 本文基于2.2.9.RELEASE的SpringCloud Gateway实现。
        return client.tcpConfiguration(tcpClient ->
                tcpClient.bootstrap(b ->
                        BootstrapHandlers.updateConfiguration(b, "client-log", (connectionObserver, channel) -> {
                            channel.pipeline().addFirst("client-log", new HttpClientLoggingHandler());
                        })
                )
        );
    }
}

通过上述自定义的方法,一笔完整的调用中请求、响应body的大小,已经被计算保存在netty channel内,只需要自定义SpringCloud Gateway Filter获取到结果。

java 复制代码
import io.netty.util.Attribute;
import io.netty.util.AttributeKey;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.cloud.gateway.filter.NettyWriteResponseFilter;
import org.springframework.core.Ordered;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import reactor.netty.Connection;

import java.util.function.Consumer;

import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.CLIENT_RESPONSE_CONN_ATTR;

/**
 * @author luobo on 2023/08/01 3:51 PM
 */
@Slf4j
@Component
public class BodySizeFilter implements GlobalFilter, Ordered {

    private static final AttributeKey<Long> REQ_SIZE = AttributeKey.valueOf("req-size");

    private static final AttributeKey<Long> RESP_SIZE = AttributeKey.valueOf("resp-size");

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        return chain.filter(exchange)
                .then(Mono.defer(() -> {
                    // SpringCloud Gateway内将每个调用的Connection保存在exchange内
                    // connection 可以获取到 channel
                    Connection connection = exchange.getAttribute(CLIENT_RESPONSE_CONN_ATTR);
                    Attribute<Long> respSize = connection.channel().attr(RESP_SIZE);
                    Attribute<Long> reqSize = connection.channel().attr(REQ_SIZE);
                    long resp;
                    if (respSize.get() == null) {
                        resp = 0L;
                    } else {
                        resp = respSize.get();
                    }
                    long req;
                    if (reqSize.get() == null) {
                        req = 0L;
                    } else {
                        req = reqSize.get();
                    }
                    log.info("------------------------> resp size: {}", resp);
                    log.info("------------------------> req size: {}", req);
                    // 每次调用结束需要清空保存的值(因为连接会复用)
                    respSize.set(null);
                    reqSize.set(null);
                    return Mono.empty();
                }));
    }

    @Override
    public int getOrder() {
        return NettyWriteResponseFilter.WRITE_RESPONSE_FILTER_ORDER - 1;
    }
}

通过此方法获取的body大小会比真实的body大 ,因为它包含了请求和响应头的信息。


总结

本人更加推荐使用方式一。

相关推荐
serve the people13 分钟前
python环境搭建 (六) Makefile 简单使用方法
java·服务器·python
重生之后端学习17 分钟前
146. LRU 缓存
java·数据结构·算法·leetcode·职场和发展
萧曵 丶19 分钟前
懒加载单例模式中DCL方式和原理解析
java·开发语言·单例模式·dcl
回忆是昨天里的海22 分钟前
k8s部署的微服务动态扩容
java·运维·kubernetes
萧曵 丶22 分钟前
单例模式 7 种实现方式对比表
java·单例模式
lang2015092832 分钟前
Tomcat Maven插件全解析:开发部署一体化
java·tomcat·maven
JHC_binge39 分钟前
国内Ubuntu 22.04 LTS安装Milvus向量数据库
java·linux·ubuntu
2501_941148151 小时前
C++ map / multimap 保姆级教程
java·开发语言·c++
Yield & Allure1 小时前
EasyExcel使用
java
符哥20081 小时前
Fastjson2.X 使用详解
android·java