關(guān)于Spring MVC在Controller層中注入request的坑詳解
前言
記一次為了節(jié)省代碼沒(méi)有在方法體中聲明HttpServletRequest,而用autowire直接注入所鉆的坑
結(jié)論:給心急的人。 直接在Controller的成員變量上使用@Autowire聲明HttpServletRequest,這是線程安全的!
@Controller
public class TestController{
@Autowire
HttpServletRequest request;
@RequestMapping("/")
public void test(){
request.getAttribute("uid");
}
}
結(jié)論如上。
背景
是這樣的,由于項(xiàng)目中我在Request的頭部加入身份驗(yàn)證信息,而我在攔截器截獲信息并且驗(yàn)證通過(guò)后,會(huì)將當(dāng)前用戶的身份加到request的Attribute中,方便在Controller層拿出來(lái)復(fù)用。
疑問(wèn):為什么不直接在Controller上使用@RequestHeader取出來(lái)呢? 因?yàn)閔eader里面是加密后的數(shù)據(jù),且要經(jīng)過(guò)一些復(fù)雜的身份驗(yàn)證判斷,所以直接將這一步直接丟在了攔截器執(zhí)行。
所以當(dāng)解密后,我將用戶信息(如uid)用request.setAttribute()設(shè)入request中在Controller提取。
而如果需要使用request,一般需要在方法上聲明,如:
public Result save(HttpServletRequest request){
// dosomething();
}
那么我每個(gè)方法都要用到uid的豈不是每個(gè)方法都要聲明一個(gè)request參數(shù),為了節(jié)省著個(gè)冗余步驟。我寫(xiě)了一個(gè)基類。
public class CommonController{
@Autowire
HttpServletReqeust request;
public String getUid(){
return (String)request.getAttribute("uid");
}
}
后來(lái)我就擔(dān)心,因?yàn)閏ontroller是單例的,這么寫(xiě)會(huì)不會(huì)導(dǎo)致后面的reqeust覆蓋前面的request,在并發(fā)條件下有線程安全問(wèn)題。 于是我就到segmentFault上提問(wèn),大部分網(wǎng)友說(shuō)到,確實(shí)有線程問(wèn)題!segmentFault問(wèn)題地址 ###驗(yàn)證過(guò)程 因?yàn)榫W(wǎng)友大部分的觀點(diǎn)是只能在方法上聲明,我自然不想就此放棄多寫(xiě)那么多代碼,于是開(kāi)始我的驗(yàn)證過(guò)程。 熱心的程序員們給我提供了好幾種解決方案,我既然花力氣證明了,就把結(jié)果放在這里,分享給大家。
方法1
第一個(gè)方法就是在controller的方法中顯示聲明HttpServletReqeust,代碼如下:
@RequestMapping("/test")
@RestController
public class CTest {
Logger logger = LoggerFactory.getLogger(getClass());
@RequestMapping("/iiii")
public String test(HttpServletRequest request) {
logger.info(request.hashCode() + "");
return null;
}
}
在瀏覽器狂按F5
輸出

當(dāng)時(shí)我是懵逼的,**說(shuō)好的線程安全呢!**這特么不是同一個(gè)request嗎!特么的在逗我! 為此我還找了很久request是不是重寫(xiě)了hashcode()!
啊,事實(shí)是這樣的,因?yàn)槲矣脼g覽器狂按F5,再怎么按他也是模擬不了并發(fā)的。那么就相當(dāng)于,服務(wù)器一直在用同一個(gè)線程處理我的請(qǐng)求就足夠了,至于這個(gè)request的hashcode,按照jdk的說(shuō)法是根據(jù)obj在jvm的虛擬地址計(jì)算的,后面的事情是我猜的,如果有知道真正真想的還望告知!
猜測(cè)
服務(wù)器中每個(gè)thread所申請(qǐng)的request的內(nèi)存空間在這個(gè)服務(wù)器啟動(dòng)的時(shí)候就是固定的,那么我每次請(qǐng)求,他都會(huì)在他所申請(qǐng)到的內(nèi)存空間(可能是類似數(shù)組這樣的結(jié)構(gòu))中新建一個(gè)request,(類似于數(shù)組的起點(diǎn)總是同一個(gè)內(nèi)存地址),那么我發(fā)起一個(gè)請(qǐng)求,他就會(huì)在起始位置新建一個(gè)Request傳遞給Servlet并開(kāi)始處理,處理結(jié)束后就會(huì)銷(xiāo)毀,那么他下一個(gè)請(qǐng)求所新建的Request,因?yàn)橹暗膔equest銷(xiāo)毀了,所以又從起始地址開(kāi)始創(chuàng)建,這樣一切就解釋得通了!
猜測(cè)完畢
驗(yàn)證猜想:
我不讓他有銷(xiāo)毀的時(shí)間不就可以了嗎 測(cè)試代碼
@RequestMapping("/test")
@RestController
public 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秒,如果他是共用一個(gè)reqeust的話,那么后面的請(qǐng)求將覆蓋這個(gè)休眠中的reqeust,所傳入的uid即為接口地址。先發(fā)起/oooo后發(fā)起/iiii
輸出
controller.CTest:33 - 364716268 controller.CTest:34 - iiii controller.CTest:26 - 1892130707 controller.CTest:27 - oooo
結(jié)論: 1、后發(fā)起的/iiii沒(méi)有覆蓋前面/oooo的數(shù)據(jù),沒(méi)有線程安全問(wèn)題。 2、request的hashcode不一樣,因?yàn)?oooo的阻塞,導(dǎo)致另一個(gè)線程需要去處理,所以他新建了request,而不是向之前一樣全部hashcode相同。
二輪驗(yàn)證
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();
}
}
}
在模擬并發(fā)條件下,header中的uid300個(gè)完全接受,沒(méi)有覆蓋
所以這種方式,沒(méi)有線程安全問(wèn)題。
方法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");
}
}
這樣子是有線程安全問(wèn)題的!后面的request有可能覆蓋掉之前的!
驗(yàn)證代碼
@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();
}
}
}
截取了部分輸出結(jié)果
controller.CTest:26 - v###52 controller.CTest:26 - v###13 controller.CTest:26 - v###57 controller.CTest:26 - v###57 controller.CTest:26 - v###21 controller.CTest:26 - v###10 controller.CTest:26 - v###82 controller.CTest:26 - v###82 controller.CTest:26 - v###93 controller.CTest:26 - v###71 controller.CTest:26 - v###71 controller.CTest:26 - v###85 controller.CTest:26 - v###85 controller.CTest:26 - v###14 controller.CTest:26 - v###47 controller.CTest:26 - v###47 controller.CTest:26 - v###69 controller.CTest:26 - v###22 controller.CTest:26 - v###55 controller.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");
}
}
測(cè)試接口同上,結(jié)果喜人! 100個(gè)request沒(méi)有任何覆蓋,我加大范圍測(cè)了五六次,上千次請(qǐng)求沒(méi)一個(gè)覆蓋,可以證明這種寫(xiě)法沒(méi)有線程安全問(wèn)題了!
另外還有一點(diǎn)有趣的是,無(wú)論使用多少并發(fā),request的hashcode始終是相同的,而且,測(cè)試同一個(gè)Controller中不同的接口,他也相同,使用sleep強(qiáng)行阻塞,hashcode也是相同。但是訪問(wèn)不同的controller,hashcode卻是不同的,具體里面如何實(shí)現(xiàn)我也就沒(méi)有繼續(xù)深挖了。
但是結(jié)論是出來(lái)的,就如文章最開(kāi)始所說(shuō)一樣。
總結(jié)
以上就是這篇文章的全部?jī)?nèi)容了,希望本文的內(nèi)容對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,如果有疑問(wèn)大家可以留言交流,謝謝大家對(duì)腳本之家的支持。
相關(guān)文章
使用Java8進(jìn)行分組(多個(gè)字段的組合分組)
本文主要介紹了使用Java8進(jìn)行分組(多個(gè)字段的組合分組),文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2022-07-07
java中對(duì)象和JSON格式的轉(zhuǎn)換方法代碼
JSON格式可以輕松地以面向?qū)ο蟮姆绞睫D(zhuǎn)換為Java對(duì)象,下面這篇文章主要給大家介紹了關(guān)于java中對(duì)象和JSON格式的轉(zhuǎn)換方法,文中通過(guò)代碼介紹的非常詳細(xì),需要的朋友可以參考下2023-12-12
Java日常練習(xí)題,每天進(jìn)步一點(diǎn)點(diǎn)(13)
下面小編就為大家?guī)?lái)一篇Java基礎(chǔ)的幾道練習(xí)題(分享)。小編覺(jué)得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧,希望可以幫到你2021-07-07

