【重写SpringFramework】第二章aop模块:本章小结(chapter 2-8)
1. 前言
在 Spring 框架中,IOC 和 AOP 共同构成了整个体系的基础。第一章介绍了 IOC 机制,本章关注的是 AOP 功能。相对于 beans 模块而言,aop 模块的功能更加单一,因此从代码层面来说,aop 模块有一条非常清晰的主线,分为三个阶段。
-
准备工作:对 AOP 规范进行分析,得到的结论是,要实现 AOP 功能需要满足三要素,即增强、切面和代理。
-
主体部分:实现了 AOP 的三要素。首先是增强,对
Advice
接口进行扩展,主要关注方法的拦截和调用。然后是切面,通过Advisor
组件完成构建切面的工作。最后是代理,将增强和切面织入到目标对象之中。显而易见,增强、切面、代理通过拼积木的方式逐步扩展,直至形成一个整体。 -
扩展部分:自动代理除了将创建代理的流程自动化之外,更重要的是将 AOP 功能与 IOC 容器联系到了一起。这种巧妙的设计,正是画龙点睛之笔。
2. AOP 规范
AOP 联盟(aop alliance)完成了 AOP 规范的顶层设计,通过寥寥数个接口对 AOP 做了高度的抽象。总的来说,AOP 规范可以分为两个部分。其中 Advice
表示增强的语义,Joinpoint
表示系统的执行点。增强逻辑一定是作用于某段代码上的,Joinpoint
充当了桥梁的作用,使得 Advice
可以嵌入到业务代码中。
静态连接点表示程序中的某处位置,AccessbileObject
表示一个可访问的对象,包括方法、构造器和字段三种类型。Joinpoint
与 Advice
是相关联的,因此每个可访问对象都有一组与之对应的 Joinpoint
和 Advice
实例。
MethodInvocation
表示方法调用,对应的增强组件是MethodInterceptor
。ConstructorInvocation
表示构造器调用,对应的增强组件是ConstructorInterceptor
。FieldAccess
表示字段访问,对应的增强组件是FieldInterceptor
。(虽然没有定义相关接口,但这些内容是可以推导出来的)
3. Spring Advice
Spring 对增强组件的处理,主要体现在两个方面。首先,理论上来说可访问对象有三种,AOP 规范定义了两种,而 Spring 只考虑方法的拦截和调用。这是因为在应用程序中,业务逻辑都出现在方法中,与字段和构造器无关。
其次,对于方法来说,拦截的时机是必须要考虑的。鉴于此,Spring AOP 对 Advice
进一步抽象。具体来说,Advice
的子接口分别负责前置通知、返回通知和异常通知,而 MethodInterceptor
接口的子类则负责对应通知的调用时机。以前置通知为例,MethodBeforeAdvice
的关注点是增强逻辑,而 MethodBeforeAdviceInterceptor
则注重增强逻辑切入的时机。这样一来,拦截逻辑和调用时机被分离开来,降低了系统的耦合度,用户只需要关心拦截逻辑的实现即可。
当前置通知、返回通知和异常通知同时作用于一个方法时,便构成了一个环绕通知,这种环绕通知仅具有逻辑上的意义。此外,我们还可以通过 MethodInterceptor
接口来实现环绕通知,二者在功能上等价的。
4. Advisor
4.1 概述
Advice
提供了增强的语义,可以对所有的方法进行增强。对于 AOP 来说,同样重要的还有切面,需要判断增强逻辑应用在哪些方法上。Spring 在处理切面的问题上进行了综合考量,通过 Advisor
组件解决了三个问题。
- 增强:持有一个
Advice
实例,对于切面来说最终目的是应用增强逻辑。 - 切面:构建切面的具体方式有两种,即切点和引入。
- 适配:系统中存在多种类型的增强组件,需要进行统一的抽象。
4.2 切面
Spring 提供了两种构建切面的方式,一是切点,二是引入。为了简化代码,我们只关心切点的实现。切点实际上是一个组合体,由 ClassFilter
和 MethodMatcher
构成,前者对类进行过滤,后者对方法进行匹配。大多数情况下,我们只关心方法的匹配。
方法匹配又分为静态匹配和动态匹配,所谓静态匹配是指方法签名,包括方法上的注解之类,这些信息在编译期就能确定。所谓动态匹配是指除了静态匹配的内容,还要匹配运行时传入方法的参数。我们仅实现 Spring AOP,因此格外关心静态匹配。至于动态匹配,是通过 AspectJ 构建切面时应用的。
综上所述,Spring AOP 构建切面的主要方式是对方法进行静态匹配。对于框架自身来说,Spring AOP 的功能是完全够用的。在第四章 tx 模块中,我们将通过 Spring AOP 实现事务功能。至于 AspectJ 的 AOP 实现,感兴趣的读者可尝试完成。
4.3 适配
在 AOP 规范的定义中,存在两种增强对象,即 Advice
和 MethodInterceptor
接口。此外,Spring 还定义了 Advisor
组件。这样一来,同时存在三种增强组件,因此有必要对所有的增强组件进行统一的抽象,这一工作是由 AdvisorAdapter
接口完成的。
从原理上来说,适配后的类型是 MethodInterceptor
,因此实际上只需要对 Advice
类型进行适配。我们知道,Spring 将 Advice
分为三种,即前置通知、返回通知、异常通知,相应地定义三个适配器对象即可。如下所示:
MethodBeforeAdviceAdapter
:如果是前置通知,包装成MethodBeforeAdviceInterceptor
AfterReturningAdviceAdapter
:如果是返回通知,包装成AfterReturningAdviceInterceptor
ThrowsAdviceAdapter
:如果是异常通知,包装成ThrowsAdviceInterceptor
5. AOP 代理
5.1 概述
AOP 功能最终的落脚点是一个对象(的方法),因此需要将增强和切面织入到对象之中。Spring 通过代理模式实现了对目标对象的整合,AOP 代理的构成比较复杂,大体分为四个部分:
ProxyFactory
表示代理工厂,屏蔽了创建代理的细节实现,负责设置相关属性和创建对象,相当于兼具BeanDefinition
与BeanFactory
的功能。AopProxy
表示代理对象,根据创建方式分为两种,JdkDynamicAopProxy
表示 JDK 动态代理,CglibAopProxy
表示 Cglib 代理。TargetSource
表示目标对象,SingletonTargetSource
表示 Spring 容器托管的单例,LazyInitTargetSource
的作用是包装延迟初始化的单例。Advisor
组件整合了增强和切面的功能,与AopProxy
一起实现了 Spring AOP 的基本功能。
5.2 AOP 代理的本质
AOP 代理实际上是个包装类,持有一个目标对象,以及一个 Advisor
集合。一个类中有 N 个方法,所有方法一共用到了 M 种增强。换句话说,一个方法可以有多个增强,也可以没有任何增强,这种情况被当作普通方法调用。我们发现,从创建代理对象到调用方法的过程中,一共出现了两次过滤。第一次是为目标对象寻找合适的 Advisor
集合,这是面向类的粗粒度过滤。第二次是调用方法时,寻找适合当前方法的 Advisor
并转换成 MethodInterceptor
,也就是拦截器链,这是面向方法的细粒度过滤。
如下图所示,代理对象持有一个目标对象,目标对象拥有 method1
和 method2
两个方法。此外,代理对象还持有一组 Advisor
集合,这是目标对象的所有方法一共用到的。但对于具体方法来说,假设 method1
是一个增强方法,那么 AOP 代理会构建一个拦截器链,先于目标方法之前执行增强逻辑。假设 method2
不是增强方法,也就是说 AOP 代理不能构建拦截器链,当作普通方法调用即可。
综上所述,AOP 代理的实质是通过代理对象接管对目标对象的访问,在调用目标方法前寻找拦截器链,依次执行每个拦截器的增强逻辑,最后调用目标方法。关键在于必须通过代理对象来访问,以此触发拦截器链的执行。
5.3 AOP 失效的问题
当我们在一个增强方法中调用同一个类中的另一个增强方法,第二个方法的增强逻辑会失效,这就是 AOP 失效的问题。这是因为调用第一个方法时,引用指向的是代理对象。首先执行的是拦截器链,也就是增强逻辑。当进入第一个方法内部,this 引用指向的是目标对象,此时不存在拦截器链,第二个方法只是一个普通方法。
在 AOP 代理回调的过程中,代理对象被绑定到当前线程上,我们可以通过 AopContext
工具类的 currentProxy
方法获取代理对象,前提是将 AopConfig
类的 exposeProxy
属性设置为 true。通过对外暴露代理,解决了 AOP 失效的问题。具体来说,在增强方法中获取代理对象,然后通过代理对象来调用其他增强方法。
但这种方式也有不足之处。代理对象保存在 AopContext
的 ThreadLocal
类型字段中,也就是说代理对象是和当前线程绑定的。如果尝试在新线程中获取代理对象,会抛出异常。这一缺陷与 AOP 代理的底层逻辑有关,应当尽量避免这一情况的出现。
6. 自动代理
6.1 概述
在实际使用中,我们不可能一个个地去创建代理对象,迫切需要一种能自动完成创建代理对象的机制。具体思路是,依托 Spring 容器强大的管理能力,在 Bean 创建流程中检查对象是否需要被代理,从而实现自动创建代理的功能。自动代理的功能是通过 BeanPostProcessor
来扩展的,ProxyProcessorSupport
作为父类提供了两种基本实现,简单介绍如下:
AbstractAdvisingBeanPostProcessor
:持有一个Advisor
实例,需要手动设置。比如AsyncAnnotationBeanPostProcessor
实现了异步操作,允许声明了@Async
注解的方法在新线程中执行。这种代理的功能单一,直接将AsyncAnnotationAdvisor
作为默认设置,外界不能进行干预。AbstractAutoProxyCreator
:持有一组Advisor
实例,并且支持自动设置。具体做法是检索 Spring 容器中所有的Advisor
实例,然后过滤出符合条件的实例,可能存在多个。在实际使用中,AbstractAutoProxyCreator
具有通用性,因此我们主要关注该类的实现。
6.2 AbstractAutoProxyCreator
AbstractAutoProxyCreator
实现了 InstantiationAwareBeanPostProcessor
接口及其父接口 BeanPostProcessor
的三个方法,它们都能用来创建代理对象,区别在于调用时机和用途。这三个方法按照执行顺序,分别介绍如下:
postProcessBeforeInstantiation
方法:在对象实例化之前执行,这种方式比较特殊,比如延迟初始化的实现。getEarlyBeanReference
方法:在填充对象的过程中执行,具体来说是在依赖解析时调用,专门负责处理代理对象的循环依赖。postProcessAfterInitialization
方法:在对象初始化之后执行,是创建代理对象的主要方式。
为了确保创建代理的操作只执行一次,这三种创建代理的方式是互斥的。具体来说,如果在实例化之前创建对象,那么就不会执行后两种方式。在这一基础上,后两种创建代理的方式也是互斥的。
7. 关于 AspectJ
Spring AOP 是一个简单的面向切面编程的解决方案,事实上,AspectJ 这种成熟的框架也是一个很好的选择。我们没有介绍基于 AspectJ 的 AOP 实现,主要原因有两点。
- AspectJ 的难度较大,不利于学习。事实上,我们已经搭建起了 AOP 的基本骨架,AspectJ 的实现主要是填充一些细节,比如一笔带过的引入和动态方法匹配等。另外,还有就是 AspectJ 自身定义的组件。这些内容自成体系,且异常琐碎,如果展开来讲要占用大量篇幅,偏离了本教程的主旨。
- 虽然 AspectJ 的功能很强大,但对于本教程来说,Spring AOP 的功能完全够用。在第四章 tx 模块中,事务管理就是通过 Spring AOP 实现的。
综上所述,我们通过 Spring AOP 介绍了面向切面编程的思想,逐一厘清了众多概念,并实现了一个基本可用的 AOP 框架。至于 AspectJ 的实现,感兴趣的读者可以尝试自行完成,从而加深对本章所涉及内容的理解。
欢迎关注公众号【Java编程探微】,加群一起讨论。
原创不易,觉得内容不错请关注、点赞、收藏。
转载自:https://juejin.cn/post/7395133032409317395