前言:尽管动态代理看起来似乎有一定难度,但却必须拿下。因为Spring的事务控制依赖于AOP,AOP底层实现便是动态代理,环环相扣。到最后,还是看基本功。
主要内容:
- 一个小需求:给原有方法添加日志打印
- 静态代理实现日志打印
- 静态代理的问题
一个小需求:给原有方法添加日志打印
假设现在我们有一个类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;
}
//乘法、除法...
}
现有一个需求:在每个方法执行前后打印日志。
直接修改
最直观的想法是直接修改Calculator类:
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有几十个、上百个方法,修改量太大
- 存在重复代码(都是在核心代码前后打印日志)
- 日志打印硬编码在代理类中,不利于后期维护:比如你花了一上午终于写完了,组长告诉你这个功能取消,于是你又要打开Calculator花十分钟删除日志打印的代码!
所以,此种方案PASS!
静态代理实现日志打印
“静态代理”四个字包含了两个概念:静态、代理。我们先来了解什么叫“代理”,至于何为“静态”,需要和“动态”对比着讲。
代理是一种模式,提供了对目标对象的间接访问方式,即通过代理访问目标对象。如此便于在目标实现的基础上增加额外的功能操作,前拦截,后拦截等,以满足自身的业务需求。
常用的代理方式可以粗分为:静态代理和动态代理。
静态代理的实现比较简单:编写一个代理类,实现与目标对象相同的接口,并在内部维护一个目标对象的引用。通过构造器塞入目标对象,在代理对象中调用目标对象的同名方法,并添加前拦截,后拦截等所需的业务功能。
按上面的描述,代理类和目标类需要实现同一个接口,所以我打算这样做:
- 将Calculator抽取为接口
- 创建目标类CalculatorImpl实现Calculator
- 创建代理类CalculatorProxy实现Calculator
接口:
/**
* Calculator接口
*/
public interface Calculator {
int add(int a, int b);
int subtract(int a, int b);
}
目标对象实现类:
/**
* 目标对象实现类,实现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);
}
}
静态代理的优点:可以在不修改目标对象的前提下,对目标对象进行功能的扩展和拦截。但是它也仅仅解决了上一种方案4大缺点中的第1点:
- 直接修改源程序,不符合开闭原则。应该对扩展开放,对修改关闭 √
- 如果Calculator有几十个、上百个方法,修改量太大 ×
- 存在重复代码(都是在核心代码前后打印日志) ×
- 日志打印硬编码在代理类中,不利于后期维护:比如你花了一上午终于写完了,组长告诉你这个功能取消,于是你又要打开Calculator花十分钟删除全部新增代码!×
静态代理的问题
上面案例中,代理类是我们事先编写的,而且要和目标对象类实现相同接口。由于CalculatorImpl(目标对象)需要日志功能,我们即编写了CalculatorProxy(代理对象),并通过构造器传入CalculatorImpl(目标对象),调用目标对象同名方法的同时添加增强代码。
但是这里有个问题!代理对象构造器的参数类型是Calculator,这意味着它只能接受Calculator的实现类对象,亦即我们写的代理类CalculatorProxy只能给Calculator做代理,它们绑定死了!
如果现在我们系统需要全面改造,给其他类也添加日志打印功能,就得为其他几百个接口都各自写一份代理类:
自己手动写一个类并实现接口实在太麻烦了。仔细一想,我们其实想要的并不是代理类,而是代理对象!那么,能否让JVM根据接口自动生成代理对象呢?比如,有没有一个方法,我传入接口,它就给我自动返回代理对象呢?
答案是肯定的。
我们来看看JDK如何自动生成代理对象!
在使用动态代理之前我们先进行接口创建对象的可行性分析。
虽然知道源代码经过javac命令编译后会在磁盘中得到字节码文件(.class文件),也知道java命令会启动JVM将字节码文件加载进内存,但也仅仅止步于此了。至于从字节码文件加载进内存到堆中产生对象,期间具体发生了什么,他们并不清楚。
所谓“万物皆对象”,字节码文件也难逃“被对象”的命运。它被加载进内存后,JVM为其创建了一个对象,以后所有该类的实例,皆以它为模板。这个对象叫Class对象,它是Class类的实例。
也就是说,要得到一个类的实例,关键是先得到该类的Class对象!
只不过new这个关键字实在太方便,为我们隐藏了底层很多细节,我在刚开始学习Java时甚至没意识到Class对象的存在。
- 接口Class对象没有构造方法,所以Calculator接口不能直接new对象
- 实现类Class对象有构造方法,所以CalculatorImpl实现类可以new对象
- 接口Class对象有两个方法add()、subtract()
- 实现类Class对象除了add()、subtract(),还有从Object继承的方法
也就是说,接口和实现类的Class信息除了构造器,基本相似。
既然我们希望通过接口创建实例,就无法避开下面两个问题:
- 接口方法体缺失问题
首先,接口的Class对象已经得到,它描述了方法信息。但它没方法体。没关系,反正代理对象的方法是个空壳,只要调用目标对象的方法即可。JVM可以在创建代理对象时,随便糊弄一个空的方法体,反正后期我们会想办法把目标对象塞进去调用。所以这个问题,勉强算是解决。 - 接口Class没有构造器
无法new这个问题好像无解…毕竟这么多年了,的确没听哪位仁兄直接new接口的。但是,仔细想想,接口之所以不能new,是因为它缺少构造器,它本身是具备完善的类结构信息的。那动态代理是怎么创建实例的呢?
动态代理
不错,动态代理确实存在getXxxClass()这样的方法。
我们需要java.lang.reflect.InvocationHandler接口
和 java.lang.reflect.Proxy类
的支持。Proxy后面会用到InvocationHandler,因此我打算以Proxy为切入点。首先,再次明确我们的思路:
通过查看API,我们发现Proxy类有一个静态方法可以帮助我们。
Proxy.getProxyClass():返回代理类的Class对象。
也就说,只要传入目标类实现的接口的Class对象,getProxyClass()方法即可返回代理Class对象,而不用实际编写代理类。这相当于什么概念?
也就是说,通过给Proxy.getProxyClass()传入类加载器和接口Class对象,我们得到了一个加强版的Class
:既包含接口的方法信息add()、subtract(),又包含了构造器$Proxy0(InvocationHandler),还有一些自己特有的方法以及从Object继承的方法。
梳理一下:
- 原先我们本打算直接根据接口Class得到代理对象,无奈接口Class只有方法信息,没有构造器
- 于是,我们想,有没有办法创建一个Class对象,既有接口Class的方法信息,同时又包含构造器方便创建代理实例呢?
- 利用Proxy类的静态方法getProxyClass()方法,给它传一个接口Class对象,它能返回一个加强版Class对象。也就是说getProxyClass()的本质是:用Class,造Class。
要谢谢Proxy类和JVM,让我们不写代理类却直接得到代理Class对象,进而得到代理对象。
既然Class<$Proxy0>
有方法信息,又有构造器,我们试着用它得到代理实例吧:
我们发现,newInstance()创建对象失败。因为Class的newInstance()方法底层会走无参构造器。而之前打印$Proxy0
的Class信息时,我们发现它没有无参构造,只有有参构造$Proxy0(InvocationHandler)。那就靠它了:
竟然发生了空指针异常。纵观整个代码,新写的add()和subtract()返回值是int,不会是空指针。而再往上的代码之前编译都是通过的,应该没问题啊。再三思量,我们发现匿名对象InvocationHandler的invoke()返回null。难道是它?做个实验:让invoke()返回1,然后观察结果。
就目前的实验来看,调用过程应该是这样:
动态代理底层调用逻辑:
动态代理:constructor反射创建代理对象时,需要传入InvocationHandler,我猜,代理对象内部有一个成员变量InvocationHandler:
动态代理的大致设计思路就是:
但是我们真正想要的结果是:调用代理对象的方法时,去调用目标对象的方法。
所以,接下来努力的方向就是:设法在invoke()方法得到目标对象,并调用目标对象的同名方法。
代理对象调用目标对象方法
使用动态代理,让我们避免手写代理类,只要给getProxy()方法传入target就可以生成对应的代理对象。但是日志打印仍是硬编码在invoke()方法中。虽然修改时只要改一处,但是别忘了“开闭原则”。所以最好是能把日志打印单独拆出来,像目标对象一样作为参数传入。
日志打印其实就是AOP里的通知概念。我打算定义一个Advice接口,并且写一个MyLogger实现该接口。
通知接口
public interface Advice {
void beforeMethod(Method method);
void afterMethod(Method method);
}
日志打印
public class MyLogger implements Advice {
public void beforeMethod(Method method) {
System.out.println(method.getName() + "方法执行开始...");
}
public void afterMethod(Method method) {
System.out.println(method.getName() + "方法执行结束...");
}
}
测试类
public class ProxyTest {
public static void main(String[] args) throws Throwable {
CalculatorImpl target = new CalculatorImpl();
Calculator calculatorProxy = (Calculator) getProxy(target, new MyLogger());
calculatorProxy.add(1, 2);
calculatorProxy.subtract(2, 1);
}
private static Object getProxy(final Object target, Advice logger) throws Exception {
/*代理对象的方法最终都会被JVM导向它的invoke方法*/
Object proxy = Proxy.newProxyInstance(
target.getClass().getClassLoader(),/*类加载器*/
target.getClass().getInterfaces(),/*让代理对象和目标对象实现相同接口*/
(proxy1, method, args) -> {
logger.beforeMethod(method);
Object result = method.invoke(target, args);
System.out.println(result);
logger.afterMethod(method);
return result;
}
);
return proxy;
}
}
小结
静态代理
代理类CalculatorProxy是我们事先写好的,编译后得到Proxy.class字节码文件。随后和目标类一起被ClassLoader(类加载器)加载进内存,生成Class对象,最后生成实例对象。代理对象中有目标对象的引用,调用同名方法并前后加上日志打印。
- 优点:不用修改目标类源码
- 缺点:高度绑定,不通用。硬编码,不易于维护。
动态代理
我们本想通过接口Class直接创建代理实例,无奈的是,接口Class虽然有方法信息描述,却没有构造器,无法创建对象。所以我们希望JDK能提供一套API,我们传入接口Class,它自动复制里面的方法信息,造出一个有构造器、能创建实例的代理Class对象。
- 优点:
不用写代理类,根据目标对象直接生成代理对象
通知可以传入,不是硬编码