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


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

[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 发布!

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 160,026评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,655评论 1 296
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,726评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,204评论 0 213
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,558评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,731评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,944评论 2 314
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,698评论 0 203
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,438评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,633评论 2 247
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,125评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,444评论 3 255
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,137评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,103评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,888评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,772评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,669评论 2 271

推荐阅读更多精彩内容