2天时间,聊今年最热的 Agent、上下文工程、AI 产品创新等话题。2025 年最后一场~ 了解详情
写点什么

调用链系列三:解读 UAVStack 中的调用链技术

  • 2020-02-11
  • 本文字数:2394 字

    阅读完需:约 8 分钟

调用链系列三:解读UAVStack中的调用链技术

在 Java 中,HTTP 协议的请求/响应模型是由 Servlet 规范+Servlet 容器(如 Tomcat)实现的。换句话说,在类 Tomcat 容器中,一次完整的 HTTP 请求都是通过实现 Servlet 规范完成的;Spring、Jesery 等技术栈也是在 Servlet 规范基础上封装的。因此我们可以借助底层的 Servlet 规范来获取 Java 技术栈中 HTTP 的 body 和 header,即通过拦截用户自定义实现的 HttpServlet 类中的 HttpServletRequest 和 HttpServletResponse,获取 HTTP 的 body 和 header。


通过阅读前几篇文章大家知道,调用链模型和架构都是依托 UAVStack 的中间件增强框架技术实现的。在这篇文章中,我会向大家具体介绍如何从零开始捕获 body 和 header。

拦截 http 请求

想要在尽可能少改动代码的前提下从请求中提取 body 和 header,必须对进入容器的请求进行统一拦截,否则就需要在所有 HttpServlet 实现类中嵌入代码。这里要再次感谢 Servlet 规范制定者为我们提供的 filter 机制。


根据 Servlet 规范,filter 是一个可重用的代码段,可以转换 HTTP requests、responses 和 header 信息的内容。过滤器一般不会为一个 request 创建一个响应,而是会修改或适配一个 request 和 response。filter 主要提供四种拦截方式:


  • REQUEST:直接访问目标资源时执行过滤器。包括:在地址栏中直接访问、表单提交、超链接、重定向,只要在地址栏中可以看到目标资源的路径,就是 REQUEST;

  • FORWARD:转发访问执行过滤器。包括 RequestDispatcher#forward()方法、< jsp:forward>标签都是转发访问;

  • INCLUDE:包含访问执行过滤器。包括 RequestDispatcher#include()方法、< jsp:include>标签都是包含访问;

  • ERROR:当目标资源在 web.xml 中配置为< error-page>中时,并且真的出现了异常,转发到目标资源时,会执行过滤器。


这里我们只需使用 REQUEST 模式。配置 filter 以后,我们就可以从 filter 的 doFilter 方法中获取到 HttpServletRequest 和 HttpServletResponse(后文简称 request 和 response)了。

获取 header

上文中我们已经通过 filter 机制获取了 request 和 response。打开对应源码实现我们可以发现如下 API:


1551237143561038990.png


规范中已经为我们提供 API 直接获取 header,通过组合使用 getHeaderNames()和 getHeader(String name)方法我们可以轻松获取到 request 和 response 中的 header。

获取 body

request 和 response 获取 body 的方式大体相同。此处我们先以 request 为例,后文会对不同之处进行适配。


从 request 的 API 中可以发现,body 在 Java 中是以 ServletInputStream 形式存储的,并且 ServletInputStream 是继承的 InputStream。若直接读取,用户获取到的 body 将为空(因为 InputStream 只能被读取一次,除非把指针回执)。这里我们就需要借助 Servlet 的 wrapper 机制了。

Servlet 中的 wrapper

这里简单介绍一下 requestWrapper 和 responseWrapper。wrapper 是一种装饰模式,在 Servlet 规范中通过继承 HttpServletResponseWrapper 和 HttpServletRequestWrapper 实现,相当于为 request 和 response 进行了一次套壳,类似于 Java 中的代理,这样所有操作 request 和 response 的动作都会经过我们的自定义 wrapper,使重复获取 request 和 response 中的 body 成为可能。

编写自己的 wrapper

我们以 request 为例,解释如何编写自定义 wrapper。打开 servlet-api 源码可见 HttpServletRequestWrapper 继承了 ServletRequestWrapper 并且实现了 HttpServletRequest 接口。


1551237151328071800.png


ServletRequestWrapper 已经帮我们实现了大部分的方法。


1551237162163017474.jpg


我们只需要将关心的几个方法覆写即可,如:getInputStream 和 getReader 等。


1551237169911062083.jpg


当用户尝试调用 getReader 或 getInputStream 时,我们将之替换为自己的流,并且额外提供一个 getContent()方法,将提前从 StringBuilder 或 byte[]中读取到的 body 内容进行提取。


编写完自定义 wrapper 以后,我们就可以将其放入我们上文定义好的 filter 中,并将原 request 进行包装替换,进而将用户的 request 都变成我们的 requestWrapper。

优化提取逻辑

上文的方法相当于是将包含 body 的 inputStream 提前进行一次读取,将其存储在中间 byte[]或 StringBuilder 当中,当用户在调用 getInputStream 时,将 byte[]或 StringBuilder 转成 inputStream 返给用户。如果用户根本不关心本次 http 请求的 body,即用户根本没有使用此次请求的 body,那我们将其提前读取出来相当于做了一次无用功(浪费了宝贵的 CPU 时间和内存资源)。如何保证只有在用户使用时才读取 inputStream,并且当用户或后续逻辑多次获取 body 时都只读一次是我们优化的目标。


答案还是继续从源码中寻找。既然我们的数据在 inputStream 中,那我们可以跟进源码,看看 inputStream 是如何被读取到的。在 Servlet 规范中,inputStream 被封装成了 ServletInputStream,而 ServletInputStream 又提供了一个 readLine 方法。仔细观察可以发现,他们都是调用了 inputStream 中的 read 方法,如下图:


1551237176632045190.png


既然 read 方法是统一入口,是否只需要自定义实现一个 ServletInputStream 并覆写其中的 read()方法就能修改所有读取方式了呢?答案是肯定的。只要在用户调用 read 方法时,悄悄复制一份我们关心的内容,就能保证只有在用户使用 body 时才读取 inputStream。


下一个问题就是如何保证在用户多次调用 read 时只读取一次 inputStream。这里需要借助一个 AtomicBoolean 标志:当已经进行了一次完整读取后,将其置为 true;否则为 false。最终效果如下:


1551237183193085247.png


举一反三


这里我们使用 Servlet 规范中的 filter 和 wrapper 机制来获取进入我们容器(Tomcat)中所有 Http 请求的 body 和 header。这个能力在实际生产中还能进一步拓展,如:传输某些敏感数据时,在 Client 端进行加密,然后在 Server 端统一解密,并格式化 Client 端上送的数据格式等。


本文转载自宜信技术学院。


原文链接:http://college.creditease.cn/detail/218


2020-02-11 20:16952

评论

发布
暂无评论
发现更多内容

再不解决延迟不当,小心你的内存被打爆

华为云开发者联盟

线程 延迟 内存 并发 Sleep

redis在微服务领域的贡献

捉虫大师

redis dubbo RPC 协议 注册中心

全新F1洞察精彩亮相,帮你理解赛道上的瞬间决定!

亚马逊云科技 (Amazon Web Services)

GitHub开源的10个超棒后台管理面板

不脱发的程序猿

GitHub 开源 后台管理面板

详解RS232、RS485、RS422、串口和握手

不脱发的程序猿

串口 通信总线 RS232、RS485、RS422 握手通信

Django 之路由篇

若尘

django Python编程 路由 5月日更

选择排序&插入排序 - DAY 15

Qien Z.

排序算法 插入排序 5月日更

ThreadLocal内存溢出代码演示和原因分析!

王磊

Java 多线程

腾讯云实名认证流程

三掌柜

5月日更

☕【JVM 技术之旅】深入JVM原理分析synchronized

码界西柚

synchronized 重量级锁 5月日更 同步锁 ObjectMontior

密码学系列之:memory-hard函数

程序那些事

加密解密 密码学 程序那些事

小傅哥,一个有“副业”的码农!

小傅哥

Java 小傅哥 技术成长 码农副业

详解 WebRTC 高音质低延时的背后 — AGC(自动增益控制)

阿里云CloudImagine

阿里云 WebRTC 3A算法 音频技术 视频云

字节、美团等客户与华为联合创新DCI智能控制器,共筑互联网基础设施新生态

NUCLEO-L432KC实现ADC配置(STM32L432KC)

不脱发的程序猿

嵌入式 单片机 NUCLEO-L432KC STM32L432KC 光敏电阻传感器

Feed流系统重构-架构篇

勇哥java实战分享

架构 RocketMQ 分库分表 ShardingJDBC redisson

通用连接池帮你解决资源管理难题

万俊峰Kevin

MySQL redis mongodb pool Go 语言

记一次与写作朋友的线下沙龙

架构精进之路

技术交流 杂记 5月日更

强化基于位置的4种营销策略

郑州埃文科技

IP 营销 ISP

Cilium 1.10 重磅发布!】支持 Wireguard, BGP, Egress IP 网关, XDP 负载均衡, 阿里云集成

公众号:云原生Serverless

云原生 cilium cni

探索专有领域的端到端ASR解决之道

华为云开发者联盟

端到端 ASR 自动语音识别 语境偏移 专有领域

手把手带你体验 Amazon Graviton2 的高性价比!文末有惊喜

亚马逊云科技 (Amazon Web Services)

【Flutter 专题】120 Flutter & 腾讯移动通讯 TPNS~

阿策小和尚

5月日更 Flutter 小菜 0 基础学习 Flutter Android 小菜鸟

k8s 集群下微服务 pod 的各种指标信息监控

Damon

微服务 5月日更

Nginx调试必备的几种技能

运维研习社

nginx 运维 实用技巧 5月日更

掌握学习方法,成为技术大牛

实力程序员

5G掀起工业互联网浪潮,水泥厂智能管理模式收效颇丰

一只数据鲸鱼

数据可视化 工业互联网 智慧工厂 水泥厂 智能工厂

驾云驭能,云科技点燃制造创新之旅!

亚马逊云科技 (Amazon Web Services)

CG行业云渲染服务的演进之路

华为云开发者联盟

公有云 CG 渲染 云渲染 影视动画

☕【JVM 技术之旅】攻克技术盲点之“JVM常量池们“

码界西柚

JVM 5月日更 字符串常量池 静态常量池 运行时常量池

屏幕共享的实现与应用

anyRTC开发者

音视频 WebRTC RTC sdk

调用链系列三:解读UAVStack中的调用链技术_区块链_李崇_InfoQ精选文章