JDK动态代理
一、学习Spring的AOP前掌握动态代理
因为Spring的事务控制依赖于AOP,AOP底层实现便是动态代理 + 责任链,环环相扣。
二、拟定需求:给原有方法添加日志打印
直接修改源码的缺点:
直接修改源程序,不符合开闭原则,即好的程序设计应该对扩展开放,对修改关闭;
如果“目标类”内部有几十个、上百个方法,修改量太大;
存在重复代码(都是在核心代码前后打印日志);
日志打印硬编码在代理类中,不利于后期维护:比如你花了一上午终于写完了,组长告诉你这个功能不做了,于是你又要打开修改“目标类”花十分钟删除日志打印的代码(或回滚分支)!
三、静态代理
代理:
代理是一种模式,提供了对目标对象的间接访问方式,即通过代理访问目标对象。如此便于在目标实现的基础上增加额外的功能操作,前拦截,后拦截等,以满足自身的业务需求。
静态代理实现方式:
编写一个代理类,实现与目标对象相同的接口,并在内部维护一个目标对象的引用。通过构造器塞入目标对象,在代理对象中调用目标对象的同名方法,并添加前拦截,后拦截等所需的业务功能。
更好的实现应该是:(按上面的描述,代理类和目标类需要实现同一个接口)
- 将Calculator抽取为接口
- 创建目标类CalculatorImpl实现Calculator
- 创建代理类CalculatorProxy实现Calculator
静态代理完成需求度:
静态代理的优点:可以在不修改目标对象的前提下,对目标对象进行功能的扩展和拦截。但是它也仅仅解决了“直接修改源码”方案4大缺点中的第1、4两点:
直接修改源程序,不符合开闭原则,即好的程序设计应该对扩展开放,对修改关闭(✅,如果一开始就面向接口编程,这一步其实是不需要的)
如果“目标类”内部有几十个、上百个方法,修改量太大(❎,目标类有多少个方法,代理类就要重写多少个方法)
存在重复代码(都是在核心代码前后打印日志)(❎,代理类中的日志代码是重复的)
日志打印硬编码在代理类中,不利于后期维护:比如你花了一上午终于写完了,组长告诉你这个功能不做了(✅,别用代理类就好了)
我们会发现:
我们其实想要的并不是代理类,而是代理对象!!!
四、动态代理
动态代理如果要优于静态代理,至少需要解决两个问题:
- 自动生成代理对象,让程序员免受编写代理类的痛苦
- 将增强代码与代理类(代理对象)解耦,从而达到代码复用
1、实现自动生成代理对象
实现自动生成代理对象依靠的是反射机制,我们学习对象的创建过程会知道:
代理类和实例对象之间其实还隔着一个Class对象。如果能得到Class对象,就能生成实例。所以,现在的问题又变成:如何不写代理类,直接得到Class对象。
Class类(Class类就是用来描述“类”的):所谓“万物皆对象”,字节码文件也难逃“被对象”的命运。当字节码文件(.class文件)被加载进内存后,JVM也为其创建了一个对象,以后所有该类的实例,皆以它为模板。这个对象叫Class对象,它是Class类的实例。
Person类有两个不同维度的对象:
* 根据Person类实例化得到的Person p1、 p2、p3对象
* Class类实例化得到的ClasspersonClass对象
补充:
得到Class对象的方法:
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();
但是,这三种方式都需要先有类,但我们不想编写代理类!
这里好像断了思路;
但是我们如果仔细思考会发现,代理类或者代理对象其实只是起到中间更改的作用,而最重要的是最后要实现的结果,也就是说最重要的其实是 增强代码 + 目标对象。我们对代理对象的要求很低,只需要与目标对象拥有相同的方法即可。
所以本质上,代理对象只要有方法申明即可,甚至不需要方法体,或者只要一个空的方法体即可,反正我们会把目标对象返回去。
那么,如何知道一个类有哪些方法信息呢?如果能得到类的方法信息,我们或许可以直接造一个代理对象。
有两个途径:
- 目标类本身
- 目标类实现的接口
这两个思路造就了两种不同的代理机制,一个被后人称为CGLib动态代理,另一个则被JDK收录,世人称之为JDK动态代理。本文重点介绍JDK动态代理。
但是接口Class对象没有构造方法,所以接口不能直接new对象;(接口缺少构造器信息)
那怎么根据一个接口得到代理对象?
JDK选择了 不改变接口本身,直接拷贝接口的信息到另一个Class,然后给那个Class装上构造器呢。
也就是:拷贝接口Class的信息,产生一个新的Class对象。
也就是说,JDK动态代理的本质是:用Class造Class,即用接口Class造出一个代理类Class。
实现了这个方法的API是:
- Proxy.getProxyClass():返回代理类的Class对象。
也就说,只要传入接口的Class对象,getProxyClass()方法即可返回代理Class对象,而不用实际编写代理类。这相当于什么概念?
而Proxy.getProxyClass()返回的Class对象是有构造器的!
开头说了,动态代理的使命有两个:
- 自动生成代理对象,让程序员免受编写代理类的痛苦
- 将增强代码与代理类(代理对象)解耦,从而达到代码复用
现在我们已经得到了代理Class,只需通过反射即可得到代理对象。
Proxy.getProxyClass()使用流程:
- 先获得proxyClazz
- 再根据proxyClazz.getConstructor()获取构造器
- 最后constructor.newInstance()生成代理对象
JDK已经提供了一步到位的方法Proxy.newProxyInstance(),来帮我们封装上面的步骤。
实例代码对比:
使用InvocationHandler对象的的目的就是为了实现 将增强代码与代理类(代理对象)解耦,从而达到代码复用。
未使用Proxy.newProxyInstance():
public class ProxyTest {
public static void main(String[] args) throws Throwable {
// 1.得到目标对象
CalculatorImpl target = new CalculatorImpl();
// 2.传入目标对象,得到增强对象(如果需要对目标对象进行别的增强,可以另外编写getXxInvocationHandler)
InvocationHandler logInvocationHandler = getLogInvocationHandler(target);
// 3.传入接口+增强对象(含目标对象),得到代理对象
Calculator calculatorProxy = (Calculator) getProxy(
logInvocationHandler, // 增强对象(包含 目标对象 + 增强代码)
target.getClass().getClassLoader(), // 随便传入一个类加载器
target.getClass().getInterfaces() // 需要代理的接口
);
calculatorProxy.add(1, 2);
}
/**
* 传入接口+增强(已经包含了目标对象),获取代理对象
*
* @param handler
* @param classLoader
* @param interfaces
* @return
* @throws Exception
*/
private static Object getProxy(final InvocationHandler handler, final ClassLoader classLoader, final Class<?>... interfaces) throws Exception {
// 参数1:随便找个类加载器给它 参数2:需要代理的接口
Class<?> proxyClazz = Proxy.getProxyClass(classLoader, interfaces);
Constructor<?> constructor = proxyClazz.getConstructor(InvocationHandler.class);
return constructor.newInstance(handler);
}
/**
* 日志增强代码
*
* @param target
* @return
*/
private static InvocationHandler getLogInvocationHandler(final CalculatorImpl target) {
return 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;
}
};
}
}
使用了Proxy.newProxyInstance():
public class ProxyTest {
public static void main(String[] args) throws Throwable {
// 1.得到目标对象
CalculatorImpl target = new CalculatorImpl();
// 2.传入目标对象,得到增强对象(如果需要对目标对象进行别的增强,可以另外编写getXxInvocationHandler)
InvocationHandler logInvocationHandler = getLogInvocationHandler(target);
// 3.传入目标对象+增强代码,得到代理对象(直接用JDK的方法!!!)
Calculator calculatorProxy = (Calculator) Proxy.newProxyInstance(
target.getClass().getClassLoader(), // 随便传入一个类加载器
target.getClass().getInterfaces(), // 需要代理的接口
logInvocationHandler // 增强对象(包含 目标对象 + 增强代码)
);
calculatorProxy.add(1, 2);
}
/**
* 日志增强代码
*
* @param target
* @return
*/
private static InvocationHandler getLogInvocationHandler(final CalculatorImpl target) {
return 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;
}
};
}
}
学习自:
知乎-bravo1988