SpringCloudGateway获取报文大小

Overview

SpringCloud Gateway使用过程中,希望获取报文大小。由于SpringCloud Gateway底层基于Netty实现,直接读取报文,会大幅影响网关性能。因此本文将通过其他方式获取报文大小。本文基于2.2.9 SpringCloud Gateway开发。

读取请求报文大小

实现自定义Filter,读取请求报文大小,具体可参考以下代码。

java 复制代码
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferUtils;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpRequestDecorator;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

@Slf4j
@Component
public class ReadRequestBodyFilter implements GlobalFilter {

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        // 分配空的DataBuffer
        DataBuffer emptyBuffer = exchange.getResponse().bufferFactory().allocateBuffer(0);
        // 使用 DataBufferUtils.join 将DataBuffer数据流聚合为一个Mono
        // 聚合后的 DataBuffer 为一个完整报文的 DataBuffer。
        // 如果请求报文为空,使用分配的空 DataBuffer。
        return DataBufferUtils.join(exchange.getRequest().getBody().defaultIfEmpty(emptyBuffer))
                .flatMap(dataBuffer -> {
                    // 获取报文大小,不直接获取报文内容。
                    int size = dataBuffer.readableByteCount();
                    log.info("=====> request body size: {}", size);
                    if (size == 0) {
                        // 如果报文内容为空,需要主动释放创建的空的DataBuffer。
                        DataBufferUtils.release(dataBuffer);
                        return chain.filter(exchange);
                    }
                    // 复制一份DataBuffer,slice方法不会retain DataBuffer,只是复制的指针坐标。
                    Flux<DataBuffer> cachedFlux = Flux.defer(() -> Flux.just(
                            dataBuffer.slice(0, dataBuffer.readableByteCount())));
                    // 构建新的 Request,重写 getBody 返回复制的报文
                    ServerHttpRequest mutatedRequest = new ServerHttpRequestDecorator(
                            exchange.getRequest()) {
                        @Override
                        public Flux<DataBuffer> getBody() {
                            return cachedFlux;
                        }
                    };
                    // 继续处理请求
                    return chain.filter(exchange.mutate().request(mutatedRequest).build());
                });
    }
}

读取响应报文大小

重写NettyWriteResponseFilter,实现响应报文大小获取。此方法有点不够优雅。

java 复制代码
// 重写 filter 方法
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
   // NOTICE: nothing in "pre" filter stage as CLIENT_RESPONSE_CONN_ATTR is not added
   // until the NettyRoutingFilter is run
   // @formatter:off
   return chain.filter(exchange)
         .doOnError(throwable -> cleanup(exchange))
         .then(Mono.defer(() -> {
            Connection connection = exchange.getAttribute(CLIENT_RESPONSE_CONN_ATTR);

            if (connection == null) {
               return Mono.empty();
            }
            if (log.isTraceEnabled()) {
               log.trace("NettyWriteResponseFilter start inbound: "
                     + connection.channel().id().asShortText() + ", outbound: "
                     + exchange.getLogPrefix());
            }
            ServerHttpResponse response = exchange.getResponse();

            // TODO: needed?
            final Flux<DataBuffer> body = connection
                  .inbound()
                  .receive()
                  .retain()
                  .map(byteBuf -> wrap(byteBuf, response));

            MediaType contentType = null;
            try {
               contentType = response.getHeaders().getContentType();
            } catch (Exception e) {
               if (log.isTraceEnabled()) {
                  log.trace("invalid media type", e);
               }
            }
            
            // 重写部分代码,如果为 stream 类型报文,则忽略报文读取
            // stream media type, we can not join databuffer, just ignore body size.
            if (isStreamingMediaType(contentType)) {
               return response.writeAndFlushWith(body.map(Flux::just));
            }
            // 分配空的 DataBuffer
            DataBuffer emptyBuffer = exchange.getResponse().bufferFactory().allocateBuffer(0);
            // 聚合 DataBuffer 构建大的 DataBuffer 包含完整报文
            return DataBufferUtils.join(body.defaultIfEmpty(emptyBuffer))
                  .flatMap(dataBuffer -> {
                     // 读取报文大小
                     int size = dataBuffer.readableByteCount();
                     log.info("=====> response body size: " + size);
                     // 复制 DataBuffer 
                     Flux<DataBuffer> cachedFlux = Flux.defer(() -> Flux.just(dataBuffer.slice(0, size)));
                     // 向 response 写入数据
                     return response.writeWith(cachedFlux);
                  });

         })).doOnCancel(() -> cleanup(exchange));
}

总结

通过上述方式,可以获取到完整的请求和响应报文大小。根据压测结果,此方法对性能无影响。

相关推荐
SoleMotive.7 小时前
谢飞机爆笑面经:Java大厂3轮12问真题拆解(Redis穿透/Kafka分区/MCP Agent)
redis·spring cloud·kafka·java面试·mcp
MrSYJ9 小时前
Redis 做分布式 Session
后端·spring cloud·微服务
研究司马懿12 小时前
【云原生】Gateway API高级功能
云原生·go·gateway·k8s·gateway api
瑶山12 小时前
Spring Cloud微服务搭建五、集成负载均衡,远程调用,熔断降级
spring cloud·微服务·负载均衡·远程调用·熔断降级
金牌归来发现妻女流落街头1 天前
【从SpringBoot到SpringCloud】
java·spring boot·spring cloud
Java后端的Ai之路1 天前
【Spring全家桶】-一文弄懂Spring Cloud Gateway
java·后端·spring cloud·gateway
vx_Biye_Design2 天前
【关注可免费领取源码】房屋出租系统的设计与实现--毕设附源码40805
java·spring boot·spring·spring cloud·servlet·eclipse·课程设计
Volunteer Technology2 天前
sentinel基本操作
spring cloud·sentinel
Dragon Wu2 天前
Spring Security Oauth2.1 授权码模式实现前后端分离的方案
java·spring boot·后端·spring cloud·springboot·springcloud