优秀的编程知识分享平台

网站首页 > 技术文章 正文

spring AOP的实现原理(spring aop 原理)

nanyue 2024-08-03 17:40:41 技术文章 6 ℃

基于代理(Proxy)的AOP实现

首先,这是一种基于代理(Proxy)的实现方式。下面这张图很好地表达了这层关系:

这张图反映了参与到AOP过程中的几个关键组件(以@Before Advice为例):

  1. 调用者Beans - 即调用发起者,它只知道目标方法所在Bean,并不清楚代理以及Advice的存在
  2. 目标方法所在Bean - 被调用的目标方法
  3. 生成的代理 - 由Spring AOP为目标方法所在Bean生成的一个代理对象
  4. Advice - 切面的执行逻辑

它们之间的调用先后次序反映在上图的序号中:

  1. 调用者Bean尝试调用目标方法,但是被生成的代理截了胡
  2. 代理根据Advice的种类(本例中是@Before Advice),对Advice首先进行调用
  3. 代理调用目标方法
  4. 返回调用结果给调用者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
最近发表
标签列表