1. 程式人生 > >Spring 中獲取 request 的幾種方法,及其線程安全性分析

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

變種 bject input sta cli 方法參數 一律 內存空間 足夠

概述
在使用Spring MVC開發Web系統時,經常需要在處理請求時使用request對象,比如獲取客戶端ip地址、請求的url、header中的屬性(如cookie、授權信息)、body中的數據等。由於在Spring MVC中,處理請求的Controller、Service等對象都是單例的,因此獲取request對象時最需要註意的問題,便是request對象是否是線程安全的:當有大量並發請求時,能否保證不同請求/線程中使用不同的request對象。
這裏還有一個問題需要註意:前面所說的“在處理請求時”使用request對象,究竟是在哪裏使用呢?考慮到獲取request對象的方法有微小的不同,大體可以分為兩類:
在Spring的Bean中使用request對象:既包括Controller、Service、Repository等MVC的Bean,也包括了Component等普通的Spring Bean。為了方便說明,後文中Spring中的Bean一律簡稱為Bean。
在非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
br/>@Override
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
br/>e.printStackTrace();
}
}
}.start();
}
}
}
服務器中Controller代碼如下(暫時省略了獲取request對象的代碼):
@Controller

// 存儲已有參數,用於判斷參數是否重復,從而判斷線程是否安全
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代碼:
@Controller
br/>方法1:Controller中加參數
代碼示例
這種方法實現最簡單,直接上Controller代碼:
@Controller
br/>@RequestMapping("/test")
// 模擬程序執行了一段時間Thread.sleep(1000);
}
}
該方法實現的原理是,在Controller方法開始處理請求時,Spring會將request對象賦值到方法參數中。除了request對象,可以通過這種方法獲取的參數還有很多
Controller中獲取request對象後,如果要在其他方法中(如service方法、工具類方法等)使用request對象,需要在調用這些方法時將request對象作為參數傳入。
線程安全性
測試結果:線程安全
分析:此時request對象是方法參數,相當於局部變量,毫無疑問是線程安全的。
優缺點
這種方法的主要缺點是request對象寫起來冗余太多,主要體現在兩點:
如果多個controller方法中都需要request對象,那麽在每個方法中都需要添加一遍request參數
request對象的獲取只能從controller開始,如果使用request對象的地方在函數調用層級比較深的地方,那麽整個調用鏈上的所有方法都需要添加request參數
實際上,在整個請求處理的過程中,request對象是貫穿始終的;也就是說,除了定時器等特殊情況,request對象相當於線程內部的一個全局變量。而該方法,相當於將這個全局變量,傳來傳去。
方法2:自動註入
代碼示例
先上代碼:
@Controller
br/>Thread.sleep(1000);
}
}
該方法實現的原理是,在Controller方法開始處理請求時,Spring會將request對象賦值到方法參數中。除了request對象,可以通過這種方法獲取的參數還有很多
Controller中獲取request對象後,如果要在其他方法中(如service方法、工具類方法等)使用request對象,需要在調用這些方法時將request對象作為參數傳入。
線程安全性
測試結果:線程安全
分析:此時request對象是方法參數,相當於局部變量,毫無疑問是線程安全的。
優缺點
這種方法的主要缺點是request對象寫起來冗余太多,主要體現在兩點:
如果多個controller方法中都需要request對象,那麽在每個方法中都需要添加一遍request參數
request對象的獲取只能從controller開始,如果使用request對象的地方在函數調用層級比較深的地方,那麽整個調用鏈上的所有方法都需要添加request參數
實際上,在整個請求處理的過程中,request對象是貫穿始終的;也就是說,除了定時器等特殊情況,request對象相當於線程內部的一個全局變量。而該方法,相當於將這個全局變量,傳來傳去。
方法2:自動註入
代碼示例
先上代碼:
@Controller

@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")
    br/>*/
    @SuppressWarnings("serial")
    private final ObjectFactory<?> objectFactory;
    public ObjectFactoryDelegatingInvocationHandler(ObjectFactory<?> objectFactory) {
    this.objectFactory = objectFactory;}
    @Override
    br/>}
    @Override
    // ……省略無關代碼
    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")
    br/>*/
    @SuppressWarnings("serial")
    br/>@Override
    return currentRequestAttributes().getRequest();}
    @Override
    br/>}
    @Override
    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
    br/>@Autowired
    Controller代碼如下;這裏列舉了BaseController的兩個派生類,由於此時測試代碼會有所不同,因此服務端測試代碼沒有省略;客戶端也需要進行相應的修改(同時向2個url發送大量並發請求)。
    @Controller
    br/>}
    Controller代碼如下;這裏列舉了BaseController的兩個派生類,由於此時測試代碼會有所不同,因此服務端測試代碼沒有省略;客戶端也需要進行相應的修改(同時向2個url發送大量並發請求)。
    @Controller

    // 存儲已有參數,用於判斷參數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")
br/>@RequestMapping("/test2")
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
br/>測試結果:線程安全
分析:在理解了方法2的線程安全性的基礎上,很容易理解方法3是線程安全的:當創建不同的派生類對象時,基類中的域(這裏是註入的request)在不同的派生類對象中會占據不同的內存空間,也就是說將註入request的代碼放在基類中對線程安全性沒有任何影響;測試結果也證明了這一點。
優缺點
與方法2相比,避免了在不同的Controller中重復註入request;但是考慮到java只允許繼承一個基類,所以如果Controller需要繼承其他類時,該方法便不再好用。
無論是方法2和方法3,都只能在Bean中註入request;如果其他方法(如工具類中static方法)需要使用request對象,則需要在調用這些方法時將request參數傳遞進去。下面介紹的方法4,則可以直接在諸如工具類中的static方法中使用request對象(當然在各種Bean中也可以使用)。
方法4:手動調用
代碼示例
@Controller
br/>@RequestMapping("/test")
HttpServletRequest request = ((ServletRequestAttributes) (RequestContextHolder.currentRequestAttributes())).getRequest();
// 模擬程序執行了一段時間Thread.sleep(1000);
}
}
線程安全性
測試結果:線程安全
分析:該方法與方法2(自動註入)類似,只不過方法2中通過自動註入實現,本方法通過手動方法調用實現。因此本方法也是線程安全的。
優缺點
優點:可以在非Bean中直接獲取。缺點:如果使用的地方較多,代碼非常繁瑣;因此可以與其他方法配合使用。
方法5:@ModelAttribute方法
br/>Thread.sleep(1000);
}
}
線程安全性
測試結果:線程安全
分析:該方法與方法2(自動註入)類似,只不過方法2中通過自動註入實現,本方法通過手動方法調用實現。因此本方法也是線程安全的。
優缺點
優點:可以在非Bean中直接獲取。缺點:如果使用的地方較多,代碼非常繁瑣;因此可以與其他方法配合使用。
方法5:@ModelAttribute方法
@Controller
br/>下面這種方法及其變種(變種:將request和bindRequest放在子類中)在網上經常見到:
@Controller
private HttpServletRequest request;@ModelAttribute
br/>@ModelAttribute
this.request = request;}
@RequestMapping("/test")
br/>}
@RequestMapping("/test")
// 模擬程序執行了一段時間Thread.sleep(1000);
}
}
線程安全性
測試結果:線程不安全
分析:@ModelAttribute註解用在Controller中修飾方法時,其作用是Controller中的每個@RequestMapping方法執行前,該方法都會執行。因此在本例中,bindRequest()的作用是在test()執行前為request對象賦值。雖然bindRequest()中的參數request本身是線程安全的,但由於TestController是單例的,request作為TestController的一個域,無法保證線程安全。
br/>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對象的方法時,重點討論該方法的線程安全性、代碼的繁瑣程度等;在實際的開發過程中,還必須考慮所在項目的規範、代碼維護等問題(此處感謝網友的批評指正)。

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