我准备战斗到最后,不是因为我勇敢,是我想见证一切。 --双雪涛《猎人》
[TOC]
Thinking
- 一个技术,为什么要用它,解决了那些问题?
- 如果不用会怎么样,有没有其它的解决方法?
- 对比其它的解决方案,为什么最终选择了这种,都有何利弊?
- 你觉得项目中还有那些地方可以用到,如果用了会带来那些问题?
- 这些问题你又如何去解决的呢?
思考
Java是一个强类型语言,而Java提供的编译期和运行期加载的机制,让Java更加灵活的塑造自己。其中动态加载可以说是Java生态中非常重要的一环。
提到动态代理,应该大多数人都会第一时间想到spring
提供的AOP。它就是通过动态代理在JVM运行期动态编织再通过依赖注入,将动态代理出的真实对象注入到对应的类中。可想而知,动态代理在spring
中的重要地位。
详情可以参读spring aop 的实现原理
首先提出几个疑问:
- 动态代理到底代理了那个类?
- JDK动态代理为什么只针对接口呢?
- 动态代理到底是什么时候将类创建出来的?又是如何创建的?创建的具体是哪个类对象呢?
带着疑问,往下走🙂
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);
- Subject subject = (Subject) Proxy.newProxyInstance(aClass.getClassLoader(),
2、深入源码
这里建议使用debug 一步一步跟进!🙂 let·s go
- 进入到
java.lang.reflect.Proxy#newProxyInstance
明确一下传入的参数的值,以免后面忘记了。
-
然后会经过一系列的检测和接口clone和安全检测
-
下面就是重点了,进入到了真正的查找或者创建代理对象的方法
-
进入该方法后,会尝试在缓存中获取代理类对象(如果存在)
-
在源码中我们发现了调用了全局变量
proxyClassCache
的get方法,先看一下proxyClassCache
都做了些什么?-
创建了一个
java.lang.reflect.WeakCache
类,并且传入了两个实例对象,再逐步跟进去查看一下这三个实例的初始化过程。- 大致情况就是创建一个密钥!
- 上述的两个工厂对象都是实现了函数式接口
java.util.function.BiFunction
,所以后续都会有相应的java.util.function.BiFunction#apply
方法调用。
-
- 这里重点说一下
java.lang.reflect.WeakCache
中的map变量,其中map变量是实现缓存的核心变量,他是一个双重的Map结构: (key, sub-key) -> value。其中key是传进来的Classloader进行包装后的对象,sub-key是由WeakCache构造函数传人的KeyFactory()生成的。value就是产生代理类的对象,是由WeakCache构造函数传人的ProxyClassFactory()生成的。
- 这里重点说一下
-
再进入到生成的
java.lang.reflect.WeakCache
的get方法。- 该方法会尝试在缓存中获取,或者自己根据类加载器和接口数组创建代理对象(缓存获取不到的情况下)
- 调用KeyFacroty 对象中的apply方法,生成密钥
- 在上面的一系列操作后,发现在缓存中获取不到数据,会进入一个while循环体
-
- 此时的逻辑执行完毕,再次进入循环体中,此时的supplier 则不为空。
-
- 这里的
valueFactory
在类初始化时,就已经赋值成功了,为:java.lang.reflect.Proxy.ProxyClassFactory
,接下来进入真正的创建代理类的逻辑了。
- 这里的
-
在进行完一系列的检测和数据拼接后,终于到了最终构建代理类实例的时候了
-
- 这里方法是具体的构建代理类对象中的所有字节码结构,包括(常量池、字段、方法、等),进去瞄一眼!!!
-
首先看到该类下有一个静态的代码块,对Object类下的hashCode、toString、equals三个放进行封装
- 将这三个方法构建到代理类中,并且会保持在代理类的真实方法之前。
- 将代理类中的所有方法添加到构建中
- 后面的一系列操作都是对代理类的二进制文件进行构建。有兴趣可以看一下源码
-
代理对象的二进制文件构建完之后会有一个判断
- 这个判断既是系统属性,在开启之后,会将生成的代理对象的二进制文件输出到磁盘中
- 具体操作如下:
- debug运行后会得到一个
$Proxy0.class
文件在项目的根路径
-
在生成二进制文件之后,会调用
调用本地方法生成具体的代理对象,根据制定的类加载器,类名,二进制文件
然后将生成的代理对象放入到cache中等一系列的操作在这里就不赘述了。
- 再回到
java.lang.reflect.Proxy#newProxyInstance
中- 会通过class 文件获取构造函数
-
参数类型为:
-
然后将传入的handle 当作参数进行对象的初始化
- 这里使用对象数组,是因为获取的构造函数是使用InvocationHandler数据获取到的。
- 至此:代理对象就创建完毕了。很简单对不对😊
3、通过上述的源码学习到了
- 在源码中使用到了JDK8的函数式编程,所以代码逻辑可能比较绕。但是读下来还是很轻松的。
- 在读到源码的数据构建中,明白了具体的构建参数
在代理类的构建中,会在真的是代理类的方法之前加入Object中的三个方法。保证了在代理类中使用到hashCode、toString、equals
时,会直接使用代理类中的重写方法,但是如果用到其它的方法例如clone
等等的方法呢?
查看一下JAVADOC
明确的指到:在使用没有重写的方法会直接调用Object类的方法,跟平时无异
4、再看字节码文件
在添加系统属性之后,会生成代理类的class 文件。来看一下具体的class文件
首先看到有四个变量。都为Method类型。也很好理解。method可以通过反射直接调用指定的方法。
再看有一个静态的代码块。用于对成员变量的初始化。可以很清楚的看到生成的具体方法,也很好的对应了源码中的逻辑。
再看构造函数,可以发现啊,在源码中使用构造函数调用时会指定启动程序指定的InvocationHandler
参数,在这里会初始化到父类的成员变量中
所以这里就更加清楚的知道在class文件中每一个方法都是使用super.h.invoke
的方式进行调用的。
这里看一下程序的启动类,具体是怎么定义的。
所以不难看出这里初始化的h
是指向启动程序中的DynamicSubject
再看request方法
我们再看生成的代理类中的request
方法。
图中的疑问,一样也非常好解答
就是在启动类中,在初始化DynamicSubject
时,定义了sub
的属性。
所以在调用invoke方法时,会根据具体的实例对象进行调用。
所以现在应该非常明确的知道在动态代理时,所有的执行流程了
5、疑问清除术
- 动态代理到底代理了那个类?
- JDK动态代理为什么只针对接口呢?
- 动态代理到底是什么时候将类创建出来的?又是如何创建的?创建的具体是哪个类对象呢?
- 动态代理其实是根据给定的类加载器和类的接口数组,在Java运行期由JDK自行创建的一个类,用于执行一些相关逻辑,这种设计极大程度的丰富了Java的设计理念,和动态加载的丰富性。例如:spring AOP。在程序运行时对程序进行增强。
- JDK 源码中只会根据接口数组进行对数据的构建。并不支持继承等其它形式
- 动态代理是在程序运行时将类创建出来的,可以最大程序的将程序变得更加灵活。会根据具体指定的接口对象进行具体操作和实现。
6、TODO
JDK的动态代理是有局限的,所以在spring中已经集成了cjlib
基于继承的方式进行动态代理。有空再说咯 🙂
本文应该存在大量的错误,因为都是笔者自行理解做出的总结,仅供个人学习备忘。
本文仅供笔者本人学习,有错误的地方还望指出,一起进步!望海涵!
转载请注明出处!
欢迎关注我的公共号,无广告,不打扰。不定时更新Java后端知识,我们一起超神。
——努力努力再努力xLg
加油!
本文由博客一文多发平台 OpenWrite 发布!