1 什么是 ThreadLocal?
ThreadLocal 是一个关于创建线程局部变量的类。
通常情况下,我们创建的变量是可以被任何一个线程访问并修改的。而使用 ThreadLocal 创建的变量只能被当前线程访问,其他线程则无法访问和修改。ThreadLocal 在设计之初就是为解决并发问题而提供一种方案,每个线程维护一份自己的数据,达到线程隔离的效果。
2 有什么作用?
2.1 set once,get everywhere
在现在的系统设计中,前后端分离已基本成为常态,分离之后如何获取用户信息就成了一件麻烦事,通常在用户登录后, 用户信息会保存在 Session 或者 Token 中。这个时候,我们如果使用常规的手段去获取用户信息会很费劲,拿 Session 来说,我们要在接口参数中加上 HttpServletRequest 对象,然后调用 getSession 方法,且每一个需要用户信息的接口都要加上这个参数,才能获取 Session,这样实现就很麻烦了。
在实际的系统设计中,我们肯定不会采用上面所说的这种方式,而是使用 ThreadLocal,我们会选择在拦截器的业务中, 获取到保存的用户信息,然后存入 ThreadLocal,那么当前线程在任何地方如果需要拿到用户信息都可以使用 ThreadLocal 的 get()方法 (异步程序中 ThreadLocal 是不可靠的)
2.2 线程安全,空间换时间
在 Spring 的 Web 项目中,我们通常会将业务分为 Controller 层,Service 层,Dao 层, 我们都知道@Autowired注解默认使用单例模式,那么不同请求线程进来之后,由于 Dao 层使用单例,那么负责数据库连接的 Connection 也只有一个, 如果每个请求线程都去连接数据库,那么就会造成线程不安全的问题,Spring 是如何解决这个问题的呢?
在 Spring 项目中 Dao 层中装配的 Connection 肯定是线程安全的,其解决方案就是采用 ThreadLocal 方法,当每个请求线程使用 Connection 的时候, 都会从 ThreadLocal 获取一次,如果为 null,说明没有进行过数据库连接,连接后存入 ThreadLocal 中,如此一来,每一个请求线程都保存有一份 自己的 Connection。于是便解决了线程安全问题
3 ThreadLocal 实战应用
3.1 ehr 中的使用
在登录拦截器中将用户信息写入,后续使用时方便取值
package com.cloud.api.interceptor;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import com.auth0.jwt.exceptions.JWTDecodeException;
import com.cloud.bean.User;
import com.cloud.common.context.ApiContextHolder;
import com.cloud.common.context.ApiResponseUtil;
import com.cloud.common.redis.RedisPre;
import com.cloud.common.result.CommonResult;
import com.cloud.common.util.JwtUtil;
import com.cloud.common.vo.AuthVo;
import com.cloud.service.IUserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
public class AuthInterceptor implements HandlerInterceptor {
public static Map<String, User> USER_ACCOUNT = new HashMap<>();
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Autowired
private IUserService iUserService;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String token = request.getHeader("token");// 从 http 请求头中取出 token
// 如果为空,则返回未登录
if (StrUtil.isEmpty(token)){
ApiContextHolder.clearAuthVo();
ApiResponseUtil.sendJsonMessage(response, CommonResult.unauthorized("token为空"));
return false;
}
if (token.contains("token")){
token = token.substring(6);
}
ApiContextHolder.setToken(token);
AuthVo authVo = null;
try {
authVo = JwtUtil.getToken(token);
}
catch (JWTDecodeException ex){
ApiContextHolder.clearAuthVo();
ApiResponseUtil.sendJsonMessage(response, CommonResult.unauthorized("JWT解析错误"));
return false;
}
if (ObjectUtil.isNull(authVo)){
ApiContextHolder.clearAuthVo();
ApiResponseUtil.sendJsonMessage(response, CommonResult.unauthorized("JWT解析为空"));
return false;
}
User user = USER_ACCOUNT.get(authVo.getUserAccount());
if(USER_ACCOUNT.isEmpty()){
USER_ACCOUNT = iUserService.getAccountMap();
user = USER_ACCOUNT.get(authVo.getUserAccount());
if(Objects.isNull(user)){
USER_ACCOUNT = iUserService.getAccountMap();
user = USER_ACCOUNT.get(authVo.getUserAccount());
}
}
if (ObjectUtil.isNull(user)){
ApiContextHolder.clearAuthVo();
ApiResponseUtil.sendJsonMessage(response, CommonResult.unauthorized("用户不存在"));
return false;
}
if(!JwtUtil.verifierToken(token)){
ApiContextHolder.clearAuthVo();
ApiResponseUtil.sendJsonMessage(response, CommonResult.unauthorized("验证失败"));
return false;
}
// 检查该用户是否被T出
String tokenCache = stringRedisTemplate.opsForValue().get(RedisPre.TOKEN_PRE + authVo.getUserAccount());
if (StrUtil.isEmpty(tokenCache)){
ApiContextHolder.clearAuthVo();
ApiResponseUtil.sendJsonMessage(response, CommonResult.signal());
return false;
}
if (!token.equals(tokenCache)){
ApiContextHolder.clearAuthVo();
ApiResponseUtil.sendJsonMessage(response, CommonResult.signal());
return false;
}
// 存入CONTEXT信息入缓存
ApiContextHolder.setAuthVo(authVo);
return true;
}
}
package com.cloud.common.context;
import com.cloud.common.vo.AuthVo;
public class ApiContextHolder {
private static final ThreadLocal<AuthVo> CONTEXT = new ThreadLocal<>();
private static final ThreadLocal<String> CONTEXT_TOKEN = new ThreadLocal<>();
// AuthVo
public static void setAuthVo(AuthVo authVo){
CONTEXT.set(authVo);
}
public static AuthVo getAuthVo(){
return CONTEXT.get();
}
public static void clearAuthVo(){
CONTEXT.remove();
}
// TOKEN
public static void setToken(String token){
CONTEXT_TOKEN.set(token);
}
public static String getToken(){
return CONTEXT_TOKEN.get();
}
}
在使用 ThreadLocal 类型变量进行相关操作时,都会通过当前线程获取到 ThreadLocalMap 来完成操作。每个线程的 ThreadLocalMap 是属于线程自己的,ThreadLocalMap 中维护的值也是属于线程自己的。这就保证了 ThreadLocal 类型的变量在每个线程中是独立的,在多线程环境下不会相互影响。
不同账号用户登录,同一个线程通过ThreadLocal获取的用户信息都是各自自己的信息,相互隔离。
参考:ThreadLocal源码解析及实战应用_Java_京东科技开发者_InfoQ写作社区