写点什么

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

评论

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

week-4-part2 学习总结

陈龙

【架构师训练营 1 期】第四周作业及学习总结

诺乐

架构第四周总结

Geek_Gu

总结

一期二班-吴水金-第四课作业

吴水金

SpringBoot系列(4)- 记录请求日志

引花眠

springmvc springboot

架构师训练营第四周作业

Erwa

第四周 作业1

mm马

极客大学架构师训练营

第四周作业

熊桂平

极客大学架构师训练营

架构师 01 期,第三周课后作业

子文

ARTS打卡 第20周

引花眠

微服务 ARTS 打卡计划 springboot

第四周 系统架构 学习总结

应鹏

学习 极客大学架构师训练营

架构师训练营第 1 期 - 第 4 周 - 作业

wgl

架构师训练营第一期——第四周作业

tao

系统架构

Zzzz

极客大学架构师训练营

第四周 总结

mm马

极客大学架构师训练营

week-4-part1 大型互联网应用系统使用的技术

陈龙

第四周-系统架构-作业

刘希文

架构师训练营第 1 期 -Week4 - 课后练习

鲁大江

极客大学架构师训练营 架构方案 架构手段

架构师训练营第 1 期 -Week4 - 系统架构学习总结

鲁大江

极客大学架构师训练营 互联网架构

架构师训练营第四周总结

Erwa

一期二班-吴水金-第四课总结

吴水金

作业-2020-10-18

芝麻酱

架构师训练营 第二周作业

haha

极客大学架构师训练营

第四周 系统架构 作业一

应鹏

极客大学架构师训练营 课程作业

系统架构

wing

架构师一期

Week_04作业+总结

golangboy

极客大学架构师训练营

桂林漫游流水记

穿过生命散发芬芳

美食 旅行

第4周

paul

架构师 01 期,第四周课后作业

子文

架构师训练营第4周:系统架构

子青

算法判断循环链表、数据工程师练级攻略、python从入门到精通、UML精粹读后感、John 易筋 ARTS 打卡 Week 22

John(易筋)

ARTS 打卡计划 UML精粹 数据工程师必备技能 python从入门到精通 循环链表

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