前端时间,去参加网易的实习生招聘,面试官问了一个Spring AOP相关的问题:如果有一个没被 aspect 织入的函数A去调用被 aspect 织入的函数B,那么函数A在执行时会有函数B的织入效果吗?
当时是这个问题没有回答上来,确实没有试过这种情况。现在就来试试这种情况,以及分析一下Spring AOP的相关原理。我们都知道Spring AOP中使用的是动态代理的技术,其中包括了JDK动态代理和CGLIB动态代理,这两种代理分别对应对接口和类进行动态代理类的生成。下面我们通过具体的例子来看这两种方式的区别。
1. JDK动态代理
首先我们先看看Java中最常使用的动态代理方式。JDK动态代理使用的代理设计模式来进行设计的,针对接口生成代理类。具体例子:
public interface Person {
String sayHello(String name);
void eat(String food);
}
public class Chinese implements Person {
@Override
public String sayHello(String name) {
System.out.println("-- sayHello() --");
return name + " hello";
}
@Override
public void eat(String food) {
System.out.println("我正在吃:" + food);
}
}
首先是一个 Person 接口,对应的具体实现类是 Chinese 类。JDK动态代理需要定义一个 InvocationHandler 实现类,在类中传入一个 Chinese 实例,通过 Chinese 的实例对方法进行修饰。
public class ChineseInvocation implements InvocationHandler {
private Chinese chinese;
public ChineseInvocation(Chinese chinese) {
this.chinese = chinese;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if (method.getName().equals("sayHello")) {
System.out.println("事务开始");
method.invoke(chinese, args);
System.out.println("事务结束");
} else {
method.invoke(chinese, args);
}
return null;
}
}
public class ChineseProxy {
public static void main(String args[]) {
Chinese chinese = new Chinese();
ChineseInvocation chineseInvocation = new ChineseInvocation(chinese);
Person chineseProxy = (Person) Proxy.newProxyInstance(
chinese.getClass().getClassLoader(),
chinese.getClass().getInterfaces(),
chineseInvocation
);
chineseProxy.sayHello("hello");
chineseProxy.eat("Apple");
}
}
最后执行的代码如上,新建一个 Chinese 实例,传入 ChineseInvocation 中,使用 Proxy.newProxyInstance() 生成代理实例 chineseProxy,执行结果:
由于在 ChineseInvocation 对方法进行了判断,只有 sayHello 方法才会在前后加上“事务”标识。这是一个标准的代理模式实现,把具体实现类 Chinese 封装到代理类中。其实没有 Chinese 具体实现类,也是可以产生代理类的,此时 invoke 方法就是代理类中的方法。
public class ChineseInvocation implements InvocationHandler {
// private Chinese chinese;
// public ChineseInvocation(Chinese chinese) {
// this.chinese = chinese;
// }
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if (method.getName().equals("sayHello")) {
System.out.println("事务开始");
// method.invoke(chinese, args);
System.out.println("事务结束");
} else {
// method.invoke(chinese, args);
System.out.println("do nothing!");
}
return null;
}
}
比如,把 ChineseInvocation 改为上面代码,执行结果就变为:
我们需要特别注意一个参数,那就是 InvocationHandler 中 invoke 方法中的第一个参数 proxy,这个参数有什么意义?
我们可以看到 invoke 的返回值类型是 Object ,所以执行完代理类方法以后我们可以把这个 proxy 返回以进行连续调用,还有一个作用是可以使用反射获取到代理类的相关信息。
2.Spring AOP
关于Spring AOP的实现以及如何在JDK动态代理和CGLIB动态代理中选择参考:Spring AOP的实现原理。
3.Spring AOP中的动态代理
我们需要知道的是String AOP中的JDK动态代理是根据 PointCut 定义找到 Aspect 需要织入的方法,然后根据动态代理生成新的代理类,新的代理类中调用具体实现类的方法,比如上面 Chinese 中的方法,并且通过 Aspect 的定义在具体实现类的前后加上切片代码,最常见的比如日志操作和事务管理。
所以就回到了最开始的问题。Spring的依赖注入功能在注入有切片的实例时,会根据动态代理生成代理对象,代理对象中根据具体实现类的方法织入切片代码。(是否织入代码和哪些方法织入哪些代码根据pointcut和aspect来确定。)加上代码这一原理与我们平时写Java动态代理代码时一致的,都需要生成一个具体实例,然后再调用这个实例的方法前后加上切片代码。
所以,这其中涉及到了两个主要实例,动态代理生成的实例和我们自己定义的具体实现实例。而代理实例执行时会调用具体实现实例中的方法,切片代码会在代理实例中加入,所以一个A方法调用B方法,其实只是实例内部调用,不涉及代理实例中的B方法,因此不会出现切片效果。