项目场景:
项目接入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
包装的线程池
@Configuration
public 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;替换为新配置的线程池:
@Resource
private 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
说明
如果文中有疑问的欢迎讨论、指正,互相学习,感谢关注。