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

概述

在使用Spring MVC開發Web系統時,常常須要在處理請求時使用request對象,好比獲取客戶端ip地址、請求的url、header中的屬性(如cookie、受權信息)、body中的數據等。因爲在Spring MVC中,處理請求的Controller、Service等對象都是單例的,所以獲取request對象時最須要注意的問題,即是request對象是不是線程安全的:當有大量併發請求時,可否保證不一樣請求/線程中使用不一樣的request對象。html

這裏還有一個問題須要注意:前面所說的「在處理請求時」使用request對象,到底是在哪裏使用呢?考慮到獲取request對象的方法有微小的不一樣,大致能夠分爲兩類:java

  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能夠向響應寫入數據。web

最後,獲取request對象的方法與Spring及MVC的版本也有關係;本文基於Spring4進行討論,且所作的實驗都是使用4.1.1版本。spring

如何測試線程安全性

既然request對象的線程安全問題須要特別關注,爲了便於後面的討論,下面先說明如何測試request對象是不是線程安全的。安全

測試的基本思路,是模擬客戶端大量併發請求,而後在服務器判斷這些請求是否使用了相同的request對象。服務器

判斷request對象是否相同,最直觀的方式是打印出request對象的地址,若是相同則說明使用了相同的對象。然而,在幾乎全部web服務器的實現中,都使用了線程池,這樣就致使前後到達的兩個請求,可能由同一個線程處理:在前一個請求處理完成後,線程池收回該線程,並將該線程從新分配給了後面的請求。而在同一線程中,使用的request對象極可能是同一個(地址相同,屬性不一樣)。所以即使是對於線程安全的方法,不一樣的請求使用的request對象地址也可能相同。cookie

爲了不這個問題,一種方法是在請求處理過程當中使線程休眠幾秒,這樣可讓每一個線程工做的時間足夠長,從而避免同一個線程分配給不一樣的請求;另外一種方法,是使用request的其餘屬性(如參數、header、body等)做爲request是否線程安全的依據,由於即使不一樣的請求前後使用了同一個線程(request對象地址也相同),只要使用不一樣的屬性分別構造了兩次request對象,那麼request對象的使用就是線程安全的。本文使用第二種方法進行測試。session

客戶端測試代碼以下(建立1000個線程分別發送請求):架構

1併發

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

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對象的代碼):

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

@Controller

public class TestController {

 

    // 存儲已有參數,用於判斷參數是否重複,從而判斷線程是否安全

    public static Set<String> set = new ConcurrentSkipListSet<>();

 

    @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);

    }

}

補充:上述代碼原使用HashSet來判斷value是否重複,經網友批評指正,使用線程不安全的集合類驗證線程安全性是欠妥的,現已改成ConcurrentSkipListSet。

若是request對象線程安全,服務器中打印結果以下所示:

若是存在線程安全問題,服務器中打印結果可能以下所示:

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

方法1:Controller中加參數

代碼示例

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

1

2

3

4

5

6

7

8

@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:自動注入

代碼示例

先上代碼:

1

2

3

4

5

6

7

8

9

10

11

12

@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:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

/**

 * 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代碼以下:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

/**

 * 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對象,該方法的實現以下:

1

2

3

4

5

6

7

8

9

10

/**

 * 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中,其中相關代碼以下(省略了該類中的無關代碼):

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

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

基類代碼:

1

2

3

4

public class BaseController {

    @Autowired

    protected HttpServletRequest request;    

}

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

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

@Controller

public class TestController extends BaseController {

 

    // 存儲已有參數,用於判斷參數value是否重複,從而判斷線程是否安全

    public static Set<String> set = new ConcurrentSkipListSet<>();

 

    @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:手動調用

代碼示例

1

2

3

4

5

6

7

8

9

@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放在子類中)在網上常常見到:

1

2

3

4

5

6

7

8

9

10

11

12

13

@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)得到。

此外,本文在討論獲取request對象的方法時,重點討論該方法的線程安全性、代碼的繁瑣程度等;在實際的開發過程當中,還必須考慮所在項目的規範、代碼維護等問題(此處感謝網友的批評指正)。

歡迎學Java和大數據的朋友們加入java架構交流: 855835163
加羣連接:https://jq.qq.com/?_wv=1027&k=5dPqXGI
羣內提供免費的架構資料還有:Java工程化、高性能及分佈式、高性能、深刻淺出。高架構。性能調優、Spring,MyBatis,Netty源碼分析和大數據等多個知識點高級進階乾貨的免費直播講解  能夠進來一塊兒學習交流哦
直播課堂地址:https://ke.qq.com/course/260263?flowToken=1007014​​​​​​​

相關文章
相關標籤/搜索