关于ThreadLocal的使用
ThreadLocal的使用
最近在跟一个SpringBoot+Vue博客项目(传送门),第一次学习使用ThreadLocal,记录一下
需求:
- 项目有登录功能(jwt+redis),希望可以在控制层直接获取当前用户信息
思路:
- 使用拦截器实现,拦截需要验证的请求路径
- preHandle中进行验证token的有效性,以及拿到用户信息,将用户信息存入ThreadLocal
- afterCompletion中进行清除ThreadLocal,防止内存泄漏
实现:
-
创建
ThreadLocal/** * 使用ThreadLocal 作为线程副本,保存用户信息 */ public class UserThreadLocal { private UserThreadLocal(){} //静态变量 这里对ThreadLocal是存在强引用,但是也只存在线程运行期间 private static final ThreadLocalLOCAL = 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
新手上路,有问题望指教。