前言

本文是「如何实现一个简易版的 Spring 系列」的第五篇,在之前介绍了 Spring 中的核心技术之一 IoC,从这篇开始我们再来看看 Spring 的另一个重要的技术——AOP。用过 Spring 框架进行开发的朋友们相信或多或少应该接触过 AOP,用中文描述就是面向切面编程。学习一个新技术了解其产生的背景是至关重要的,在刚开始接触 AOP 时不知道你有没有想过这个问题,既然在面向对象的语言中已经有了 OOP 了,为什么还需要 AOP 呢?换个问法也就是说在 OOP 中有哪些场景其实处理得并不优雅,需要重新寻找一种新的技术去解决处理?(P.S. 这里建议暂停十秒钟,自己先想一想...)

为什么需要 AOP

我们做软件开发的最终目的是为了解决公司的各种需求,为业务赋能,注意,这里的需求包含了业务需求和系统需求,对于绝大部分的业务需求的普通关注点,都可以通过面向对象(OOP)的方式对其进行很好的抽象、封装以及模块化,但是对于系统需求使用面向对象的方式虽然很好的对其进行分解并对其模块化,但是却不能很好的避免这些类似的系统需求在系统的各个模块中到处散落的问题。

如何实现一个简易版的 Spring - 如何实现 AOP(上)-LMLPHP

因此,需要去重新寻找一种更好的办法,可以在基于 OOP 的基础上提供一套全新的方法来处理上面的问题,或者说是对 OOP 面向对象的开发模式做一个补充,使其可以更优雅的处理上面的问题,迄今为止 Spring 提供一个的解决方案就是面向切面编程——AOP。有了 AOP 后,我们可以将这些事务管理、系统日志以及安全检查等系统需求(横切关注点:cross-cutting concern)进行模块化的组织,使得整个系统更加的模块化方便后续的管理和维护。细心的你应该发现在 AOP 里面引入了一个关键的抽象就是切面(Aspect),用于对于系统中的一些横切关注点进行封装,要明确的一点是 AOP 和 OOP 不是非此即彼的对立关系,AOP 是对 OOP 的一种补充和完善,可以相互协作来完成需求,Aspect 对于 AOP 的重要程度就像 Class 对 OOP 一样。

如何实现一个简易版的 Spring - 如何实现 AOP(上)-LMLPHP

几个重要的概念

我们最终的目的是要模仿 Spring 框架自己去实现一个简易版的 AOP 出来,虽然是简易版但是会涉及到 Spring AOP 中的核心思想和主要实现步骤,不过在此之前先来看看 AOP 中的重要概念,同时也是为以后的实现打下理论基础,这里需要说明一点是我不会使用中文翻译去描述这些 AOP 定义的术语(另外,业界 AOP 术语本来就不太统一),你需要重点理解的是术语在 AOP 中代表的含义,就像我们不会把 Spring 给翻译成春天一样,在软件开发交流你知道它表示一个 Java 开发框架就可以了。下面对其关键术语进行逐个介绍:

Joinpoint

通过之前的介绍可知,在我们的系统运行之前,需要将 AOP 定义的一些横切关注点(功能模块)织入(可以简单理解为嵌入)到系统的一些业务模块当中去,想要完成织入的前提是我们需要知道可以在哪些执行点上进行操作,这些执行点就是 Joinpoint。下面看个简单示例:

/**
 * @author mghio
 * @since 2021-05-22
 */
public class Developer {

  private String name;

  private Integer age;

  private String siteUrl;

  private String position;

  public Developer(String name, String siteUrl) {
    this.name = name;
    this.siteUrl = siteUrl;
  }

  public void setSiteUrl(String siteUrl) {
    this.siteUrl = siteUrl;
  }

  public void setAge(Integer age) {
    this.age = age;
  }

  public void setName(String name) {
    this.name = name;
  }

  public void setPosition(String position) {
    this.position = position;
  }

  public void showMainIntro() {
    System.out.printf("name:[%s], siteUrl:[%s]\n", this.name, this.siteUrl);
  }

  public void showAllIntro() {
    System.out.printf("name:[%s], age:[%s], siteUrl:[%s], position:[%s]\n",
        this.name, this.age, this.siteUrl, this.position);
  }

}
/**
 * @author mghio
 * @since 2021-05-22
 */
public class DeveloperTest {

  @Test
  public void test() {
    Developer developer = new Developer("mghio", "https://www.mghio.cn");
    developer.showMainIntro();
    developer.setAge(18);
    developer.setPosition("中国·上海");
    developer.showAllIntro();
  }

}

理论上,在上面示例的这个 test() 方法调用中,我们可以选择在 Developer 的构造方法执行时进行织入,也可以在 showMainIntro() 方法的执行点上进行织入(被调用的地方或者在方法内部执行的地方),或者在 setAge() 方法设置 sge 字段时织入,实际上,只要你想可以在 test() 方法的任何一个执行点上执行织入,这些可以织入的执行点就是 Joinpoint。
这么说可能比较抽象,下面通过 test() 方法调用的时序图来直观的看看:

如何实现一个简易版的 Spring - 如何实现 AOP(上)-LMLPHP

从方法执行的时序来看不难发现,会有如下的一些常见的 Joinpoint 类型:

  • 构造方法调用(Constructor Call)。对某个对象调用其构造方法进行初始化的执行点,比如以上代码中的 Developer developer = new Developer("mghio", "https://www.mghio.cn");。
  • 方法调用(Method call)。调用某个对象的方法时所在的执行点,实际上构造方法调用也是方法调用的一种特殊情况,只是这里的方法是构造方法而已,比如示例中的 developer.showMainIntro(); 和 developer.showAllIntro(); 都是这种类型。
  • 方法执行(Method execution)。当某个方法被调用时方法内部所处的程序的执行点,这是被调用方法内部的执行点,与方法调用不同,方法执行入以上方法时序图中标注所示。
  • 字段设置(Field set)。调用对象 setter 方法设置对象字段的代码执行点,触发点是对象的属性被设置,和设置的方式无关。以上示例中的 developer.setAge(18); 和 developer.setPosition("中国.上海"); 都是这种类型。
  • 类初始化(Class initialization)。类中的一些静态字段或者静态代码块的初始化执行点,在以上示例中没有体现。
  • 异常执行(Exception execution)。类的某些方法抛出异常后对应的异常处理逻辑的执行点,在以上示例中没有这种类型。

虽然理论上,在程序执行中的任何执行点都可以作为 Joinpoint,但是在某些类型的执行点上进行织入操作,付出的代价比较大,所以在 Spring 中的 Joinpoint 只支持方法执行(Method execution)这一种类型(这一点从 Spring 的官方文档上也有说明),实际上这种类型就可以满足绝大部分的场景了。

Pointcut

Pointcut 表示的是一类 Jointpoint 的表述方式,在进行织入时需要根据 Pointcut 的配置,然后往那些匹配的 Joinpoint 织入横切的逻辑。这里面临的第一个问题:用人类的自然语言可以很快速的表述哪些我们需要织入的 Joinpoint,但是在代码里要如何去表述这些 Joinpoint 呢?
目前有如下的一些表述 Joinpoint 定义的方式:

  • 直接指定织入的方法名。显而易见,这种表述方式虽然简单,但是所支持的功能比较单一,只适用于方法类型的 Joinpoint,而且当我们系统中需要织入的方法比较多时,一个一个的去定义织入的 Pointjoint 时过于麻烦。
  • 正则表达式方式。正则表达式相信大家都有一些了解,功能很强大,可以匹配表示多个不同方法类型的 Jointpoint,Spring 框架的 AOP 也支持这种表述方式。
  • Pointcut 特定语言方式。这个因为是一种特定领域语言(DSL),所以其提供的功能也是最为灵活和丰富的,这也导致了不管其使用和实现复杂度都比较高,像 AspectJ 就是使用的这种表述方式,当然 Spring 也支持。

另外 Pointcut 也支持进行一些简单的逻辑运算,这时我们就可以将多个简单的 Pointcut 通过逻辑运算组合为一个比较复杂的 Pointcut 了,比如在 Spring 配置中的 and 和 or 等逻辑运算标识符以及 AspectJ 中的 && 和 || 等逻辑运算符。

Advice

Advice 表示的是一个注入到 Joinpoint 的横切逻辑,是一个横切关注点逻辑的抽象载体。按照 Advice 的执行点的位置和功能的不同,分为如下几种主要的类型:

  • Before Advice。Before Advice 表示是在匹配的 Joinpoint 位置之前执行的类型。如果被成功织入到方法类型的 Joinpoint 中,那么 Beofre Advice 就会在这个方法执行之前执行,还有一点需要注意的是,如果需要在 Before Advice 中结束方法的执行,我们可以通过在 Advice 中抛出异常的方式来结束方法的执行。
  • After Advice。显而易见,After Advice 表示在配置的 Joinpoint 位置之后执行的类型。可以在细分为 After returning Advice、After throwing Advice 和 After finally Advice 三种类型。其中 After returning Advice 表示的是匹配的 Joinpoint 方法正常执行完成(没有抛出异常)后执行;After throwing Advice 表示匹配的 Joinpoint 方法执行过程中抛出异常没有正常返回后执行;After finally Advice 表示方法类型的 Joinpoint 的不管是正常执行还是抛出异常都会执行。
    这几种 Advice 类型在方法类型的 Joinpoint 中执行顺序如下图所示:
    如何实现一个简易版的 Spring - 如何实现 AOP(上)-LMLPHP
  • Around Advice。这种类型是功能最为强大的 Advice,可以匹配的 Joinpoint 之前、之后甚至终端原来 Joinpoint 的执行流程,正常情况下,会先执行 Joinpoint 之前的执行逻辑,然后是 Joinpoint 自己的执行流程,最后是执行 Joinpoint 之后的执行逻辑。细心的你应该发现了,这不就是上面介绍的 Before Advice 和 After Advice 类型的组合吗,是的,它可以完成这两个类型的功能,不过还是要根据具体的场景选择合适的 Advice 类型。

Aspect

Aspect 是对我们系统里的横切关注点(crosscutting concern)包装后的一个抽象概念,可以包含多个 Joinpoint 以及多个 Advice 的定义。Spring 集成了 AspectJ 后,也可以使用 @AspectJ 风格的声明式指定一个 Aspect,只要添加 @Aspect 注解即可。

Target object

目标对象一般是指那些可以匹配上 Pointcut 声明条件,被织入横切逻辑的对象,正常情况下是由 Pointcut 来确定的,会根据 Pointcut 设置条件的不同而不同。
有了 AOP 这些概念后就可以把上文的例子再次进行整理,各个概念所在的位置如下图所示:

如何实现一个简易版的 Spring - 如何实现 AOP(上)-LMLPHP

总结

本文首先对 AOP 技术的诞生背景做了简要介绍,后面介绍了 AOP 的几个重要概念为后面我们自己实现简易版 AOP 打下基础,AOP 是对 OOP 的一种补充和完善,文中列出的几个概念只是 AOP 中涉及的概念中的冰山一角,想要深入了解更多的相关概念的朋友们可以看 官方文档 学习,下篇是介绍 AOP 实现依赖的一些基础技术,敬请期待。转发、分享都是对我的支持,我将更有动力坚持原创分享!

05-23 14:10