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

本专题前几篇文章主要从架构层面介绍了如何实现分布式调用追踪系统。这篇文章咱们不谈架构,就其中的一项关键技术实现进行深刻探讨:如何从超文本传输协议(HTTP)中获取request和response的body和header。git


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

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

拦截http请求api

想要在尽量少改动代码的前提下从请求中提取body和header,必须对进入容器的请求进行统一拦截,不然就须要在全部HttpServlet实现类中嵌入代码。这里要再次感谢Servlet规范制定者为咱们提供的filter机制。架构

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

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

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

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

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

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

获取header

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

1
规范中已经为咱们提供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接口。

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

3
咱们只须要将关心的几个方法覆写便可,如:getInputStream和getReader等。

4
当用户尝试调用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方法,以下图:

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

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

6

触类旁通

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


读完本文,你们应该可以在不影响原代码的前提下,经过简单代码获取进入容器的全部Http请求的body和header。不过对于特殊技术栈,还须要进行适配。若是项目中使用了Jersey且使用application/x-www-form-urlencoded形式传递参数等信息,而服务端没有使用@FormParam注解来获取参数,那么获取body之后用户将没法获取参数。但至少咱们已经验证了这条路是可行的,因此已经成功了一半。但愿这份技术分享可以在工做中帮到你们。

本月调用链专题的推送就到此为止啦,若是对调用链技术仍有疑问,欢迎后台留言,咱们会尽快回复你们~下月咱们将开启新的专题,敬请期待~~~

官方网站

开源地址

UAVStack已在Github上开放源码,并提供了安装部署、架构说明和用户指南等双语文档,欢迎访问-给星-拉取~~~

扫一扫下方二维码,关注一个不会让你失望的公众号

7
相关文章
相关标签/搜索