Dubbo RPC 调用中用户上下文传递问题的解决

2025.10.14 正在将在医院管理系统项目重构成微服务架构。之前业务逻辑直接在 Web 层调用,没有经过 Dubbo。最近重构后,统一改为 Dubbo RPC调用。重构时有注意到model上下文传递问题,但是这个tokenService藏在反射处理里没发现。。

10.14二编:有藏得很深的来了 放了3个切面注解放在mapper上层的RepositoryImpl文件里

文章目录

一、架构和修改原则

重构使用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 传递上下文

  1. 创建 DubboContextFilter
  2. 创建 LoginUserContextHolder
  3. 修改业务代码

方案二被我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 解决方案

  1. DubboContextFilter - 传递用户上下文

    @Activate(group = [CommonConstants.CONSUMER, CommonConstants.PROVIDER])
    class DubboContextFilter : Filter {
    // Consumer 端:从 HttpServletRequest 获取 LoginUser,通过 RpcContext 传递
    // Provider 端:从 RpcContext 获取 LoginUser,设置到 LoginUserContextHolder
    }

  2. LoginUserContextHolder - 存储用户上下文

    object LoginUserContextHolder {
    private val context = ThreadLocal<LoginUser>()
    // 使用 ThreadLocal 存储 LoginUser 对象
    }

  3. 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,服务于不同的目的

相关推荐
雯0609~9 小时前
宝塔配置:IP文件配置,根据端口配置多个项目文件(不配置域名的情况)
服务器·网络协议·tcp/ip
小无名呀12 小时前
socket_udp
linux·网络·c++·网络协议·计算机网络·udp
花阴偷移13 小时前
逆向基础--汇编基础(CS与IP) (05)
网络·汇编·网络协议·tcp/ip
天玺-vains13 小时前
借助Github Action实现通过 HTTP 请求触发邮件通知
网络协议·http·github
ZhengEnCi14 小时前
N2G-为什么90%的人不会计算子网掩码?大厂网络工程师的CIDR与子网掩码完全解析
网络协议
薛之谦_15 小时前
【SSL】什么是自签名证书及使用Java生成SSL自签名证书
java·网络协议·ssl
捷米研发三部15 小时前
EtherNet/IP转EtherNet/IP协议转换网关实现欧姆龙 PLC与罗克韦尔PLC通讯的配置案例
网络·网络协议
小武~15 小时前
嵌入式网络编程深度优化 --网络协议栈配置实战指南
linux·网络·网络协议
拾忆,想起18 小时前
TCP粘包拆包全解析:数据流中的“藕断丝连”与“一刀两断”
java·网络·数据库·网络协议·tcp/ip·哈希算法
小皮虾18 小时前
告别胶水代码!一行命令,让你的小程序云函数实现API路由自动化
前端·rpc·小程序·云开发