調用鏈系列(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...

做者:李崇

來源:宜信技術學院

相關文章
相關標籤/搜索