一:概述以及目录
前两篇的源码解析,涉及到很多基础知识,但是源码的解读都不难,这篇⽂章刚好相反,依赖的基础知识不多,但 是源码⽐较难懂。 下⾯我会简单介绍⼀下 AOP 的基础知识,以及使⽤⽅法,然后直接对源码进⾏拆解。目录如下:
二:基础知识
2.1 什么是 AOP ?
AOP 的全称是 “Aspect Oriented Programming”,即⾯向切⾯编程。
在 AOP 的思想⾥⾯,周边功能(⽐如性能统计,⽇志,事务管理等)被定义为切⾯,核⼼功能和切⾯功能分别独 ⽴进⾏开发,然后把核⼼功能和切⾯功能“编织”在⼀起,这就叫 AOP。
AOP 能够将那些与业务⽆关,却为业务模块所共同调⽤的逻辑封装起来,便于减少系统的重复代码,降低模块间的 耦合度,并有利于未来的可拓展性和可维护性。
2.2 AOP 基础概念
- 连接点(Join point):能够被拦截的地⽅,Spring AOP 是基于动态代理的,所以是⽅法拦截的,每个成员⽅ 法都可以称之为连接点;
- 切点(Poincut):每个⽅法都可以称之为连接点,我们具体定位到某⼀个⽅法就成为切点; 增强/通知
- (Advice):表示添加到切点的⼀段逻辑代码,并定位连接点的⽅位信息,简单来说就定义了是⼲什 么的,具体是在哪⼲;
- 织⼊(Weaving):将增强/通知添加到⽬标类的具体连接点上的过程;
- 引⼊/引介(Introduction):允许我们向现有的类添加新⽅法或属性,是⼀种特殊的增强;
- 切⾯(Aspect):切⾯由切点和增强/通知组成,它既包括了横切逻辑的定义、也包括了连接点的定义。
上⾯的解释偏官⽅,下⾯⽤“⽅⾔”再给⼤家解释⼀遍。
切⼊点(Pointcut):在哪些类,哪些⽅法上切⼊(where); 通知(Advice):在⽅法执⾏的什么时机(when:⽅法前/⽅法后/⽅法前后)做什么(what:增强的功 能); 切⾯(Aspect):切⾯ = 切⼊点 + 通知,通俗点就是在什么时机,什么地⽅,做什么增强; 织⼊(Weaving):把切⾯加⼊到对象,并创建出代理对象的过程,这个由 Spring 来完成。
5 种通知的分类:
- 前置通知(Before Advice):在⽬标⽅法被调⽤前调⽤通知功能;
- 后置通知(After Advice):在⽬标⽅法被调⽤之后调⽤通知功能;
- 返回通知(After-returning):在⽬标⽅法成功执⾏之后调⽤通知功能;
- 异常通知(After-throwing):在⽬标⽅法抛出异常之后调⽤通知功能;
- 环绕通知(Around):把整个⽬标⽅法包裹起来,在被调⽤前和调⽤之后分别调⽤通知功能。
2.3 AOP 简单示例
新建 Model 类:
@Data
@Service
public class Model {
public void everyDay() {
System.out.println("睡觉");
}
}
复制代码
添加 ModelAspect 切⾯:
@Aspect
@Component
public class ModelAspect {
@Pointcut("execution(* com.java.Model.everyDay())")
private void myPointCut() {
}
// 前置通知
@Before("myPointCut()")
public void myBefore() {
System.out.println("吃饭");
}
// 后置通知
@AfterReturning(value = "myPointCut()")
public void myAfterReturning() {
System.out.println("打⾖⾖。。。");
}
}
复制代码
applicationContext.xml 添加:
<!--启⽤@Autowired等注解-->
<context:annotation-config/>
<context:component-scan base-package="com" />
<aop:aspectj-autoproxy proxy-target-class="true"/>
复制代码
程序⼊⼝:
public class MyTest {
public static void main(String[] args) {
ApplicationContext context =new
ClassPathXmlApplicationContext("classpath:applicationContext.xml");
Model model = (Model) context.getBean("model");
model.everyDay();
}
}
复制代码
输出:
- 吃饭
- 睡觉
- 打⾖⾖。。。
这个示例⾮常简单,“睡觉” 加了前置和后置通知,但是 Spring 在内部是如何⼯作的呢?
2.4 Spring AOP ⼯作流程
为了⽅便⼤家能更好看懂后⾯的源码,我先整体介绍⼀下源码的执⾏流程,让⼤家有⼀个整体的认识,否则容易被 绕进去。
整个 Spring AOP 源码,其实分为 3 块,我们会结合上⾯的示例,给⼤家进⾏讲解。
第⼀块就是前置处理,我们在创建 Model Bean 的前置处理中,会遍历程序所有的切⾯信息,然后将切⾯信息保存 在缓存中,⽐如示例中 ModelAspect 的所有切⾯信息。
第⼆块就是后置处理,我们在创建 Model Bean 的后置处理器中,⾥⾯会做两件事情:
- 获取 Model 的切⾯⽅法:⾸先会从缓存中拿到所有的切⾯信息,和 Model 的所有⽅法进⾏匹配,然后找到 Louzai 所有需要进⾏ AOP 的⽅法。
- 创建 AOP 代理对象:结合 Model 需要进⾏ AOP 的⽅法,选择 Cglib 或 JDK,创建 AOP 代理对象。
第三块就是执⾏切⾯,通过“责任链 + 递归”,去执⾏切⾯。
三:源码解读
注意:Spring 的版本是 5.2.15.RELEASE,否则和我的代码不⼀样!!!
3.1 代码⼊⼝
这⾥需要多跑⼏次,把前⾯的 beanName 跳过去,只看 Model。
进⼊ doGetBean(),进⼊创建 Bean 的逻辑。
3.2 前置处理
主要就是遍历切⾯,放⼊缓存。
这⾥是重点!敲⿊板!!!
- 我们会先遍历所有的类;
- 判断是否切⾯,只有切⾯才会进⼊后⾯逻辑;
- 获取每个 Aspect 的切⾯列表;
- 保存 Aspect 的切⾯列表到缓存 advisorsCache 中。
到这⾥,获取切⾯信息的流程就结束了,因为后续对切⾯数据的获取,都是从缓存 advisorsCache 中拿到。
下⾯就对上⾯的流程,再深⼊解读⼀下。
3.2.1 判断是否是切⾯
上图的第 2 步,逻辑如下:
3.2.2 获取切⾯列表
进⼊到 getAdvice(),⽣成切⾯信息。
3.3 后置处理![](https://i-blog.csdnimg.cn/blog_migrate/fbe88a2ed84ae75dbc08d24901ecf0e9.png)
主要就是从缓存拿切⾯,和 model 的⽅法匹配,并创建 AOP 代理对象。
进⼊ doCreateBean(),⾛下⾯逻辑。
这⾥是重点!敲⿊板!!!
- 先获取 louzai 类的所有切⾯列表;
- 创建⼀个 AOP 的代理对象。
3.3.1 获取切⾯
我们先进⼊第⼀步,看是如何获取 model 的切⾯列表。
进⼊ buildAspectJAdvisors(),这个⽅法应该有印象,就是前⾯将切⾯信息放⼊缓存 advisorsCache 中,现在这⾥ 就是要获取缓存。
再回到 findEligibleAdvisors(),从缓存拿到所有的切⾯信息后,继续往后执⾏。
3.3.2 创建代理对象
有了 model 的切⾯列表,后⾯就可以开始去创建 AOP 代理对象。
这⾥是重点!敲⿊板!!!
我们再回到创建代理对象的⼊⼝,看看创建的代理对象。
3.4 切⾯执⾏
通过 “责任链 + 递归”,执⾏切⾯和⽅法。 这⾥有 2 种创建 AOP 代理对象的⽅式,我们是选⽤ Cglib 来创建。
前⽅⾼能!这块逻辑⾮常复杂!!!
下⾯就是“执⾏切⾯”最核⼼的逻辑,简单说⼀下设计思路:
- 设计思路:采⽤递归 + 责任链的模式;
- 递归:反复执⾏ CglibMethodInvocation 的 proceed();
- 退出递归条件:interceptorsAndDynamicMethodMatchers 数组中的对象,全部执⾏完毕;
- 责任链:示例中的责任链,是个⻓度为 3 的数组,每次取其中⼀个数组对象,然后去执⾏对象的 invoke()。
因为我们数组⾥⾯只有 3 个对象,所以只会递归 3 次,下⾯就看这 3 次是如何递归,责任链是如何执⾏的,设计得 很巧妙!
3.4.1 第⼀次递归
数组的第⼀个对象是 ExposeInvocationInterceptor,执⾏ invoke(),注意⼊参是 CglibMethodInvocation。
⾥⾯啥都没⼲,继续执⾏ CglibMethodInvocation 的 process()。
3.4.2 第⼆次递归
数组的第⼆个对象是 MethodBeforeAdviceInterceptor,执⾏ invoke()。
3.4.3 第三次递归
数组的第⼆个对象是 AfterReturningAdviceInterceptor,执⾏ invoke()。
执⾏完上⾯逻辑,就会退出递归,我们看看 invokeJoinpoint() 的执⾏逻辑,其实就是执⾏主⽅法。
再回到第三次递归的⼊⼝,继续执⾏后⾯的切⾯。
切⾯执⾏逻辑,前⾯已经演示过,直接看执⾏⽅法。
后⾯就依次退出递归,整个流程结束。
3.4.4 设计思路
这块代码,研究了很长时间,因为这个不是单纯的责任链模式。
单纯的责任链模式,对象内部有⼀个⾃身的 next 对象,执⾏完当前对象的⽅法末尾,就会启动 next 对象的执⾏, 直到最后⼀个 next 对象执⾏完毕,或者中途因为某些条件中断执⾏,责任链才会退出。
这⾥ CglibMethodInvocation 对象内部没有 next 对象,全程是通过 interceptorsAndDynamicMethodMatchers ⻓度为 3 的数组控制,依次去执⾏数组中的对象,直到最后⼀个对象执⾏完毕,责任链才会退出。
这个也属于责任链,只是实现⽅式不⼀样,后⾯会详细剖析,下⾯再讨论⼀下,这些类之间的关系。
我们的主对象是 CglibMethodInvocation,继承于 ReflectiveMethodInvocation,然后 process() 的核⼼逻辑,其 实都在 ReflectiveMethodInvocation 中。
ReflectiveMethodInvocation 中的 process() 控制整个责任链的执⾏。
ReflectiveMethodInvocation 中的 process() ⽅法,⾥⾯有个⻓度为 3 的数组 interceptorsAndDynamicMethodMatchers,⾥⾯存储了 3 个对象,分别为
- ExposeInvocationInterceptor、
- MethodBeforeAdviceInterceptor、
- AfterReturningAdviceInterceptor。
注意!!!这 3 个对象,都是继承 MethodInterceptor 接⼝。
然后每次执⾏ invoke() 时,⾥⾯都会去执⾏ CglibMethodInvocation 的 process()。
是不是晦涩难懂?甭着急,这里我们重新再梳理⼀下。
对象和⽅法的关系:
- 接⼝继承:数组中的 3 个对象,都是继承 MethodInterceptor 接⼝,实现⾥⾯的 invoke() ⽅法;
- 类继承:我们的主对象 CglibMethodInvocation,继承于 ReflectiveMethodInvocation,复⽤它的 process() ⽅法;
- 两者结合(策略模式):invoke() 的⼊参,就是 CglibMethodInvocation,执⾏ invoke() 时,内部会执⾏ CglibMethodInvocation.process(),这个其实就是个策略模式。
可能有同学会说,invoke() 的⼊参是 MethodInvocation,没错!但是 CglibMethodInvocation 也继承了 MethodInvocation,不信⾃⼰可以去看。
执⾏逻辑:
- 程序⼊⼝:是 CglibMethodInvocation 的 process() ⽅法;
- 链式执⾏(衍⽣的责任链模式):process() 中有个包含 3 个对象的数组,依次去执⾏每个对象的 invoke() ⽅ 法。
- 递归(逻辑回退):invoke() ⽅法会执⾏切⾯逻辑,同时也会执⾏ CglibMethodInvocation 的 process() ⽅ 法,让逻辑再⼀次进⼊ process()。
- 递归退出:当数字中的 3 个对象全部执⾏完毕,流程结束。
所以这⾥设计巧妙的地⽅,是因为纯粹责任链模式,⾥⾯的 next 对象,需要保证⾥⾯的对象类型完全相同。
但是数组⾥⾯的 3 个对象,⾥⾯没有 next 成员对象,所以不能直接⽤责任链模式,那怎么办呢?就单独搞了⼀个 CglibMethodInvocation.process(),通过去⽆限递归 process(),来实现这个责任链的逻辑。
这就是我们为什么要看源码,学习⾥⾯优秀的设计思路!
四. 总要有总结
我们再⼩节⼀下,⽂章先介绍了什么是 AOP,以及 AOP 的原理和示例。
之后再剖析了 AOP 的源码,分为 3 块:
- 将所有的切⾯都保存在缓存中;
- 取出缓存中的切⾯列表,和 louzai 对象的所有⽅法匹配,拿到属于 louzai 的切⾯列表;
- 创建 AOP 代理对象;
- 通过“责任链 + 递归”,去执⾏切⾯逻辑。
最难的地⽅还是抠 “切⾯执⾏”的设计思路,虽然流程能⾛通,但是把整个设计思想能总结出来,并讲通俗易懂还是有难度的。