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

相关推荐
像素之间9 小时前
HTTP之content-disposition
网络·网络协议·http
程序员Aries13 小时前
自定义网络协议与序列化/反序列化
linux·网络·c++·网络协议·程序人生
正见TrueView14 小时前
阿里美团京东从“三国杀”到“双雄会”:本地生活无限战争的终局猜想
dubbo·生活
FIavor.15 小时前
我发送给Apifox是http://localhost:9002/goods/getByUserName?name=张三 为什么会是500哪里错了?
java·服务器·网络协议·http
IT大灰狼16 小时前
拌合站软件开发(27)监测各项IP设备可访问性
网络·网络协议·tcp/ip
FIavor.17 小时前
怎么办这是Apifox里执行http://localhost:9002/goods/getByUserName?name=“张三“为什么我改了还是500?
java·网络·网络协议·http
mit6.82417 小时前
[cpprestsdk] http_client_config | GET | request()
网络·网络协议·http
码不停蹄Zzz19 小时前
xdma IP使用教程1-xdma ip核配置
网络协议·tcp/ip·fpga开发
潇凝子潇20 小时前
网络协议的零拷贝 和 操作系统的零拷贝异同
网络·网络协议