前言
本文是「如何实现一个简易版的 Spring
系列」的第五篇,在之前介绍了 Spring
中的核心技术之一 IoC
,从这篇开始我们再来看看 Spring
的另一个重要的技术——AOP。用过 Spring
框架进行开发的朋友们相信或多或少应该接触过 AOP
,用中文描述就是面向切面编程。学习一个新技术了解其产生的背景是至关重要的,在刚开始接触 AOP
时不知道你有没有想过这个问题,既然在面向对象的语言中已经有了 OOP 了,为什么还需要 AOP
呢?换个问法也就是说在 OOP
中有哪些场景其实处理得并不优雅,需要重新寻找一种新的技术去解决处理?(P.S. 这里建议暂停十秒钟,自己先想一想…)
为什么需要 AOP
我们做软件开发的最终目的是为了解决公司的各种需求,为业务赋能,注意,这里的需求包含了业务需求
和系统需求
,对于绝大部分的业务需求的普通关注点,都可以通过面向对象(OOP
)的方式对其进行很好的抽象、封装以及模块化,但是对于系统需求使用面向对象的方式虽然很好的对其进行分解并对其模块化,但是却不能很好的避免这些类似的系统需求在系统的各个模块中到处散落的问题。
因此,需要去重新寻找一种更好的办法,可以在基于 OOP
的基础上提供一套全新的方法来处理上面的问题,或者说是对 OOP
面向对象的开发模式做一个补充,使其可以更优雅的处理上面的问题,迄今为止 Spring
提供一个的解决方案就是面向切面编程——AOP
。有了 AOP
后,我们可以将这些事务管理
、系统日志
以及安全检查
等系统需求(横切关注点:cross-cutting concern
)进行模块化的组织,使得整个系统更加的模块化方便后续的管理和维护。细心的你应该发现在 AOP
里面引入了一个关键的抽象就是切面(Aspect
),用于对于系统中的一些横切关注点进行封装,要明确的一点是 AOP
和 OOP
不是非此即彼的对立关系,AOP
是对 OOP
的一种补充和完善,可以相互协作来完成需求,Aspect
对于 AOP
的重要程度就像 Class
对 OOP
一样。
几个重要的概念
我们最终的目的是要模仿 Spring
框架自己去实现一个简易版的 AOP
出来,虽然是简易版但是会涉及到 Spring AOP
中的核心思想和主要实现步骤,不过在此之前先来看看 AOP
中的重要概念,同时也是为以后的实现打下理论基础,这里需要说明一点是我不会使用中文翻译去描述这些 AOP
定义的术语(另外,业界 AOP
术语本来就不太统一),你需要重点理解的是术语在 AOP
中代表的含义,就像我们不会把 Spring
给翻译成春天
一样,在软件开发交流你知道它表示一个 Java
开发框架就可以了。下面对其关键术语进行逐个介绍:
Joinpoint
A point during the execution of a program, such as the execution of a method or the handling of an exception. In Spring AOP, a join point always represents a method execution. — Spring Docs
通过之前的介绍可知,在我们的系统运行之前,需要将 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()
方法调用的时序图来直观的看看:
从方法执行的时序来看不难发现,会有如下的一些常见的 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
A predicate that matches join points. Advice is associated with a pointcut expression and runs at any join point matched by the pointcut (for example, the execution of a method with a certain name). The concept of join points as matched by pointcut expressions is central to AOP, and Spring uses the AspectJ pointcut expression language by default.– by Spring Docs
Pointcut
表示的是一类 Jointpoint
的表述方式,在进行织入时需要根据 Pointcut
的配置,然后往那些匹配的 Joinpoint
织入横切的逻辑。这里面临的第一个问题:用人类的自然语言可以很快速的表述哪些我们需要织入的 Joinpoint
,但是在代码里要如何去表述这些 Joinpoint
呢?
目前有如下的一些表述 Joinpoint
定义的方式:
- 直接指定织入的方法名。显而易见,这种表述方式虽然简单,但是所支持的功能比较单一,只适用于方法类型的
Joinpoint
,而且当我们系统中需要织入的方法比较多时,一个一个的去定义织入的Pointjoint
时过于麻烦。 - 正则表达式方式。正则表达式相信大家都有一些了解,功能很强大,可以匹配表示多个不同方法类型的
Jointpoint
,Spring
框架的AOP
也支持这种表述方式。 - Pointcut 特定语言方式。这个因为是一种特定领域语言(
DSL
),所以其提供的功能也是最为灵活和丰富的,这也导致了不管其使用和实现复杂度都比较高,像AspectJ
就是使用的这种表述方式,当然Spring
也支持。
另外 Pointcut
也支持进行一些简单的逻辑运算
,这时我们就可以将多个简单的 Pointcut
通过逻辑运算组合为一个比较复杂的 Pointcut
了,比如在 Spring
配置中的 and
和 or
等逻辑运算标识符以及 AspectJ
中的 &&
和 ||
等逻辑运算符。
Advice
Action taken by an aspect at a particular join point. Different types of advice include “around”, “before” and “after” advice. (Advice types are discussed later.) Many AOP frameworks, including Spring, model an advice as an interceptor and maintain a chain of interceptors around the join point.– by Spring Docs
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
中执行顺序如下图所示: - Around Advice。这种类型是功能最为强大的
Advice
,可以匹配的Joinpoint
之前、之后甚至终端原来Joinpoint
的执行流程,正常情况下,会先执行Joinpoint
之前的执行逻辑,然后是Joinpoint
自己的执行流程,最后是执行Joinpoint
之后的执行逻辑。细心的你应该发现了,这不就是上面介绍的Before Advice
和After Advice
类型的组合吗,是的,它可以完成这两个类型的功能,不过还是要根据具体的场景选择合适的Advice
类型。
Aspect
A modularization of a concern that cuts across multiple classes. Transaction management is a good example of a crosscutting concern in enterprise Java applications. In Spring AOP, aspects are implemented by using regular classes (the schema-based approach) or regular classes annotated with the @Aspect annotation (the @AspectJ style). — Spring Docs
Aspect
是对我们系统里的横切关注点(crosscutting concern
)包装后的一个抽象概念,可以包含多个 Joinpoint
以及多个 Advice
的定义。Spring
集成了 AspectJ
后,也可以使用 @AspectJ
风格的声明式指定一个 Aspect
,只要添加 @Aspect
注解即可。
Target object
An object being advised by one or more aspects. Also referred to as the “advised object”. Since Spring AOP is implemented by using runtime proxies, this object is always a proxied object. — by Spring Docs
目标对象一般是指那些可以匹配上 Pointcut
声明条件,被织入横切逻辑的对象,正常情况下是由 Pointcut
来确定的,会根据 Pointcut
设置条件的不同而不同。
有了 AOP
这些概念后就可以把上文的例子再次进行整理,各个概念所在的位置如下图所示:
总结
本文首先对 AOP
技术的诞生背景做了简要介绍,后面介绍了 AOP
的几个重要概念为后面我们自己实现简易版 AOP
打下基础,AOP
是对 OOP
的一种补充和完善,文中列出的几个概念只是 AOP
中涉及的概念中的冰山一角,想要深入了解更多的相关概念的朋友们可以看 官方文档 学习,下篇是介绍 AOP
实现依赖的一些基础技术,敬请期待。转发、分享都是对我的支持,我将更有动力坚持原创分享!
原创文章,作者:奋斗,如若转载,请注明出处:https://blog.ytso.com/175720.html