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

总结

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

相关推荐
Leo July9 小时前
【Java】Spring Cloud 微服务生态全解析与企业级架构实战
java·spring cloud
一条咸鱼_SaltyFish10 小时前
WebFlux vs MVC:Gateway集成若依框架的技术选型之争
java·开发语言·微服务·gateway·mvc·开源软件·webflux
李慕婉学姐13 小时前
【开题答辩过程】以《基于springcloud的空气质量监控管理系统》为例,不知道这个选题怎么做的,不知道这个选题怎么开题答辩的可以进来看看
后端·spring·spring cloud
CV_J1 天前
安装kibana
java·elasticsearch·spring cloud·docker·容器
小马爱打代码2 天前
实时搜索:SpringCloud + Elasticsearch + Redis + Kafka
redis·elasticsearch·spring cloud
努力也学不会java2 天前
【Spring Cloud】环境和工程基本搭建
java·人工智能·后端·spring·spring cloud·容器
爱吃山竹的大肚肚2 天前
达梦(DM)数据库中设置表空间
java·数据库·sql·mysql·spring·spring cloud·oracle
亚林瓜子2 天前
AWS API Gateway添加OAuth2请求头传递app id信息
云计算·gateway·aws·oauth2·请求头·principalid
这儿有个昵称3 天前
Java面试场景:从音视频到微服务的技术深挖
java·spring boot·spring cloud·微服务·面试·kafka·音视频
Remember_9933 天前
深入理解 Java String 类:从基础原理到高级应用
java·开发语言·spring·spring cloud·eclipse·tomcat