解决若依框架中 AI 工具回调的权限上下文丢失问题
在使用若依(RuoYi)这类基于 Spring MVC 架构的应用中集成 Spring AI 进行工具(Tool Call)调用时,我们经常会遇到 `java.lang.IllegalStateException: No thread-bound request found` 权限报错。本文将深入分析该问题,并提供一套基于 MVC/WebFlux 双模上下文持有者的通用解决方案,确保在异步的 AI
解决若依框架中 AI 工具回调的权限上下文丢失问题
摘要
在使用若依(RuoYi)这类基于 Spring MVC 架构的应用中集成 Spring AI 进行工具(Tool Call)调用时,我们经常会遇到 java.lang.IllegalStateException: No thread-bound request found 权限报错。本文将深入分析该问题,并提供一套基于 MVC/WebFlux 双模上下文持有者的通用解决方案,确保在异步的 AI 工具回调线程中也能正确获取并应用用户的数据权限(Data Scope)。
一、问题根源:ThreadLocal 上下文的局限性
若依框架广泛采用 Spring MVC 的 ThreadLocal 机制来存储会话级信息,例如:
- 权限信息:使用
SecurityUtils.getLoginUser()获取当前登录用户及其权限集。 - 请求上下文:使用
RequestContextHolder存储当前 HTTP 请求的属性(例如,在PermissionContextHolder.isMvcRequest()中尝试访问)。
然而,当 Spring AI 引擎进行工具调用(Tool Call)时,它通常会在一个独立的、异步的线程中执行该工具方法(例如,一个 ExecutorService 线程或 WebFlux 的 Reactor 线程)。
核心矛盾点在于:
异步线程无法继承原始 HTTP 请求线程的 ThreadLocal 变量。当工具方法内部的权限切面(如 DataScopeAspect)尝试通过 RequestContextHolder 获取上下文时,由于 ThreadLocal 为空,便会抛出 No thread-bound request found 异常。
二、解决方案:双模上下文持有者(Dual-Mode Context Holder)
解决之道在于抽象出权限上下文的存储和获取机制,使其能够同时支持同步的 ThreadLocal (MVC) 和异步的 Reactive Context (WebFlux/AI) 两种模式。
我们引入了 PermissionContextHolder,并利用它来封装线程检测和上下文传递逻辑。
1. 上下文持有者 PermissionContextHolder
PermissionContextHolder 的核心功能是判断当前线程环境并提供对应的上下文存取方法。
关键代码逻辑:
- 环境检测:
isMvcRequest()通过尝试访问RequestContextHolder来判断是否处于 MVC 线程。 - WebFlux/AI 异步上下文:使用 Project Reactor 的
Mono.deferContextual和contextWrite来进行上下文的存储和获取,以适应异步调用链。
public class PermissionContextHolder {
// ...
// 判断当前线程是否是 MVC 请求
public static boolean isMvcRequest() {
try {
RequestContextHolder.currentRequestAttributes();
return true;
} catch (IllegalStateException e) {
// MVC ThreadLocal 不可用,判断为异步环境
return false;
}
}
// WebFlux/AI 异步环境获取上下文
public static Mono<String> getReactiveContext() {
return Mono.deferContextual(ctx -> Mono.justOrEmpty(ctx.get(PERMISSION_CONTEXT_ATTRIBUTES)));
}
// WebFlux/AI 异步环境设置上下文
public static <T> reactor.core.publisher.Mono<T> withReactiveContext(String permission, reactor.core.publisher.Mono<T> mono) {
return mono.contextWrite(ctx -> ctx.put(PERMISSION_CONTEXT_ATTRIBUTES, permission));
}
}
2. 权限服务中的上下文注入 PermissionService
在若依的权限校验服务 PermissionService 中,我们利用 isMvcRequest() 在权限校验时提前将权限字符串(如 system:user:list)注入到正确的上下文中。
关键代码逻辑:
- MVC 模式:权限直接存入
ThreadLocal(PermissionContextHolder.setMvcContext(permission)). - AI/异步模式:权限通过
PermissionContextHolder.withReactiveContext(...)写入Mono的上下文,并使用.block()阻塞等待,以适配 Spring AI 往往需要同步返回结果的工具方法调用。
@Service("ss")
public class PermissionService {
// ...
public boolean hasPermi(String permission) {
// ... [权限检查]
if (isMvcRequest()) {
// MVC 情况:设置 ThreadLocal
PermissionContextHolder.setMvcContext(permission);
return hasPermissions(loginUser.getPermissions(), permission);
}
// WebFlux/AI Tooling 情况:将权限写入 Reactive Context
return Boolean.TRUE.equals(PermissionContextHolder.withReactiveContext(permission,
Mono.just(hasPermissions(loginUser.getPermissions(), permission))
).block());
// 注意:这里的 .block() 是关键,它允许同步工具调用方法获取异步结果。
}
}
3. 数据权限切面中的上下文获取 DataScopeAspect
最后,在执行数据过滤逻辑的切面中,我们也必须使用双模逻辑来获取权限信息。
关键代码逻辑:
- MVC 模式:直接从
PermissionContextHolder.getMvcContext()获取String权限。 - AI/异步模式:从
PermissionContextHolder.getReactiveContext()获取Mono<String>,然后通过.subscribe()订阅获取权限值,并执行数据过滤 (dataScopeFilter)。
protected void handleDataScope(final JoinPoint joinPoint, DataScope controllerDataScope) {
// ... [用户检查]
if (PermissionContextHolder.isMvcRequest()) {
// MVC 线程
String permission = PermissionContextHolder.getMvcContext();
dataScopeFilter(joinPoint, currentUser, ..., permission);
} else {
// WebFlux/AI 异步线程
PermissionContextHolder.getReactiveContext().defaultIfEmpty(controllerDataScope.permission())
.subscribe(permission -> dataScopeFilter(joinPoint, currentUser,
controllerDataScope.deptAlias(), controllerDataScope.userAlias(), permission));
}
}
三、总结与实践意义
通过这套双模上下文机制,我们成功地在若依框架中解决了 Spring AI 异步工具调用中权限上下文丢失的问题:
- 解决了
IllegalStateException:异步线程不再依赖不可用的ThreadLocal。 - 确保了数据安全:无论权限校验发生在 Web 请求线程还是 AI Tooling 异步线程,都能正确获取到当前请求关联的权限信息,并执行数据过滤。
这套模式对于任何在传统 Spring MVC 项目中引入异步/响应式技术栈(如 Spring AI、WebClient、CompletableFuture 等)的场景都具有极高的借鉴价值。
更多推荐



所有评论(0)