一个需求:给原有方法添加日志打印
假设你刚进入一个项目组,项目中存在一个Calculator类,代表一个计算器,它可以进行加减乘除操作:
public class Calculator {
// 加
public int add(int a, int b) {
int result = a + b;
return result;
}
// 减
public int subtract(int a, int b) {
int result = a - b;
return result;
}
// 乘法、除法...
}
现在老大给你提了一个需求:在每个方法执行前后打印日志。
方案一:直接修改
public class Calculator {
// 加
public int add(int a, int b) {
System.out.println("add方法开始...");
int result = a + b;
System.out.println("add方法结束...");
return result;
}
// 减
public int subtract(int a, int b) {
System.out.println("subtract方法开始...");
int result = a - b;
System.out.println("subtract方法结束...");
return result;
}
// 乘法、除法...
}
上面的方案是有问题的:
- 直接修改源程序,不符合开闭原则,即好的程序设计应该对扩展开放,对修改关闭
- 如果类中有很多类,修改量大
- 存在重复代码(都是在核心代码前后打印日志)
- 日志打印硬编码在代理类中,不利于后期维护:比如你花了一上午终于写完了,组长告诉你这个功能不做了,于是你又要打开Calculator花十分钟删除日志打印的代码(或回滚分支)!
pass!!!
方案二:静态代理实现日志打印
先理解一下什么是代理:
代理是一种模式,提供了对目标对象的简介访问方式,即通过代理访问目标对象,如此便于在目标实现的基础上增加额外的功能操作,前拦截,后拦截等,以满足自身的业务需求。
常用的代理方式可以粗分为:静态代理和动态代理。
静态代理的实现比较简单:编写一个代理类,实现于目标对象相同的接口,并在内部维护一个目标对象的引用,通过构造器塞入目标对象,在代理对象中调用目标对象的同名方法,并添加前拦截,后拦截等所需的业务功能。
抽取接口
public interface Calculator {
int add(int a, int b);
int subtract(int a, int b);
}
原目标类实现接口
/**
* 目标类,实现Calculator接口(如果一开始就面向接口编程,其实是不存在这一步的,CalculatorImpl原本就实现Calculator接口)
*/
public class CalculatorImpl implements Calculator {
// 加
public int add(int a, int b) {
int result = a + b;
return result;
}
// 减
public int subtract(int a, int b) {
int result = a - b;
return result;
}
// 乘法、除法...
}
新增代理类并实现接口
/**
* 静态代理类,实现Calculator接口
*/
public class CalculatorProxy implements Calculator {
// 代理对象内部维护一个目标对象引用
private Calculator target;
// 通过构造方法,传入目标对象
public CalculatorProxy(Calculator target) {
this.target = target;
}
// 调用目标对象的add,并在前后打印日志
@Override
public int add(int a, int b) {
System.out.println("add方法开始...");
int result = target.add(a, b);
System.out.println("add方法结束...");
return result;
}
// 调用目标对象的subtract,并在前后打印日志
@Override
public int subtract(int a, int b) {
System.out.println("subtract方法开始...");
int result = target.subtract(a, b);
System.out.println("subtract方法结束...");
return result;
}
// 乘法、除法...
}
测试案例
public class Test {
public static void main(String[] args) {
// 把目标对象通过构造器塞入代理对象
Calculator calculator = new CalculatorProxy(new CalculatorImpl());
// 代理对象调用目标对象方法完成计算,并在前后打印日志
calculator.add(1, 2);
calculator.subtract(2, 1);
}
}
静态代理的问题
上面的代码中,为了给目标类做日志增强,我们编写了代理类,而且准备了一个构造器接收目标对象。代理代理对象构造器的参数类型是Calculator,这意味着它只能接受Calculator的实现类对象,亦即我们写的代理类CalculatorProxy只能给Calculator做代理,它们绑定死了!
如果现在我们系统需要全面改造,要给其他类也添加日志打印功能,就得为其他几百个接口都各自写一份代理类…
自己手动写一个类并实现接口实在太麻烦了。仔细一想,我们其实想要的并不是代理类,而是代理对象!
换言之,静态代理的解耦能力还是太薄弱了,要想对源程序实现不同的增强功能,必须编写不同的代理类,有多少种增强需求,就要写多少个静态代理类!
我们的诉求是:增强代码我可以写(这个省不了,不然鬼知道你要打印日志还是啥),但代理类能不能不写?
要想完成上面的诉求,至少需要解决两个问题:
● 自动生成代理对象,让程序员免受编写代理类的痛苦
● 将增强代码与代理类(代理对象)解耦,从而达到代码复用(可插拔式的增强,给我增强的代码,我就返回一个实现了该增强的代理对象)
思考过程:如何自动生成代理对象
复习对象的创建
所谓“万物皆对象”,字节码文件也难逃“被对象”的命运。当字节码文件(.class文件)被加载进内存后,JVM也为其创建了一个对象,以后所有该类的实例,皆以它为模板。这个对象叫Class对象,它是Class类的实例。
可以看出,要创建一个实例,最关键的就是得到对应的Class对象。只不过对于初学者来说,new这个关键字配合构造方法,实在太好用了,底层隐藏了太多细节,一句 Person p = new Person();直接把对象返回给你了。
回到之前的问题上:如何不写代理类,直接得到代理对象。
按照上面的截图:代理类和实例对象之间其实还隔着一个Class对象,如果能得到Class对象,就能生成实例。所以,现在的问题又变成:如何不写代理类,直接得到Class对象。
Class类与Class对象
要得到Class对象,就要先明白Class对象是什么,又是怎么来的。
这有一个很重要的概念:Class类。
类是用来描述一类事物的,我们有Person类描述“人”,Student类描述“学生”,而Class类就是用来描述“类”的。是不是觉得有点绕?换句话说:
类可以用来描述任意事物,所以理论上我们也能定义一个类,用来描述类本身,但这个Class类不需要我们自己写,JDK已经帮我们定义好了,放在java.lang包下。Class类、Person类、Student类本质相同,只不过Class类描述的东西比较特殊罢了。
理论上,只要编写了类,那么通过JVM通常可以得到该类的对象,Class类的对象就是Class对象。
那么Person类的实例对象是Person p,那么Person类的Class对象怎么表示?
Person类有两个不同维度的对象:
● 根据Person类实例化得到的Person p1、 p2、p3对象
● Class类实例化得到的Class personClass对象
第一个“对象”好理解,就是我们经常new的那种对象,关键是Class对象。Class类只有一个,却要描述形形色色的各种类,比如Person类、Student类,那么如何区分谁是谁的Class对象呢?
答案就是泛型。
Class类是泛型类,JDK利用泛型区分不同的Class对象,比如Class personClass、Class studentClass。
Class对象只能由JVM创建。虽然不能new,但Java还是提供了其他方式让我们得到Class对象,底层会告诉JVM帮我们创建:
- Class.forName(xxx):Class clazz = Class.forName(“com.bravo.Person”);
- xxx.class:Class clazz = Person.class;
- xxx.getClass():Class 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继承的方法
至此,我们至少知道从接口获取方法信息是可能的!接下来的努力方向就是:怎么根据一个接口得到代理对象。
引入JDK动态代理
通过上面的实验,我们知道了接口确实包含了我们需要的方法信息,而且还知道接口缺少构造器信息。那么,是否存在一种机制,能给接口安装上构造器呢?或者,不改变接口本身,直接拷贝接口的信息到另一个Class,然后给那个Class装上构造器呢?
很显然,不论是从开闭原则还是常规设计考虑,直接修改接口Class的做法相对来说不是很合理。JDK选择了后者:拷贝接口Class的信息,产生一个新的Class对象。
也就是说,JDK动态代理的本质是:用Class造Class,即用接口Class造出一个代理类Class。
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());
}
}
}
Proxy.getProxyClass()返回的Class对象是有构造器的!
开头说了,动态代理的使命有两个:
● 自动生成代理对象,让程序员免受编写代理类的痛苦
● 将增强代码与代理类(代理对象)解耦,从而达到代码复用
上面我们可以看到代理Class有一个构造器,需要传入InvocationHandler,虽然我们不知道这是啥,但可以试着传一下(JDK源码自带InvocationHandler):
public class ProxyTest {
public static void main(String[] args) throws Exception {
/*
* 参数1:类加载器,随便给一个
* 参数2:需要生成代理Class的接口,比如Calculator
* */
Class<?> calculatorProxyClazz = Proxy.getProxyClass(Calculator.class.getClassLoader(), Calculator.class);
// 得到唯一的有参构造 $Proxy(InvocationHandler h),和反射的Method有点像,可以理解为得到对应的构造器执行器
Constructor<?> constructor = calculatorProxyClazz.getConstructor(InvocationHandler.class);
// 用构造器执行器执行构造方法,得到代理对象。构造器需要InvocationHandler入参
Calculator calculatorProxyImpl = (Calculator) constructor.newInstance(new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
return 10086;
}
});
// 看,有同名方法!
System.out.println(calculatorProxyImpl.add(1, 2));
}
}
但,InvocationHandler是干嘛的呢?从实验结果看,会发现每次调用代理对象的方法,最终都会调用InvocationHandler的invoke()方法:
怎么做到的呢
上面的代码中,根据代理Classd的构造器创建对象时,需要传入InvocationHandler。通过构造器传入一个对象引用,那么必然有个成员变量去接收它,没错,代理对象的内部确实有个成员变量invocationHandler,而且代理对象的每个方法内部都会调用handler.invoke()!也就是说,动态代理为了实现代理对象和增强代码的解耦,把增强代码也抽取出去了,让InvocationHandler作为它与目标对象的桥梁。
大致流程就是这样的:
JDK动态代理最终生成的Class,最终代理对象是proxy对象,而且实现了接口。
注意,静态代理的做法是把目标对象传入代理对象,而动态代理则把增强代码传入代理对象。那么,目标对象怎么办,这样一来虽然能执行增强代码,但执行不到目标方法了!
别慌!来看看invoke()方法的参数有哪些:
● Object proxy:很遗憾,是代理对象本身,而不是目标对象(不要调用,会无限递归,一般不会使用)
● Method method:方法执行器,用来执行方法(有点不好解释,Method只是一个执行器,传入目标对象就执行目标对象的方法)
● Obeject[] args:方法参数
至此,我们初步实现动态代理。
如何复用增强代码
上面这种方式,太low了,不忍直视…改进一下:
public class ProxyTest {
public static void main(String[] args) throws Throwable {
CalculatorImpl target = new CalculatorImpl();
// 传入目标对象
Calculator calculatorProxy = (Calculator) getProxy(target);
calculatorProxy.add(1, 2);
}
/**
* 传入目标对象,获取代理对象
*
* @param target
* @return
* @throws Exception
*/
private static Object getProxy(final Object target) throws Exception {
// 参数1:随便找个类加载器给它 参数2:需要代理的接口
Class<?> proxyClazz = Proxy.getProxyClass(target.getClass().getClassLoader(), target.getClass().getInterfaces());
Constructor<?> constructor = proxyClazz.getConstructor(InvocationHandler.class);
return constructor.newInstance(new InvocationHandler() {
@Override
public Object invoke(Object proxy1, Method method, Object[] args) throws Throwable {
System.out.println(method.getName() + "方法开始执行...");
Object result = method.invoke(target, args);
System.out.println(result);
System.out.println(method.getName() + "方法执行结束...");
return result;
}
});
}
}
解耦代理对象与增强代码
上面的代码还有问题:虽然传入任意对象我们都可以返回增强后的代理对象,但增强代码是写死的。如果我需要的增强不是打印日志而是其他操作呢?难道重新写一个getProxy()方法吗?所以,我们应该抽取InvocationHander,将增强代码和代理对象解耦(其实重写getProxy()和抽取InvocationHander本质相同,但后者细粒度小一些)。
public class ProxyTest2 {
public static void main(String[] args) throws Exception {
//获取目标类对象
CalculatorImpl target = new CalculatorImpl();
//传入目标对象,得到增强对象(如果需要对目标对象进行别的增强,可以另外编写getXxInvocationHandler)
InvocationHandler handler = getXxInvocationHandler(target);
//获取代理类对象
Calculator proxy = (Calculator)getProxy(handler);
int subtract = proxy.subtract(2, 4);
System.out.println(subtract);
}
private static InvocationHandler getXxInvocationHandler(Object target){
return new InvocationHandler(){
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println(method.getName()+"执行了");
Object o = method.invoke(target,args);
System.out.println(method.getName()+"执行结束");
return o;
}
};
}
private static Object getProxy(InvocationHandler handler) throws Exception {
Class<?> proxyClass = Proxy.getProxyClass(Calculator.class.getClassLoader(), Calculator.class);
Constructor<?> constructor = proxyClass.getConstructor(InvocationHandler.class);
Object o = constructor.newInstance(handler);
return o;
}
}
更好用的API Proxy.newProxyInstance()
目前为止,我们学习都是:Proxy.getProxyClass()
- 先获取proxyClazz
- 在根据proxyClazz获取构造器,需要传入InvocationHnandler
- 最后在生成代理对象,newInstance()
JDK已经提供了一步到位的方法Proxy.newProxyInstance()
public class ProxyTest3 {
public static void main(String[] args) throws Exception {
//获取目标类对象
CalculatorImpl target = new CalculatorImpl();
//传入目标对象,得到增强对象(如果需要对目标对象进行别的增强,可以另外编写getXxInvocationHandler)
InvocationHandler handler = getXxInvocationHandler(target);
//获取代理类对象
Calculator o = (Calculator)Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass().getInterfaces(), handler);
int subtract = o.subtract(3, 9);
System.out.println(subtract);
}
private static InvocationHandler getXxInvocationHandler(Object target){
return new InvocationHandler(){
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println(method.getName()+"执行了");
Object o = method.invoke(target,args);
System.out.println(method.getName()+"执行结束");
return o;
}
};
}
}
完美
探究代理Class和代理对象
如果你只是想学会动态代理,上面的内容足够了。但我相信,对于JDK生成的代理Class对象和最终生成的代理对象,大家都有点云里雾里,想刨根问底。
所以我们来研究一下Proxy
Proxy.getProxyClass()会先克隆接口信息得到新的Class对象,然后进行后续的一系列处理。
小结
接口Class对象就好比是个太监,里面的方法和字段就是好像是他的一身武艺,但是他没有XDD(构造器),所以不能new实例,后继无人。
那怎么办呢?
正常途径(静态代理):
写一个类,实现该接口。这个就相当于大街上拉了一个人,认他做干爹。一身武艺传给他,只是比他干爹多了小DD,可以new实例。只要再传入目标对象,就能得到增强后的代理对象。
非正常途径(动态代理):
通过妙手圣医Proxy的克隆大法(Proxy.getProxyClass()),克隆一个Class,但是有小DD。所以这个克隆人Class可以创建实例,也就是代理对象。代理Class其实就是附有构造器的接口Class,一样的类结构信息,却能创建实例。
原本代理对象直接调用目标对象,现在是代理对象调InvocationHandler,InvocationHandler再调目标对象。Proxy代理对象内部有InvocationHandler对象,而InvocationHandler对象内部有我们塞进去的目标对象,所以最终通过代理对象可以调用到目标对象,并且得到了增强。所以,代理模式就是俄罗斯套娃…
从设计上理解JDK动态代理
山寨proxy
/**
* 山寨Proxy类
*/
public static class MyProxy implements java.io.Serializable {
protected MyInvocationHandler h;
private MyProxy() {
}
protected MyProxy(MyInvocationHandler h) {
Objects.requireNonNull(h);
this.h = h;
}
public static Object newProxyInstance(ClassLoader classLoader,
Class<?>[] interfaces,
MyInvocationHandler h) throws Exception {
// 拷贝一份接口Class(接口可能有多个,所以拷贝的Class也有多个)
final Class<?>[] interfaceCls = interfaces.clone();
// 这里简化处理,只取第一个
Class<?> copyClazzOfInterface = interfaceCls[0];
// 获取Proxy带InvocationHandler参数的那个有参构造器
Constructor<?> constructor = copyClazzOfInterface.getConstructor(MyInvocationHandler.class);
// 创建一个Proxy代理对象,并把InvocationHandler塞到代理对象内部,返回代理对象
return constructor.newInstance(h);
}
}
/**
* 山寨InvocationHandler接口
*/
interface MyInvocationHandler {
Object invoke(Object proxy, Method method, Object[] args) throws Throwable;
}
目前上面的MyProxy有两个问题没解决:
● 返回的代理对象只是Proxy类型的,没法强转为目标接口类型
● 返回的代理对象即使能调用接口的同名方法,如何最终调用到它内部的InvocationHandler#invoke()呢
底层原理
首先,Proxy是JDK已经写好的类,一开始就没有实现Calculator接口,那么它的实例对象就不能强转为Calculator。那么java是如何解决这个问题的呢?方式很简单粗暴,因为JVM确确实实在运行时动态构造了代理类,并让代理类实现了接口,也就是我们经常看到的$Proxy0。
也就是说,我们通常理解的代理对象,并不是JDK Proxy的直接实例对象,而是JDK Proxy的子类 $ Proxy0的实例对象,而 $Proxy0 extends Proxy implements Calculator
当我们期望使用Proxy创建代理对象时,JDK会先动态生成一个代理类$Proxy0:
// 1.自动实现目标接口,所以代理对象可以转成Calculator
final class $Proxy0 extends Proxy implements InvocationHandlerTest.Calculator {
private static Method m3;
public $Proxy0(InvocationHandler invocationHandler) {
super(invocationHandler);
}
static {
// 2.获取目标方法Method
m3 = Class.forName("com.bravo.demo.InvocationHandlerTest$Calculator").getMethod("add", Integer.TYPE, Integer.TYPE);
}
public final int add(int n, int n2) {
// 3.通过InvocationHandler执行方法,现在你能理解invoke()三个参数的含义了吗?
// this:就是$Proxy0的实例,所以是代理对象,不是目标对象
return (Integer)this.h.invoke(this, m3, new Object[]{n, n2});
}
}
最后一个问题是,代理对象 $proxy调用add()时,是如何最终调用到目标对象的add()方法的呢?观察上面的代码可以发现,代理对象的方法调用都是通过this.h.invoke()桥接过去的,而这个h就是InvocationHandler,在 $Proxy的父类Proxy中已经存在,而且会被赋值。