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

来源:https://www.cnblogs.com/kismetv/archive/2018/04/10/8757260.html
-Advertisement-
Play Games

前言 本文將介紹在Spring MVC開發的web系統中,獲取request對象的幾種方法,並討論其線程安全性。 原創不易,如果覺得文章對你有幫助,歡迎點贊、評論。文章有疏漏之處,歡迎批評指正。 歡迎轉載,轉載請註明原文鏈接:http://www.cnblogs.com/kismetv/p/8757 ...


前言

本文將介紹在Spring MVC開發的web系統中,獲取request對象的幾種方法,並討論其線程安全性。

原創不易,如果覺得文章對你有幫助,歡迎點贊、評論。文章有疏漏之處,歡迎批評指正。

歡迎轉載,轉載請註明原文鏈接:http://www.cnblogs.com/kismetv/p/8757260.html

目錄

概述

如何測試線程安全性

方法1:Controller中加參數

方法2:自動註入

方法3:基類中自動註入

方法4:手動調用

方法5:@ModelAttribute方法

總結

概述

在使用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個線程分別發送請求):

public class Test {
	public static void main(String[] args) throws Exception {
		String prefix = UUID.randomUUID().toString().replaceAll("-", "") + "::";
		for (int i = 0; i < 1000; i++) {
			final String value = prefix + i;
			new Thread() {
				@Override
				public void run() {
					try {
						CloseableHttpClient httpClient = HttpClients.createDefault();
						HttpGet httpGet = new HttpGet("http://localhost:8080/test?key=" + value);
						httpClient.execute(httpGet);
						httpClient.close();
					} catch (IOException e) {
						e.printStackTrace();
					}
				}
			}.start();
		}
	}
}

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

@Controller
public class TestController {

	// 存儲已有參數,用於判斷參數是否重覆,從而判斷線程是否安全
	public static Set<String> set = new HashSet<>();

	@RequestMapping("/test")
	public void test() throws InterruptedException {
		
		// …………………………通過某種方式獲得了request對象………………………………

		// 判斷線程安全
		String value = request.getParameter("key");
		if (set.contains(value)) {
			System.out.println(value + "\t重覆出現,request併發不安全!");
		} else {
			System.out.println(value);
			set.add(value);
		}
		
		// 模擬程式執行了一段時間
		Thread.sleep(1000);
	}
}

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

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

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

方法1:Controller中加參數

代碼示例

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

@Controller
public class TestController {
	@RequestMapping("/test")
	public void test(HttpServletRequest request) throws InterruptedException {
		// 模擬程式執行了一段時間
		Thread.sleep(1000);
	}
}

該方法實現的原理是,在Controller方法開始處理請求時,Spring會將request對象賦值到方法參數中。除了request對象,可以通過這種方法獲取的參數還有很多,具體可以參見:https://docs.spring.io/spring/docs/current/spring-framework-reference/web.html#mvc-ann-methods

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

線程安全性

測試結果:線程安全

分析:此時request對象是方法參數,相當於局部變數,毫無疑問是線程安全的。

優缺點

這種方法的主要缺點是request對象寫起來冗餘太多,主要體現在兩點:

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

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

實際上,在整個請求處理的過程中,request對象是貫穿始終的;也就是說,除了定時器等特殊情況,request對象相當於線程內部的一個全局變數。而該方法,相當於將這個全局變數,傳來傳去。

方法2:自動註入

代碼示例

先上代碼:

@Controller
public class TestController{
	
	@Autowired
	private HttpServletRequest request; //自動註入request
	
	@RequestMapping("/test")
	public void test() throws InterruptedException{
		//模擬程式執行了一段時間
		Thread.sleep(1000);
	}
}

線程安全性

測試結果:線程安全

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

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

 

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

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

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

	/**
	 * Reflective InvocationHandler for lazy access to the current target object.
	 */
	@SuppressWarnings("serial")
	private static class ObjectFactoryDelegatingInvocationHandler implements InvocationHandler, Serializable {
		private final ObjectFactory<?> objectFactory;
		public ObjectFactoryDelegatingInvocationHandler(ObjectFactory<?> objectFactory) {
			this.objectFactory = objectFactory;
		}
		@Override
		public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
			// ……省略無關代碼
			try {
				return method.invoke(this.objectFactory.getObject(), args); // 代理實現核心代碼
			}
			catch (InvocationTargetException ex) {
				throw ex.getTargetException();
			}
		}
	}

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

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

	/**
	 * Factory that exposes the current request object on demand.
	 */
	@SuppressWarnings("serial")
	private static class RequestObjectFactory implements ObjectFactory<ServletRequest>, Serializable {
		@Override
		public ServletRequest getObject() {
			return currentRequestAttributes().getRequest();
		}
		@Override
		public String toString() {
			return "Current HttpServletRequest";
		}
	}

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

	/**
	 * Return the current RequestAttributes instance as ServletRequestAttributes.
	 */
	private static ServletRequestAttributes currentRequestAttributes() {
		RequestAttributes requestAttr = RequestContextHolder.currentRequestAttributes();
		if (!(requestAttr instanceof ServletRequestAttributes)) {
			throw new IllegalStateException("Current request is not a servlet request");
		}
		return (ServletRequestAttributes) requestAttr;
	}

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

public abstract class RequestContextHolder {
	public static RequestAttributes currentRequestAttributes() throws IllegalStateException {
		RequestAttributes attributes = getRequestAttributes();
		// 此處省略不相關邏輯…………
		return attributes;
	}
	public static RequestAttributes getRequestAttributes() {
		RequestAttributes attributes = requestAttributesHolder.get();
		if (attributes == null) {
			attributes = inheritableRequestAttributesHolder.get();
		}
		return attributes;
	}
	private static final ThreadLocal<RequestAttributes> requestAttributesHolder = 
			new NamedThreadLocal<RequestAttributes>("Request attributes");
	private static final ThreadLocal<RequestAttributes> inheritableRequestAttributesHolder = 
			new NamedInheritableThreadLocal<RequestAttributes>("Request context");
}

通過這段代碼可以看出,生成的RequestAttributes對象是線程局部變數(ThreadLocal),因此request對象也是線程局部變數;這就保證了request對象的線程安全性。

優缺點

該方法的主要優點:

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相比,將註入部分代碼放入到了基類中。

基類代碼:

public class BaseController {
    @Autowired
    protected HttpServletRequest request;      
}

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

@Controller
public class TestController extends BaseController {

	// 存儲已有參數,用於判斷參數value是否重覆,從而判斷線程是否安全
	public static Set<String> set = new HashSet<>();

	@RequestMapping("/test")
	public void test() throws InterruptedException {
		String value = request.getParameter("key");
		// 判斷線程安全
		if (set.contains(value)) {
			System.out.println(value + "\t重覆出現,request併發不安全!");
		} else {
			System.out.println(value);
			set.add(value);
		}
		// 模擬程式執行了一段時間
		Thread.sleep(1000);
	}
}

@Controller
public class Test2Controller extends BaseController {
	@RequestMapping("/test2")
	public void test2() throws InterruptedException {
		String value = request.getParameter("key");
		// 判斷線程安全(與TestController使用一個set進行判斷)
		if (TestController.set.contains(value)) {
			System.out.println(value + "\t重覆出現,request併發不安全!");
		} else {
			System.out.println(value);
			TestController.set.add(value);
		}
		// 模擬程式執行了一段時間
		Thread.sleep(1000);
	}
}

線程安全性

測試結果:線程安全

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

優缺點

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

無論是方法2和方法3,都只能在Bean中註入request;如果其他方法(如工具類中static方法)需要使用request對象,則需要在調用這些方法時將request參數傳遞進去。下麵介紹的方法4,則可以直接在諸如工具類中的static方法中使用request對象(當然在各種Bean中也可以使用)。

方法4:手動調用

代碼示例

@Controller
public class TestController {
	@RequestMapping("/test")
	public void test() throws InterruptedException {
		HttpServletRequest request = ((ServletRequestAttributes) (RequestContextHolder.currentRequestAttributes())).getRequest();
		// 模擬程式執行了一段時間
		Thread.sleep(1000);
	}
}

線程安全性

測試結果:線程安全

分析:該方法與方法2(自動註入)類似,只不過方法2中通過自動註入實現,本方法通過手動方法調用實現。因此本方法也是線程安全的。

優缺點

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

方法5:@ModelAttribute方法

代碼示例

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

@Controller
public class TestController {
	private HttpServletRequest request;
	@ModelAttribute
	public void bindRequest(HttpServletRequest request) {
		this.request = request;
	}
	@RequestMapping("/test")
	public void test() throws InterruptedException {
		// 模擬程式執行了一段時間
		Thread.sleep(1000);
	}
}

線程安全性

測試結果:線程不安全

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

總結

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

參考文獻

https://docs.spring.io/spring/docs/4.1.x/spring-framework-reference/html/beans.html#beans-factory-scopes-other-injection

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

https://stackoverflow.com/questions/10541934/spring-aop-and-aspect-thread-safety-for-an-autowired-httpservletrequest-bean

http://www.phpchina.com/portal.php?mod=view&aid=40966

https://stackoverflow.com/questions/22674044/inject-httpservletrequest-into-controller

https://stackoverflow.com/questions/3320674/spring-how-do-i-inject-an-httpservletrequest-into-a-request-scoped-bean

https://my.oschina.net/sluggarddd/blog/678603?fromerr=XhvpvVTi

https://stackoverflow.com/questions/8504258/spring-3-mvc-accessing-httprequest-from-controller

 

原創不易,如果覺得文章對你有幫助,歡迎點贊、評論。文章有疏漏之處,歡迎批評指正。

歡迎轉載,轉載請註明原文鏈接:http://www.cnblogs.com/kismetv/p/8757260.html


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 最近在讀《Head First設計模式》一書,此系列會引用源書內容,但文章內容會更加直接,以及加入一些自己的理解。 觀察者模式(有時又被稱為模型-視圖(View)模式、源-收聽者(Listener)模式或從屬者模式)。在此種模式中,一個目標物件管理所有相依於它的觀察者物件,並且在它本身的狀態改變時主 ...
  • 責任鏈的目的是通過特定的設計對請求者和接收者之間進行解耦,請求者調用操作的對象,接收者接收請求並執行相關操作,通過解耦請求者不需要關心接收者的介面,同時也可增強職責的靈活性,通過改變鏈內的成員或調用次序,允許動態新增或刪除責任。 作用 責任鏈模式通過將多個對象連成鏈式模式,並沿著這個鏈傳遞命令或者請 ...
  • 工作流模塊 1.模型管理 :web線上流程設計器、預覽流程xml、導出xml、部署流程 2.流程管理 :導入導出流程資源文件、查看流程圖、根據流程實例反射出流程模型、激活掛起 3.運行中流程:查看流程信息、當前任務節點、當前流程圖、作廢暫停流程、指派待辦人 4.歷史的流程:查看流程信息、流程用時、流 ...
  • 1、一個".java"源文件中是否可以包括多個類(不是內部類)?有什麼限制? 可以有多個類,但只能有一個public的類,並且public的類名必須與文件名相一致。 2、Java有沒有goto? java中的保留字,現在沒有在java中使用。 3、說說&和&&的區別。 &和&&都可以用作邏輯與的運算 ...
  • Nexus2可以通過管理界面來上傳jar包到私庫中,而最新的Nexus3卻找不到了上傳界面,只能通過以下方式來發佈到私庫。 ...
  • 在java中要想實現多線程,有兩種手段,一種是繼續Thread類,另外一種是實現Runable介面。 對於直接繼承Thread的類來說,代碼大致框架是: ? 1 2 3 4 5 6 7 8 9 10 11 class 類名 extends Thread{ 方法1; 方法2; … public voi ...
  • 上篇文章我們瞭解了怎麼配置struts.xml文件,以及前端控制器配置怎麼配置,,Action進階,Result結果配置,Struts2中的Servlet的API的訪問,以及怎麼獲得請求參數.今天我們在深入講解一下OGNL表達式,OGNL中的符號,和常用的攔截器,標簽庫 一,OGNL表達式 1.概述 ...
  • 首先,聲明下,以下知識點並非全部來自BAT的面試題。 如果覺得在本文中筆者總結的內容能對你有所幫助,可以點贊關註一下。 本文會以引出問題為主,後面有時間的話,筆者陸續會抽些重要的知識點進行詳細的剖析與解答。 基礎篇 基本功 1、面向對象的特征 2、final, finally, finalize 的 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...