基于代理(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