> 技术文档 > 【踩坑记录:Sa-Token的非 web 上下文无法获取 HttpServletRequest异常、异步线程事务管理未激活synchronization is not active的问题】_cn.dev33.satoken.exception.satokencontextexception

【踩坑记录:Sa-Token的非 web 上下文无法获取 HttpServletRequest异常、异步线程事务管理未激活synchronization is not active的问题】_cn.dev33.satoken.exception.satokencontextexception


项目场景:

项目接入sa-token权限认证,版本号:1.40.0
官网地址:Sa-Token
(当前版本是1.40.0,可切换所需版本)


问题一描述

保存或者更新数据库的时候报错:
Error updating database. Cause: cn.dev33.satoken.exception.NotWebContextException: 非 web 上下文无法获取 HttpServletRequest

【踩坑记录:Sa-Token的非 web 上下文无法获取 HttpServletRequest异常、异步线程事务管理未激活synchronization is not active的问题】_cn.dev33.satoken.exception.satokencontextexception


原因分析:

这个问题的本质是:
线程上下文中没有 Sa-Token 的登录信息,导致无法获取当前用户!!!

当前项目是结合MyBatis Plus 自动填充,自动填充逻辑中使用了StpUtil.getLoginId()来填充创建人和更新人字段,这个方法依赖于 Sa-TokenWeb 上下文(HttpServletRequest),因此在 **非 Web 环境(如异步任务、定时任务等)中调用时会抛出 NotWebContextException 异常。

【踩坑记录:Sa-Token的非 web 上下文无法获取 HttpServletRequest异常、异步线程事务管理未激活synchronization is not active的问题】_cn.dev33.satoken.exception.satokencontextexception


解决方案:

结合ThreadLocal+ TTL 传递登录信息

Web 请求的通过拦截器设置到 ThreadLocal
异步任务的通过TTL 透传 ThreadLocal

一、添加TransmittableThreadLocal依赖
<dependency> <groupId>com.alibaba</groupId> <artifactId>transmittable-thread-local</artifactId> <version>2.14.5</version></dependency>
二、创建 ThreadLocal 工具类(支持跨线程传递)
import com.alibaba.ttl.TransmittableThreadLocal;public class LoginUserContext { private static final TransmittableThreadLocal<String> currentUser = new TransmittableThreadLocal<>(); public static void setCurrentUserId(String userId) { currentUser.set(userId); } public static String getCurrentUserId() { return currentUser.get(); } public static void clear() { currentUser.remove(); }}
三、在 Web 请求入口设置当前用户

(此处是结合了sa-token的拦截器做了校验和角色权限判断,LoginUserContext.setCurrentUserId(StpUtil.getLoginIdAsString());

import cn.dev33.satoken.annotation.SaCheckPermission;import cn.dev33.satoken.annotation.SaCheckRole;import cn.dev33.satoken.stp.StpUtil;import com.mochasoft.iap.exception.ExceptionCode;import com.mochasoft.iap.exception.SystemRunTimeException;import com.mochasoft.iap.util.LoginUserContext;import jakarta.servlet.http.HttpServletRequest;import jakarta.servlet.http.HttpServletResponse;import org.springframework.web.method.HandlerMethod;import org.springframework.web.servlet.HandlerInterceptor;import java.lang.reflect.AnnotatedElement;/** * 自定义Sa-Token 拦截器 */public class SaTokenInterceptor implements HandlerInterceptor { @Override public boolean preHandle(final HttpServletRequest request, final HttpServletResponse response, final Object handler) { if (!StpUtil.isLogin()) { throw new SystemRunTimeException(ExceptionCode.LOGIN_ERROR, \"当前用户暂未登录,请登录后重试\"); } // 1. 设置当前用户ID到上下文中 LoginUserContext.setCurrentUserId(StpUtil.getLoginIdAsString()); // 2. 检查注解 if (handler instanceof HandlerMethod handlerMethod) { // 检查类级别和方法级别的访问控制注解 if (checkAccess(handlerMethod.getBeanType()) || checkAccess(handlerMethod.getMethod())) { throw new SystemRunTimeException(ExceptionCode.NO_AUTH_TO_ACESS, \"无权限访问当前资源\"); } } return true; } @Override public void afterCompletion(final HttpServletRequest request, final HttpServletResponse response, final Object handler, final Exception ex) throws Exception { LoginUserContext.clear(); // 清理资源,防止内存泄漏 } /** * 检查是否有访问指定元素(类/方法)的权限。 */ private boolean checkAccess(final AnnotatedElement element) { final SaCheckRole roleAnnotation = element.getAnnotation(SaCheckRole.class); final SaCheckPermission permissionAnnotation = element.getAnnotation(SaCheckPermission.class); // 如果既没有角色也没有权限要求,则允许访问 if (roleAnnotation == null && permissionAnnotation == null) { return false; } // 角色检查失败 if (roleAnnotation != null && !StpUtil.hasRoleOr(roleAnnotation.value())) { return true; } // 权限检查失败 if (permissionAnnotation != null && !StpUtil.hasPermissionOr(permissionAnnotation.value())) { return true; } // 允许访问 return false; }}
四、最终就是修改你的 DateMetaObjectHandler(如果有的话,保存时的也是)

【踩坑记录:Sa-Token的非 web 上下文无法获取 HttpServletRequest异常、异步线程事务管理未激活synchronization is not active的问题】_cn.dev33.satoken.exception.satokencontextexception


问题二描述

上面的问题解决了,发现了新的问题:

SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@1118292] was not registered for synchronization because synchronization is not active
JDBC Connection [HikariProxyConnection@2119130419 wrapping org.postgresql.jdbc.PgConnection@12d8d78a] will not be managed by Spring

【踩坑记录:Sa-Token的非 web 上下文无法获取 HttpServletRequest异常、异步线程事务管理未激活synchronization is not active的问题】_cn.dev33.satoken.exception.satokencontextexception


原因分析:

我这里的主要问题还是线程池未正确配置导致的:

原先的异步线程用的是 import org.springframework.core.task.AsyncTaskExecutor;
并通过 taskExecutor.execute(() -> { ... }) 直接执行异步任务。
这种写法不会自动传递事务、Web 请求上下文、Sa-Token 登录状态等信息,因此导致了上面的问题


解决方案:

一、首先接着问题一的配置,配置 TTL 包装的线程池
@Configurationpublic class AsyncConfig { @Bean public AsyncTaskExecutor ttlAsyncTaskExecutor() { ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); executor.setCorePoolSize(10); executor.setMaxPoolSize(20); executor.setQueueCapacity(100); executor.setThreadNamePrefix(\"TTL-Async-\"); executor.initialize(); // 使用 TTL 包装线程池 return TtlExecutors.getTtlExecutor(executor); }}
二、替换原有 AsyncTaskExecutor,将原有的 @Resource private AsyncTaskExecutor taskExecutor;替换为新配置的线程池:
@Resourceprivate AsyncTaskExecutor taskExecutor;// 使用 TTL 包装的线程池taskExecutor.execute(() -> { // 现在可以安全访问事务、Web 请求上下文、Sa-Token 登录信息});

经过上述配置。再启动调用异步方法,则可顺利完成!!!


补充

如果数据库配置了HikariCP,则要确保 HikariCP 的配置不会导致连接过早关闭,避免连接池与事务生命周期冲突:

spring: datasource: hikari: maximum-pool-size: 10 minimum-idle: 5 idle-timeout: 30000 max-lifetime: 600000 # 增大 max-lifetime,避免连接过早失效 connection-timeout: 5000

说明

如果文中有疑问的欢迎讨论、指正,互相学习,感谢关注。