呼叫鏈系列(3):如何從零開始捕獲body和header
擴充閱讀: 呼叫鏈系列(1):解讀UAVStack中的貪吃蛇
在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/,如需轉載,請註明出處,否則將追究法律責任。
相關文章
- 劇透!全鏈路壓測從零開始系列目錄
- 從零開始,開發一個 Web Office 套件(10):捕獲鍵盤事件,輸入文字Web套件事件
- 從零開始學Spring Boot系列-SpringApplicationSpring BootAPP
- 從零開始搭建部落格系列
- 從零開始學Spring Boot系列-整合mybatisSpring BootMyBatis
- 從零開始學Spring Boot系列-整合MySQLSpring BootMySql
- 從零開始學Spring Boot系列-整合KafkaSpring BootKafka
- 從零開始
- 不怕從零開始,只怕從未開始!
- 從零開始學Spring Boot系列-外部化配置Spring Boot
- 如何從零開始整合DTM Android SDKAndroid
- pb呼叫ole異常捕獲
- VUE從零開始系列(上手),呆萌小白上手VUEVue
- VUE從零開始系列(axios),呆萌小白上手VUEVueiOS
- PYTHON系列-從零開始的爬蟲入門指南Python爬蟲
- 從零開始學Spring Boot系列-返回json資料Spring BootJSON
- 從零開始學Spring Boot系列-整合MyBatis-PlusSpring BootMyBatis
- ?從零開始學習webpack系列二(配置檔案)Web
- PHP從零開始系列二(學習筆記):序言PHP筆記
- PHP從零開始系列一(學習筆記):前言PHP筆記
- 從零開始學習如何部署程式碼
- 從零開始學Java,如何拿高工資?Java
- 【D3.js 入門系列一】從零開始繪製一個柱形圖JS
- 從零開始系列-Laravel編寫api服務介面:4.Migration 和 SeedLaravelAPI
- flutter 從零開始-1Flutter
- 從零開始學PythonPython
- ?從零開始學習webpack系列五(解析打包樣式)Web
- 從零開始學C語言 第3版pdfC語言
- 從零開始:NetBox 4.1 Docker 部署和升級Docker
- 軟體測試如何從零開始學習
- 我是如何從零開始學習前端的前端
- 原生專案如何從零開始整合 React NativeReact Native
- 從零開始使用 Webpack 搭建 Vue3 開發環境WebVue開發環境
- 從零開始, 開發一個 Web Office 套件 (3): 滑鼠事件Web套件事件
- 從零開始--webpack 4 配置Web
- 從零開始學 Spring BootSpring Boot
- 線段樹從零開始
- 從零開始學正則