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

总结

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

相关推荐
guojl10 小时前
RestTemplate使用手册
spring cloud·微服务
guojl10 小时前
RestTemplate原理分析
spring cloud·微服务
Ken_111510 小时前
SpringCloud系列(51)--SpringCloud Stream之使用分组解决消息重复消费问题
spring cloud
lwb_011811 小时前
SpringCloud——Gateway新一代网关
spring·spring cloud·gateway
你是人间五月天12 小时前
gateway断言配置详解
gateway
weixin_3875456412 小时前
深入解析 AI Gateway:新一代智能流量控制中枢
人工智能·gateway
述雾学java15 小时前
Spring Cloud Feign 整合 Sentinel 实现服务降级与熔断保护
java·spring cloud·sentinel
何苏三月18 小时前
SpringCloud系列 - Sentinel 服务保护(四)
spring·spring cloud·sentinel
麦兜*1 天前
Spring Boot启动优化7板斧(延迟初始化、组件扫描精准打击、JVM参数调优):砍掉70%启动时间的魔鬼实践
java·jvm·spring boot·后端·spring·spring cloud·系统架构
白仑色3 天前
Spring Cloud 微服务(统一网关设计)
spring cloud·微服务·服务治理·统一配置管理·分布式配置中心