Dubbo RPC 调用中用户上下文传递问题的解决
2025.10.14 正在将在医院管理系统项目重构成微服务架构。之前业务逻辑直接在 Web 层调用,没有经过 Dubbo。最近重构后,统一改为 Dubbo RPC调用。重构时有注意到model上下文传递问题,但是这个tokenService藏在反射处理里没发现。。
10.14二编:有藏得很深的来了 放了3个切面注解放在mapper上层的RepositoryImpl文件里
文章目录
- 一、架构和修改原则
- 二、问题
- 三、分析
- 3.1 代码分析
- 3.2 根本原因
- 四、解决方案
- 4.1 设计思路
- 4.2 技术实现
- 4.3 技术要点
- 五、解决的
- 六、二编,问题升级
- 6.1 解决方案
- 七 流程
一、架构和修改原则
重构使用Spring Cloud + Dubbo 混合架构
Web 层(dovip-hospital-3b-web):处理 HTTP 请求,负责用户交互
Business 层(dovip-hospital-3b-business):Dubbo Provider,提供业务逻辑服务
设计原则
遵循"开闭原则",对扩展开放,对修改关闭
提供降级方案,保证系统的健壮性
二、问题
在开发考试权限管理功能时,遇到了一个严重的生产问题:
症状:
用户登录后访问考试管理页面时,系统抛出 NullPointerException
错误堆栈显示:at com.xz.auth.core.TokenService.getLoginUser(TokenService.java:70)
该问题只在 Dubbo RPC 调用时出现,本地直接调用时正常
错误日志:
三、分析
3.1 代码分析
首先查看了 TokenService 的实现:
关键发现: TokenService 依赖 ServletUtils.getRequest() 获取 HttpServletRequest,然后从 request 的 attribute 中获取登录用户信息。
3.2 根本原因
Web 层的用户认证流程:
ArgumentResolverInterceptor 拦截器从 token 获取用户信息
将 LoginUser 对象设置到 request.setAttribute(“loginUser”, loginUser)
TokenService 从 request.getAttribute(“loginUser”) 获取用户信息
Dubbo RPC 调用的问题:
Web 层的 HttpServletRequest 不会自动传递到 Dubbo 服务端
Dubbo 服务端运行在独立的线程中,没有 HttpServletRequest 对象
ServletUtils.getRequest() 通过 RequestContextHolder.getRequestAttributes() 获取 request
在 Dubbo Provider 端,RequestContextHolder 是空的
导致 getRequest() 返回 null,调用 getAttribute() 时抛出 NPE
四、解决方案
4.1 设计思路
核心:通过 Dubbo 的 RpcContext 传递用户上下文
Dubbo 提供了 RpcContext 机制,可以在 Consumer 和 Provider 之间传递上下文信息:
Consumer 端:通过 RpcContext.getContext().setAttachment() 设置上下文
Provider 端:通过 RpcContext.getContext().getAttachment() 获取上下文
4.2 技术实现
方案一:创建 Dubbo Filter 传递上下文
- 创建 DubboContextFilter
- 创建 LoginUserContextHolder
- 修改业务代码
方案二被我pass了不说了- -
4.3 技术要点
-
Dubbo Filter 机制
使用 @Activate 注解自动激活 Filter
通过 isConsumerSide 和 isProviderSide 区分调用端
在 finally 块中清理 ThreadLocal,避免内存泄漏 -
ThreadLocal 的使用
在 Provider 端使用 ThreadLocal 存储用户信息
确保线程安全,每个线程有独立的用户上下文
在 finally 块中清理,避免内存泄漏 -
降级策略
如果 Dubbo 上下文中没有用户信息,回退到数据库查询
保证系统的健壮性和向后兼容性
五、解决的
解决了 Dubbo RPC 调用中用户上下文丢失的问题
实现了透明的用户上下文传递机制
保证了系统的稳定性和健壮性
性能影响
上下文传递的性能开销极小
没有额外的网络开销
不影响现有业务逻辑的性能
可维护性
职责分明,易于扩展(可以传递其他上下文信息)
六、二编,问题升级
还有一个 DataScopeAspect AOP 切面也在使用 TokenService.getLoginUser(),而且这个切面在多个地方被触发。我跪了T T
@DataScope(queryScopeKey = DataScopeAspect.EXAM_SCOPE_KEY, dataScopeArray = [DataScopeEnums.DEPARTMENT])override fun findPagedListByCriterion(pageNumber: Int, pageSize: Int, examinationAccountUUID: String, criterion: ExaminationCriterion): DovipPage<Examination> {PageHelper.startPage<Examination>(pageNumber, pageSize)...BeanUtils.copyProperties(PageInfo(resultList), page)return page}
6.1 解决方案
- DubboContextFilter - 传递用户上下文
@Activate(group = [CommonConstants.CONSUMER, CommonConstants.PROVIDER])
class DubboContextFilter : Filter {// Consumer 端:从 HttpServletRequest 获取 LoginUser,通过 RpcContext 传递// Provider 端:从 RpcContext 获取 LoginUser,设置到 LoginUserContextHolder
}
- LoginUserContextHolder - 存储用户上下文
object LoginUserContextHolder {private val context = ThreadLocal<LoginUser>()// 使用 ThreadLocal 存储 LoginUser 对象
}
- TokenServiceAspect - AOP 切面 - 拦截 TokenService.getLoginUser() 方法
@Aspect
@Component
@Order(1)
class TokenServiceAspect {@Around("execution(* com.xz.auth.core.TokenService.getLoginUser())")fun aroundGetLoginUser(joinPoint: ProceedingJoinPoint): LoginUser? {// 优先从 Dubbo 上下文获取val loginUser = LoginUserContextHolder.get()if (loginUser != null) {return loginUser}// 否则执行原方法return joinPoint.proceed() as? LoginUser}
}
七 流程
Web层(Consumer端):
从Spring的ThreadLocal中获取HttpServletRequest
从中提取loginUser属性
通过Dubbo的RpcContext传递给服务端
Business层(Provider端):
从RpcContext接收loginUser
存储到自定义的LoginUserContextHolder的ThreadLocal中
供业务代码使用
所以这两个是完全独立的ThreadLocal,服务于不同的目的