# 一、前言 最近在做一個項目,有個比較耗時的操作是啟用線程進行非同步操作,當時在啟用的線程時,突然發現子線程無法獲取父線程中的HttpServletRequest請求對象,因為是第一次遇到這種問題,所以記錄一下解決方案。 # 二、問題模擬 在這裡,我們簡單模擬一下出現的問題。我們首先編寫一個簡單的h ...
一、前言
最近在做一個項目,有個比較耗時的操作是啟用線程進行非同步操作,當時在啟用的線程時,突然發現子線程無法獲取父線程中的HttpServletRequest請求對象,因為是第一次遇到這種問題,所以記錄一下解決方案。
二、問題模擬
在這裡,我們簡單模擬一下出現的問題。我們首先編寫一個簡單的hello請求,代碼如下:
/**
* 主線程獲取
* @return
*/
@GetMapping("/hello")
public String hello() {
String name = "";
HttpServletRequest request = RequestUtils.getRequest();
if (null == request) {
log.info("未獲取到request對象!");
} else {
name = request.getParameter("name");
log.info("獲取到的內容為{}", name);
}
return "hello";
}
這是一個正常的請求,我們啟動項目,訪問介面地址。
從上圖中,我們不難發現,我們成功的拿到了HttpServletRequest中的參數。
接著,我們稍微修改一下我們的代碼,另起一個線程,在子線程中獲取HttpServletRequest中的name屬性,代碼如下:
/**
* 主線程獲取
* @return
*/
@GetMapping("/hello")
public String hello() {
new Thread(() -> {
HttpServletRequest request = RequestUtils.getRequest();
if (null == request) {
log.info("未獲取到request對象!");
} else {
String name = request.getParameter("name");
log.info("獲取到的內容為{}", name);
}
}).start();
return "hello";
}
我們再次啟動項目並訪問介面地址:
我們發現,這時候的request對象已經變為空,我們根本沒辦法獲取請求中的name屬性。
結論:如果採用多線程,我們就獲取不到父線程中的HttpServletRequest對象了。
三、解決方法
解決上面的問題其實很簡單,只需要在開啟子線程時,調用一下 RequestContextHolder.setRequestAttributes(requestAttributes, true);
方法,將第二個參數設為true就可以了。
我們修改上面的代碼如下:
/**
* 主線程獲取
* @return
*/
@GetMapping("/hello")
public String hello() {
/**
* 解決子線程無法獲取HttpServletRequest請求對象中數據的問題
*/
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
RequestContextHolder.setRequestAttributes(requestAttributes, true);
new Thread(() -> {
HttpServletRequest request = RequestUtils.getRequest();
if (null == request) {
log.info("未獲取到request對象!");
} else {
String name = request.getParameter("name");
log.info("獲取到的內容為{}", name);
}
}).start();
return "hello";
}
啟動項目,訪問介面地址,結果如下:
可以發現,我們可以在子線程中獲取HttpServletRequest對象了。
四、原理
點開RequestContextHolder.setRequestAttributes(requestAttributes, true)
方法,查看源碼:
/**
* Bind the given RequestAttributes to the current thread.
* @param attributes the RequestAttributes to expose,
* or {@code null} to reset the thread-bound context
* @param inheritable whether to expose the RequestAttributes as inheritable
* for child threads (using an {@link InheritableThreadLocal})
*/
public static void setRequestAttributes(@Nullable RequestAttributes attributes, boolean inheritable) {
if (attributes == null) {
resetRequestAttributes();
}
else {
if (inheritable) {
inheritableRequestAttributesHolder.set(attributes);
requestAttributesHolder.remove();
}
else {
requestAttributesHolder.set(attributes);
inheritableRequestAttributesHolder.remove();
}
}
}
這個方法很簡單,主要只要繼續查看requestAttributesHolder
與inheritableRequestAttributesHolder
這兩個類的繼承關係就可以了。
requestAttributesHolder
private static final ThreadLocal<RequestAttributes> requestAttributesHolder =
new NamedThreadLocal<>("Request attributes");
public class NamedThreadLocal<T> extends ThreadLocal<T> {}
我們發現,requestAttributesHolder對象類型為NamedThreadLocal,NamedThreadLocal父類是ThreadLocal。
inheritableRequestAttributesHolder
private static final ThreadLocal<RequestAttributes> inheritableRequestAttributesHolder =
new NamedInheritableThreadLocal<>("Request context");
public class NamedInheritableThreadLocal<T> extends InheritableThreadLocal<T> {}
我們發現inheritableRequestAttributesHolder的類型為NamedInheritableThreadLocal,NamedInheritableThreadLocal是InheritableThreadLocal的子類。
看到這裡,就很清晰了。調用RequestContextHolder.setRequestAttributes(requestAttributes, true)
這個方法,將原本放在ThreadLocal對象中的屬性放到了類型為InheritableThreadLocal的對象中了,所以我們啟動的子線程可以獲取到父線程中的屬性。
五、總結
當子線程中無法獲取父線程中的HttpServletRequest的方法時,我們可以通過調用RequestContextHolder.setRequestAttributes(requestAttributes, true)
方法,使得子線程也可以獲取父線程中HttpServletRequest對象。