<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

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

          共 1857字,需瀏覽 4分鐘

           ·

          2021-11-06 11:48

          之前聊一聊 Spring 中的線程安全性這篇文章非常受歡迎,但是依然不夠詳細(xì),還有一些問(wèn)題,比如即便Controller不是單例的,如果用了RequestScope,獲取到的對(duì)象也是Request域的。今天讓我們深度分析看一下。


          來(lái)源:編程迷思

          https://www.cnblogs.com/kismetv/p/8757260.html

          目錄

          概述

          如何測(cè)試線程安全性

          方法1:Controller中加參數(shù)

          方法2:自動(dòng)注入

          方法3:基類中自動(dòng)注入

          方法4:手動(dòng)調(diào)用

          方法5:@ModelAttribute方法

          總結(jié)

          概述

          在使用Spring MVC開(kāi)發(fā)Web系統(tǒng)時(shí),經(jīng)常需要在處理請(qǐng)求時(shí)使用request對(duì)象,比如獲取客戶端ip地址、請(qǐng)求的url、header中的屬性(如cookie、授權(quán)信息)、body中的數(shù)據(jù)等。由于在Spring MVC中,處理請(qǐng)求的Controller、Service等對(duì)象都是單例的,因此獲取request對(duì)象時(shí)最需要注意的問(wèn)題,便是request對(duì)象是否是線程安全的:當(dāng)有大量并發(fā)請(qǐng)求時(shí),能否保證不同請(qǐng)求/線程中使用不同的request對(duì)象。

          這里還有一個(gè)問(wèn)題需要注意:前面所說(shuō)的“在處理請(qǐng)求時(shí)”使用request對(duì)象,究竟是在哪里使用呢?考慮到獲取request對(duì)象的方法有微小的不同,大體可以分為兩類:

          1)????? 在Spring的Bean中使用request對(duì)象:既包括Controller、Service、Repository等MVC的Bean,也包括了Component等普通的Spring Bean。為了方便說(shuō)明,后文中Spring中的Bean一律簡(jiǎn)稱為Bean。

          2)????? 在非Bean中使用request對(duì)象:如普通的Java對(duì)象的方法中使用,或在類的靜態(tài)方法中使用。

          此外,本文討論是圍繞代表請(qǐng)求的request對(duì)象展開(kāi)的,但所用方法同樣適用于response對(duì)象、InputStream/Reader、OutputStream/ Writer等;其中InputStream/Reader可以讀取請(qǐng)求中的數(shù)據(jù),OutputStream/ Writer可以向響應(yīng)寫(xiě)入數(shù)據(jù)。

          最后,獲取request對(duì)象的方法與Spring及MVC的版本也有關(guān)系;本文基于Spring4進(jìn)行討論,且所做的實(shí)驗(yàn)都是使用4.1.1版本。

          如何測(cè)試線程安全性

          既然request對(duì)象的線程安全問(wèn)題需要特別關(guān)注,為了便于后面的討論,下面先說(shuō)明如何測(cè)試request對(duì)象是否是線程安全的。

          測(cè)試的基本思路,是模擬客戶端大量并發(fā)請(qǐng)求,然后在服務(wù)器判斷這些請(qǐng)求是否使用了相同的request對(duì)象。

          判斷request對(duì)象是否相同,最直觀的方式是打印出request對(duì)象的地址,如果相同則說(shuō)明使用了相同的對(duì)象。然而,在幾乎所有web服務(wù)器的實(shí)現(xiàn)中,都使用了線程池,這樣就導(dǎo)致先后到達(dá)的兩個(gè)請(qǐng)求,可能由同一個(gè)線程處理:在前一個(gè)請(qǐng)求處理完成后,線程池收回該線程,并將該線程重新分配給了后面的請(qǐng)求。而在同一線程中,使用的request對(duì)象很可能是同一個(gè)(地址相同,屬性不同)。因此即便是對(duì)于線程安全的方法,不同的請(qǐng)求使用的request對(duì)象地址也可能相同。

          為了避免這個(gè)問(wèn)題,一種方法是在請(qǐng)求處理過(guò)程中使線程休眠幾秒,這樣可以讓每個(gè)線程工作的時(shí)間足夠長(zhǎng),從而避免同一個(gè)線程分配給不同的請(qǐng)求;另一種方法,是使用request的其他屬性(如參數(shù)、header、body等)作為request是否線程安全的依據(jù),因?yàn)榧幢悴煌恼?qǐng)求先后使用了同一個(gè)線程(request對(duì)象地址也相同),只要使用不同的屬性分別構(gòu)造了兩次request對(duì)象,那么request對(duì)象的使用就是線程安全的。本文使用第二種方法進(jìn)行測(cè)試。

          客戶端測(cè)試代碼如下(創(chuàng)建1000個(gè)線程分別發(fā)送請(qǐng)求):

          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();
          ????????}
          ????}
          }

          服務(wù)器中Controller代碼如下(暫時(shí)省略了獲取request對(duì)象的代碼):

          @Controller
          public?class?TestController?{

          ????//?存儲(chǔ)已有參數(shù),用于判斷參數(shù)是否重復(fù),從而判斷線程是否安全
          ????public?static?Set?set?=?new?ConcurrentSkipListSet<>();

          ????@RequestMapping("/test")
          ????public?void?test()?throws?InterruptedException?{

          ????????//?…………………………通過(guò)某種方式獲得了request對(duì)象………………………………

          ????????//?判斷線程安全
          ????????String?value?=?request.getParameter("key");
          ????????if?(set.contains(value))?{
          ????????????System.out.println(value?+?"\t重復(fù)出現(xiàn),request并發(fā)不安全!");
          ????????}?else?{
          ????????????System.out.println(value);
          ????????????set.add(value);
          ????????}

          ????????//?模擬程序執(zhí)行了一段時(shí)間
          ????????Thread.sleep(1000);
          ????}
          }

          補(bǔ)充:上述代碼原使用HashSet來(lái)判斷value是否重復(fù),經(jīng)網(wǎng)友批評(píng)指正,使用線程不安全的集合類驗(yàn)證線程安全性是欠妥的,現(xiàn)已改為ConcurrentSkipListSet。

          如果request對(duì)象線程安全,服務(wù)器中打印結(jié)果如下所示:

          如果存在線程安全問(wèn)題,服務(wù)器中打印結(jié)果可能如下所示:

          如無(wú)特殊說(shuō)明,本文后面的代碼中將省略掉測(cè)試代碼。

          方法1:Controller中加參數(shù)

          代碼示例

          這種方法實(shí)現(xiàn)最簡(jiǎn)單,直接上Controller代碼:

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

          該方法實(shí)現(xiàn)的原理是,在Controller方法開(kāi)始處理請(qǐng)求時(shí),Spring會(huì)將request對(duì)象賦值到方法參數(shù)中。除了request對(duì)象,可以通過(guò)這種方法獲取的參數(shù)還有很多,具體可以參見(jiàn):https://docs.spring.io/spring/docs/current/spring-framework-reference/web.html#mvc-ann-methods

          Controller中獲取request對(duì)象后,如果要在其他方法中(如service方法、工具類方法等)使用request對(duì)象,需要在調(diào)用這些方法時(shí)將request對(duì)象作為參數(shù)傳入。

          線程安全性

          測(cè)試結(jié)果:線程安全

          分析:此時(shí)request對(duì)象是方法參數(shù),相當(dāng)于局部變量,毫無(wú)疑問(wèn)是線程安全的。

          優(yōu)缺點(diǎn)

          這種方法的主要缺點(diǎn)是request對(duì)象寫(xiě)起來(lái)冗余太多,主要體現(xiàn)在兩點(diǎn):

          1)????? 如果多個(gè)controller方法中都需要request對(duì)象,那么在每個(gè)方法中都需要添加一遍request參數(shù)

          2)????? request對(duì)象的獲取只能從controller開(kāi)始,如果使用request對(duì)象的地方在函數(shù)調(diào)用層級(jí)比較深的地方,那么整個(gè)調(diào)用鏈上的所有方法都需要添加request參數(shù)

          實(shí)際上,在整個(gè)請(qǐng)求處理的過(guò)程中,request對(duì)象是貫穿始終的;也就是說(shuō),除了定時(shí)器等特殊情況,request對(duì)象相當(dāng)于線程內(nèi)部的一個(gè)全局變量。而該方法,相當(dāng)于將這個(gè)全局變量,傳來(lái)傳去。

          方法2:自動(dòng)注入

          代碼示例

          先上代碼:

          @Controller
          public?class?TestController{

          ????@Autowired
          ????private?HttpServletRequest?request;?//自動(dòng)注入request

          ????@RequestMapping("/test")
          ????public?void?test()?throws?InterruptedException{
          ????????//模擬程序執(zhí)行了一段時(shí)間
          ????????Thread.sleep(1000);
          ????}
          }

          線程安全性

          測(cè)試結(jié)果:線程安全

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

          使用這種方式,當(dāng)Bean(本例的TestController)初始化時(shí),Spring并沒(méi)有注入一個(gè)request對(duì)象,而是注入了一個(gè)代理(proxy);當(dāng)Bean中需要使用request對(duì)象時(shí),通過(guò)該代理獲取request對(duì)象。

          ?

          下面通過(guò)具體的代碼對(duì)這一實(shí)現(xiàn)進(jìn)行說(shuō)明。

          在上述代碼中加入斷點(diǎn),查看request對(duì)象的屬性,如下圖所示:

          在圖中可以看出,request實(shí)際上是一個(gè)代理:代理的實(shí)現(xiàn)參見(jiàn)AutowireUtils的內(nèi)部類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?{
          ????????//?……省略無(wú)關(guān)代碼
          ????????try?{
          ????????????return?method.invoke(this.objectFactory.getObject(),?args);?//?代理實(shí)現(xiàn)核心代碼
          ????????}
          ????????catch?(InvocationTargetException?ex)?{
          ????????????throw?ex.getTargetException();
          ????????}
          ????}
          }

          也就是說(shuō),當(dāng)我們調(diào)用request的方法method時(shí),實(shí)際上是調(diào)用了由objectFactory.getObject()生成的對(duì)象的method方法;objectFactory.getObject()生成的對(duì)象才是真正的request對(duì)象。

          繼續(xù)觀察上圖,發(fā)現(xiàn)objectFactory的類型為WebApplicationContextUtils的內(nèi)部類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對(duì)象需要先調(diào)用currentRequestAttributes()方法獲得RequestAttributes對(duì)象,該方法的實(shí)現(xiàn)如下:

          /**
          ?*?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對(duì)象的核心代碼在類RequestContextHolder中,其中相關(guān)代碼如下(省略了該類中的無(wú)關(guān)代碼):

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

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

          優(yōu)缺點(diǎn)

          該方法的主要優(yōu)點(diǎn):

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

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

          3)????? 減少代碼冗余:只需要在需要request對(duì)象的Bean中注入request對(duì)象,便可以在該Bean的各個(gè)方法中使用,與方法1相比大大減少了代碼冗余。

          但是,該方法也會(huì)存在代碼冗余??紤]這樣的場(chǎng)景:web系統(tǒng)中有很多controller,每個(gè)controller中都會(huì)使用request對(duì)象(這種場(chǎng)景實(shí)際上非常頻繁),這時(shí)就需要寫(xiě)很多次注入request的代碼;如果還需要注入response,代碼就更繁瑣了。下面說(shuō)明自動(dòng)注入方法的改進(jìn)方法,并分析其線程安全性及優(yōu)缺點(diǎn)。

          方法3:基類中自動(dòng)注入

          代碼示例

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

          基類代碼:

          public?class?BaseController?{
          ????@Autowired
          ????protected?HttpServletRequest?request;?????
          }

          Controller代碼如下;這里列舉了BaseController的兩個(gè)派生類,由于此時(shí)測(cè)試代碼會(huì)有所不同,因此服務(wù)端測(cè)試代碼沒(méi)有省略;客戶端也需要進(jìn)行相應(yīng)的修改(同時(shí)向2個(gè)url發(fā)送大量并發(fā)請(qǐng)求)。

          @Controller
          public?class?TestController?extends?BaseController?{

          ????//?存儲(chǔ)已有參數(shù),用于判斷參數(shù)value是否重復(fù),從而判斷線程是否安全
          ????public?static?Set?set?=?new?ConcurrentSkipListSet<>();

          ????@RequestMapping("/test")
          ????public?void?test()?throws?InterruptedException?{
          ????????String?value?=?request.getParameter("key");
          ????????//?判斷線程安全
          ????????if?(set.contains(value))?{
          ????????????System.out.println(value?+?"\t重復(fù)出現(xiàn),request并發(fā)不安全!");
          ????????}?else?{
          ????????????System.out.println(value);
          ????????????set.add(value);
          ????????}
          ????????//?模擬程序執(zhí)行了一段時(shí)間
          ????????Thread.sleep(1000);
          ????}
          }

          @Controller
          public?class?Test2Controller?extends?BaseController?{
          ????@RequestMapping("/test2")
          ????public?void?test2()?throws?InterruptedException?{
          ????????String?value?=?request.getParameter("key");
          ????????//?判斷線程安全(與TestController使用一個(gè)set進(jìn)行判斷)
          ????????if?(TestController.set.contains(value))?{
          ????????????System.out.println(value?+?"\t重復(fù)出現(xiàn),request并發(fā)不安全!");
          ????????}?else?{
          ????????????System.out.println(value);
          ????????????TestController.set.add(value);
          ????????}
          ????????//?模擬程序執(zhí)行了一段時(shí)間
          ????????Thread.sleep(1000);
          ????}
          }

          線程安全性

          測(cè)試結(jié)果:線程安全

          分析:在理解了方法2的線程安全性的基礎(chǔ)上,很容易理解方法3是線程安全的:當(dāng)創(chuàng)建不同的派生類對(duì)象時(shí),基類中的域(這里是注入的request)在不同的派生類對(duì)象中會(huì)占據(jù)不同的內(nèi)存空間,也就是說(shuō)將注入request的代碼放在基類中對(duì)線程安全性沒(méi)有任何影響;測(cè)試結(jié)果也證明了這一點(diǎn)。

          優(yōu)缺點(diǎn)

          與方法2相比,避免了在不同的Controller中重復(fù)注入request;但是考慮到j(luò)ava只允許繼承一個(gè)基類,所以如果Controller需要繼承其他類時(shí),該方法便不再好用。

          無(wú)論是方法2和方法3,都只能在Bean中注入request;如果其他方法(如工具類中static方法)需要使用request對(duì)象,則需要在調(diào)用這些方法時(shí)將request參數(shù)傳遞進(jìn)去。下面介紹的方法4,則可以直接在諸如工具類中的static方法中使用request對(duì)象(當(dāng)然在各種Bean中也可以使用)。

          方法4:手動(dòng)調(diào)用

          代碼示例

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

          線程安全性

          測(cè)試結(jié)果:線程安全

          分析:該方法與方法2(自動(dòng)注入)類似,只不過(guò)方法2中通過(guò)自動(dòng)注入實(shí)現(xiàn),本方法通過(guò)手動(dòng)方法調(diào)用實(shí)現(xiàn)。因此本方法也是線程安全的。

          優(yōu)缺點(diǎn)

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

          方法5:@ModelAttribute方法

          代碼示例

          下面這種方法及其變種(變種:將request和bindRequest放在子類中)在網(wǎng)上經(jīng)常見(jiàn)到:

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

          線程安全性

          測(cè)試結(jié)果:線程不安全

          分析:@ModelAttribute注解用在Controller中修飾方法時(shí),其作用是Controller中的每個(gè)@RequestMapping方法執(zhí)行前,該方法都會(huì)執(zhí)行。因此在本例中,bindRequest()的作用是在test()執(zhí)行前為request對(duì)象賦值。雖然bindRequest()中的參數(shù)request本身是線程安全的,但由于TestController是單例的,request作為T(mén)estController的一個(gè)域,無(wú)法保證線程安全。

          總結(jié)

          綜上所述,Controller中加參數(shù)(方法1)、自動(dòng)注入(方法2和方法3)、手動(dòng)調(diào)用(方法4)都是線程安全的,都可以用來(lái)獲取request對(duì)象。如果系統(tǒng)中request對(duì)象使用較少,則使用哪種方式均可;如果使用較多,建議使用自動(dòng)注入(方法2 和方法3)來(lái)減少代碼冗余。如果需要在非Bean中使用request對(duì)象,既可以在上層調(diào)用時(shí)通過(guò)參數(shù)傳入,也可以直接在方法中通過(guò)手動(dòng)調(diào)用(方法4)獲得。

          此外,本文在討論獲取request對(duì)象的方法時(shí),重點(diǎn)討論該方法的線程安全性、代碼的繁瑣程度等;在實(shí)際的開(kāi)發(fā)過(guò)程中,還必須考慮所在項(xiàng)目的規(guī)范、代碼維護(hù)等問(wèn)題(此處感謝網(wǎng)友的批評(píng)指正)。

          參考文獻(xiàn)

          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


          瀏覽 37
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  亚洲第一页在线观看 | 操b视频在线国产 | 欧美一区二区丁香五月天激情 | 中文字幕人妻乱码 | 国产精品123区在线观看 |