写点什么

调用链系列三:解读 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:16864

评论

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

南京大学与阿里云联合启动人工智能人才培养合作计划,已将通义灵码引入软件学院课程体系

阿里巴巴云原生

阿里云 云原生 通义灵码

《Operating System Concepts》阅读笔记:p552-p579

codists

操作系统

Centos系统云主机中nvme盘不可用解决方法

天翼云开发者社区

云主机

东京 Voice AI Agent 工作坊!1 小时让你的 AI 能听能说

声网

【超详细】Mac读取移动硬盘速度很慢的原因及解决方法

阿拉灯神丁

文件存储 磁盘管理 Mac软件 苹果电脑 Tuxera NTFS2024

阿里云可观测 2025 年 3 月产品动态

阿里巴巴云原生

阿里云 云原生 可观测

Techub 财报解读:Circle 冲刺 IPO,但收入增长难掩利润困局

TechubNews

Kyutai 推出 Moshi 语音模型微调工具包;语音智能体平台 Vapi 2.0 上线,已提供 4400 万通电话服务丨日报

声网

人工智能+牙科:临床应用中的几个问题

算AI

人工智能 算法 医疗AI 视觉模型

开源项目:一行代码,批量 PDF 转 Word 轻松搞定!

程序员晚枫

PDF word 自动化办公

智能体爆发元年,谁在“无人区”绘制地形图?

脑极体

AI

Apipost vs Apifox:高效API协作的差异化功能解析

数据追梦人

TiDB × AI :DeepSeek 时代你需要什么样的数据基座

TiDB 社区干货传送门

GraalVM 24 正式发布阿里巴巴贡献重要特性 —— 支持 Java Agent 插桩

阿里巴巴云原生

阿里云 云原生

CAD裁剪块方法

极客天地

卫浴“家电化”:一场科技驱动洗牌赛拉开序幕

Alter

AI框架不牢,模型地动山摇

白洞计划

AI

高可靠架构+智能运维,华为云会议“始终在线”!

平平无奇爱好科技

AI框架不牢,模型地动山摇

脑极体

AI

百度开放3000+实习机会,AI相关岗位占比87%

科技大数据

Rust 如何轻松实现 RTMP 流媒体推送?深入解析直播推流场景与解决方案

Yeauty

rust ffmpeg Video RTMP media

LLM 不断提升智能下限,MCP 不断提升创意上限

阿里巴巴云原生

阿里云 微服务 云原生 LLM

AI 浪潮下企业身份管理:特点凸显,安全挑战升级

TechLead Studio

AI 企业身份安全

如何利用科学的预算管理为企业释放更多价值

智达方通

数字化转型 全面预算管理 财务管理 财务规划

Python + 腾讯云,多页PDF发票识别一键搞定!

程序员晚枫

开源

NineData云原生智能数据管理平台新功能发布|2025年3月版

NineData

南京大学与阿里云联合启动人工智能人才培养合作计划,已将通义灵码引入软件学院课程体系

阿里云云效

阿里云 云原生 通义灵码

掌握 JSON 到表格转换:全面指南

数据追梦人

数据库选型指南:TiDB 与 MySQL 全方位对比清单新鲜出炉!

TiDB 社区干货传送门

TiDB 观测性解读(一)丨索引观测:快速识别无用索引与低效索引

TiDB 社区干货传送门

TiDB 可观测性解读(二)丨算子执行信息性能诊断案例分享

TiDB 社区干货传送门

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