05 | 动态代理:面向接口编程,屏蔽 RPC 处理流程

简介: 本文深入讲解动态代理在RPC中的核心作用。通过动态代理,RPC实现接口的远程调用伪装成本地调用,屏蔽网络通信细节。以Java为例,剖析JDK动态代理生成过程,揭示方法调用被拦截并转发至远程服务的机制,并对比Javassist、Byte Buddy等框架在性能与易用性上的差异,帮助理解RPC“透明调用”背后的魔法。

上一讲我分享了网络通信,其实要理解起来也很简单,RPC 是用来解决两个应用之间的通信,而网络则是两台机器之间的「桥梁」,只有架好了桥梁,我们才能把请求数据从一端传输另外一端。其实关于网络通信,你只要记住一个关键字就行了——可靠的传输。
那么接着上一讲的内容,我们再来聊聊动态代理在 RPC 里面的应用。
如果我问你,你知道动态代理吗? 你可能会如数家珍般地告诉我动态代理的作用以及好处。那我现在接着问你,你在项目中用过动态代理吗?这时候可能有些人就会犹豫了。那我再换一个方式问你,你在项目中有实现过统一拦截的功能吗?比如授权认证、性能统计等等。你可能立马就会想到,我实现过呀,并且我知道可以用 Spring 的 AOP 功能来实现。
没错,进一步再想,在 Spring AOP 里面我们是怎么实现统一拦截的效果呢?并且是在我们不需要改动原有代码的前提下,还能实现非业务逻辑跟业务逻辑的解耦。这里的核心就是采用动态代理技术,通过对字节码进行增强,在方法调用的时候进行拦截,以便于在方法调用前后,增加我们需要的额外处理逻辑。
那话说回来,动态代理跟 RPC 又有什么关系呢?
远程调用的魔法
我说个具体的场景,你可能就明白了。
在项目中,当我们要使用 RPC 的时候,我们一般的做法是先找服务提供方要接口,通过 Maven 或者其他的工具把接口依赖到我们项目中。我们在编写业务逻辑的时候,如果要调用提供方的接口,我们就只需要通过依赖注入的方式把接口注入到项目中就行了,然后在代码里面直接调用接口的方法 。
我们都知道,接口里并不会包含真实的业务逻辑,业务逻辑都在服务提供方应用里,但我们通过调用接口方法,确实拿到了想要的结果,是不是感觉有点神奇呢?想一下,在 RPC 里面,我们是怎么完成这个魔术的。
这里面用到的核心技术就是前面说的动态代理。RPC 会自动给接口生成一个代理类,当我们在项目中注入接口的时候,运行过程中实际绑定的是这个接口生成的代理类。这样在接口方法被调用的时候,它实际上是被生成代理类拦截到了,这样我们就可以在生成的代理类里面,加入远程调用逻辑。
通过这种「偷梁换柱」的手法,就可以帮用户屏蔽远程调用的细节,实现像调用本地一样地调用远程的体验,整体流程如下图所示:

实现原理
动态代理在 RPC 里面的作用,就像是个魔术。现在我不妨给你揭秘一下,我们一起看看这是怎么实现的。之后,学以致用自然就不难了。
我们以 Java 为例,看一个具体例子,代码如下所示:
package cn.mrcode.study.rpc.s05;

/**

  • 要代理的接口
    */
    public interface Hello {
    String say();
    }
    package cn.mrcode.study.rpc.s05;

/**

  • 真实调用对象
    */
    public class RealHello {
    public String invoke() {
     return "i'm proxy";
    
    }
    }
    package cn.mrcode.study.rpc.s05;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;

/**

  • JDK 代理类生成
    */
    public class JDKProxy implements InvocationHandler {
    private Object target;

    JDKProxy(Object target) {

     this.target = target;
    

    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

     return ((RealHello) target).invoke();
    

    }
    }
    package cn.mrcode.study.rpc.s05;

import java.lang.reflect.Proxy;

/**

  • 测试例子
    */
    public class TestProxy {
    public static void main(String[] args) {

     // 构建代理器
     JDKProxy proxy = new JDKProxy(new RealHello());
    
     final ClassLoader classLoader = ClassLoader.getSystemClassLoader();
    

    // ClassLoader classLoader = ClassLoaderUtils.getCurrentClassLoader();

     // 该 ClassLoaderUtils 类没有找到,不知道是否是其他的工具类
    
     // 把生成的代理类保存到文件
     System.setProperty("sun.misc.ProxyGenerator.saveGeneratedFiles", "true");
    
     // 生成代理类
     Hello test = (Hello) Proxy.newProxyInstance(classLoader, new Class[]{Hello.class}, proxy);
     // 方法调用
     System.out.println(test.say());
    

    }
    }
    这段代码想表达的意思就是:给 Hello 接口生成一个动态代理类,并调用接口 say() 方法,但真实返回的值居然是来自 RealHello 里面的 invoke() 方法返回值。你看,短短 50 行的代码,就完成了这个功能,是不是还挺有意思的?
    那既然重点是代理类的生成,那我们就去看下 Proxy.newProxyInstance 里面究竟发生了什么?
    一起看下下面的流程图,具体代码细节你可以对照着 JDK 的源码看(上文中有类和方法,可以直接定位),我是按照 1.7.X 版本梳理的。

在生成字节码的那个地方(是 java.lang.reflect.Proxy.ProxyClassFactory#apply 中处理的生成字节码文件 ),也就是 ProxyGenerator.generateProxyClass() 方法里面,通过代码我们可以看到,里面是用参数 saveGeneratedFiles (sun.misc.ProxyGenerator#saveGeneratedFiles 这个变量取值就是上面写的那一串) 来控制是否把生成的字节码保存到本地磁盘。同时为了更直观地了解代理的本质,我们需要把参数 saveGeneratedFiles 设置成 true,但这个参数的值是由 key 为 sun.misc.ProxyGenerator.saveGeneratedFiles 的 Property 来控制的,动态生成的类会保存在工程根目录下的 com/sun/proxy 目录里面(没有继承公共的代理类,则默认的包名是这个)。现在我们找到刚才生成的 $Proxy0.class ,通过反编译工具打开 class 文件,你会看到这样的代码:

package com.sun.proxy;

import cn.mrcode.study.rpc.s05.Hello;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.lang.reflect.UndeclaredThrowableException;

public final class $Proxy0 extends Proxy implements Hello {
private static Method m1;
private static Method m2;
private static Method m3;
private static Method m0;

public $Proxy0(InvocationHandler var1) throws  {
    super(var1);
}

public final boolean equals(Object var1) throws  {
    try {
        return (Boolean)super.h.invoke(this, m1, new Object[]{var1});
    } catch (RuntimeException | Error var3) {
        throw var3;
    } catch (Throwable var4) {
        throw new UndeclaredThrowableException(var4);
    }
}

public final String toString() throws  {
    try {
        return (String)super.h.invoke(this, m2, (Object[])null);
    } catch (RuntimeException | Error var2) {
        throw var2;
    } catch (Throwable var3) {
        throw new UndeclaredThrowableException(var3);
    }
}

public final String say() throws  {
    try {
        return (String)super.h.invoke(this, m3, (Object[])null);
    } catch (RuntimeException | Error var2) {
        throw var2;
    } catch (Throwable var3) {
        throw new UndeclaredThrowableException(var3);
    }
}

public final int hashCode() throws  {
    try {
        return (Integer)super.h.invoke(this, m0, (Object[])null);
    } catch (RuntimeException | Error var2) {
        throw var2;
    } catch (Throwable var3) {
        throw new UndeclaredThrowableException(var3);
    }
}

static {
    try {
        m1 = Class.forName("java.lang.Object").getMethod("equals", Class.forName("java.lang.Object"));
        m2 = Class.forName("java.lang.Object").getMethod("toString");
        m3 = Class.forName("cn.mrcode.study.rpc.s05.Hello").getMethod("say");
        m0 = Class.forName("java.lang.Object").getMethod("hashCode");
    } catch (NoSuchMethodException var2) {
        throw new NoSuchMethodError(var2.getMessage());
    } catch (ClassNotFoundException var3) {
        throw new NoClassDefFoundError(var3.getMessage());
    }
}

}

我们可以看到 $Proxy0 类里面有一个跟 Hello 一样签名的 say() 方法,其中 super.h 绑定的是刚才传入的 JDKProxy 对象(构造器中传入的 InvocationHandler var1),所以当我们调用 Hello.say() 的时候,其实它是被转发到了 JDKProxy.invoke() 。到这儿,整个魔术过程就透明了。
实现方法
其实在 Java 领域,除了 JDK 默认的 InvocationHandler 能完成代理功能,我们还有很多其他的第三方框架也可以,比如像 Javassist、Byte Buddy 这样的框架。
单纯从代理功能上来看,JDK 默认的代理功能是有一定的局限性的,它要求被代理的类只能是接口。原因是因为生成的代理类会继承 Proxy 类,但 Java 是不支持多重继承的。
这个限制在 RPC 应用场景里面还是挺要紧的,因为对于服务调用方来说,在使用 RPC 的时候本来就是面向接口来编程的,这个我们刚才在前面已经讨论过了。使用 JDK 默认的代理功能,最大的问题就是性能问题。它生成后的代理类是使用反射来完成方法调用的,而这种方式相对直接用编码调用来说,性能会降低,但好在 JDK8 及以上版本对反射调用的性能有很大的提升,所以还是可以期待一下的。
相对 JDK 自带的代理功能,Javassist 的定位是能够操纵底层字节码,所以使用起来并不简单,要生成动态代理类恐怕是有点复杂了。但好的方面是,通过 Javassist 生成字节码,不需要通过反射完成方法调用,所以性能肯定是更胜一筹的。在使用中,我们要注意一个问题,通过 Javassist 生成一个代理类后,此 CtClass 对象会被冻结起来,不允许再修改;否则,再次生成时会报错。
Byte Buddy 则属于后起之秀,在很多优秀的项目中,像 Spring、Jackson 都用到了 Byte Buddy 来完成底层代理。相比 Javassist,Byte Buddy 提供了更容易操作的 API,编写的代码可读性更高。更重要的是,生成的代理类执行速度比 Javassist 更快。
虽然以上这三种框架使用的方式相差很大,但核心原理却是差不多的,区别就只是通过什么方式生成的代理类以及在生成的代理类里面是怎么完成的方法调用。同时呢,也正是因为这些细小的差异,才导致了不同的代理框架在性能方面的表现不同。因此,我们在设计 RPC 框架的时候,还是需要进行一些比较的,具体你可以综合它们的优劣以及你的场景需求进行选择。
总结
今天我们介绍了动态代理在 RPC 里面的应用,虽然它只是一种具体实现的技术,但我觉得只有理解了方法调用是怎么被拦截的,才能厘清在 RPC 里面我们是怎么做到面向接口编程,帮助用户屏蔽 RPC 调用细节的,最终呈现给用户一个像调用本地一样去调用远程的编程体验。
既然动态代理是一种具体的技术框架,那就会涉及到选型。我们可以从这样三个角度去考虑:
● 因为代理类是在运行中生成的,那么代理框架生成代理类的速度、生成代理类的字节码大小等等,都会影响到其性能—— 生成的字节码越小,运行所占资源就越小。
● 还有就是我们生成的代理类,是用于接口方法请求拦截的,所以每次调用接口方法的时候,都会执行生成的代理类,这时 生成的代理类的执行效率就需要很高效。
● 最后一个是从我们的使用角度出发的,我们肯定希望选择一个使用起来很方便的代理类框架,比如我们可以考虑:API 设计是否好理解、社区活跃度、还有就是依赖复杂度等等
最后,我想再强调一下。动态代理在 RPC 里面,虽然看起来只是一个很小的技术点,但就是这个创新使得用户可以不用关注细节了。其实,我们在日常设计接口的时候也是一样的,我们会想尽一切办法把细节对调用方屏蔽,让调用方的接入尽可能的简单。这就好比,让你去设计一个商品发布的接口,你并不需要暴露给用户一些细节,比如,告诉他们商品数据是怎么存储的。

相关文章
|
Java
如何在Java中进行多线程编程
Java多线程编程常用方式包括:继承Thread类、实现Runnable接口、Callable接口(可返回结果)及使用线程池。推荐线程池以提升性能,避免频繁创建线程。结合同步与通信机制,可有效管理并发任务。
435 6
|
10月前
|
存储 JSON 编解码
06 | RPC 实战:剖析 gRPC 源码,动手实现一个完整的 RPC
本讲通过剖析gRPC源码,实战实现RPC框架。以HelloWorld为例,讲解如何通过Protocol Buffer定义接口、生成客户端与服务端代码,深入分析gRPC基于HTTP/2的请求发送与接收原理,揭示序列化、Frame构造、Netty传输等核心机制,帮助读者将理论知识落地为代码实践。(238字)
|
存储 运维 物联网
RFID电力巡检管理应用
RFID技术在电力巡检中广泛应用,可显著提升效率、准确性和可靠性。通过为设备安装RFID标签,记录全生命周期信息,结合GIS技术优化巡检路线。巡检人员使用手持终端读取标签,自动记录到达时间与数据,减少人工错误。系统支持异常上报、数据分析和维护提醒,实现设备精细化管理。RFID技术助力电力企业降低运维成本,延长设备寿命,保障电网稳定运行。
|
10月前
|
消息中间件 数据库 UED
1.1 同步调用与异步调用
本文介绍了微服务间的同步与异步调用。同步调用需等待结果返回,顺序执行,适合实时性高、操作简单的场景;异步调用发出请求后可继续执行其他任务,提升效率与资源利用率,适用于耗时操作。通过支付、点餐、挂号等生活实例对比,阐述了二者特点、适用场景及优缺点。
|
10月前
|
消息中间件 Java Nacos
SpringCloud概述
Spring Cloud应微服务需求而生,提供统一解决方案,具备约定大于配置、组件丰富、开箱即用等特点。通过地铁站命名版本,避免子项目冲突。Spring Cloud Alibaba融合阿里实践,推出Nacos、Sentinel、Seata等优秀组件,弥补Netflix停更短板,成为主流选择。本节概述其演进、生态及技术选型依据,为后续实战奠基。(238字)
|
10月前
|
缓存 Java Nacos
@RefreshScope热更新原理
@RefreshScope通过组合@Scope("refresh")实现配置热更新,其核心是利用动态代理与缓存机制。标注该注解的Bean会被放入自定义refresh scope缓存,配置变更时清空缓存,触发Spring重新创建Bean实例,从而重新注入最新配置值,实现毫秒级动态刷新。
|
10月前
|
自然语言处理
主流大模型结构
主流大模型架构分为四类:Encoder-Decoder(如T5)、Decoder-Only(如GPT、LLaMA)、Encoder-Only(如BERT)和Prefix-Decoder(如GLM),分别适用于生成、理解或兼顾任务。GPT系列从GPT-1到GPT-4,参数量跃升至约1.7万亿,支持多模态与长上下文;LLaMA系列持续优化开源模型;国产模型如Qwen、ChatGLM、Kimi、DeepSeek在中文场景表现突出,涵盖对话、数学推理等能力。
|
10月前
|
机器学习/深度学习
位置编码详解
位置编码为Transformer提供序列位置信息,弥补注意力机制无位置感知的缺陷。主要分绝对(如可学习、Sinusoidal)和相对(如RoPE、ALiBi)两类。RoPE通过旋转矩阵支持长序列,ALiBi以线性偏置增强外推能力。不同方法在长度外推、效率上各有优劣,广泛应用于LLaMA、BLOOM等大模型,是面试考察重点。
|
10月前
|
机器学习/深度学习 缓存 自然语言处理
注意力机制详解
注意力机制是Transformer核心,实现动态信息聚焦。涵盖自注意力、交叉注意力、多头(MHA)、分组(GQA)、多查询(MQA)及潜在注意力(MLA),在参数量、速度与质量间权衡。广泛应用于NLP与多模态任务,面试常考其原理与复杂度。
|
10月前
|
自然语言处理 算法
分词器详解
分词器将文本转为模型可处理的数字序列,主流算法包括BPE、WordPiece和SentencePiece。BPE高效但中文支持弱;WordPiece适合英文,用于BERT;SentencePiece语言无关,尤擅中文。实战中需结合语种与需求选择,并合理配置参数与特殊标记。