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

一、背景

如果我问你,你知道动态代理吗? 你可能会如数家珍般地告诉我动态代理的作用以及好处。那我现在接着问你,你在项目中用过动态代理吗?这时候可能有些人就会犹豫了。那我再换一个方式问你,你在项目中有实现过统一拦截的功能吗?比如授权认证、性能统计等等。你可能立马就会想到,我实现过呀,并且我知道可以用 Spring 的 AOP 功能来实现。

没错,进一步再想,在 Spring AOP 里面我们是怎么实现统一拦截的效果呢?并且是在我们不需要改动原有代码的前提下,还能实现非业务逻辑跟业务逻辑的解耦。这里的核心就是采用动态代理技术,通过对字节码进行增强,在方法调用的时候进行拦截,以便于在方法调用前后,增加我们需要的额外处理逻辑。

那话说回来,动态代理跟 RPC 又有什么关系呢?

二、远程调用的魔法

我说个具体的场景,你可能就明白了。

在项目中,当我们要使用 RPC 的时候,我们一般的做法是先找服务提供方要接口,通过 Maven 或者其他的工具把接口依赖到我们项目中。我们在编写业务逻辑的时候,如果要调用提供方的接口,我们就只需要通过依赖注入的方式把接口注入到项目中就行了,然后在代码里面直接调用接口的方法 。

我们都知道,接口里并不会包含真实的业务逻辑,业务逻辑都在服务提供方应用里,但我们通过调用接口方法,确实拿到了想要的结果,是不是感觉有点神奇呢?想一下,在 RPC 里面,我们是怎么完成这个魔术的。

这里面用到的核心技术就是前面说的动态代理。RPC 会自动给接口生成一个代理类,当我们在项目中注入接口的时候,运行过程中实际绑定的是这个接口生成的代理类。这样在接口方法被调用的时候,它实际上是被生成代理类拦截到了,这样我们就可以在生成的代理类里面,加入远程调用逻辑。

通过这种“偷梁换柱”的手法,就可以帮用户屏蔽远程调用的细节,实现像调用本地一样地调用远程的体验,整体流程如下图所示:

三、实现原理

动态代理在 RPC 里面的作用,就像是个魔术。现在我不妨给你揭秘一下,我们一起看看这是怎么实现的。之后,学以致用自然就不难了。

一起看下下面的流程图,具体代码细节你可以对照着 JDK 的源码看(上文中有类和方法,可以直接定位),我是按照 1.7.X 版本梳理的。

JDK 动态代理的核心是InvocationHandler 接口。这里提供一个 InvocationHandler 的Demo 实现,代码如下:

public class DemoInvokerHandler implements InvocationHandler {
 
    private Object target; // 真正的业务对象,也就是RealSubject对象
 
    public DemoInvokerHandler(Object target) { // 构造方法
 
        this.target = target;
 
    }
 
    public Object invoke(Object proxy, Method method, Object[] args)
 
             throws Throwable {
 
        // ...在执行业务方法之前的预处理...
 
        Object result = method.invoke(target, args);
 
        // ...在执行业务方法之后的后置处理...
 
        return result;
 
    }
 
    public Object getProxy() {
 
        // 创建代理对象
 
        return Proxy.newProxyInstance(Thread.currentThread()
 
            .getContextClassLoader(),
 
                target.getClass().getInterfaces(), this);
 
    }
 
}

接下来,我们可以创建一个 main() 方法来模拟上层调用者,创建并使用动态代理:

public class Main {
 
    public static void main(String[] args) {
 
        Subject subject = new RealSubject();
 
        DemoInvokerHandler invokerHandler = 
 
            new DemoInvokerHandler(subject);
 
        // 获取代理对象
 
        Subject proxy = (Subject) invokerHandler.getProxy();
 
        // 调用代理对象的方法,它会调用DemoInvokerHandler.invoke()方法
 
        proxy.operation();
 
    }
 
}

对于需要相同代理逻辑的业务类,只需要提供一个 InvocationHandler 接口实现类即可。在 Java 运行的过程中,JDK会为每个 RealSubject 类动态生成相应的代理类并加载到 JVM 中,然后创建对应的代理实例对象,返回给上层调用者。

JDK 动态代理相关实现的入口是 Proxy.newProxyInstance() 这个静态方法,它的三个参数分别是加载动态生成的代理类的类加载器、业务类实现的接口和上面介绍的InvocationHandler对象。

Proxy.newProxyInstance()方法的具体实现如下:

public static Object newProxyInstance(ClassLoader loader,
     Class[] interfaces, InvocationHandler h) 
 
         throws IllegalArgumentException {
 
    final Class<?>[] intfs = interfaces.clone();
 
    // ...省略权限检查等代码
 
    Class<?> cl = getProxyClass0(loader, intfs);  // 获取代理类
 
    // ...省略try/catch代码块和相关异常处理
 
    // 获取代理类的构造方法
 
    final Constructor<?> cons = cl.getConstructor(constructorParams);
 
    final InvocationHandler ih = h;
 
    return cons.newInstance(new Object[]{h});  // 创建代理对象
 
}

通过 newProxyInstance()方法的实现可以看到,JDK 动态代理是在 getProxyClass0() 方法中完成代理类的生成和加载。getProxyClass0() 方法的具体实现如下:

private static Class getProxyClass0 (ClassLoader loader, 
        Class... interfaces) {
 
    // 边界检查,限制接口数量(略)
 
    // 如果指定的类加载器中已经创建了实现指定接口的代理类,则查找缓存;
 
    // 否则通过ProxyClassFactory创建实现指定接口的代理类
 
    return proxyClassCache.get(loader, interfaces);
 
}

proxyClassCache 是定义在 Proxy 类中的静态字段,主要用于缓存已经创建过的代理类,定义如下:

private static final WeakCache[], Class> proxyClassCache
 
     = new WeakCache<>(new KeyFactory(), 
 
           new ProxyClassFactory());

WeakCache.get() 方法会首先尝试从缓存中查找代理类,如果查找不到,则会创建 Factory 对象并调用其 get() 方法获取代理类。Factory 是 WeakCache 中的内部类,Factory.get() 方法会调用 ProxyClassFactory.apply() 方法创建并加载代理类。

ProxyClassFactory.apply() 方法首先会检测代理类需要实现的接口集合,然后确定代理类的名称,之后创建代理类并将其写入文件中,最后加载代理类,返回对应的 Class 对象用于后续的实例化代理类对象。该方法的具体实现如下:

public Class apply(ClassLoader loader, Class[] interfaces) {
 
    // ... 对interfaces集合进行一系列检测(略)
 
    // ... 选择定义代理类的包名(略)
 
    // 代理类的名称是通过包名、代理类名称前缀以及编号这三项组成的
 
    long num = nextUniqueNumber.getAndIncrement();
 
    String proxyName = proxyPkg + proxyClassNamePrefix + num;
 
    // 生成代理类,并写入文件
 
    byte[] proxyClassFile = ProxyGenerator.generateProxyClass(
 
            proxyName, interfaces, accessFlags);
 
    
 
    // 加载代理类,并返回Class对象
 
    return defineClass0(loader, proxyName, proxyClassFile, 0, 
 
      proxyClassFile.length);
 
}

ProxyGenerator.generateProxyClass() 方法会按照指定的名称和接口集合生成代理类的字节码,并根据条件决定是否保存到磁盘上。该方法的具体代码如下:

public static byte[] generateProxyClass(final String name,
 
       Class[] interfaces) {
 
    ProxyGenerator gen = new ProxyGenerator(name, interfaces);
 
    // 动态生成代理类的字节码,具体生成过程不再详细介绍,感兴趣的读者可以继续分析
 
    final byte[] classFile = gen.generateClassFile();
 
    // 如果saveGeneratedFiles值为true,会将生成的代理类的字节码保存到文件中
 
    if (saveGeneratedFiles) { 
 
        java.security.AccessController.doPrivileged(
 
            new java.security.PrivilegedAction() {
 
                public Void run() {
 
                    // 省略try/catch代码块
 
                    FileOutputStream file = new FileOutputStream(
 
                        dotToSlash(name) + ".class");
 
                    file.write(classFile);
 
                    file.close();
 
                    return null;
 
                }
 
            }
 
        );
 
    }
 
    return classFile; // 返回上面生成的代理类的字节码
 
}

最后,为了清晰地看到JDK动态生成的代理类的真正定义,我们需要将上述生成的代理类的字节码进行反编译。上述示例为RealSubject生成的代理类,反编译后得到的代码如下:

public final class $Proxy37 
 
      extends Proxy implements Subject {  // 实现了Subject接口
 
    // 这里省略了从Object类继承下来的相关方法和属性
 
    private static Method m3;
 
    static {
 
        // 省略了try/catch代码块
 
        // 记录了operation()方法对应的Method对象
 
        m3 = Class.forName("com.xxx.Subject")
 
          .getMethod("operation", new Class[0]);
 
    }
 
    // 构造方法的参数就是我们在示例中使用的DemoInvokerHandler对象
 
    public $Proxy11(InvocationHandler var1) throws {
 
        super(var1); 
 
    }
 
    public final void operation() throws {
 
        // 省略了try/catch代码块
 
        // 调用DemoInvokerHandler对象的invoke()方法
 
        // 最终调用RealSubject对象的对应方法
 
        super.h.invoke(this, m3, (Object[]) null);
 
    }
 
}

总结一下,JDK 动态代理的实现原理是动态创建代理类并通过指定类加载器进行加载,在创建代理对象时将InvocationHandler对象作为构造参数传入。当调用代理对象时,会调用 InvocationHandler.invoke() 方法,从而执行代理逻辑,并最终调用真正业务对象的相应方法。

四、实现方法

其实在 Java 领域,除了JDK 默认的nvocationHandler能完成代理功能,我们还有很多其他的第三方框架也可以,比如像 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里面,虽然看起来只是一个很小的技术点,但就是这个创新使得用户可以不用关注细节了。其实,我们在日常设计接口的时候也是一样的,我们会想尽一切办法把细节对调用方屏蔽,让调用方的接入尽可能的简单。这就好比,让你去设计一个商品发布的接口,你并不需要暴露给用户一些细节,比如,告诉他们商品数据是怎么存储的。

  • 16
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小颜-

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值