关于ThreadLocal的使用


ThreadLocal的使用


最近在跟一个SpringBoot+Vue博客项目(传送门),第一次学习使用ThreadLocal,记录一下

需求:

  • 项目有登录功能(jwt+redis),希望可以在控制层直接获取当前用户信息

思路:

  • 使用拦截器实现,拦截需要验证的请求路径
  • preHandle中进行验证token的有效性,以及拿到用户信息,将用户信息存入ThreadLocal
  • afterCompletion中进行清除ThreadLocal,防止内存泄漏

实现:

  • 创建ThreadLocal

    
    /**
     * 使用ThreadLocal 作为线程副本,保存用户信息
     */
    public class UserThreadLocal {
    
        private UserThreadLocal(){}
    
        //静态变量 这里对ThreadLocal是存在强引用,但是也只存在线程运行期间
        private static final  ThreadLocal LOCAL = new ThreadLocal<>();
    
    
        public static void put(SysUser sysUser){
            LOCAL.set(sysUser);
        }
    
        public static SysUser get(){
            return  LOCAL.get();
        }
    
        public static void remove(){
            LOCAL.remove();
        }
    }
    
    
    
    
  • 拦截器的具体逻辑

    
    
    /**
     * 登录拦截器,注意:注解的标注 @Component
     */
    @Component
    @Slf4j
    public class LoginInterceptor implements HandlerInterceptor {
    
        @Autowired
        private LoginService loginService;
        /**
         * 自定义拦截器,在controller调用之前调用
         * @param request
         * @param response
         * @param handler
         * @return
         * @throws Exception
         */
        @Override
        public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
            /*
            *  1.判断接口路径,是否为 HanderMethod(controller方法)
            *  2、判读token是否有效
            *  3、如果携带token,进行登录认证,token有效性
            *  4、认证成功 放行
            * */
    
            //如果不是HandlerMethod方法,
            if(!(handler instanceof HandlerMethod)){
                //handler 可能是RequestResourceHandler  springboot中访问静态资源  要放行
                return true;//放行
            }
    
            String token = request.getHeader("Authorization");
    
            log.info("=================request start===========================");
            String requestURI = request.getRequestURI();
            log.info("request uri:{}",requestURI);
            log.info("request method:{}",request.getMethod());
            log.info("token:{}", token);
            log.info("=================request end===========================");
    
            if(StringUtils.isBlank(token)){
                Result result = Result.fail(ErrorCode.NO_LOGIN.getCode(), ErrorCode.NO_LOGIN.getMsg());
                response.setContentType("application/json;charset=utf-8");
                response.getWriter().print(JSON.toJSONString(result));
                return false;
            }
            SysUser sysUser = loginService.checkToken(token);
            if(sysUser == null){
                Result result = Result.fail(ErrorCode.NO_LOGIN.getCode(), ErrorCode.NO_LOGIN.getMsg());
                response.setContentType("application/json;charset=utf-8");
                response.getWriter().print(JSON.toJSONString(result));
                return false;
            }
    
            //登录验证成功,放行
            //我希望在controller中 直接获取用户的信息 怎么获取?
            //使用ThreadLocal 存放用户信息。使用线程副本,线程间互不影响
            UserThreadLocal.put(sysUser);
    
            return true;
        }
    
        /**
         * controlle层运行之后运行,及时清理
         * @param request
         * @param response
         * @param handler
         * @param ex
         * @throws Exception
         */
        @Override
        public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
            //清除ThreadLocal,防止内存泄漏
            UserThreadLocal.remove();
        }
    }
    
    
  • 配置类中将拦截器注入并配置拦截路径

    
    @Configuration
    public class WebMvcConfig implements WebMvcConfigurer {
        /**
         * 这里将我们自定义的 登录拦截器 LoginInterceptor 注入
         * 将登录验证使用拦截器拦截,是个不错的思路
         */
        @Autowired
        private LoginInterceptor loginInterceptor;
    
        /**
         * 加载拦截器;   配置自定义拦截路径addPathPatterns,以及排除路径excludePathPatterns
         * addPathPatterns是要拦截的路径
         * @param registry
         */
        @Override
        public void addInterceptors(InterceptorRegistry registry) {
            //这里配置了拦截器拦截路径,拦截/test。这个接口中有UserThreadLocal.get(); 获取用户信息,使用ThreadLocal,可以
            registry.addInterceptor(loginInterceptor).addPathPatterns("/test");
        }
    
    
    }
    
    
  • 控制层测试(/test)

    
    @RestController
    @Slf4j
    @RequestMapping("test")
    public class TestController {
        @RequestMapping
        public Result test(){
            //在这里可以直接获取用户信息
            SysUser sysUser = UserThreadLocal.get();
            System.out.println(sysUser);
            return Result.success(null);
        }
    }
    
    

总结:

? 关于ThreadLocal,主要有一下应用场景 参考

  • 每个线程需要一个独享的对象(通常是工具类,典型需要使用的类有SimpleDateFormat和Random)
  • 每个线程内需要保存全局变量(例如在拦截器中获取用户信息),可以让不同方法直接使用,避免参数传递的麻烦。【本文应用】

关于ThreadLocal

? 先浏览看一下这个图

查看源图像

上图中的虚线为弱引用,实现为强引用

在每一个Thread中都会维护一个threadLocals类型为ThreadLocal.ThreadLocalMap

/* ThreadLocal values pertaining to this thread. This map is maintained
     * by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;

看一下这个ThreadLocalMap,实际上ThreadLocalMap维护的是若干个Entry(ThreadLocal<?> k, Object v)

    static class ThreadLocalMap {

        /**
         * The entries in this hash map extend WeakReference, using
         * its main ref field as the key (which is always a
         * ThreadLocal object).  Note that null keys (i.e. entry.get()
         * == null) mean that the key is no longer referenced, so the
         * entry can be expunged from table.  Such entries are referred to
         * as "stale entries" in the code that follows.
         */
        //可以看到这里的key是一个弱引用
        static class Entry extends WeakReference> {
            /** The value associated with this ThreadLocal. */
            Object value;

            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }

可以看到这里的key 为弱引用,这样设计可以保证在线程结束后ThreadLocal可以被gc掉,但是注意,这里的value是强引用,所以在每次使用完之后要将value清除,防止内存泄漏

小疑问,既然把ThreadLocal设计为弱引用,那如果在线程使用期间这个ThreadLocal被gc了怎么办?

? 其实,是不存在的,因为在线程使用期间,一直存在一个ThreadLocal的强引用,就是在new这个ThreadLocal的时候

? 如上案例中的:private static final ThreadLocal LOCAL = new ThreadLocal<>();


新手上路,有问题望指教。