ThreadLocal 使用场景
用户会话信息
public class UserContext {
private static ThreadLocal<String> userHolder = ThreadLocal.withInitial(() -> null);
public static void setUser(String user) {
userHolder.set(user);
}
public static String getUser() {
return userHolder.get();
}
public static void clear() {
userHolder.remove();
}
}
// 在某个请求处理线程中使用
UserContext.setUser("UserA");
String currentUser = UserContext.getUser();
System.out.println("Current User: " + currentUser);
UserContext.clear();
格式化工具
public class DateFormatter {
private static ThreadLocal<SimpleDateFormat> dateFormatHolder = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd"));
public static String format(Date date) {
return dateFormatHolder.get().format(date);
}
}
// 在某个线程中使用
String formattedDate = DateFormatter.format(new Date());
System.out.println("Formatted Date: " + formattedDate);
上下文日志信息传递
public class LogContext {
private static ThreadLocal<String> requestIdHolder = ThreadLocal.withInitial(() -> null);
public static void setRequestId(String requestId) {
requestIdHolder.set(requestId);
}
public static String getRequestId() {
return requestIdHolder.get();
}
public static void clear() {
requestIdHolder.remove();
}
}
// 在某个请求处理线程中使用
LogContext.setRequestId("123456");
String requestId = LogContext.getRequestId();
System.out.println("Request ID: " + requestId);
LogContext.clear();
TransmittableThreadLocal (TTL)
TTL实现原理
- 上下文拷贝:在任务提交时,TTL 会拷贝当前线程的上下文到任务中。
- 任务执行前设置上下文:在任务执行前,TTL 会将拷贝的上下文设置到当前线程中。
- 任务执行后清理上下文:任务执行完毕后,TTL 会清理线程中的上下文,防止内存泄漏。
TTL使用场景
- 分布式追踪:在分布式系统中传递追踪 ID,方便日志的关联和问题排查。
- 事务管理:在分布式事务中传递事务上下文,确保事务的一致性。
- 上下文信息传递:在多线程环境中传递用户会话、请求上下文等信息。
#### TTL与ThreadLocal对比
Feature | ThreadLocal | TTL |
---|---|---|
上下文传递 | 仅在当前线程内存储, 无法跨线程传递 | 能够在线程池和多线程框架中传递上下文信息 |
线程复用支持 | 线程池复用线程时无法保证变量一致性 | 支持线程池复用, 确保变量在任务间传递和保持一致 |
无侵入性 | 手动管理变量设置和清除 | 替换ThreadLocal即可自动管理上下文传递和清除 |
集成方便 | 适用于简单线程环境 | 可与各种线程池和多线程框架无缝集成 |
使用场景 | 适用于单线程或不用跨线程传递上下文 | 适用于复杂多线程环境, 特别是跨线程传递上下文 |
TTL实战
- 将用户登录后的信息保存在上下文变量中,并进行跨线程之间传递
- 用户登录之后会返回 token,之后的请求将会带上这个 token,当然 token 中会携带有用户的信息,
- 所有请求最先经过网关的过滤器 AuthFilter,在过滤器中用户信息放到请求头,
- 所有请求经过网关后会来到自定义请求头拦截器 HeaderInterceptor,
- 在拦截器中拿出请求头中的用户信息放到 TTL 中,链路上的服务可以直接从 TTL 中取出用户信息
/**
* 网关鉴权
*
* @author canghe
*/
@Component
public class AuthFilter implements GlobalFilter, Ordered {
private static final Logger log = LoggerFactory.getLogger(AuthFilter.class);
private static final String BEGIN_VISIT_TIME = "begin_visit_time";//开始访问时间
// 排除过滤的 uri 地址,nacos自行添加
@Autowired
private IgnoreWhiteProperties ignoreWhite;
@Autowired
private RedisService redisService;
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
ServerHttpRequest.Builder mutate = request.mutate();
String url = request.getURI().getPath();
// 跳过不需要验证的路径
if (StringUtils.matches(url, ignoreWhite.getWhites())) {
return chain.filter(exchange);
}
String token = getToken(request);
if (StringUtils.isEmpty(token)) {
return unauthorizedResponse(exchange, "令牌不能为空");
}
Claims claims = JwtUtils.parseToken(token);
if (claims == null) {
return unauthorizedResponse(exchange, "令牌已过期或验证不正确!");
}
String userkey = JwtUtils.getUserKey(claims);
boolean islogin = redisService.hasKey(getTokenKey(userkey));
if (!islogin) {
return unauthorizedResponse(exchange, "登录状态已过期");
}
String userid = JwtUtils.getUserId(claims);
String username = JwtUtils.getUserName(claims);
if (StringUtils.isEmpty(userid) || StringUtils.isEmpty(username)) {
return unauthorizedResponse(exchange, "令牌验证失败");
}
// 设置用户信息到请求
addHeader(mutate, SecurityConstants.USER_KEY, userkey);
addHeader(mutate, SecurityConstants.DETAILS_USER_ID, userid);
addHeader(mutate, SecurityConstants.DETAILS_USERNAME, username);
// 内部请求来源参数清除(防止网关携带内部请求标识,造成系统安全风险)
removeHeader(mutate, SecurityConstants.FROM_SOURCE);
//先记录下访问接口的开始时间
exchange.getAttributes().put(BEGIN_VISIT_TIME, System.currentTimeMillis());
return chain.filter(exchange.mutate().request(mutate.build()).build());
}
private void addHeader(ServerHttpRequest.Builder mutate, String name, Object value) {
if (value == null) {
return;
}
String valueStr = value.toString();
}
- 在过滤器
**AuthFilter**
中将用户信息放到请求头
/**
* 自定义请求头拦截器,将Header数据封装到线程变量中方便获取
* 注意:此拦截器会同时验证当前用户有效期自动刷新有效期
*
* @author canghe
*/
public class HeaderInterceptor implements AsyncHandlerInterceptor {
// 需要免登录的路径集合
private static final Set<String> EXEMPTED_PATHS = new HashSet<>();
static {
// 在这里添加所有需要免登录默认展示首页的的路径
EXEMPTED_PATHS.add("/system/user/getInfo");
EXEMPTED_PATHS.add("/project/statistics");
EXEMPTED_PATHS.add("/project/doing");
EXEMPTED_PATHS.add("/project/queryMyTaskList");
EXEMPTED_PATHS.add("/project/select");
EXEMPTED_PATHS.add("/system/menu/getRouters");
}
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
if (!(handler instanceof HandlerMethod)) {
return true;
}
SecurityContextHolder.setUserId(ServletUtils.getHeader(request, SecurityConstants.DETAILS_USER_ID));
SecurityContextHolder.setUserName(ServletUtils.getHeader(request, SecurityConstants.DETAILS_USERNAME));
SecurityContextHolder.setUserKey(ServletUtils.getHeader(request, SecurityConstants.USER_KEY));
String token = SecurityUtils.getToken();
if (StringUtils.isNotEmpty(token)) {
LoginUser loginUser = AuthUtil.getLoginUser(token);
if (StringUtils.isNotNull(loginUser)) {
AuthUtil.verifyLoginUserExpire(loginUser);
SecurityContextHolder.set(SecurityConstants.LOGIN_USER, loginUser);
}
} else {
// 首页免登场景展示
// 检查请求路径是否匹配特定路径
String requestURI = request.getRequestURI();
if (isExemptedPath(requestURI)) {
// 创建一个默认的 LoginUser 对象
LoginUser defaultLoginUser = createDefaultLoginUser();
SecurityContextHolder.set(SecurityConstants.LOGIN_USER, defaultLoginUser);
}
}
return true;
}
// 判断请求路径是否匹配特定路径
private boolean isExemptedPath(String requestURI) {
// 你可以根据需要调整特定路径的匹配逻辑
return EXEMPTED_PATHS.stream().anyMatch(requestURI::startsWith);
}
// 创建一个默认的 LoginUser 对象
private LoginUser createDefaultLoginUser() {
LoginUser defaultLoginUser = new LoginUser();
defaultLoginUser.setUserId(173L); // 设置默认的用户ID
defaultLoginUser.setUsername(Constants.DEMO_ACCOUNT); // 设置默认的用户名
SysUser demoSysUser = new SysUser();
demoSysUser.setUserId(173L);
demoSysUser.setUserName(Constants.DEMO_ACCOUNT);
demoSysUser.setDeptId(100L);
demoSysUser.setStatus("0");
defaultLoginUser.setUser(demoSysUser);
// 设置其他必要的默认属性
return defaultLoginUser;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex)
throws Exception {
SecurityContextHolder.remove();
}
}