天天看點

Spring 擷取 request 的幾種方法及其線程安全性分析

本文将介紹在Spring MVC開發的Web系統中,擷取request對象的幾種方法,并讨論其線程安全性。

一、概述

在使用Spring MVC開發Web系統時,經常需要在處理請求時使用request對象,比如擷取用戶端IP位址、請求的URL、header中的屬性(如cookie、授權資訊)、body中的資料等。由于在Spring MVC中,處理請求的Controller、Service等對象都是單例的,是以擷取request對象時最需要注意的問題,便是request對象是否是線程安全的:當有大量并發請求時,能否保證不同請求/線程中使用不同的request對象。

這裡還有一個問題需要注意:前面所說的“在處理請求時”使用request對象,究竟是在哪裡使用呢?考慮到擷取request對象的方法有微小的不同,大體可以分為兩類:

1、在Spring的Bean中使用request對象:既包括Controller、Service、Repository等MVC的Bean,也包括了Component等普通的Spring Bean。為了友善說明,後文中Spring中的Bean一律簡稱為Bean。

2、在非Bean中使用request對象:如普通的Java對象的方法中使用,或在類的靜态方法中使用。

此外,本文讨論是圍繞代表請求的request對象展開的,但所用方法同樣适用于response對象、InputStream/Reader、OutputStream/ Writer等;其中InputStream/Reader可以讀取請求中的資料,OutputStream/Writer可以向響應寫入資料。

最後,擷取request對象的方法與Spring及MVC的版本也有關系;本文基于Spring4進行讨論,且所做的實驗都是使用4.1.1版本。

二、如何測試線程安全性

既然request對象的線程安全問題需要特别關注,為了便于後面的讨論,下面先說明如何測試request對象是否是線程安全的。

測試的基本思路,是模拟用戶端大量并發請求,然後在伺服器判斷這些請求是否使用了相同的request對象。

判斷request對象是否相同,最直覺的方式是列印出request對象的位址,如果相同則說明使用了相同的對象。然而,在幾乎所有web伺服器的實作中,都使用了線程池,這樣就導緻先後到達的兩個請求,可能由同一個線程處理:在前一個請求處理完成後,線程池收回該線程,并将該線程重新配置設定給了後面的請求。而在同一線程中,使用的request對象很可能是同一個(位址相同,屬性不同)。是以即便是對于線程安全的方法,不同的請求使用的request對象位址也可能相同。

為了避免這個問題,一種方法是在請求處理過程中使線程休眠幾秒,這樣可以讓每個線程工作的時間足夠長,進而避免同一個線程配置設定給不同的請求;另一種方法,是使用request的其他屬性(如參數、header、body等)作為request是否線程安全的依據,因為即便不同的請求先後使用了同一個線程(request對象位址也相同),隻要使用不同的屬性分别構造了兩次request對象,那麼request對象的使用就是線程安全的。本文使用第二種方法進行測試。

用戶端測試代碼如下(建立1000個線程分别發送請求):

Spring 擷取 request 的幾種方法及其線程安全性分析

伺服器中Controller代碼如下(暫時省略了擷取Request對象的代碼):

Spring 擷取 request 的幾種方法及其線程安全性分析

如果request對象線程安全,伺服器中列印結果如下所示:

Spring 擷取 request 的幾種方法及其線程安全性分析

如果存線上程安全問題,伺服器中列印結果可能如下所示:

Spring 擷取 request 的幾種方法及其線程安全性分析

如無特殊說明,本文後面的代碼中将省略掉測試代碼。

三、方法1:Controller中加參數

1、代碼示例

這種方法實作最簡單,直接上Controller代碼:

Spring 擷取 request 的幾種方法及其線程安全性分析

該方法實作的原理是,在Controller方法開始處理請求時,Spring會将request對象指派到方法參數中。除了request對象,可以通過這種方法擷取的參數還有很多,具體可以參見:

https://docs.spring.io/spring/docs/current/spring-framework-reference/web.html#mvc-ann-methods

Controller中擷取request對象後,如果要在其他方法中(如service方法、工具類方法等)使用request對象,需要在調用這些方法時将request對象作為參數傳入。

2、線程安全性

測試結果:線程安全

分析:此時request對象是方法參數,相當于局部變量,毫無疑問是線程安全的。線程安全的 Map 可以點此檢視這篇文章。

3、優缺點

這種方法的主要缺點是request對象寫起來備援太多,主要展現在兩點:

(1)如果多個controller方法中都需要request對象,那麼在每個方法中都需要添加一遍request參數

(2) request對象的擷取隻能從controller開始,如果使用request對象的地方在函數調用層級比較深的地方,那麼整個調用鍊上的所有方法都需要添加request參數

實際上,在整個請求處理的過程中,request對象是貫穿始終的;也就是說,除了定時器等特殊情況,request對象相當于線程内部的一個全局變量。而該方法,相當于将這個全局變量,傳來傳去。點選此處檢視公衆号全套Spring系列免費技術教程。

四、方法2:自動注入

先上代碼:

Spring 擷取 request 的幾種方法及其線程安全性分析

分析:在Spring中,Controller的scope是singleton(單例),也就是說在整個web系統中,隻有一個TestController;但是其中注入的request卻是線程安全的,原因在于:

使用這種方式,當Bean(本例的TestController)初始化時,Spring并沒有注入一個request對象,而是注入了一個代理(proxy);當Bean中需要使用request對象時,通過該代理擷取request對象。

下面通過具體的代碼對這一實作進行說明。

在上述代碼中加入斷點,檢視request對象的屬性,如下圖所示:

Spring 擷取 request 的幾種方法及其線程安全性分析

在圖中可以看出,request實際上是一個代理:代理的實作參見AutowireUtils的内部類ObjectFactoryDelegatingInvocationHandler:

Spring 擷取 request 的幾種方法及其線程安全性分析

也就是說,當我們調用request的方法method時,實際上是調用了由objectFactory.getObject()生成的對象的method方法;objectFactory.getObject()生成的對象才是真正的request對象。

繼續觀察上圖,發現objectFactory的類型為WebApplicationContextUtils的内部類RequestObjectFactory;而RequestObjectFactory代碼如下:

Spring 擷取 request 的幾種方法及其線程安全性分析

其中,要獲得request對象需要先調用currentRequestAttributes()方法獲得RequestAttributes對象,該方法的實作如下:

Spring 擷取 request 的幾種方法及其線程安全性分析

生成RequestAttributes對象的核心代碼在類RequestContextHolder中,其中相關代碼如下(省略了該類中的無關代碼):

Spring 擷取 request 的幾種方法及其線程安全性分析

通過這段代碼可以看出,生成的RequestAttributes對象是線程局部變量(ThreadLocal),是以request對象也是線程局部變量;這就保證了request對象的線程安全性。點選此處檢視公衆号全套Spring系列免費技術教程。

該方法的主要優點:

(1)注入不局限于Controller中:在方法1中,隻能在Controller中加入request參數。而對于方法2,不僅可以在Controller中注入,還可以在任何Bean中注入,包括Service、Repository及普通的Bean。

(2)注入的對象不限于request:除了注入request對象,該方法還可以注入其他scope為request或session的對象,如response對象、session對象等;并保證線程安全。

(3)減少代碼備援:隻需要在需要request對象的Bean中注入request對象,便可以在該Bean的各個方法中使用,與方法1相比大大減少了代碼備援。

但是,該方法也會存在代碼備援。考慮這樣的場景:web系統中有很多controller,每個controller中都會使用request對象(這種場景實際上非常頻繁),這時就需要寫很多次注入request的代碼;如果還需要注入response,代碼就更繁瑣了。下面說明自動注入方法的改進方法,并分析其線程安全性及優缺點。

五、方法3:基類中自動注入

與方法2相比,将注入部分代碼放入到了基類中。

基類代碼:

Spring 擷取 request 的幾種方法及其線程安全性分析

Controller代碼如下;這裡列舉了BaseController的兩個派生類,由于此時測試代碼會有所不同,是以服務端測試代碼沒有省略;用戶端也需要進行相應的修改(同時向2個url發送大量并發請求)。

Spring 擷取 request 的幾種方法及其線程安全性分析

分析:在了解了方法2的線程安全性的基礎上,很容易了解方法3是線程安全的:當建立不同的派生類對象時,基類中的域(這裡是注入的request)在不同的派生類對象中會占據不同的記憶體空間,也就是說将注入request的代碼放在基類中對線程安全性沒有任何影響;測試結果也證明了這一點。線程安全的 Map 可以點此檢視這篇文章。

與方法2相比,避免了在不同的Controller中重複注入request;但是考慮到java隻允許繼承一個基類,是以如果Controller需要繼承其他類時,該方法便不再好用。

無論是方法2和方法3,都隻能在Bean中注入request;如果其他方法(如工具類中static方法)需要使用request對象,則需要在調用這些方法時将request參數傳遞進去。下面介紹的方法4,則可以直接在諸如工具類中的static方法中使用request對象(當然在各種Bean中也可以使用)。點選此處檢視公衆号全套Spring系列免費技術教程。

六、方法4:手動調用

分析:該方法與方法2(自動注入)類似,隻不過方法2中通過自動注入實作,本方法通過手動方法調用實作。是以本方法也是線程安全的。

優點:可以在非Bean中直接擷取。缺點:如果使用的地方較多,代碼非常繁瑣;是以可以與其他方法配合使用。

七、方法5:@ModelAttribute方法

下面這種方法及其變種(變種:将request和bindRequest放在子類中)在網上經常見到:

測試結果:線程不安全

分析:@ModelAttribute注解用在Controller中修飾方法時,其作用是Controller中的每個@RequestMapping方法執行前,該方法都會執行。是以在本例中,bindRequest()的作用是在test()執行前為request對象指派。雖然bindRequest()中的參數request本身是線程安全的,但由于TestController是單例的,request作為TestController的一個域,無法保證線程安全。

八、總結

綜上所述,Controller中加參數(方法1)、自動注入(方法2和方法3)、手動調用(方法4)都是線程安全的,都可以用來擷取request對象。如果系統中request對象使用較少,則使用哪種方式均可;如果使用較多,建議使用自動注入(方法2 和方法3)來減少代碼備援。如果需要在非Bean中使用request對象,既可以在上層調用時通過參數傳入,也可以直接在方法中通過手動調用(方法4)獲得。