前言
在我們日常
Java Web
開發中難免遇到需要把一個參數層層的傳遞到最内層,然後中間層根本不需要使用這個參數,或者是僅僅在特定的工具類中使用,這樣我們完全沒有必要在每一個方法裡面都傳遞這樣一個
通用
的參數。如果有一個辦法能夠在任何一個類裡面想用的時候直接拿來使用就太好了。
Java
的
Web
項目大部分都是基于
Tomcat
,每次通路都是一個新的線程,這樣讓我們聯想到了
ThreadLocal
,每一個線程都獨享一個
ThreadLocal
,在接收請求的時候
set
特定内容,在需要的時候
get
這個值。下面我們就進入主題。
ThreadLocal
維持線程封閉性的一種更規範的方法就是使用
ThreadLocal
,這個類能使線程中的某個值與儲存的值的對象關聯起來。
ThreadLocal
提供
get
和
set
等接口或方法,這些方法為每一個使用這個變量的線程都存有一份獨立的副本,是以
get
總是傳回由目前線程在調用
set
時設定的最新值。
ThreadLocal
有如下方法:
1public T get() { }
2public void set(T value) { }
3public void remove() { }
4protected T initialValue() { }
get()
方法是用來擷取
ThreadLocal
在目前線程中儲存的變量副本
set()
用來設定目前線程中變量的副本
remove()
用來移除目前線程中變量的副本
initialValue()
是一個
protected
方法,一般是用來在使用時進行重寫的,如果在沒有set的時候就調用
get
,會調用
initialValue
方法初始化内容。
為了使用的更放心,我們簡單的看一下具體的實作:
set方法
1public void set(T value) {
2 Thread t = Thread.currentThread();
3 ThreadLocalMap map = getMap(t);
4 if (map != null)
5 map.set(this, value);
6 else
7 createMap(t, value);
8 }
set
方法會擷取目前的線程,通過目前線程擷取
ThreadLocalMap
對象。然後把需要存儲的值放到這個
map
裡面。如果沒有就調用
createMap
建立對象。
getMap方法
1 ThreadLocalMap getMap(Thread t) {
2 return t.threadLocals;
3 }
getMap
方法直接傳回目前
Thread
threadLocals
變量,這樣說明了之是以說
ThreadLocal
是
線程局部變量
就是因為它隻是通過
ThreadLocal
把
變量
存在了
Thread
本身而已。
createMap方法
1void createMap(Thread t, T firstValue) {
2 t.threadLocals = new ThreadLocalMap(this, firstValue);
3}
在
set
的時候如果不存在
threadLocals
,直接建立對象。由上看出,放入
map
key
是目前的
ThreadLocal
,
value
是需要存放的内容,是以我們設定屬性的時候需要注意存放和擷取的是一個
ThreadLocal
。
get方法
1public T get() {
2 Thread t = Thread.currentThread();
3 ThreadLocalMap map = getMap(t);
4 if (map != null) {
5 ThreadLocalMap.Entry e = map.getEntry(this);
6 if (e != null)
7 return (T)e.value;
8 }
9 return setInitialValue();
10 }
get
方法就比較簡單,擷取目前線程,嘗試擷取目前線程裡面的
threadLocals
,如果沒有擷取到就調用
setInitialValue
方法,
setInitialValue
基本和
set
是一樣的,就不累累述了。
場景
本文應用
ThreadLocal
的場景:在調用API接口的時候傳遞了一些公共參數,這些公共參數攜帶了一些裝置資訊,服務端接口根據不同的資訊組裝不同的格式資料傳回給用戶端。假定伺服器端需要通過裝置類型(device)來下發下載下傳位址,當然接口也有同樣的其他邏輯,我們隻要在傳回資料的時候判斷好是什麼類型的用戶端就好了。如下:
場景一
請求
1GET api/users?device=android
傳回
1 {
2 user : {
3 },
4 link : "https://play.google.com/store/apps/details?id=***"
5 }
場景二
1GET api/users?device=ios
1 {
2 user : {
3 },
4 link : "https://itunes.apple.com/us/app/**"
5 }
實作
首先準備一個
BaseSigntureRequest
類用來存放公共參數
1public class BaseSignatureRequest {
2 private String device;
3
4 public String getDevice() {
5 return device;
6 }
7
8 public void setDevice(String device) {
9 this.device = device;
10 }
11}
然後準備一個
static
ThreadLocal
類用來存放
ThreadLocal
,以便存儲和擷取時候的
ThreadLocal
一緻。
1public class ThreadLocalCache {
2 public static ThreadLocal<BaseSignatureRequest>
3 baseSignatureRequestThreadLocal = new ThreadLocal<>();
4}
然後編寫一個
Interceptor
,在請求的時候擷取
device
參數,存入目前線程的
ThreadLocal
中。這裡需要注意的是,重寫了
afterCompletion
方法,當請求結束的時候把
ThreadLocal
remove
,移除不必須要鍵值對。
1public class ParameterInterceptor implements HandlerInterceptor {
2 @Override
3 public boolean preHandle(HttpServletRequest request, HttpServletResponse response,
4 Object handler) throws Exception {
5 String device = request.getParameter("device");
6 BaseSignatureRequest baseSignatureRequest = new BaseSignatureRequest();
7 baseSignatureRequest.setDevice(device);
8 ThreadLocalCache.baseSignatureRequestThreadLocal.set(baseSignatureRequest);
9 return true;
10 }
11
12 @Override
13 public void afterCompletion(HttpServletRequest request, HttpServletResponse response,
14 Object handler, Exception ex) throws Exception {
15 ThreadLocalCache.baseSignatureRequestThreadLocal.remove();
16 }
17
18 @Override
19 public void postHandle(HttpServletRequest httpServletRequest,
20 HttpServletResponse httpServletResponse,
21 Object o, ModelAndView modelAndView) throws Exception {
22
23 }
24}
當然需要在
spring
裡面配置
interceptor
1 <mvc:interceptors>
2 <mvc:interceptor>
3 <mvc:mapping path="/api/**"/>
4 <bean class="life.majiang.ParameterInterceptor"></bean>
5 </mvc:interceptor>
6 </mvc:interceptors>
最後在
Converter
裡面轉換實體的時候直接使用即可,這樣就大功告成了。
1public class UserConverter {
2 public static ResultDO toDO(User user) {
3 ResultDO resultDO = new ResultDO();
4 resultDO.setUser(user);
5 BaseSignatureRequest baseSignatureRequest = ThreadLocalCache.baseSignatureRequestThreadLocal.get();
6 String device = baseSignatureRequest.getDevice();
7 if (StringUtils.equals(device, "ios")) {
8 resultDO.setLink("https://itunes.apple.com/us/app/**");
9 } else {
10 resultDO.setLink("https://play.google.com/store/apps/details?id=***");
11 }
12 return resultDO;
13 }
總結
這種機制很友善,因為他避免了在調用每一個方法時都要傳遞執行上下文資訊,合理的使用
ThreadLocal
可以起到事倍功半的效果,但是需要避免濫用,例如将所有的全局變量作為
ThreadLocal
對象,
ThreadLocal
類似全局變量,他能降低代碼的可重用性,并在類之間引入隐含的耦合性,是以再使用前需要格外小心。
原文釋出時間為:2018-09-07
本文作者:碼匠筆記
本文來自雲栖社群合作夥伴“IT先森養成記”,了解相關資訊可以關注“IT先森養成記”。