美文网首页
JVM-从字节码角度深入探讨JDK动态代理

JVM-从字节码角度深入探讨JDK动态代理

作者: 小安的大情调 | 来源:发表于2020-04-10 23:29 被阅读0次

    我准备战斗到最后,不是因为我勇敢,是我想见证一切。 --双雪涛《猎人》

    [TOC]
    Thinking

    1. 一个技术,为什么要用它,解决了那些问题?
    2. 如果不用会怎么样,有没有其它的解决方法?
    3. 对比其它的解决方案,为什么最终选择了这种,都有何利弊?
    4. 你觉得项目中还有那些地方可以用到,如果用了会带来那些问题?
    5. 这些问题你又如何去解决的呢?

    思考

    ​ Java是一个强类型语言,而Java提供的编译期和运行期加载的机制,让Java更加灵活的塑造自己。其中动态加载可以说是Java生态中非常重要的一环。

    ​ 提到动态代理,应该大多数人都会第一时间想到spring提供的AOP。它就是通过动态代理在JVM运行期动态编织再通过依赖注入,将动态代理出的真实对象注入到对应的类中。可想而知,动态代理在spring中的重要地位。

    详情可以参读spring aop 的实现原理

    首先提出几个疑问:

    1. 动态代理到底代理了那个类?
    2. JDK动态代理为什么只针对接口呢?
    3. 动态代理到底是什么时候将类创建出来的?又是如何创建的?创建的具体是哪个类对象呢?

    带着疑问,往下走🙂

    1、编写一个JDK动态代理

    public interface Subject {
        void request();
    }
    
    public class RealSubject implements Subject {
        @Override
        public void request() {
            System.out.println("RealSubject is running");
        }
    }
    
    public class DynamicSubject implements InvocationHandler {
        // 将真实的对象 作为成员变量 通过构造方法传入进来
        private Object sub;
    
        public DynamicSubject(Object sub) {
            this.sub = sub;
        }
    
        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            System.out.println("before calling: " + method);
            method.invoke(this.sub, args);
            System.out.println("after calling: " + method);
    
            return null;
        }
    }
    
    // Test
    public class Main {
        public static void main(String[] args) {
            RealSubject realSubject = new RealSubject();
            InvocationHandler handler = new DynamicSubject(realSubject);
            Class<? extends RealSubject> aClass = realSubject.getClass();
    
            // 真正的 类型则是动态绑定的
            Subject subject = (Subject) Proxy.newProxyInstance(aClass.getClassLoader(),
                    aClass.getInterfaces(), handler);
            subject.request();
            System.out.println(subject.getClass());
            System.out.println(subject.getClass().getSuperclass());
            // class com.sun.proxy.$Proxy0
            // class java.lang.reflect.Proxy
        }
    }
    
    • 从上面的 代码可以看到真正的代理类的创建代码为:
      • Subject subject = (Subject) Proxy.newProxyInstance(aClass.getClassLoader(),
        aClass.getInterfaces(), handler);

    2、深入源码

    这里建议使用debug 一步一步跟进!🙂 let·s go

    1. 进入到java.lang.reflect.Proxy#newProxyInstance
    image-20200410205747052

    明确一下传入的参数的值,以免后面忘记了。

    1. 然后会经过一系列的检测和接口clone和安全检测

      1. image-20200410205947048
    2. 下面就是重点了,进入到了真正的查找或者创建代理对象的方法

      1. image-20200410210052797
    3. 进入该方法后,会尝试在缓存中获取代理类对象(如果存在)

      1. image-20200410211012222
      2. 在源码中我们发现了调用了全局变量proxyClassCache的get方法,先看一下proxyClassCache都做了些什么?

        1. image-20200410211316918
        2. 创建了一个java.lang.reflect.WeakCache类,并且传入了两个实例对象,再逐步跟进去查看一下这三个实例的初始化过程。

          1. image-20200410211536301
          • 大致情况就是创建一个密钥!
          1. image-20200410211738397
          • 上述的两个工厂对象都是实现了函数式接口java.util.function.BiFunction,所以后续都会有相应的java.util.function.BiFunction#apply方法调用。
          1. image-20200410212539975
            • 这里重点说一下java.lang.reflect.WeakCache中的map变量,其中map变量是实现缓存的核心变量,他是一个双重的Map结构: (key, sub-key) -> value。其中key是传进来的Classloader进行包装后的对象,sub-key是由WeakCache构造函数传人的KeyFactory()生成的。value就是产生代理类的对象,是由WeakCache构造函数传人的ProxyClassFactory()生成的。
    4. 再进入到生成的java.lang.reflect.WeakCache的get方法。

      • 该方法会尝试在缓存中获取,或者自己根据类加载器和接口数组创建代理对象(缓存获取不到的情况下)
      • image-20200410215338299
      • image-20200410215524436
      • 调用KeyFacroty 对象中的apply方法,生成密钥
        • image-20200410215627655
        • 在上面的一系列操作后,发现在缓存中获取不到数据,会进入一个while循环体
    5. image-20200410215823350
      • image-20200410220225798
      • 此时的逻辑执行完毕,再次进入循环体中,此时的supplier 则不为空。
    6. image-20200410220403891
    7. image-20200410220803598
      • 这里的valueFactory在类初始化时,就已经赋值成功了,为:java.lang.reflect.Proxy.ProxyClassFactory,接下来进入真正的创建代理类的逻辑了。
    8. 在进行完一系列的检测和数据拼接后,终于到了最终构建代理类实例的时候了

      1. image-20200410221147821
      2. image-20200410221216203
      3. image-20200410221310586
        1. 这里方法是具体的构建代理类对象中的所有字节码结构,包括(常量池、字段、方法、等),进去瞄一眼!!!
        2. 首先看到该类下有一个静态的代码块,对Object类下的hashCode、toString、equals三个放进行封装 image-20200410221609074
        3. 将这三个方法构建到代理类中,并且会保持在代理类的真实方法之前。
        4. image-20200410221745030
        5. 将代理类中的所有方法添加到构建中
        6. image-20200410221839172
        7. 后面的一系列操作都是对代理类的二进制文件进行构建。有兴趣可以看一下源码
        8. image-20200410221934935
    9. 代理对象的二进制文件构建完之后会有一个判断

      • image-20200410222058627
      • 这个判断既是系统属性,在开启之后,会将生成的代理对象的二进制文件输出到磁盘中
      • image-20200410222203254
      • 具体操作如下:
        • image-20200410222227455
        • debug运行后会得到一个$Proxy0.class文件在项目的根路径
        • image-20200410222329274
    10. 在生成二进制文件之后,会调用 image-20200410222621477

      调用本地方法生成具体的代理对象,根据制定的类加载器,类名,二进制文件

    11. 然后将生成的代理对象放入到cache中等一系列的操作在这里就不赘述了。


    1. 再回到java.lang.reflect.Proxy#newProxyInstance image-20200410222930327
      1. 会通过class 文件获取构造函数
      2. image-20200410223340825
      3. 参数类型为: image-20200410223405623
      4. 然后将传入的handle 当作参数进行对象的初始化 image-20200410223501823
        1. 这里使用对象数组,是因为获取的构造函数是使用InvocationHandler数据获取到的。
    2. 至此:代理对象就创建完毕了。很简单对不对😊

    3、通过上述的源码学习到了

    - 在源码中使用到了JDK8的函数式编程,所以代码逻辑可能比较绕。但是读下来还是很轻松的。

    - 在读到源码的数据构建中,明白了具体的构建参数

    ​ 在代理类的构建中,会在真的是代理类的方法之前加入Object中的三个方法。保证了在代理类中使用到hashCode、toString、equals时,会直接使用代理类中的重写方法,但是如果用到其它的方法例如clone等等的方法呢?

    ​ 查看一下JAVADOC

    image-20200410224325790

    明确的指到:在使用没有重写的方法会直接调用Object类的方法,跟平时无异

    4、再看字节码文件

    ​ 在添加系统属性之后,会生成代理类的class 文件。来看一下具体的class文件

    image-20200410224608268

    首先看到有四个变量。都为Method类型。也很好理解。method可以通过反射直接调用指定的方法。

    image-20200410224718908

    再看有一个静态的代码块。用于对成员变量的初始化。可以很清楚的看到生成的具体方法,也很好的对应了源码中的逻辑。

    image-20200410224917342

    再看构造函数,可以发现啊,在源码中使用构造函数调用时会指定启动程序指定的InvocationHandler参数,在这里会初始化到父类的成员变量中

    image-20200410225051984 image-20200410225108876

    所以这里就更加清楚的知道在class文件中每一个方法都是使用super.h.invoke的方式进行调用的。


    这里看一下程序的启动类,具体是怎么定义的。

    image-20200410225330594

    所以不难看出这里初始化的h是指向启动程序中的DynamicSubject

    image-20200410225459682

    再看request方法

    ​ 我们再看生成的代理类中的request方法。

    image-20200410230158395

    图中的疑问,一样也非常好解答

    就是在启动类中,在初始化DynamicSubject时,定义了sub的属性。

    image-20200410230347371

    所以在调用invoke方法时,会根据具体的实例对象进行调用。

    所以现在应该非常明确的知道在动态代理时,所有的执行流程了

    image-20200410230705690

    5、疑问清除术

    1. 动态代理到底代理了那个类?
    2. JDK动态代理为什么只针对接口呢?
    3. 动态代理到底是什么时候将类创建出来的?又是如何创建的?创建的具体是哪个类对象呢?
    1. 动态代理其实是根据给定的类加载器和类的接口数组,在Java运行期由JDK自行创建的一个类,用于执行一些相关逻辑,这种设计极大程度的丰富了Java的设计理念,和动态加载的丰富性。例如:spring AOP。在程序运行时对程序进行增强。
    2. JDK 源码中只会根据接口数组进行对数据的构建。并不支持继承等其它形式
    3. 动态代理是在程序运行时将类创建出来的,可以最大程序的将程序变得更加灵活。会根据具体指定的接口对象进行具体操作和实现。

    6、TODO

    ​ JDK的动态代理是有局限的,所以在spring中已经集成了cjlib基于继承的方式进行动态代理。有空再说咯 🙂

    本文应该存在大量的错误,因为都是笔者自行理解做出的总结,仅供个人学习备忘。

    本文仅供笔者本人学习,有错误的地方还望指出,一起进步!望海涵!

    转载请注明出处!

    欢迎关注我的公共号,无广告,不打扰。不定时更新Java后端知识,我们一起超神。


    qrcode.jpg

    ——努力努力再努力xLg

    加油!
    本文由博客一文多发平台 OpenWrite 发布!

    相关文章

      网友评论

          本文标题:JVM-从字节码角度深入探讨JDK动态代理

          本文链接:https://www.haomeiwen.com/subject/pqzjmhtx.html