HelloCoder HelloCoder
首页
《Java小白求职之路》
《小白学Java》
计算机毕设
  • 一些免费计算机资源
  • 脚手架工具
  • 《从0到1学习Java多线程》
  • 《从0到1搭建服务器》
  • 《可观测和监控》
  • 《k8s学习心得》
随笔
关于作者
首页
《Java小白求职之路》
《小白学Java》
计算机毕设
  • 一些免费计算机资源
  • 脚手架工具
  • 《从0到1学习Java多线程》
  • 《从0到1搭建服务器》
  • 《可观测和监控》
  • 《k8s学习心得》
随笔
关于作者
  • 《从0到1学习Java多线程》

  • 《从0到1搭建服务器》

  • 源码学习

  • 可观测和监控

  • 玩转IDEA

  • AI学习

  • 03-RPC

    • rmi-远程方法调用
    • SPI机制
    • RPC下线窗口期
    • 目标函数调用
    • 粘包问题
  • 05-《Java日志框架》

  • k8s

  • 专栏
  • 03-RPC
#03-RPC
HaC
2026-06-28
目录

目标函数调用

当 Dubbo 的解码器精准地切出一个完整的业务数据包(Body)后,接下来就要进入反序列化与反射调用的阶段了。

我们可以把这个过程分为两部分来看:首先看客户端到底传了哪些信息过来(也就是 Body 里装了啥),其次看服务端拿到这些信息后是如何一步步找到并执行目标函数的。

# 一、 客户端传入的信息有哪些?(Body 的秘密)

虽然你在写代码时只是调用了一个普通的接口方法(例如 userService.getUserById(123)),但 Dubbo 的客户端(Consumer)在把请求发出去之前,会把这次调用拆解成非常详细的元数据,统统打包进 Body 里。

在 Dubbo 协议中,请求体(Body)反序列化后,主要包含以下核心信息:

  1. Dubbo 版本号 (Dubbo Version): 例如 2.7.x,让服务端知道客户端的框架版本,以便做兼容处理。
  2. 服务接口全限定名 (Service Name): 例如 com.example.UserService。这是定位服务的第一步。
  3. 服务版本号 (Service Version): 例如 1.0.0。Dubbo 支持同一个接口有多个版本,必须明确指定。
  4. 方法名 (Method Name): 字符串格式,例如 "getUserById"。
  5. 参数类型列表 (Parameter Types): 这是一个字符串数组或类名列表,例如 ["java.lang.Long"]。这非常关键! 因为 Java 支持方法重载(方法名相同但参数不同),单靠方法名是不够的,必须加上参数类型才能唯一确定一个方法。
  6. 实际参数值 (Arguments): 具体的入参对象,例如 [123]。
  7. 隐式传参/上下文 (Attachments): 这是一个 Map(键值对)。用来传递一些不适合放在方法参数里的附加信息(扩展Map),比如:
    • 链路追踪的 TraceID(用于分布式日志连通)。
    • 路由标签(Tag),用于灰度发布。
    • 接口的超时时间(Timeout)等。

# 二、 服务端收到后,如何调用目标函数?

服务端解码器(Netty 接收端)拿到了上述 7 样武器后,会经过一个标准的“流水线”来完成最终的函数调用。整个过程可以分为 4 个核心步骤:

# 步骤 1:线程分流(派发策略 Dispatcher)

Netty 的 IO 线程(Boss/Worker 线程)是非常宝贵的,只负责收发数据,绝对不能在网络线程里跑耗时的业务逻辑。

  • Netty 收到完整的 Body 字节流后,通常会立刻把这些数据封装成一个任务,扔进 Dubbo 专门的业务线程池(ExecutorService)中。
  • 接下来的反序列化和调用,都是在这个独立的业务线程池里执行的,从而保证了网络通信的高吞吐。

# 步骤 2:反序列化 (Deserialization)

业务线程拿到字节流后,根据前面 16 字节 Header 里指定的序列化类型(如 Hessian2),将字节流还原为上述的 Java 对象和字符串(接口名、方法名、参数值等)。

# 步骤 3:寻找服务实现类(Invoker 路由)

服务端在启动时,会把自己本地真正的业务实现类(比如标了 @DubboService 的 UserServiceImpl)注册到一个内部的容器中(通常是一个大 Map)。

  • 服务端以 接口名:版本号:分组 作为 Key,去这个 Map 里查找,就能精准找到对应的代理对象 Invoker。

    三合一匹配条件,缺一不可:

    1. 接口全类名 interfaceName

    2. 服务分组 group + 版本 version(存在 attachments 里)

    3. 方法名 methodName + 参数类型数组 parameterTypes

# 步骤 4:反射调用目标方法(Reflection / Javassist)

找到了实现类,也知道了方法名和参数类型,接下来就是执行代码了。

如果你自己写,可能会想到用 Java 原生的反射:

// 伪代码概念
Method method = serviceClass.getMethod(methodName, parameterTypes);
Object result = method.invoke(targetInstance, arguments);

但在追求极致性能的 RPC 框架中,原生的 Method.invoke() 反射性能是比较慢的。Dubbo 为了更快,默认采用了 Javassist(动态字节码生成) 技术:

  • Dubbo 在动态编译时,会为每个服务实现类生成一个包装类(Wrapper)。
  • 这个包装类里有一个动态生成的 invokeMethod 方法,里面全是一堆 if-else。
  • 当调用传过来时,它实际上是直接运行编译好的 Java 代码,例如 if("getUserById".equals(m)) { return ((UserServiceImpl)target).getUserById((Long)args[0]); }。
  • 这种做法把“反射调用”直接变成了“本地直接调用”,速度极快。

为什么原生反射性能差?

Method.invoke() 底层痛点:

  1. 每次调用要做权限校验、参数装箱 / 拆箱、安全检查;
  2. JIT 很难对反射调用做深度优化,循环调用损耗巨大;
  3. 重载方法需要循环匹配参数类型数组,多一层判断开销。

可以把创建对象想象成进入一栋大楼:

  • 反射:你每次都要去前台(Class 对象),出示工牌(访问权限检查),询问会议室位置(方法查找),再签字登记(AccessibleObject.setAccessible())。前台每次都会认真核实你的权限,还要确认你这个人有没有进错楼层。这个过程安全,但每次都有手续开销。
  • Javassist:它自己就是大楼的建筑师,在开工前(编译期或类加载时)就给你发了一张门禁卡,卡里写好了你的权限和路线。当你需要进入时,直接刷卡走专用通道,和普通员工一样,没有额外的人工验证环节。

Dubbo 通过 Javassist 动态生成 UserServiceImpl$Wrapper 包装类,硬编码所有方法,不需要运行时根据字符串找 Method:

// Javassist动态生成的Wrapper简化源码
public class UserServiceImpl$Wrapper implements Wrapper {
    // 缓存所有方法元数据(启动一次性反射获取)
    public static final Method m0; // getUserById(Long)
    static {
        m0 = UserServiceImpl.class.getMethod("getUserById", Long.class);
    }

    // 核心方法:硬编码分发,无循环匹配、无Method.invoke
    @Override
    public Object invokeMethod(Object impl, String methodName, Class<?>[] types, Object[] args) throws Throwable {
        UserServiceImpl target = (UserServiceImpl) impl;
        // 硬编码if判断,直接调用原生方法,纯Java普通调用
        if ("getUserById".equals(methodName) && types.length == 1 && types[0] == Long.class) {
            return target.getUserById((Long) args[0]);
        }
        // 其他方法硬编码分支...
        throw new NoSuchMethodException();
    }
}

服务启动、暴露 @DubboService 实现类 UserServiceImpl 时:

  1. Dubbo 使用 Javassist 读取实现类全部方法(方法名、参数类型、重载区分);
  2. 动态生成字节码 UserServiceImpl$Wrapper;
  3. Wrapper 内部把当前类所有方法写死硬编码分支,缓存到全局单例,后续所有 RPC 复用,不再重复生成。

运行时 RPC 调用,直接走硬编码普通方法调用,完全避开 Method.invoke 反射。

# 三、 如何返回

  1. 目标函数执行完毕,拿到了返回值(比如一个 User 对象,或者抛出了一个 Exception)。
  2. 服务端把这个返回值、以及最初请求里带过来的 Invoke ID(请求唯一标识) 一起,重新打包成一个响应报文。
  3. 同样加上 16 字节的 Dubbo 响应头,写好响应体的 Data Length。
  4. 通过 Netty 发回给客户端。
  5. 客户端根据 Invoke ID 找到当时正在死等(或者异步回调)的那个线程,把结果塞给它,你的客户端代码就成功拿到了远程返回的对象。

这就是一次完整的 RPC 调用从“拆包成功”到“执行完毕”的生命周期。

#03-RPC
上次更新: 2026-06-28 03:58:08
最近更新
01
悲观锁和乐观锁
06-28
02
MySQL-like是否可以使用索引
06-28
03
MySQL大表索引重建
06-28
更多文章>
Theme by Vdoing | Copyright © 2020-2026 HaC
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式