绝了,76张手绘图,彻底剖析清楚Spring AOP
下面我会简单介绍一下 AOP 的基础知识,以及使用方法,然后直接对源码进行拆解。不 BB,上文章目录。
![](https://filescdn.proginn.com/d71c9c537a2288751e25d36865a5c26b/dd57c218cda3b1fe6dbb076e0edea2aa.webp)
1. 基础知识
1.1 什么是 AOP ?
AOP 的全称是 “Aspect Oriented Programming”,即面向切面编程。
在 AOP 的思想里面,周边功能(比如性能统计,日志,事务管理等)被定义为切面,核心功能和切面功能分别独立进行开发,然后把核心功能和切面功能“编织”在一起,这就叫 AOP。
AOP 能够将那些与业务无关,却为业务模块所共同调用的逻辑封装起来,便于减少系统的重复代码,降低模块间的耦合度,并有利于未来的可拓展性和可维护性。
1.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) :把整个目标方法包裹起来,在被调用前和调用之后分别调用通知功能。
1.3 AOP 简单示例
新建 Louzai 类:
@Data
@Service
public class Louzai {
public void everyDay() {
System.out.println("睡觉");
}
}
添加 LouzaiAspect 切面:
@Aspect
@Component
public class LouzaiAspect {
@Pointcut("execution(* com.java.Louzai.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");
Louzai louzai = (Louzai) context.getBean("louzai");
louzai.everyDay();
}
}
输出:
吃饭
睡觉
打豆豆。。。
这个示例非常简单,“睡觉” 加了前置和后置通知,但是 Spring 在内部是如何工作的呢?
1.4 Spring AOP 工作流程
为了方便大家能更好看懂后面的源码,我先整体介绍一下源码的执行流程,让大家有一个整体的认识,否则容易被绕进去。
整个 Spring AOP 源码,其实分为 3 块,我们会结合上面的示例,给大家进行讲解。
![](https://filescdn.proginn.com/193178589e4028079113962e54f0856c/cd18a9ad991e871efd80be9235b5b47a.webp)
第一块就是前置处理,我们在创建 Louzai Bean 的前置处理中,会遍历程序所有的切面信息,然后将切面信息保存在缓存中,比如示例中 LouzaiAspect 的所有切面信息。
第二块就是后置处理,我们在创建 Louzai Bean 的后置处理器中,里面会做两件事情:
获取 Louzai 的切面方法:首先会从缓存中拿到所有的切面信息,和 Louzai 的所有方法进行匹配,然后找到 Louzai 所有需要进行 AOP 的方法。 创建 AOP 代理对象:结合 Louzai 需要进行 AOP 的方法,选择 Cglib 或 JDK,创建 AOP 代理对象。
![](https://filescdn.proginn.com/1d4ae1eff66206a19980ccd33b48907a/ff1eadeb35a82a77c33a9715e760d100.webp)
第三块就是执行切面,通过“责任链 + 递归”,去执行切面。
2. 源码解读
注意:Spring 的版本是 5.2.15.RELEASE,否则和我的代码不一样!!!
除了原理部分,上面的知识都不难,下面才是我们的重头戏,让你跟着楼仔,走一遍代码流程。
2.1 代码入口
![](https://filescdn.proginn.com/ae077a1ebaf9d3425097c8ff941d6841/9f73d19c1fbc7bbfee61e960ffcf48c6.webp)
![](https://filescdn.proginn.com/426d4248405bdc261f25779f7d4b3418/3fefa027944bf5e48cc32ecc03e99ee3.webp)
这里需要多跑几次,把前面的 beanName 跳过去,只看 louzai。
![](https://filescdn.proginn.com/72ff6443dfbd0c3325e6d157cd7ba929/d157d503ff50b22eba690fac3e21ceed.webp)
![](https://filescdn.proginn.com/7ed7fe2e5c163b44a33594810aa20053/764bde0289604cf369334338499be90c.webp)
进入 doGetBean(),进入创建 Bean 的逻辑。
![](https://filescdn.proginn.com/359faf57e32657967857479b080c16d3/714fcb431a22ecd0c6474caa7257c5eb.webp)
2.2 前置处理
![](https://filescdn.proginn.com/634181418d350eafa0ec2f1a58675e86/7da1b0e060e2a4b432e5a716040b348d.webp)
主要就是遍历切面,放入缓存。
![](https://filescdn.proginn.com/b036c9ec62148ae201ccc921a1544ceb/524cb9fc6bed4f416767e2debaa6ca52.webp)
![](https://filescdn.proginn.com/4687d9fee45ae6ae332a0e5c243f31c5/da6ee07c01dd81442512e9fd384eb59b.webp)
![](https://filescdn.proginn.com/bb0f7fcc1aba039e777d74f0878f8a76/149ded53d12f986ee29b1380a6d19c11.webp)
![](https://filescdn.proginn.com/7f9e3cc4c776a0cc3d5da4f4ff278525/93c9458c190ad1276e73f4960d1c81e8.webp)
![](https://filescdn.proginn.com/455aa07dd95757173740b38a31ecf288/81f5184c72ecdb1119bc8d826b2c7aba.webp)
![](https://filescdn.proginn.com/8cf2489c86f2b68c8612742bdad8fa34/bd6fea410bf63854eab97ada10222981.webp)
这里是重点!敲黑板!!!
我们会先遍历所有的类; 判断是否切面,只有切面才会进入后面逻辑; 获取每个 Aspect 的切面列表; 保存 Aspect 的切面列表到缓存 advisorsCache 中。
![](https://filescdn.proginn.com/2ce39a7bd143d3ad3bdb032c5739a7a8/c672d6016fa8524f93809d34b6ab66c6.webp)
到这里,获取切面信息的流程就结束了,因为后续对切面数据的获取,都是从缓存 advisorsCache 中拿到。
下面就对上面的流程,再深入解读一下。
2.2.1 判断是否是切面
上图的第 2 步,逻辑如下:
![](https://filescdn.proginn.com/ce5824ff00a41c071002674b2107e43e/e6bac1a52c1d49788ded58b91b7c4fc6.webp)
2.2.2 获取切面列表
![](https://filescdn.proginn.com/12d1b0214299be8fea9c12350ac0efc4/8dcabae6e6c6c37b8c6922e0c2c9b85c.webp)
![](https://filescdn.proginn.com/926274bc6def8c94510cd27d3f0e5249/cfe6d51964d2017210c1949765e81deb.webp)
![](https://filescdn.proginn.com/3674c49ca3908c65e90074c89dba95af/64705478dbf1e04a6e24f97414a0bf06.webp)
![](https://filescdn.proginn.com/6c46040889d9f6c4503a22b5733b40f8/ce79b4287f890212f9e0efc9af7253e9.webp)
进入到 getAdvice(),生成切面信息。
![](https://filescdn.proginn.com/5340a50966506c4f973d88afbfec037a/6d76e0efc145d0de5b3e8470b4f2b98a.webp)
2.3 后置处理
![](https://filescdn.proginn.com/c38a61280260dbb5c793aaf804927f69/0aa6ff257b529949e7f4cdcbac5c9271.webp)
主要就是从缓存拿切面,和 louzai 的方法匹配,并创建 AOP 代理对象。
![](https://filescdn.proginn.com/f90750bb5111c494bd2253fc1002e941/f1c695afa9c6fc1c0ef2eabc58ccc7ee.webp)
进入 doCreateBean(),走下面逻辑。
![](https://filescdn.proginn.com/50e8547a59a40ba7be3ff9d7fdd98c6e/02f57c3ab456815bfe8c599649de8214.webp)
![](https://filescdn.proginn.com/ce48ffc0fc8c106c6c137eb913126681/8b593eed47e2b30f89f029047befb181.webp)
![](https://filescdn.proginn.com/c757d113f137dc08e4e987be567da335/c1c55cb1f222bcf54a90ff2853f4bc9e.webp)
![](https://filescdn.proginn.com/cea16001382fc645309051a8c3477fb2/dda6499b5dc8d62f7a246c03987f4b32.webp)
这里是重点!敲黑板!!!
先获取 louzai 类的所有切面列表; 创建一个 AOP 的代理对象。
![](https://filescdn.proginn.com/82cd06e14a519865ffa7fa8a7908bd79/895ef40f124d2b6d3bb65c40b0d8ae16.webp)
2.3.1 获取切面
我们先进入第一步,看是如何获取 louzai 的切面列表。
![](https://filescdn.proginn.com/801c1093e73ee7903b9d10ec4c35f10b/c83285a07d3776839933625a0aca11b3.webp)
![](https://filescdn.proginn.com/6918a53edcd798528e928c6d6df695e2/8815486d2cde49a5c5506d8d9d705647.webp)
![](https://filescdn.proginn.com/f93a62c86966a7764dac5c7d3bfa86bb/430d1fec8d765b2ab68941ad0f8da0f7.webp)
进入 buildAspectJAdvisors(),这个方法应该有印象,就是前面将切面信息放入缓存 advisorsCache 中,现在这里就是要获取缓存。
![](https://filescdn.proginn.com/2f12f20bb0f7c419421f4aa752ea28fc/7fd91945f6493f455fe927694dd8faee.webp)
![](https://filescdn.proginn.com/819b8f337d9bf66bcdc8430de31289df/1796e44ee7a5ad8c29122b4c6e9dcc14.webp)
再回到 findEligibleAdvisors(),从缓存拿到所有的切面信息后,继续往后执行。
![](https://filescdn.proginn.com/70e9d899202ff6b42130c76ee5947f20/5975bf9bb85ec816f6da9653494505ac.webp)
![](https://filescdn.proginn.com/c83ac3b5c454422f19fea1a0b082aa88/7ebb5b83d9c82d6c206684d9daa489c1.webp)
![](https://filescdn.proginn.com/57664e7c2ab7fd2850224e88d35f0cca/e211f140512c6ae1e2584589db33d800.webp)
![](https://filescdn.proginn.com/255893d1e71b78efd290fe49b35b83d8/8fcd562d23a8780cfd8bf302bf022320.webp)
2.3.2 创建代理对象
有了 louzai 的切面列表,后面就可以开始去创建 AOP 代理对象。
![](https://filescdn.proginn.com/87e359831798dd88d196c25ea5f74671/4db3ab4d4d92e9170d35f35341168f15.webp)
![](https://filescdn.proginn.com/757a91bb82fa3e4234ab416dff55c301/8aa9c985d00c9029abcbaa764d25f2ad.webp)
![](https://filescdn.proginn.com/6429275fdffbef7e40b9938681f4fc60/fdd7200878db77e0297dbe4c06c0669a.webp)
这里是重点!敲黑板!!!
这里有 2 种创建 AOP 代理对象的方式,我们是选用 Cglib 来创建。
![](https://filescdn.proginn.com/3880b6dc4e57b3e29deeba3a1948538e/746f7e38b78496c799c7b8c529a4c74c.webp)
![](https://filescdn.proginn.com/84ae255acef658d1d230dc7d03dda032/d92ea30807e9144e5421e2661e11a027.webp)
![](https://filescdn.proginn.com/ae41d3c00b2ee9ce96d570c9c017a9f2/6457d4d90ae7920874a0221a7d584efa.webp)
我们再回到创建代理对象的入口,看看创建的代理对象。
![](https://filescdn.proginn.com/e51da78911c02cc14f67a45c2d4cfc98/a7adbd3ebdf104275ffeb05cb0de6992.webp)
2.4 切面执行
![](https://filescdn.proginn.com/3020d270b661b51fa894d1595c4ea309/a56cff02d137ec20ec9c1e246b29e08a.webp)
通过 “责任链 + 递归”,执行切面和方法。
![](https://filescdn.proginn.com/6000cbe81685fee8e98e8ecaa771af8c/b8c8bc09445fbb1fd7bebf1c31f2aa6d.webp)
![](https://filescdn.proginn.com/84a1d4b34d119ab6eb88f375b4ac96a9/a00cb0137d64bbee42d5eb83898d24fe.webp)
前方高能!这块逻辑非常复杂!!!
![](https://filescdn.proginn.com/efdf3d914b564031ff4f8ce44da7af54/f4087ba17b04735770a0b006bd90bb53.webp)
下面就是“执行切面”最核心的逻辑,简单说一下设计思路:
设计思路:采用递归 + 责任链的模式; 递归:反复执行 CglibMethodInvocation 的 proceed(); 退出递归条件:interceptorsAndDynamicMethodMatchers 数组中的对象,全部执行完毕; 责任链:示例中的责任链,是个长度为 3 的数组,每次取其中一个数组对象,然后去执行对象的 invoke()。
![](https://filescdn.proginn.com/7a6a32859e25717148b7e012bf374044/cc9bd35f9d64f33d725012decb6cc23e.webp)
因为我们数组里面只有 3 个对象,所以只会递归 3 次,下面就看这 3 次是如何递归,责任链是如何执行的,设计得很巧妙!
2.4.1 第一次递归
数组的第一个对象是 ExposeInvocationInterceptor,执行 invoke(),注意入参是 CglibMethodInvocation。
![](https://filescdn.proginn.com/eeb547f63d39d7ee58c549e848fc75a2/12092a89ab713ea9b60012ad9d294aba.webp)
里面啥都没干,继续执行 CglibMethodInvocation 的 process()。
![](https://filescdn.proginn.com/268dadd3e28bf227171df5d5bae11988/7e8e982b28377c9eee4a7e1b08f6811c.webp)
![](https://filescdn.proginn.com/3074e863f926e1689d21d35267a73eca/a878945a2b0301574f0190d0a9b8ea8f.webp)
2.4.2 第二次递归
数组的第二个对象是 MethodBeforeAdviceInterceptor,执行 invoke()。
![](https://filescdn.proginn.com/cd6f246d1886ba2f889fcb5c6eb2c2a7/d39412113d42555a65ffc40fa604bfed.webp)
![](https://filescdn.proginn.com/3243c7729c0928ce3e74095bb0a3b45a/6ec8f49eb48b61cfc1056615eb89a3ba.webp)
![](https://filescdn.proginn.com/1e77084df0c52fd7d1baa43dc7579036/b3ed1d85ff3a3c6e9adce6dd567c33ed.webp)
![](https://filescdn.proginn.com/2692068d8412b59803a2fe52f4ec302b/767f2d4c72e7063e84b7299406b5eea2.webp)
![](https://filescdn.proginn.com/59db5ddc9fd4207544f5bed7361b9fea/a41a032af8c61c42d8664a6c52d112f1.webp)
![](https://filescdn.proginn.com/85bb30a7bb179db2b23dbebe36a35bcb/d884471122c8f918d273163133eca22e.webp)
![](https://filescdn.proginn.com/d5dd1536977b7736e9d500d54fc16490/22171026563f1b31704c6a8f6176ad98.webp)
![](https://filescdn.proginn.com/5a9016a6ebcbbd54ffd9b1621883ce2e/32b73686f85293b6631c9c1a5f55e9b7.webp)
![](https://filescdn.proginn.com/df98813e951bea46d2ce955496b6cd60/fd7903a5169b94affe13802b351977d5.webp)
2.4.3 第三次递归
数组的第二个对象是 AfterReturningAdviceInterceptor,执行 invoke()。
![](https://filescdn.proginn.com/928d92bd510ec73c3e87c652fe906581/39b0b8fd383b6ea28286afd37bd669b4.webp)
![](https://filescdn.proginn.com/42cdbf272949a3819f534ac4c1c1cc96/39e74c70d88b9302a8347c04204dcbc5.webp)
![](https://filescdn.proginn.com/a597530f36a1b2b09dcd03ea07b41eb4/f1dbd2d8549441329c11bf2a28812034.webp)
执行完上面逻辑,就会退出递归,我们看看 invokeJoinpoint() 的执行逻辑,其实就是执行主方法。
![](https://filescdn.proginn.com/a83bc9356068dafe7ea968133368bc72/f5a354ff3ab26a769f0814566f4b53db.webp)
![](https://filescdn.proginn.com/c6f999d1306a615ddaaf8edad5718942/fe09653df1f3552ec54fe8111a2ef8f8.webp)
![](https://filescdn.proginn.com/06874ad8e898254d84b8dad26774e747/b8c664c4498217cd7afe66723dfa9845.webp)
再回到第三次递归的入口,继续执行后面的切面。
![](https://filescdn.proginn.com/87b12f3d457edc54906ebf33562b4865/86aee7c0a27e20d0e39b5ebc8dc91368.webp)
切面执行逻辑,前面已经演示过,直接看执行方法。
![](https://filescdn.proginn.com/07ab3a4644b0a3a905b4ab9634629c22/9ff6cc844a1045470dd8ec1ec0dcc9e3.webp)
后面就依次退出递归,整个流程结束。
2.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 接口。
![](https://filescdn.proginn.com/53d5c9e87f4caad0a0e76f812fb389e6/b01c5f41fc5ffcaa858b41ab4db09e37.webp)
然后每次执行 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(),来实现这个责任链的逻辑。
这就是我们为什么要看源码,学习里面优秀的设计思路!
3. 总结
我们再小节一下,文章先介绍了什么是 AOP,以及 AOP 的原理和示例。之后再剖析了 AOP 的源码,分为 3 块:
将所有的切面都保存在缓存中; 取出缓存中的切面列表,和 louzai 对象的所有方法匹配,拿到属于 louzai 的切面列表; 创建 AOP 代理对象; 通过“责任链 + 递归”,去执行切面逻辑。
今天的源码解析就到这,Spring 相关的源码,还有哪些是大家想学习的呢,可以留言。
没有什么使我停留——除了目的,纵然岸旁有玫瑰、有绿荫、有宁静的港湾,我是不系之舟。
今年这情况,拿到这俩offer不错了 愤怒,竟然还有学校在教JSP 曝光秋招毁约公司 垃圾外包,离职也罢 非科班转码 推荐 10 个神级 Intellij IDEA 插件 美团率先开奖 24k,不甘心? Fleet,Java 轻量级 IDE 的未来? 先不管那么多,offer 接了再说 一套 KTV 管理系统,估价 3 万还是 30 万?
![](https://filescdn.proginn.com/ac84a9c74639c4279a185258433bb3b5/465e8e7379a13be430dc75aa7bd03155.webp)