一、博客背景
项目是前后端分离的Spring Boot项目,前端将标识用户信息的code存储在cookie中,当实现从cookie中获取到code值后,再查询数据库获取数据用户信息后,如何将用户信息一层一层的往下传递?
有人可能将用户信息存储到HttpServletRequest请求中,后续需要用到用户信息的时候,再从HttpServletRequest中获取。如果在dao层需要用户信息,还需要将HttpServletRequest一层一层传递,或者说在controller中获取到用户信息一层一层传递。而我的做法是用ThreadLocal来存储用户信息,这样在后续的代码里可以直接获取用户信息。
二、实现原理
(1) 对于请求到后端来的请求,每次都是不同的线程执行。即一次请求的发起,就会从线程池中取出一个线程取执行业务。
(详情查看:【Spring Boot】请求 与 线程 的关系)
(2) ThreadLocal提供线程局部变量,一个变量用在多个线程中分别有独立的值(副本)。
ThreadLocal简单梳理

特别注意:如果使用不当,会导致用户信息乱串的现象
- ThreadLocal 将对象保存在线程中。换句话说就是每个线程的数据会相互隔离。基于这个特性我们可以将用户信息存储在这里,这样我们能保证我们的当前线程下执行分各种方法都能通过他获取到用户信息。
- ThreadLocal内部是将已自己为key, 存储对象为value存储到当前线程中的map中,这个map会随着线程的销毁而被JVM回收。
- 但是在我们实际开发中经常会使用线程池来避免线程的重复创建及销毁,那么线程往往是不会被销毁的。
- 在Spring中集成的类似Tomcat、JBoss等web容器中都是默认使用的一定数量的线程数的。而我们在Spring中使用的线程复用功能就导致了我们在获取当前线程的用户时因为此线程被别人使用过从未导致用户信息没有被更新成功,从而引发我们上面提到的奇怪的问题。
- 那么既然是没有被更新,到这里我们就很好解决了,要么每次使用完成后都将
ThreadLocal中的数据remove,因为他内部是弱引用在下次回收就会将对象回收这样也不会造成内存泄漏的问题。
三、代码实现
UserContext类,定义ThreadLocal的静态变量,用于存储用户信息
import com.tp.common.exception.TokenException;
import com.tp.common.model.BaseUserDTO;
public class UserContext {
private static final ThreadLocal<BaseUserDTO> user = new ThreadLocal<>();
private UserContext() {
}
public static Long getUserId() {
BaseUserDTO baseUserDTO = getUser();
return baseUserDTO.getId();
}
public static BaseUserDTO getUser() {
BaseUserDTO baseUser = user.get();
if (null == baseUser) {
throw new TokenException("登录失效,请重新登录!");
}
return baseUser;
}
public static void setBaseUser(BaseUserDTO baseUser) {
user.set(baseUser);
}
public static void remove() {
user.remove();
}
}
拦截器类,用于设置用户信息到ThreadLocal中,并且在DispatcherServlet完全处理完请求后移除ThreadLoca中的用户信息
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.auth0.jwt.interfaces.Claim;
import com.auth0.jwt.interfaces.DecodedJWT;
import com.tp.common.constant.CacheConstant;
import com.tp.common.constant.CommonConstant;
import com.tp.common.exception.TokenException;
import com.tp.common.model.BaseUserDTO;
import com.tp.common.utils.IpUtils;
import com.tp.dao.model.query.UserQuery;
import com.tp.dao.model.user.User;
import com.tp.service.cache.CacheService;
import com.tp.service.user.UserService;
import com.tp.service.threadlocal.UserContext;
import com.tp.service.utils.JwtUtil;
import com.tp.web.config.ExampleDataIdConfig;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.lang.annotation.Annotation;
import java.text.ParseException;
import java.util.List;
@Slf4j
@Component
public class LoginInterceptor implements HandlerInterceptor {
@Autowired
private UserService userService;
@Autowired
private CacheService cacheService;
@Autowired
ExampleDataIdConfig exampleDataIdConfig;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws ParseException {
String token = getToken(request);
log.info("token值:{}", token);
if (StringUtils.isBlank(token)) {
log.error("url:{} token is null", request.getRequestURL());
throw new TokenException(IpUtils.getIpAddr(request), "登录失效,请重新登录!");
}
//验证token是否过期
try {
DecodedJWT decodedJWT = JwtUtil.parseToken(token);
Claim userCodeClaim = decodedJWT.getClaim("code");
String code = userCodeClaim != null ? userCodeClaim.as(String.class) : null;
if (code == null) {
log.error("code is null");
throw new TokenException("登录失效,请重新登录!code is null");
}
User user = null;
String key = CacheConstant.USER_INFO + code;
JSONObject value = (JSONObject) cacheService.get(key);
if (value != null) {
user = JSON.toJavaObject(value, User.class);
} else {
UserQuery query = new UserQuery();
query.setCode(code);
List<User> userList = userService.getUserListByQuery(query);
if (CollectionUtils.isEmpty(userList)) {
log.error("userList is null");
throw new TokenException("登录失效,请重新登录!msg:用户不存在");
}
user = userList.get(0);
cacheService.setExpire(key, user, CacheConstant.KEY_NO_EXIST, CacheConstant.SECOND, CacheConstant.USER_INFO_EXPIRE_TIME);
}
BaseUserDTO baseUserDTO = new BaseUserDTO();
baseUserDTO.setCode(user.getCode());
baseUserDTO.setId(user.getId());
baseUserDTO.setPhone(user.getPhone());
UserContext.setBaseUser(baseUserDTO);
log.info("登录信息:" + JSON.toJSONString(baseUserDTO));
} catch (Exception e) {
log.error("登录失效,请重新登录!", e);
throw new TokenException("登录失效,请重新登录!", e);
}
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
ModelAndView modelAndView) throws Exception {
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
UserContext.remove();
}
}
service方法中使用
/**
* 获取手机号
*
* @return 手机号
*/
private String getPhone() {
String phone;
try {
phone = UserContext.getUser().getPhone();
} catch (Exception exception) {
phone = "no login";
}
return phone;
}
四、代码讲解
(1) 拦截器
- preHandle:在业务处理器处理请求之前被调用。预处理,可以进行编码、安全控制、权限校验等处理;
- postHandle:在业务处理器处理请求执行完成后,生成视图之前执行。后处理(调用了Service并返回ModelAndView,但未进行页面渲染),有机会修改ModelAndView (这个博主就基本不怎么用了);
- afterCompletion:在DispatcherServlet完全处理完请求后被调用,可用于清理资源等。返回处理(已经渲染了页面);
(2) ThreadLocal
ThreadLocal的作用主要是做数据隔离,填充的数据只属于当前线程,变量的数据对别的线程而言是相对隔离的,在多线程环境下,如何防止自己的变量被其它线程篡改。详情可以自行了解
即:在拦截器或者过滤器中set完之后再请求完成之后一定要remove,不然可能会数据错乱。
参考: