基于代理(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实现了一个新的接口SampleInterface
,这个接口中并没有定义任何方法。这个时候,再次运行相关测试代码的时候就会出现异常(摘录了部分异常信息):
org.springframework.beans.factory.BeanCreationException:
Error ceating bean with name 'com.destiny1020.SampleBeanTest':
Injection of autowired dependencies failedCaused by:
org.springframework.beans.factory.NoSuchBeanDefinitionException:
No qualifying bean of type [com.destiny1020.SampleBean] found for dependency:
expected at least 1 bean which qualifies as autowire candidate for this dependency.
也就是说在Test类中对于Bean的Autowiring失败了,原因是创建SampleBeanTest Bean的时候发生了异常。那么为什么会出现创建Bean的异常呢?从异常信息来看并不明显,实际上这个问题的根源在于Spring AOP在创建代理的时候出现了问题。
这个问题的根源可以在这里得到一些线索:
Spring AOP Reference - AOP Proxies
文档中是这样描述的(每段后加上了翻译):
Spring AOP defaults to using standard JDK dynamic proxies for AOP proxies. This enables any interface (or set of interfaces) to be proxied.
Spring AOP默认使用标准的JDK动态代理来实现AOP代理。这能使任何借口(或者一组接口)被代理。
Spring AOP can also use CGLIB proxies. This is necessary to proxy classes rather than interfaces. CGLIB is used by default if a business object does not implement an interface. As it is good practice to program to interfaces rather than classes; business classes normally will implement one or more business interfaces. It is possible to force the use of CGLIB, in those (hopefully rare) cases where you need to advise a method that is not declared on an interface, or where you need to pass a proxied object to a method as a concrete type.
Spring AOP也使用CGLIB代理。对于代理classes而非接口这是必要的。如果一个业务对象没有实现任何接口,那么默认会使用CGLIB。由于面向接口而非面向classes编程是一个良好的实践;业务对象通常都会实现一个或者多个业务接口。强制使用CGLIB也是可能的(希望这种情况很少),此时你需要advise的方法没有被定义在接口中,或者你需要向方法中传入一个具体的对象作为代理对象。
因此,上面异常的原因在于:
强制使用CGLIB也是可能的(希望这种情况很少),此时你需要advise的方法没有被定义在接口中。
我们需要advise的方法是SampleBean中的advicedMethod方法。而在添加接口后,这个方法并没有被定义在该接口中。所以正如文档所言,我们需要强制使用CGLIB来避免这个问题。
强制使用CGLIB很简单:
@Configuration
@EnableAspectJAutoProxy(proxyTargetClass = true)
@ComponentScan(basePackages = "com.destiny1020")
public class CommonConfiguration {}
向@EnableAspectJAutoProxy
注解中添加属性proxyTargetClass = true
即可。
CGLIB实现AOP代理的原理是通过动态地创建一个目标Bean的子类来实现的,该子类的实例就是AOP代理,它建立起了目标Bean到Advice的联系。
当然还有另外一种解决方案,那就是将方法定义声明在新创建的接口中并且去掉之前添加的proxyTargetClass = true
:
@Component
public class SampleBean implements SampleInterface {
@Override
public void advicedMethod() {
}
@Override
public void invokeAdvicedMethod() {
advicedMethod();
}
}
public interface SampleInterface {
void invokeAdvicedMethod();
void advicedMethod();
}
@Configuration
@EnableAspectJAutoProxy
@ComponentScan(basePackages = "com.destiny1020")
public class CommonConfiguration {}
- 从Debug Stacktrace的角度也可以看出这两种AOP实现方式上的区别:
- JDK动态代理
- CGLIB
-
关于动态代理和CGLIB这两种方式的简要总结如下:
-
JDK动态代理(Dynamic Proxy)
- 基于标准JDK的动态代理功能
- 只针对实现了接口的业务对象
-
CGLIB
- 通过动态地对目标对象进行子类化来实现AOP代理,上面截图中的
SampleBean$$EnhancerByCGLIB$$1767dd4b
即为动态创建的一个子类 - 需要指定
@EnableAspectJAutoProxy(proxyTargetClass = true)
来强制使用 - 当业务对象没有实现任何接口的时候默认会选择CGLIB
- 通过动态地对目标对象进行子类化来实现AOP代理,上面截图中的
-