前言:
最近在整合SpringCloudAlibaba框架的过程中遇到了一个如何去存储和传递用户登录信息的问题,最开始的时候想法是将前端带回来的token解析成用户信息后再将其写到header中,然后从gateway传递到指定的服务中,最后指定的服务从header中取出做相应的业务逻辑操作,由于服务中可能有诸多接口需要用到用户的登录信息,按照上述操作就需要对每个接口进行修改,这样确实可以实现我们的目的,但是如此操作显然不够优雅,后期也不方便进行扩展,思考再三决定使用ThreadLocal多线程上下文传递参数
ThreadLocal简介:
ThreadLocal是一个本地线程副本变量工具类,主要用于将私有线程和该线程存放的副本对象做一个映射,各个线程之间的变量互不干扰,在高并发场景下,可以实现无状态的调用,特别适用于各个线程依赖不同的变量值完成操作的场景
ThreadLocal类提供如下几个核心方法:
public T get()
public void set(T value)
public void remove()
注:
get()方法用于获取当前线程的副本变量值;
set()方法用于保存当前线程的副本变量值;
remove()方法移除当前线程的副本变量值
具体代码实现:
1.编写用户信息上下文实体类
/**
* @author yao
* @date 2021/6/28 11:01
*/
public class UserContext {
private ThreadLocal<UserInfoDTO> threadLocal;
private UserContext() {
this.threadLocal = new ThreadLocal<>();
}
/**
* 创建实例
*
* @return
*/
public static UserContext getInstance() {
return SingletonContext.sInstance;
}
/**
* 静态内部类单例模式
* 单例初使化
*/
private static class SingletonContext {
private static final UserContext sInstance = new UserContext();
}
/**
* 用户上下文中放入信息
*
* @param userInfoDTO
*/
public void setContext(UserInfoDTO userInfoDTO) {
threadLocal.set(userInfoDTO);
}
/**
* 获取上下文中的信息
*
* @return
*/
public UserInfoDTO getContext() {
return threadLocal.get();
}
/**
* 获取上下文中的用户名
*
* @return
*/
public String getUsername() {
return getContext().getUsername();
}
/**
* 获取上下文中的用户id
*
* @return
*/
public Long getUserId() {
return getContext().getUserId();
}
/**
* 清空上下文
*/
public void clear() {
threadLocal.remove();
}
}
2.在拦截器中将用户登录信息存入ThreadLocal中
/**
* @author yao
* @date 2021/6/28 11:17
*/
@Component
public class AuthInterceptor implements HandlerInterceptor {
private static final String AUTH_ERROR_MSG = "请先登录再进行操作";
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//如果不是映射到方法直接通过
if (!(handler instanceof HandlerMethod)) {
return true;
}
Method method = ((HandlerMethod) handler).getMethod();
LoginRequired loginRequired = method.getAnnotation(LoginRequired.class);
if (Objects.nonNull(loginRequired)) {
return !loginRequired.required();
} else {
String token = request.getHeader(CommonConstants.TOKEN);
if (StringUtils.isEmpty(token)) {
throw new WebException(AUTH_ERROR_MSG);
} else {
//登录校验通过
String userId = request.getHeader(LoginConstants.USER_ID);
UserInfoDTO userInfoDTO = UserInfoDTO.builder().userId(Long.parseLong(userId)).build();
UserContext.getInstance().setContext(userInfoDTO);
return true;
}
}
}
}
3.在所需业务逻辑中使用当前线程上下文获取用户信息
/**
* @author yao
* @date 2021/6/28 9:42
*/
@Component
@Slf4j
@Aspect
public class WebLogAspect {
@Autowired
private WebLogService webLogService;
/**
* 这里我们使用注解的形式
* 当然,我们也可以通过切点表达式直接指定需要拦截的package,需要拦截的class 以及 method
* 切点表达式: execution(...)
*/
@Pointcut("@annotation(com.white.common.annotation.WebLog)")
public void logPointCut() {
}
@Around("logPointCut()")
public Object around(ProceedingJoinPoint point) throws Throwable {
Object result = point.proceed();
try {
//保存系统日志
saveLog(point);
} catch (Exception e) {
e.printStackTrace();
}
return result;
}
/**
* 保存日志
*/
private void saveLog(ProceedingJoinPoint point) {
MethodSignature signature = (MethodSignature) point.getSignature();
Method method = signature.getMethod();
WebLog declaredAnnotation = method.getDeclaredAnnotation(WebLog.class);
LoginRequired loginRequired = method.getAnnotation(LoginRequired.class);
if (Objects.nonNull(declaredAnnotation) && Objects.isNull(loginRequired)) {
//实现保存日志逻辑
WebLogDTO webLogDTO = new WebLogDTO();
webLogDTO.setAccessTime(new Date())
.setDescription(declaredAnnotation.desc())
.setUserId(UserContext.getInstance().getUserId())
.setPath(declaredAnnotation.path());
log.info("保存日志{}", webLogDTO);
webLogService.saveLog(webLogDTO);
}
}
}