呼叫鏈系列(3):如何從零開始捕獲body和header

宜信技術學院發表於2019-08-15

擴充閱讀: 呼叫鏈系列(1):解讀UAVStack中的貪吃蛇

呼叫鏈系列(2):輕呼叫鏈實現

在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:

規範中已經為我們提供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介面。

ServletRequestWrapper已經幫我們實現了大部分的方法。

我們只需要將關心的幾個方法覆寫即可,如:getInputStream和getReader等。

當使用者嘗試呼叫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方法,如下圖:

既然read方法是統一入口,是否只需要自定義實現一個ServletInputStream並覆寫其中的read()方法就能修改所有讀取方式了呢?答案是肯定的。只要在使用者呼叫read方法時,悄悄複製一份我們關心的內容,就能保證只有在使用者使用body時才讀取inputStream。

下一個問題就是如何保證在使用者多次呼叫read時只讀取一次inputStream。這裡需要藉助一個AtomicBoolean標誌:當已經進行了一次完整讀取後,將其置為true;否則為false。最終效果如下:

七、舉一反三

這裡我們使用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/uavstack

作者:李崇

來源:宜信技術學院


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/69918724/viewspace-2653750/,如需轉載,請註明出處,否則將追究法律責任。

相關文章