在接触Spring以及种类繁多的Java框架时,很多开发人员(至少包括我)都会觉得注解是个很奇妙的存在,为什么加上了@Transactional之后,方法会在一个事务的上下文中被执行呢?为什么加上了@Cacheable之后,方法的返回值会被记录到缓存中,从而让下次的重复调用能够直接利用缓存的结果呢?
随着对AOP的逐渐应用和了解,才明白注解只是一个表象,在幕后Spring AOP/AspectJ做了大量的工作才得以实现这些神奇的功能。
那么,本文就来聊一聊Spring AOP和AspectJ的那些事,它们究竟有什么魔力才让这一切成为现实。
Spring AOP
基于代理(Proxy)的AOP实现
首先,这是一种基于代理(Proxy)的实现方式。下面这张图很好地表达了这层关系:
这张图反映了参与到AOP过程中的几个关键组件(以@Before Advice为例):
- 调用者Beans - 即调用发起者,它只知道目标方法所在Bean,并不清楚代理以及Advice的存在
- 目标方法所在Bean - 被调用的目标方法
- 生成的代理 - 由Spring AOP为目标方法所在Bean生成的一个代理对象
- Advice - 切面的执行逻辑
它们之间的调用先后次序反映在上图的序号中:
- 调用者Bean尝试调用目标方法,但是被生成的代理截了胡
- 代理根据Advice的种类(本例中是@Before Advice),对Advice首先进行调用
- 代理调用目标方法
- 返回调用结果给调用者Bean(由代理返回,没有体现在图中)
为了理解清楚这张图的意思和代理在中间扮演的角色,不妨看看下面的代码:
@Component
public class SampleBean {
public void advicedMethod() {
}
public void invokeAdvicedMethod() {
advicedMethod();
}
}
@Aspect
@Component
public class SampleAspect {
@Before("execution(void advicedMethod())")
public void logException() {
System.out.println("Aspect被调用了");
}
}
sampleBean.invokeAdvicedMethod(); // 会打印出 "Aspect被调用了" 吗?
SampleBean
扮演的就是目标方法所在Bean的角色,而SampleAspect
扮演的则是Advice的角色。很显然,被AOP修饰过的方法是advicedMethod()
,而非invokeAdvicedMethod()
。然而,invokeAdvicedMethod()
方法在内部调用了advicedMethod()
。那么会打印出来Advice中的输出吗?
答案是不会。
如果想不通为什么会这样,不妨再去仔细看看上面的示意图。
这是在使用Spring AOP的时候可能会遇到的一个问题。类似这种间接调用不会触发Advice的原因在于调用发生在目标方法所在Bean的内部,和外面的代理对象可是没有半毛钱的关系哦。我们可以把这个代理想象成一个中介,只有它知道Advice的存在,调用者Bean和目标方法所在Bean知道彼此的存在,但是对于代理或者是Advice却是一无所知的。因此,没有通过代理的调用是绝无可能触发Advice的逻辑的。如下图所示:
Spring AOP的两种实现方式
Spring AOP有两种实现方式:
- 基于接口的动态代理(Dynamic Proxy)
- 基于子类化的CGLIB代理
我们在使用Spring AOP的时候,一般是不需要选择具体的实现方式的。Spring AOP能根据上下文环境帮助我们选择一种合适的。那么是不是每次都能够这么”智能”地选择出来呢?也不尽然,下面的例子就反映了这个问题:
@Component
public class SampleBean implements SampleInterface {
public void advicedMethod() {
}
public void invokeAdvicedMethod() {
advicedMethod();
}
}
public interface SampleInterface {
}
在上述代码中,我们为原来的Bean实现了一个新的接口SampleI