前言
开发中我们经常用 @Async 实现异步任务处理,但是只要一异步,就会遇到一个经典问题:异步线程里拿不到当前登录用户信息。
尤其项目使用 Sa-Token 1.34.0 旧版本框架,再加上异步内部嵌套工作流、自定义权限拦截、内置事件监听器 ,底层到处调用 LoginHelper.getLoginUser(),根本没法逐个改方法传参,踩坑直接踩到底。
本文把根源、所有无效方案、最终可用落地解法一次性讲透,看完彻底解决异步上下文丢失问题。
一、问题现象
- 普通接口同步逻辑,
LoginHelper.getLoginUser()正常获取登录用户; - 进入
@Async异步方法后,直接报错:
plaintext
makefile
NotWebContextException: 非Web上下文无法获取Request
- 尝试在异步里使用
StpUtil、SaHolder全部报错; - 特殊场景:异步内部调用工作流引擎 ,工作流内置事件、权限校验自动调用登录用户方法,改不了源码,只能被动适配。
二、根本原因
- Web 请求的登录用户、Request、Sa-Token 上下文,都是存在主线程 ThreadLocal 中;
- Spring 异步线程池是独立子线程 ,默认不会继承主线程 ThreadLocal 上下文;
- Sa-Token 1.34.0 老旧版本设计强依赖 Web Request 上下文,脱离 Web 环境直接抛非 Web 上下文异常;
- 线程池复用线程,就算临时设置上下文,不清理还会出现串用户、权限错乱问题。
三、网上常见无效踩坑方案(避坑)
方案 1:异步里直接 StpUtil.setTokenValue
看似能用,但有三大问题:
- Sa-Token 1.34.0 没有官方清除方法,无法清理线程上下文;
- 线程池复用会造成上下文污染、串登录用户;
- 异步里调用
SaHolder依然报非 Web 上下文错误。
方案 2:传递 RequestContextHolder 上下文
java
运行
ini
RequestAttributes attr = RequestContextHolder.getRequestAttributes();
// 异步传入设置
RequestContextHolder.setRequestAttributes(attr);
对 Sa-Token 1.34.0 无效,框架底层不走 Spring 上下文,依旧报错。
方案 3:自注入自己 AOP 代理
java
运行
less
@Autowired
@Lazy
private CaMainService self;
只能解决异步注解生效问题,解决不了登录用户上下文丢失。
四、终极落地解决方案(适配工作流 + 旧版 Sa-Token)
核心思路
既然异步线程天生无 Web 上下文、旧版 Sa-Token 不支持手动绑定清理,且工作流内部硬编码调用 LoginHelper.getLoginUser() 改不动,最优解:
自定义全局 ThreadLocal 承载登录用户,改造 LoginHelper 兜底适配异步场景
步骤 1:改造 LoginHelper 工具类
新增异步专用 ThreadLocal 容器,原有逻辑不变,异步场景自动兜底取值:
java
运行
csharp
public class LoginHelper {
// 异步线程专用:保存登录用户
private static final ThreadLocal<LoginUser> ASYNC_USER_LOCAL = new ThreadLocal<>();
private static final String LOGIN_USER_KEY = "loginUser";
// 原有获取用户方法,改造加兜底
public static LoginUser getLoginUser() {
try {
// 优先走Sa-Token Web上下文
return (LoginUser) SaHolder.getStorage().get(LOGIN_USER_KEY);
} catch (Exception e) {
// 非Web上下文、异步场景:从本地ThreadLocal取值
return ASYNC_USER_LOCAL.get();
}
}
// 给异步线程设置登录用户
public static void setAsyncUser(LoginUser user) {
ASYNC_USER_LOCAL.set(user);
}
// 用完清理,防止线程池复用串用户
public static void clearAsyncUser() {
ASYNC_USER_LOCAL.remove();
}
}
步骤 2:业务层异步调用改造
主线程提前获取登录用户,传给异步方法,异步入口设置上下文,finally 强制清理:
java
运行
typescript
@Service
public class CaMainServiceImpl implements CaMainService {
@Autowired
@Lazy
private CaMainService self;
public Boolean initiateBatch11(String queueKey) {
// 主线程:Web上下文正常,提前取出登录用户
LoginUser loginUser = LoginHelper.getLoginUser();
// 传给异步任务
self.handlerTaskAsync(loginUser, queueKey);
return Boolean.TRUE;
}
@Async
public void handlerTaskAsync(LoginUser loginUser, String queueKey) {
try {
// 给当前异步线程绑定登录用户
LoginHelper.setAsyncUser(loginUser);
// 此处调用工作流、任意内置事件、权限校验
// 内部所有 LoginHelper.getLoginUser() 自动正常取值
// 无需修改工作流任何源码
} finally {
// 必须清理!线程池复用防止串用户
LoginHelper.clearAsyncUser();
}
}
}
五、方案优势
- 零侵入工作流、业务源码:不用改监听器、不用改权限拦截逻辑;
- 兼容 Sa-Token 1.34.0 老旧版本:不用升级框架、不用反射硬编码;
- 线程安全:finally 强制清除 ThreadLocal,杜绝线程池串用户;
- 写法极简:所有异步任务通用一套模板,可全局复用;
- 彻底避开非 Web 上下文异常:不依赖 SaHolder、不依赖 Request 上下文。
六、总结
@Async丢失登录用户本质是 ThreadLocal 上下文不继承;- 旧版 Sa-Token 1.34.0 不要强行用 setTokenValue、SaHolder 手动绑定,坑极多;
- 嵌套工作流、内置事件无法改源码时,改造 LoginHelper + 自定义 ThreadLocal是唯一稳妥生产方案;
- 异步任务务必用完清理上下文,避免线程池复用导致权限错乱、用户串号。