【踩坑记录: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
的登录信息,导致无法获取当前用户!!!
当前项目是结合
MyBatis Plus
自动填充,自动填充逻辑中使用了StpUtil.getLoginId()
来填充创建人和更新人字段,这个方法依赖于Sa-Token
的Web
上下文(HttpServletRequest
),因此在 **非 Web 环境(如异步任务、定时任务等)中调用时会抛出NotWebContextException
异常。
解决方案:
结合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
(如果有的话,保存时的也是)
问题二描述
上面的问题解决了,发现了新的问题:
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
原因分析:
我这里的主要问题还是线程池未正确配置导致的:
原先的异步线程用的是
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
说明
如果文中有疑问的欢迎讨论、指正,互相学习,感谢关注。