调用链系列(3):如何从零开始捕获body和header

拓展阅读:调用链系列(1):解读UAVStack中的贪吃蛇git

调用链系列(2):轻调用链实现github

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

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

1、拦截http请求

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

根据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)了。app

2、获取header

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

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

3、获取body

request和response获取body的方式大致相同。此处咱们先以request为例,后文会对不一样之处进行适配。优化

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

4、Servlet中的wrapper

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

5、编写本身的wrapper

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

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

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

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

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

6、优化提取逻辑

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

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

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

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

7、触类旁通

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

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

开源地址:https://github.com/uavorg/uav...

做者:李崇

来源:宜信技术学院