作者简介:大家好,我是smart哥,前中兴通讯、美团架构师,现某互联网公司CTO
联系qq:184480602,加我进群,大家一起学习,一起进步,一起对抗互联网寒冬
动态代理的使命
在做日志需求时,我们想到的第一种方案是直接修改原代码,它的缺点是:
- 不符合开闭原则,即好的程序设计应该对扩展开放,对修改关闭
- 如果Calculator类内部有几十个、上百个方法,为每个方法加上日志打印显然工作量太大
- 存在重复代码(有多少个方法,就有多少处日志操作)
- 日志打印的操作硬编码在源程序中,不利于后期维护:比如你花了一上午终于写完了,组长却告诉你这个功能不做了,于是你又要删除/回滚相关代码!
后来我们引入了静态代理:
静态代理解决了以下问题:
- 不修改源程序,符合开闭原则(所有修改转移到代理类)
- 既然不修改源程序,那么也就不存在增强操作与源程序硬编码的问题(但又变成代理类和增强操作耦合了)
但静态代理本质上并没有太大用处,它只是把原本在源程序上做的修改,转移到代理类而已!即便引入静态代理,仍旧需要重写全部方法、仍然存在重复的日志代码。
你如果停下来稍作思考,就会发现从修改原代码到引入静态代理,其实就是趋向解耦的过程:
- 原本我们把代码直接写在目标类中,日志代码和目标类耦合了,所以一旦需求发生变更,我们又要去修改目标类
- 静态代理则把日志代码抽取出来,放在代理类中,解决了增强代码和目标类的耦合,但又造成了增强代码和代理类的耦合
换言之,静态代理的解耦能力还是太薄弱了,要想对源程序实现不同的增强功能,必须编写不同的代理类,有多少种增强需求,就要写多少个静态代理类!
我们的诉求是:增强代码我可以写(这个省不了,不然鬼知道你要打印日志还是啥),但代理类能不能不写?
要想完成上面的诉求,至少需要解决两个问题:
- 自动生成代理对象,让程序员免受编写代理类的痛苦
- 将增强代码与代理类(代理对象)解耦,从而达到代码复用(可插拔式的增强,给我增强的代码,我就返回一个实现了该增强的代理对象)
当然,上面只是一个构思,你可能还看不明白。没关系,我们一个个解决。
思考的过程:如何自动生成代理对象
复习对象的创建
在很多初学者的印象中,类和对象的关系是这样的:
虽然知道源代码经过javac命令编译后会在磁盘中得到字节码文件(.class文件),也知道java命令会启动JVM将字节码文件加载进内存,但也仅仅止步于此了。至于从字节码文件加载进内存到堆中产生对象,期间具体发生了什么,他们并不清楚。
实际上,所谓“万物皆对象”,字节码文件也难逃“被对象”的命运。当字节码文件(.class文件)被加载进内存后,JVM也为其创建了一个对象,以后所有该类的实例,皆以它为模板。这个对象叫Class对象,它是Class类的实例。
可以看出,要创建一个实例,最关键的就是得到对应的Class对象。只不过对于初学者来说,new这个关键字配合构造方法,实在太好用了,底层隐藏了太多细节,一句 Person p = new Person();直接把对象返回给你了。我自己刚开始学Java时,也没意识到Class对象的存在。
回到我们之前的问题:如何不写代理类,直接得到代理对象。
按照上面的截图,代理类和实例对象之间其实还隔着一个Class对象。如果能得到Class对象,就能生成实例。所以,现在的问题又变成:如何不写代理类,直接得到Class对象。
Class类与Class对象
要得到Class对象,就要先明白Class对象是什么,又是怎么来的。
这有一个很重要的概念:Class类。
类是用来描述一类事物的,我们有Person类描述“人”,Student类描述“学生”,而Class类就是用来描述“类”的。是不是觉得有点绕?换句话说:
类可以用来描述任意事物,所以理论上我们也能定义一个类,用来描述类本身,但这个Class类不需要我们自己写,JDK已经帮我们定义好了,放在java.lang包下。Class类、Person类、Student类本质相同,只不过Class类描述的东西比较特殊罢了。
Person类内部有name、age等字段来描述“人”的姓名、年龄等特征,那么这个Class类,它里面应该有哪些字段呢?
关于Class类内部具体有哪些字段和方法,这里不再展开,有兴趣的同学可以移步《反射》一节。
理论上只要编写了类,那么通过JVM通常可以得到该类的对象,Class类的对象就是Class对象。现在我问大家一个问题:
Person类的实例对象是Person p,那么Person类的Class对象怎么表示?
是不是又有点绕呢?
还是看看刚才的那张图吧:
Person类有两个不同维度的对象:
- 根据Person类实例化得到的Person p1、 p2、p3对象
- Class类实例化得到的Class<Person> personClass对象
第一个“对象”好理解,就是我们经常new的那种对象,关键是Class对象。Class类只有一个,却要描述形形色色的各种类,比如Person类、Student类,那么如何区分谁是谁的Class对象呢?
答案是:泛型。
Class类是泛型类,JDK利用泛型区分不同的Class对象,比如Class<Person> personClass、Class<Student> studentClass。
要得到Person p只需要new Person(),那如何得到Class<Person> personClass呢?这样吗:
Class clazz = new Class();
好像不对。我们可以去看看Class类:
上面这张截图,至少传递两个信息:
- Class类是泛型类
- Class类私有构造,意味着我们无法通过new关键字自行构造Class对象
通过上面的注释,我们得知Class对象只能由JVM创建。虽然不能new,但Java还是提供了其他方式让我们得到Class对象,底层会告诉JVM帮我们创建:
- Class.forName(xxx):Class<Person> clazz = Class.forName("com.bravo.Person");
- xxx.class:Class<Person> clazz = Person.class;
- xxx.getClass():Class<Person> clazz = person.getClass();
OK,学到这,我们已经了解了Class对象到底是什么,以及得到Class对象的三种常见方式。但是,这三种方式都需要先有类,但我们不想编写代理类!
到这里,思路似乎断了!
从接口寻求突破口!
仔细想一下,代理类或者代理对象重要吗?它几乎是个空壳,最重要的其实是 增强代码 + 目标对象。换句话说,我们对代理对象的要求很低,只需要与目标对象拥有相同的方法即可。如此一来,别人调用proxy.add()得到的效果和调用target.add()是一样的,甚至因为两者都实现了相同接口,用接口类型接收后,calculator.add()根本分不出是代理还是原对象。
所以本质上,代理对象只要有方法申明即可,甚至不需要方法体,或者只要一个空的方法体即可,反正我们会把目标对象返回去。
那么,如何知道一个类有哪些方法信息呢?如果能得到类的方法信息,我们或许可以直接造一个代理对象。
有两个途径:
- 目标类本身
- 目标类实现的接口
这两个思路造就了两种不同的代理机制,一个被后人称为CGLib动态代理,另一个则被JDK收录,世人称之为JDK动态代理。本文重点介绍JDK动态代理。
我们先来验证一下,接口是否真的包含我们需要的方法信息:
public class ProxyTest {
public static void main(String[] args) {
/**
* Calculator接口的Class对象
* 得到Class对象的三种方式:
* 1.Class.forName(xxx)
* 2.xxx.class
* 3.xxx.getClass()
* 注意,这并不是我们new了一个Class对象,而是让虚拟机加载并创建Class对象
*/
Class<Calculator> calculatorClazz = Calculator.class;
//Calculator接口的构造器信息
Constructor<?>[] calculatorClazzConstructors = calculatorClazz.getConstructors();
//Calculator接口的方法信息
Method[] calculatorClazzMethods = calculatorClazz.getMethods();
//打印
System.out.println("------接口Class的构造器信息------");
printClassInfo(calculatorClazzConstructors);
System.out.println("\n");
System.out.println("------接口Class的方法信息------");
printClassInfo(calculatorClazzMethods);
System.out.println("\n");
/**
* Calculator实现类的Class对象
*/
Class<CalculatorImpl> calculatorImplClazz = CalculatorImpl.class;
//Calculator实现类的构造器信息
Constructor<?>[] calculatorImplClazzConstructors = calculatorImplClazz.getConstructors();
//Calculator实现类的方法信息
Method[] calculatorImplClazzMethods = calculatorImplClazz.getMethods();
//打印
System.out.println("------实现类Class的构造器信息------");
printClassInfo(calculatorImplClazzConstructors);
System.out.println("\n");
System.out.println("------实现类Class的方法信息------");
printClassInfo(calculatorImplClazzMethods);
}
public static void printClassInfo(Executable[] targets) {
for (Executable target : targets) {
// 构造器/方法名称
String name = target.getName();
StringBuilder sBuilder = new StringBuilder(name);
// 拼接左括号
sBuilder.append('(');
Class<?>[] clazzParams = target.getParameterTypes();
// 拼接参数
for (Class<?> clazzParam : clazzParams) {
sBuilder.append(clazzParam.getName()).append(',');
}
//删除最后一个参数的逗号
if (clazzParams.length != 0) {
sBuilder.deleteCharAt(sBuilder.length() - 1);
}
//拼接右括号
sBuilder.append(')');
//打印 构造器/方法
System.out.println(sBuilder.toString());
}
}
}
得到以下结论:
- 接口Class对象没有构造方法,所以Calculator接口不能直接new对象
- 实现类Class对象有构造方法,所以CalculatorImpl实现类可以new对象
- 接口Class对象有两个方法add()、subtract()
- 实现类Class对象除了add()、subtract(),还有从Object继承的方法
也就是说,接口Class的对象和实现类的Class对象除了构造器,其他信息基本相似(目标类由于继承了Object,所以内部包含了Object的方法,与代理无关)。
至此,我们至少知道从接口获取方法信息是可能的!接下来的努力方向就是:怎么根据一个接口得到代理对象。
引入JDK动态代理
通过刚才的实验,我们不仅知道了接口确实包含我们所需要的方法信息,还知道了接口为什么不能直接new对象:接口缺少构造器信息。那么,是否存在一种机制,能给接口安装上构造器呢?或者,不改变接口本身,直接拷贝接口的信息到另一个Class,然后给那个Class装上构造器呢?
很显然,不论是从开闭原则还是常规设计考虑,直接修改接口Class的做法相对来说不是很合理。JDK选择了后者:拷贝接口Class的信息,产生一个新的Class对象。
也就是说,JDK动态代理的本质是:用Class造Class,即用接口Class造出一个代理类Class。
具体API就不带大家找了,直接看:
Proxy.getProxyClass():返回代理类的Class对象。
也就说,只要传入接口的Class对象,getProxyClass()方法即可返回代理Class对象,而不用实际编写代理类。这相当于什么概念?
没错,好家伙,直接跳过了代理类的编写!
public class ProxyTest {
public static void main(String[] args) {
/*
* 参数1:Calculator的类加载器(当初把Calculator加载进内存的类加载器)
* 参数2:代理对象需要和目标对象实现相同接口Calculator
* */
Class<?> calculatorProxyClazz = Proxy.getProxyClass(Calculator.class.getClassLoader(), Calculator.class);
//以Calculator实现类的Class对象作对比,看看代理Class是什么类型
System.out.println(CalculatorImpl.class.getName());
System.out.println(calculatorProxyClazz.getName());
//打印代理Class对象的构造器
Constructor<?>[] constructors = calculatorProxyClazz.getConstructors();
System.out.println("----构造器----");
printClassInfo(constructors);
System.out.println("\n");
//打印代理Class对象的方法
Method[] methods = calculatorProxyClazz.getMethods();
System.out.println("----方法----");
printClassInfo(methods);
System.out.println("\n");
}
public static void printClassInfo(Executable[] targets) {
for (Executable target : targets) {
// 构造器/方法名称
String name = target.getName();
StringBuilder sBuilder = new StringBuilder(name);
// 拼接左括号
sBuilder.append('(');
Class<?>[] clazzParams = target.getParameterTypes();
// 拼接参数
for (Class<?> clazzParam : clazzParams) {
sBuilder.append(clazzParam.getName()).append(',');
}
//删除最后一个参数的逗号
if (clazzParams.length != 0) {
sBuilder.deleteCharAt(sBuilder.length() - 1);
}
//拼接右括号
sBuilder.append(')');
//打印 构造器/方法
System.out.println(sBuilder.toString());
}
}
}
大家还记得原先接口Class的打印信息吗?
没错,Proxy.getProxyClass()返回的Class对象是有构造器的!
开头说了,动态代理的使命有两个:
- 自动生成代理对象,让程序员免受编写代理类的痛苦
- 将增强代码与代理类(代理对象)解耦,从而达到代码复用
现在我们已经得到了代理Class,只需通过反射即可得到代理对象。具体的操作放在下一篇。
作者简介:大家好,我是smart哥,前中兴通讯、美团架构师,现某互联网公司CTO
进群,大家一起学习,一起进步,一起对抗互联网寒冬