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));
}

总结

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

相关推荐
Raas1001 小时前
AI网关在架构中的位置?MAI Gateway(魔芋企业级AI网关)给出企业级答案
大数据·网络·人工智能·架构·gateway·ai网关·mai gateway
LlmCraft|大模型工程实践2 小时前
08. Docker Compose 一键编排:多容器应用的指挥家
java·spring cloud·eureka
橘子编程3 小时前
Java邮件发送全攻略:从入门到实战
java·开发语言·spring boot·spring·spring cloud·maven
苏渡苇5 小时前
HandlerInterceptor 和 WebFilter:两套 HTTP 请求拦截
spring boot·spring·http·spring cloud
随遇而安zx5 小时前
SpringCloud---安全(Security / OAuth2 / JWT)设计思想与深度解析
安全·spring cloud
骇客野人5 小时前
SpringBoot+SpringCloud高并发系统设计、搭建与落地实施方案
java·spring boot·spring cloud
随遇而安zx7 小时前
SpringCloud---Gateway vs Netflix Zuul 网关对比深度解析
spring·spring cloud·gateway
随遇而安zx1 天前
SpringCloud---Spring Cloud 分布式任务调度(XXL-Job / ElasticJob)
分布式·spring cloud
未秃头的程序猿1 天前
一次秒杀把服务打挂了,我用Sentinel规则配置化救了回来
java·后端·spring cloud
Raas1001 天前
MAI Gateway(魔芋企业级AI网关)详解:企业为什么需要AI网关,一文读懂企业AI流量治理
大数据·人工智能·gateway·api·ai网关·mai gateway