写点什么

Spring 源码解析 -Spring AOP

作者:Java你猿哥
  • 2023-03-14
    湖南
  • 本文字数:4289 字

    阅读完需:约 14 分钟

Spring源码解析-Spring 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@Servicepublic class Model {    public void everyDay() {        System.out.println("睡觉");    }}
复制代码


添加 ModelAspect 切⾯:


@Aspect@Componentpublic 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 前置处理


主要就是遍历切⾯,放⼊缓存。







这⾥是重点!敲⿊板!!!

  • 1. 我们会先遍历所有的类;

  • 2. 判断是否切⾯,只有切⾯才会进⼊后⾯逻辑;

  • 3. 获取每个 Aspect 的切⾯列表;

  • 4. 保存 Aspect 的切⾯列表到缓存 advisorsCache 中。


到这⾥,获取切⾯信息的流程就结束了,因为后续对切⾯数据的获取,都是从缓存 advisorsCache 中拿到

下⾯就对上⾯的流程,再深⼊解读⼀下。

3.2.1 判断是否是切⾯

上图的第 2 步,逻辑如下:



3.2.2 获取切⾯列表





进⼊到 getAdvice(),⽣成切⾯信息。



3.3 后置处理


主要就是从缓存拿切⾯,和 model 的⽅法匹配,并创建 AOP 代理对象。


进⼊ doCreateBean(),⾛下⾯逻辑。





这⾥是重点!敲⿊板!!!

  1. 先获取 louzai 类的所有切⾯列表;

  2. 创建⼀个 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 代理对象;

  • 通过“责任链 + 递归”,去执⾏切⾯逻辑。

最难的地⽅还是抠 “切⾯执⾏”的设计思路,虽然流程能⾛通,但是把整个设计思想能总结出来,并讲通俗易懂还是有难度的。


用户头像

Java你猿哥

关注

一只在编程路上渐行渐远的程序猿 2023-03-09 加入

关注我,了解更多Java、架构、Spring等知识

评论

发布
暂无评论
Spring源码解析-Spring AOP_Java_Java你猿哥_InfoQ写作社区