【AICon】探索八个行业创新案例,教你在教育、金融、医疗、法律等领域实践大模型技术! >>> 了解详情
写点什么

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

评论

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

回"疫"录(10):危机与希望

小天同学

疫情 回忆录 现实纪录 纪实

Java并发编程基础--Java内存模型

Java收录阁

每日算法之leetcode 50 Power

田镇珲

递归 LeetCode 分治

作为自由职业者,我的近况

一尘观世界

程序员 自由职业 复盘

自助设备系列——增长点

孙苏勇

产品 行业资讯 智能设备

工作感悟随笔

程序员小岑

感悟

理解go 的 sort

huizhou92

Go 语言

和孩子聊聊死亡

陈医僧Ethan

感悟 育儿

C++ sqlite3使用指南

泰伦卢

c c++ C#

原创 | 使用JUnit、AssertJ和Mockito编写单元测试和实践TDD (三)单元测试在整个测试体系中的位置

编程道与术

软件测试 TDD 单元测试 集成测试 验收测试

Java并发编程--ReentrantLock

Java收录阁

并发编程

游戏开发通用技术和工具

波波

编程 游戏开发 H5游戏

go 怎样做 stw

huizhou92

Go 语言

Web百度离线地图开发

玏佾

WebGIS 离线地图 Web离线地图

C++中glog源码剖析以及如何设计一个高效 log模块

泰伦卢

c c++ C#

未完成乞丐版 Args

escray

学习 CSD 认证实战营

电脑城最简单骗局,仍然有无数人上当

周三不加班

电脑城 电脑选购

程序员• 后浪

古时的风筝

程序员 后浪

Netty 源码解析(八): 回到 Channel 的 register 操作

猿灯塔

想看懂stl代码,先搞定type_traits是关键

泰伦卢

c c++ C#

程序员《后浪》-图文版

Java_若依框架教程

后浪 程序媛 娱乐

DDD 实践手册(5. Factory 与 Repository)

Joshua

企业架构 设计模式 领域驱动设计 DDD 架构模式

MacOS高效使用指南-我的体系化方案以及软件清单

lmymirror

高效工作 效率工具 知识管理 Mac 操作系统

数据湖引擎是什么鬼

数据社

大数据 数据仓库 数据湖 数据架构

时间足够爱你

rmrf

学习 思考 持之以恒

关于 WordPress-Automatic 的分布式办公实践

刘培培

分布式办公 远程办公

聊聊Redis SDS

huizhou92

redis

无所不能 就像妈妈一样

Neco.W

思考 情绪

深入浅出虚拟内存

泰伦卢

c c++ C#

Redis学习笔记(列表类型)

编程随想曲

redis

架构师快问快答2

IT民工大叔

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