0x6 Java系列:优雅的使用 ThreadLocal
前言
在我们日常 Web开发中难免遇到需要把一个参数层层的传递到最内层,然后中间层根本不需要使用这个参数,或者是仅仅在特定的工具类中使用,这样我们完全没有必要在每一个方法里面都传递这样一个 通用的参数。如果有一个办法能够在任何一个类里面想用的时候直接拿来使用就太好了。 Java的 Web项目大部分都是基于 Tomcat,每次访问都是一个新的线程,这样让我们联想到了 ThreadLocal,每一个线程都独享一个 ThreadLocal,在接收请求的时候 set特定内容,在需要的时候 get这个值。下面我们就进入主题。
ThreadLocal
维持线程封闭性的一种更规范的方法就是使用 ThreadLocal,这个类能使线程中的某个值与保存的值的对象关联起来。 ThreadLocal提供 get和 set等接口或方法,这些方法为每一个使用这个变量的线程都存有一份独立的副本,因此 get总是返回由当前线程在调用 set时设置的最新值。 ThreadLocal有如下方法
1. publicT get() { }
2. publicvoidset(T value) { }
3. publicvoidremove() { }
4. protectedT initialValue() { }
get()方法是用来获取 ThreadLocal在当前线程中保存的变量副本set()用来设置当前线程中变量的副本remove()用来移除当前线程中变量的副本initialValue()是一个 protected方法,一般是用来在使用时进行重写的,如果在没有set的时候就调用 get,会调用 initialValue方法初始化内容。 为了使用的更放心,我们简单的看一下具体的实现:
set方法
1. publicvoidset(T value) {
2. Threadt = Thread.currentThread();
3. ThreadLocalMapmap = getMap(t);
4. if(map != null)
5. map.set(this, value);
6. else
7. createMap(t, value);
8. }
set方法会获取当前的线程,通过当前线程获取 ThreadLocalMap对象。然后把需要存储的值放到这个 map里面。如果没有就调用 createMap创建对象。
getMap方法
1. ThreadLocalMapgetMap(Threadt) {
2. returnt.threadLocals;
3. }
getMap方法直接返回当前 Thread的 threadLocals变量,这样说明了之所以说 ThreadLocal是 线程局部变量就是因为它只是通过 ThreadLocal把 变量存在了 Thread本身而已。
createMap方法
1. voidcreateMap(Threadt, T firstValue) {
2. t.threadLocals = newThreadLocalMap(this, firstValue);
3. }
在 set的时候如果不存在 threadLocals,直接创建对象。由上看出,放入 map的 key是当前的 ThreadLocal, value是需要存放的内容,所以我们设置属性的时候需要注意存放和获取的是一个 ThreadLocal。
get方法
1. publicT get() {
2. Threadt = Thread.currentThread();
3. ThreadLocalMapmap = getMap(t);
4. if(map != null) {
5. ThreadLocalMap.Entrye = map.getEntry(this);
6. if (e != null)
7. return(T)e.value;
8. }
9. returnsetInitialValue();
10. }
get方法就比较简单,获取当前线程,尝试获取当前线程里面的 threadLocals,如果没有获取到就调用 setInitialValue方法, setInitialValue基本和 set是一样的,就不累累述了。
场景
本文应用 ThreadLocal的场景:在调用API接口的时候传递了一些公共参数,这些公共参数携带了一些设备信息,服务端接口根据不同的信息组装不同的格式数据返回给客户端。假定服务器端需要通过设备类型(device)来下发下载地址,当然接口也有同样的其他逻辑,我们只要在返回数据的时候判断好是什么类型的客户端就好了。如下:
场景一
请求
1. GET api/users?device=android
返回
1. {
2. user : {
3. },
4. link : "https://play.google.com/store/apps/details?id=***"
5. }
场景二
请求
1. GET api/users?device=ios
返回
1. {
2. user : {
3. },
4. link : "https://itunes.apple.com/us/app/**"
5. }
实现
首先准备一个 BaseSigntureRequest类用来存放公共参数
1. publicclassBaseSignatureRequest{
2. privateStringdevice;
3.
4. publicStringgetDevice() {
5. returndevice;
6. }
7.
8. publicvoidsetDevice(Stringdevice) {
9. this.device = device;
10. }
11. }
然后准备一个 static的 ThreadLocal类用来存放 ThreadLocal,以便存储和获取时候的 ThreadLocal一致。
1. publicclassThreadLocalCache{
2. publicstaticThreadLocal<BaseSignatureRequest>
3. baseSignatureRequestThreadLocal = newThreadLocal<>();
4. }
然后编写一个 Interceptor,在请求的时候获取 device参数,存入当前线程的 ThreadLocal中。这里需要注意的是,重写了 afterCompletion方法,当请求结束的时候把 ThreadLocalremove,移除不必须要键值对。
1. publicclassParameterInterceptorimplementsHandlerInterceptor{
2. @Override
3. publicbooleanpreHandle(HttpServletRequestrequest, HttpServletResponseresponse,
4. Objecthandler) throwsException{
5. Stringdevice = request.getParameter("device");
6. BaseSignatureRequestbaseSignatureRequest = newBaseSignatureRequest();
7. baseSignatureRequest.setDevice(device);
8. ThreadLocalCache.baseSignatureRequestThreadLocal.set(baseSignatureRequest);
9. returntrue;
10. }
11.
12. @Override
13. publicvoidafterCompletion(HttpServletRequestrequest, HttpServletResponseresponse,
14. Objecthandler, Exceptionex) throwsException{
15. ThreadLocalCache.baseSignatureRequestThreadLocal.remove();
16. }
17.
18. @Override
19. publicvoidpostHandle(HttpServletRequesthttpServletRequest,
20. HttpServletResponsehttpServletResponse,
21. Objecto, ModelAndViewmodelAndView) throwsException{
22.
23. }
24. }
当然需要在 spring里面配置 interceptor
1. <mvc:interceptors>
2. <mvc:interceptor>
3. <mvc:mappingpath="/api/**"/>
4. <beanclass="life.majiang.ParameterInterceptor"></bean>
5. </mvc:interceptor>
6. </mvc:interceptors>
最后在 Converter里面转换实体的时候直接使用即可,这样就大功告成了。
1. publicclassUserConverter{
2. publicstaticResultDOtoDO(Useruser) {
3. ResultDOresultDO = newResultDO();
4. resultDO.setUser(user);
5. BaseSignatureRequestbaseSignatureRequest = ThreadLocalCache.baseSignatureRequestThreadLocal.get();
6. Stringdevice = 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. returnresultDO;
13. }
总结
这种机制很方便,因为他避免了在调用每一个方法时都要传递执行上下文信息,合理的使用 ThreadLocal可以起到事倍功半的效果,但是需要避免滥用,例如将所有的全局变量作为 ThreadLocal对象, ThreadLocal类似全局变量,他能降低代码的可重用性,并在类之间引入隐含的耦合性,所以再使用前需要格外小心。
- 点赞
- 收藏
- 关注作者
评论(0)