SpringCloud Gateway获取请求响应body大小

news2024/12/25 13:43:21

前提

本文获取请求、响应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保存。

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保存。

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大小。

@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);
                    respSize.set(null);
                    reqSize.set(null);
                    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。

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配置到网关内。

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获取到结果。

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大 ,因为它包含了请求和响应头的信息。


总结

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/827770.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【第一阶段】kotlin语言引用数据类型

Java语言中有两种数据类型 第一种&#xff1a;基本数据类型 如int double等 第二种&#xff1a;引用数据类型。如String kotlin只有一种数据类型&#xff0c;看起来都是引用数据类型&#xff0c;实际上编译器会在Java字节码中&#xff0c;修改成基本类型 //Java语言中有两种数…

Mr. Cappuccino的第56杯咖啡——Mybatis拦截器

Mybatis拦截器 概述应用场景项目结构实现分页查询其它拦截器的使用 概述 Mybatis允许使用者在映射语句执行过程中的某一些指定的节点进行拦截调用&#xff0c;通过织入拦截器&#xff0c;在不同节点修改一些执行过程中的关键属性&#xff0c;从而影响SQL的生成、执行和返回结果…

6.s081(Fall 2022)Lab2: system calls

文章目录 前言其他篇章参考链接0. 前置准备1. System call tracing (moderate)简单分析Hint 1Hint 2Hint 3Hint 4Hint 5测试 2. Sysinfo (moderate) 前言 好像没啥前言 其他篇章 环境搭建 Lab1:Utilities 参考链接 官网链接 xv6手册链接&#xff0c;这个挺重要的&#xff…

第2集丨Vue 江湖 —— Vue中的一些必备概念

目录 一、Object.defineProperty()1.1 属性描述符1.2 共享属性1.2.1 configurable1.2.2 enumerable 1.3 数据描述符属性1.3.1 value1.3.2 writable 1.4 访问器描述符属性1.4.1 get1.4.2 set1.4.3 注意点 1.5 案例1.5.1 数据描述符1.5.2 访问器描述符 二、Vue 模板语法2.1 插值语…

作为面试官,有些事想吐槽一下

作者&#xff1a;拭心 前段时间组里有岗位招人&#xff0c;花了些时间面试&#xff0c;趁着周末把过程中的感悟和槽点总结成文和大家讲讲。 简历书写和自我介绍 1.今年的竞争很激烈&#xff1a;找工作的人数量比去年多、平均质量比去年高。裸辞的慎重&#xff0c;要做好和好学…

npm发布包

1.npm 登录 在控制台输入命令 npm login 按提示输入用户名&#xff0c;密码&#xff0c;邮箱后登录 如果出现如下提示 需要将淘宝镜像源切换为npm源&#xff0c;删除或注释以下内容就行 2.发布 进入准备发布的代码的根目录下&#xff0c;输入命令 npm publish 3.删除已发…

《TCP IP 网络编程》第十五章

第 15 章 套接字和标准I/O 15.1 标准 I/O 的优点 标准 I/O 函数的两个优点&#xff1a; 除了使用 read 和 write 函数收发数据外&#xff0c;还能使用标准 I/O 函数收发数据。下面是标准 I/O 函数的两个优点&#xff1a; 标准 I/O 函数具有良好的移植性标准 I/O 函数可以利用…

Python入门二

目录&#xff1a; python封装与property装饰器python继承与类型检查python多态与superpython 模块与包错误与异常Debug 调试与分析python类型注解python数据类dataclasspython内置装饰器python装饰器学生信息管理系统 1.python封装与property装饰器 封装的概念 封装&#x…

深度学习各层负责什么内容?

1、深度学习——神经网络简介 深度学习(Deep Learning)(也称为深度结构学习【Deep Structured Learning】、层次学习【Hierarchical Learning】或者是深度机器学习【Deep Machine Learning】)是一类算法集合&#xff0c;是机器学习的一个分支。 深度学习方法近年来&#xff0c…

Expectation (Easy Version) 2023“钉耙编程”中国大学生算法设计超级联赛(5)hdu7330

Problem - 7330 题目大意&#xff1a;有n次游戏&#xff0c;每次游戏有a/b的概率获胜&#xff0c;且相互独立&#xff0c;如果当前赢了cnt次游戏&#xff0c;那么这次游戏会赢得的分数&#xff0c;问最后得分的期望 1<n<1e6;1<m,a<b<998244353 思路&#xff…

windows10 设置代理

场景&#xff1a;同一个办公室&#xff0c;只有A的电脑有权限访问网站 http://10.129.129.129:5601&#xff0c; 那办公室其他B,C同学想访问 http://10.129.129.129:5601&#xff0c;需要怎么处理&#xff1f; A 同学电脑安装代理软件&#xff1a; 1. 下载wproxy IMFirewall, …

d3dx9_30.dll如何修复,分享几种一键修复方法

d3dx9_30.dll是DirectX 的一个动态链接库文件&#xff0c;它包含了一些用于图形和游戏的函数和资源。在了解d3dx9_30.dll的解决方法和丢失原因之前&#xff0c;我们先来了解一下DirectX。DirectX是一套由微软开发的多媒体和游戏编程接口&#xff08;API&#xff09;集合。它提供…

0802|IO进程线程 day5 进程概念

一、进程的基础 1.1 什么是进程 1&#xff09;进程是程序的一次执行过程 程序&#xff1a;是静态的&#xff0c;它是存储在外存上的可执行二进制文件&#xff1b;进程&#xff1a;动态的概念&#xff0c;它是程序的一次执行过程&#xff0c;包括了进程的创建&#xff0c;调度、…

c语言——计算两个正整数的最大公倍数

//计算两个正整数的最大公倍数 //例如40和60的最大公约数为20. //计算两个正整数的最大公倍数 //例如40和60的最大公约数为20. #include<stdio.h> int main() {int a,b,temp,i;printf("Input a & b:");scanf("%d%d",&a,&b);if(a<b){…

Cat.1如何成为物联网业务加速器?

随着Cat.1芯片及模组在功耗和成本上的不断优化&#xff0c;在窄带物联网领域&#xff0c;越来越多的终端客户把Cat.1当做与NB-IoT相比较的第二选择。越来越多的表计、烟感、市政等行业终端将Cat.1模组应用于非集中化部署的上报类终端业务中&#xff0c;Cat.1这只“网红猫”仍保…

UIKit相关

CALayer和UIView 区别 UIView继承自UIResponder&#xff0c;主要负责事件传递、事件响应&#xff0c;属于基于UIKit框架 CALayer继承自NSObject&#xff0c;负责图像渲染&#xff0c;动画和视图的显示&#xff0c;属于QuartzCore框架 而且这两大内容都符合单一职责原则&#…

【开发心得】黑客是如何攻击你的web应用的?

本文记录一次黑客利用sql注入漏洞攻击应用系统的全过程。 由于涉及到隐私&#xff0c;所以就不能上图了&#xff0c;尽量把过程描述的详细一下&#xff0c;希望能够给开发人员提个醒&#xff0c;有些漏洞&#xff0c;其实修补很简单。好在现在有了chatGPT&#xff0c;把你的代…

调度:setTimeout 和 setInterval

有时一个函数并不需要立刻执行&#xff0c;而是等待特定一段时间之后再执行。这就是所谓的“计划调用&#xff08;scheduling a call&#xff09; setTimeout() 是延时器&#xff0c;setInterval() 是定时器 setTimeout 允许我们将函数推迟到一段时间间隔之后再执行。setInte…

mongodb docker 及常用命令

MongoDB属于非关系型数据库&#xff0c;它是由C编写的分布式文档数据库。内部使用类似于Json的bson二进制格式。 中文手册 https://www.w3cschool.cn/mongodb/ 安装 https://www.mongodb.com/try/download/community 二进制安装可见另一篇&#xff1a; centos7 mongodb 4.0.28…

el-tooltip设置文字溢出时展示否则不展示

改写el-tooltip使其支持文字溢出时展示否则不展示&#xff0c;而不是需要使用js设置单独控制 新建 src/utils/rewriteElTooltip.js &#xff08;一模一样 cv就行&#xff09; export default function rewriteElTooltip(el) {el.props {...el.props,overflow: Boolea…