前言
記一次為了節省代碼沒有在方法體中聲明HttpServletRequest,而用autowire直接注入所鑽的坑
結論:給心急的人。 直接在Controller的成員變量上使用@Autowire聲明HttpServletRequest,這是線程安全的!
@Controllerpublic class TestController{ @Autowire HttpServletRequest request; @RequestMapping("/") public void test(){ request.getAttribute("uid"); }}結論如上。
背景
是這樣的,由於項目中我在Request的頭部加入身份驗證信息,而我在攔截器截獲信息並且驗證通過後,會將當前用戶的身份加到request的Attribute中,方便在Controller層拿出來復用。
疑問:為什麼不直接在Controller上使用@RequestHeader取出來呢? 因為header裡面是加密後的數據,且要經過一些複雜的身份驗證判斷,所以直接將這一步直接丟在了攔截器執行。
所以當解密後,我將用戶信息(如uid)用request.setAttribute()設入request中在Controller提取。
而如果需要使用request,一般需要在方法上聲明,如:
public Result save(HttpServletRequest request){ // dosomething();}那麼我每個方法都要用到uid的豈不是每個方法都要聲明一個request參數,為了節省著個冗餘步驟。我寫了一個基類。
public class CommonController{ @Autowire HttpServletReqeust request; public String getUid(){ return (String)request.getAttribute("uid"); }}後來我就擔心,因為controller是單例的,這麼寫會不會導致後面的reqeust覆蓋前面的request,在並發條件下有線程安全問題。 於是我就到segmentFault上提問,大部分網友說到,確實有線程問題! segmentFault問題地址###驗證過程因為網友大部分的觀點是只能在方法上聲明,我自然不想就此放棄多寫那麼多代碼,於是開始我的驗證過程。 熱心的程序員們給我提供了好幾種解決方案,我既然花力氣證明了,就把結果放在這裡,分享給大家。
方法1
第一個方法就是在controller的方法中顯示聲明HttpServletReqeust,代碼如下:
@RequestMapping("/test")@RestControllerpublic class CTest { Logger logger = LoggerFactory.getLogger(getClass()); @RequestMapping("/iiii") public String test(HttpServletRequest request) { logger.info(request.hashCode() + ""); return null; }}在瀏覽器狂按F5
輸出
當時我是懵逼的,**說好的線程安全呢! **這特麼不是同一個request嗎!特麼的在逗我! 為此我還找了很久request是不是重寫了hashcode()!
啊,事實是這樣的,因為我用瀏覽器狂按F5,再怎麼按他也是模擬不了並發的。那麼就相當於,服務器一直在用同一個線程處理我的請求就足夠了,至於這個request的hashcode,按照jdk的說法是根據obj在jvm的虛擬地址計算的,後面的事情是我猜的,如果有知道真正真想的還望告知!
猜測
服務器中每個thread所申請的request的內存空間在這個服務器啟動的時候就是固定的,那麼我每次請求,他都會在他所申請到的內存空間(可能是類似數組這樣的結構)中新建一個request,(類似於數組的起點總是同一個內存地址),那麼我發起一個請求,他就會在起始位置新建一個Request傳遞給Servlet並開始處理,處理結束後就會銷毀,那麼他下一個請求所新建的Request,因為之前的request銷毀了,所以又從起始地址開始創建,這樣一切就解釋得通了!
猜測完畢
驗證猜想:
我不讓他有銷毀的時間不就可以了嗎測試代碼
@RequestMapping("/test")@RestControllerpublic class CTest { Logger logger = LoggerFactory.getLogger(getClass()); @RequestMapping("/oooo") public String testA(HttpServletRequest request) throws Exception { Thread.sleep(3000); logger.info(request.hashCode() + ""); logger.info(reqeust.getHeader("uid"); return null; } @RequestMapping("/iiii") public String test(HttpServletRequest request) { logger.info(request.hashCode() + ""); logger.info(reqeust.getHeader("uid"); return null; }}如上,我在接口/oooo中休眠3秒,如果他是共用一個reqeust的話,那麼後面的請求將覆蓋這個休眠中的reqeust,所傳入的uid即為接口地址。先發起/oooo後發起/iiii
輸出
controller.CTest:33 - 364716268controller.CTest:34 - iiiicontroller.CTest:26 - 1892130707controller.CTest:27 - oooo
結論: 1、後發起的/iiii沒有覆蓋前面/oooo的數據,沒有線程安全問題。 2、request的hashcode不一樣,因為/oooo的阻塞,導致另一個線程需要去處理,所以他新建了request,而不是向之前一樣全部hashcode相同。
二輪驗證
public class HttpTest { public static void main(String[] args) throws Exception { for (int i = 300; i > 0; i--) { final int finalI = i; new Thread() { @Override public void run() { System.out.println("v###" + finalI); HttpRequest.get("http://localhost:8080/test/iiii?").header("uid", "v###" + finalI).send(); } }.start(); } }}在模擬並發條件下,header中的uid300個完全接受,沒有覆蓋
所以這種方式,沒有線程安全問題。
方法2
在CommonController中,使用@ModelAttribute處理。
public class CommonController {// @Autowired protected HttpServletRequest request; @ModelAttribute public void bindreq(HttpServletRequest request) { this.request = request; } protected String getUid() { System.out.println(request.toString()); return request.getAttribute("uid") == null ? null : (String) request.getAttribute("uid"); }}這樣子是有線程安全問題的!後面的request有可能覆蓋掉之前的!
驗證代碼
@RestController@RequestMapping("/test")public class CTest extends CommonController { Logger logger = LoggerFactory.getLogger(getClass()); @RequestMapping("/iiii") public String test() { logger.info(request.getHeader("uid")); return null; }} public class HttpTest { public static void main(String[] args) throws Exception { for (int i = 100; i > 0; i--) { final int finalI = i; new Thread() { @Override public void run() { System.out.println("v###" + finalI); HttpRequest.get("http://localhost:8080/test/iiii").header("uid", "v###" + finalI).send(); } }.start(); } }}截取了部分輸出結果
controller.CTest:26 - v###52controller.CTest:26 - v###13controller.CTest:26 - v###57controller.CTest:26 - v###57controller.CTest:26 - v###21controller.CTest:26 - v###10controller.CTest:26 - v###82controller.CTest:26 - v###82controller.CTest:26 - v###93controller.CTest:26 - v###71controller.CTest:26 - v###71controller.CTest:26 - v###85controller.CTest:26 - v###85controller.CTest:26 - v###14controller.CTest:26 - v###47controller.CTest:26 - v###47controller.CTest:26 - v###69controller.CTest:26 - v###22controller.CTest:26 - v###55controller.CTest:26 - v###61
可以看到57、71、85、47被覆蓋了,丟失了部分request!
這麼做是線程不安全的!
方法3
使用CommonController作為基類,將request Autowire。
public class CommonController { @Autowired protected HttpServletRequest request; protected String getUid() { System.out.println(request.toString()); return request.getAttribute("uid") == null ? null : (String) request.getAttribute("uid"); }}測試接口同上,結果喜人! 100個request沒有任何覆蓋,我加大範圍測了五六次,上千次請求沒一個覆蓋,可以證明這種寫法沒有線程安全問題了!
另外還有一點有趣的是,無論使用多少並發,request的hashcode始終是相同的,而且,測試同一個Controller中不同的接口,他也相同,使用sleep強行阻塞,hashcode也是相同。但是訪問不同的controller,hashcode卻是不同的,具體裡面如何實現我也就沒有繼續深挖了。
但是結論是出來的,就如文章最開始所說一樣。
總結
以上就是這篇文章的全部內容了,希望本文的內容對大家的學習或者工作具有一定的參考學習價值,如果有疑問大家可以留言交流,謝謝大家對武林網的支持。