Dubbo 如何实现像本地方法一样调用远程方法的?
结论先行:靠三层“伪装”——接口代理(Proxy)把方法调用截获成
Invocation对象,Invoker抽象把“本地调用”和“远程调用”统一成同一个invoke方法,动态代理(JDK 或 Javassist)让调用方拿到的对象长得和本地实现一模一样。序列化与网络收发被完全隐藏在代理之后。
一、伪装的三层
业务代码 userService.getById(1L)
│ 看起来是本地方法调用
▼
动态代理 Proxy InvokerInvocationHandler.invoke()
│ 方法名 + 参数类型 + 实参 → RpcInvocation
▼
Invoker 抽象 invoker.invoke(invocation)
├── 本地暴露:JvmInvoker,直接反射调用实现类
└── 远程暴露:DubboInvoker,序列化后走网络
▼
网络层 Netty 发送 → Provider 解码 → 反射执行 → 响应
关键在于:Invoker 是统一抽象。同一份业务代码,注入的是本地引用就本地执行(同一 JVM 内走 injvm 协议,不经过网络),注入的是远程引用就走网络,调用方无感。
二、代理是怎么生成的
| 方式 | 生成物 | 特点 |
|---|---|---|
| JDK 动态代理 | 实现同接口的 $Proxy 类 | 必须基于接口,反射调用,启动快 |
| Javassist 字节码 | 直接生成实现类 | 少一层反射,调用更快,默认方式 |
// 框架内部等价逻辑:为接口生成代理
Invoker<UserService> invoker = proxyFactory.getProxy(invoker); // 传入真实 invoker
UserService userService = (UserService) invoker.getInterface()
.cast(proxy); // 业务侧拿到的就是这个对象
业务侧写法只有一行:
@DubboReference(version = "1.0.0", check = false, timeout = 1000)
private UserService userService;
public void demo() {
User user = userService.getById(1L); // 与本地方法写法完全一致
}
三、透明化必须补齐的四件事
| 问题 | 处理方式 |
|---|---|
| 异常怎么抛 | Provider 侧异常序列化回传,消费端还原为原异常类型;框架异常(超时、无提供者)单独包装 |
| 超时怎么算 | 调用前记录时间戳,Future.get(timeout) 超时抛 RpcException |
| 重试怎么做 | cluster 层按 retries 重试其他 invoker,幂等方法才可开启 |
| 上下文怎么传 | RpcContext 的 attachment 随请求头透传,如 traceId、租户 ID |
// 隐式传参:不改变方法签名也能把上下文带到提供者
RpcContext.getClientAttachment().setAttachment("traceId", "abc-123");
userService.getById(1L);
四、同步语义下线程在做什么
“像本地方法”意味着调用线程必须原地等结果,等待期间线程是阻塞的:
| 阶段 | 消费端线程状态 | 说明 |
|---|---|---|
| 发起调用 | RUNNABLE | 组装 Invocation、序列化、写 socket |
| 等待响应 | WAITING | 挂在 DefaultFuture.get() 上,由响应唤醒 |
| 收到响应 | RUNNABLE | IO 线程完成 future.complete(),等待线程被唤醒 |
| 超时 | RUNNABLE | 定时任务让 Future 异常完成,抛出 RpcException |
因此高并发下同步 RPC 的线程数约等于“QPS × 平均耗时”,这也是异步调用存在的理由。
常见追问
- 追问:透明调用的代价是什么?网络不可靠性被隐藏了,开发者容易忘记超时、重试、幂等、序列化兼容这些分布式问题,所以“像本地一样”只是语法层面。
- 追问:为什么默认用 Javassist 而不是 JDK 代理?Javassist 生成的是真实方法调用,避免反射开销;但接口变更时需要重新生成,JDK 代理更省事,可通过
proxy=jdk切换。 - 追问:
injvm协议是什么?同一 JVM 内优先走本地调用,跳过序列化与网络,只在本地没有暴露时才走远程。
小结
- 代理截获调用,
Invocation承载方法语义,Invoker抹平本地与远程差异。 - 异常、超时、重试、上下文透传是“透明”必须补齐的四块拼图。
injvm优先是 Dubbo 的默认优化,本地有实现就不走网络。- 语法透明不等于语义透明,分布式约束仍需业务自己兜住。