文章采集调用(打开servlet-api源码可见继承和response获取方式)

优采云 发布时间: 2022-02-03 22:19

  文章采集调用(打开servlet-api源码可见继承和response获取方式)

  为了从请求中提取body和header,使代码的改动尽可能小,进入容器的请求必须被统一拦截,否则代码必须嵌入到所有的HttpServlet实现类中。在这里,我们需要再次感谢 servlet 说明符为我们提供的过滤机制。

  该规范指定过滤器是一段可重用的代码,用于转换 HTTP 请求、响应和标头信息的内容。过滤器通常不会为请求创建响应,而是修改或调整请求和响应。其中,filter主要提供四种拦截方式:

  这里我们只使用 REQUEST 模式。配置过滤器后,我们可以从过滤器的doFilter方法中获取HttpServletRequest和HttpServletResponse(简称请求和响应)。

  获取标题

  在上面,我们通过过滤机制获得了请求和响应。打开对应的源码实现,我们可以找到如下API:

  

  规范为我们提供了一个 API 来直接获取标头。通过结合 getHeaderNames() 和 getHeader(String name) 方法,我们可以轻松获取请求和响应中的标头。

  得到身体

  获取请求体和响应体的方法大致相同。这里以request为例,后面会调整差异。

  从请求API中我们可以发现body在java中是以ServletInputStream的形式存储的,而ServletInputStream是一个继承的InputStream。如果我们直接读取,用户获取到的body会是空的(因为InputStream只能读取一次),除非可以把指针放回去)。这里需要用到servlet的包装机制。

  Servlet 中的包装器

  有的人可能不会用到requestWrapper和responseWrapper,这里简单介绍一下。wrapper 是通过继承 servlet 规范中的 HttpServletResponseWrapper 和 HttpServletRequestWrapper 实现的装饰模式。相当于请求和响应的一个shell,类似于java中的代理,这样操作请求和响应的所有动作都会经过我们自定义的wrapper,使得在请求中重复获取body成为可能和回应。

  编写自己的包装器

  我们以 request 为例来说明如何编写自定义包装器。打开servlet-api源码可以看到HttpServletRequestWrapper继承了ServletRequestWrapper,实现了HttpServletRequest接口。

  

  并且大部分方法已经在 ServletRequestWrapper 中为我们实现了。

  

  我们只需要重写我们关心的几个方法,例如:getInputStream和getReader等。

  

  当用户尝试调用 getReader 或 getInputStream 时,我们将其替换为自己的流,并额外提供一个 getContent() 方法来提前读取 StringBuilder 或 byte[] 中的正文内容进行提取。

  编写好自定义包装器后,我们可以将其放入我们上面定义的过滤器中,替换原来的请求。然后把用户的请求变成我们的requestWrapper。

  优化提取逻辑

  上面提到的方法等价于预先读取收录body的inputStream,并将其存储在一个中间byte[]或StringBuilder中。当用户调用 getInputStream 时,byte[] 或 StringBuilder 被转换为 inputStream 返回给用户。如果用户根本不关心这个http请求的body,也就是用户根本不使用这个请求的body,那么我们提前把它读出来,相当于做了无用功(浪费宝贵的 CPU 时间和内存资源)。如何保证inputStream在用户使用时是只读的,当用户或后续逻辑多次获取body时,是我们的优化目标。

  对于答案,我们继续从源码中寻找。既然我们的数据在 inputStream 中,我们就跟着源码看看是怎么读取的。在servlet规范中,inputStream被封装成ServletInputStream,ServletInputStream提供了readLine方法。往里面看,可以看到它们都调用了inputStream中的read方法,如下图:

  

  既然read方法是一个统一的入口,那我们只要自定义一个ServletInputStream,重写里面的read()方法,就可以修改所有的read方法吗?答案是肯定的。只要用户调用了read方法,我们就悄悄地把我们关心的内容复制一份,这样我们只在用户使用body的时候才去采集。

  下一步是如何确保用户调用 read 多次,我们总是只读取一次。在这里,我们需要使用一个 AtomicBoolean 标志,当执行完整读取时将其设置为 true,否则设置为 false。最终效果如下:

  

  举一反三

  在这里,我们使用 servlet 规范中的过滤器和包装器机制来获取进入我们容器(tomcat)的所有 http 请求的主体和标头。这个能力在我们实际生产中可以进一步扩展,比如:我们需要在客户端加密一些敏感数据然后在服务端进行统一解密,格式化客户端发送的数据格式等等。可以限制你的是你的想象力。希望本次技术分享对您以后的工作和生活有所帮助。

  总结

  读完这篇文章,读者应该可以在不影响原代码的情况下,通过简单的代码获取所有进入容器的http请求的body和header。虽然可能需要适配一些特殊的技术栈,比如如果项目中使用了jersey,参数等信息以application/x-www-form-urlencoded的形式传递,而服务端不使用@ FormParam注解获取参数,那么我们获取body后,用户将无法获取参数;例如,如何区分媒体类型,如果是图像,则不会被提取。但我们已经验证,这条路是可行的,而且是成功的一半。

0 个评论

要回复文章请先登录注册


官方客服QQ群

微信人工客服

QQ人工客服


线