1. 前言
在现代社会中,人们在衣食住行各个方面都享受着前所未有的便利。出行可以乘坐高铁或飞机,无所不在的网络让信息流动起来,网购和快递则依赖高效的物流配送。事实证明,公共服务越完善,工作的效率越高,创造的社会效益也就越大。公共服务提供的是特定的功能,这种共性被称为横切关注点。如何理解这句话?每个人都有自己的事情要做,他们选择某一项公共服务是由需求决定的。需求驱动的服务就是关注点,横切则涵盖了一个范围,即公共服务是为特定的人群提供服务的。
面向对象编程(OOP)关注的是业务逻辑,不同业务的实现也不一样。面向切面编程(AOP)关注的是公共服务,比如日志、缓存、事务、权限、统计等,它们关注广泛存在于业务逻辑中,构成了一个个横切关注点。一般而言,AOP 可以看作是一种编程范式,作用是将横切关注点从业务逻辑中分离出来,降低了系统的耦合度,从而提高代码的可维护性和重用性。
2. AOP 规范
在深入分析 Spring AOP 的实现之前,我们有必要了解一下 AOP 的规范。令人有些意外的是,面向切面编程的规范并不是由 Java 定义的,一个名叫 AOP 联盟(aop alliance)的组织完成了顶层逻辑的设计。我们在工程中引入了 aopalliance
包,虽然只有寥寥数个接口,却对 AOP 做了高度的抽象。
从类图可以看到,AOP 大体可以分为两个部分。其中 Advice
表示增强的语义,Joinpoint
表示系统的执行点。增强逻辑一定是作用于某段代码上的,Joinpoint
充当了桥梁的作用,使得 Advice
可以嵌入到业务代码中。
3. 连接点
3.1 概述
什么是连接点,这个问题很重要,我们先来看官方给出的定义:
This interface represents a generic runtime joinpoint. A runtime joinpoint is an event that occurs on a static joinpoint.
Joinpoint
接口表示通用的运行时连接点,运行时连接点是指在静态连接点上发生的事件。
静态连接点(static joinpoint)是指程序中的某个位置,这个位置使用 AccessbileObject
来描述。AccessbileObject
表示可访问的对象,包括字段、方法和构造器。当我们定义了一个方法,可以通过反射获取方法的名称、参数、返回值、以及声明的注解等信息。这些信息在编译的时候就已经确定了,所以才说是静态的。
3.2 Joinpoint
Joinpoint
接口定义了运行时连接点,一个运行时连接点包括两部分。一是静态连接点,也就是可访问的字段、方法和构造器。二是连接点上发生的事件,也就是说这些对象将被访问。
proceed
方法:访问静态连接点,比如调用方法,并在调用方法的过程中执行增强逻辑。getThis
方法:返回连接点的静态部分所属的对象。字段、方法不能独立存在,必须定义在某个类中。getStaticPart
方法:返回连接点的静态部分,即字段、方法和构造器。
public interface Joinpoint {
Object proceed() throws Throwable;
Object getThis();
AccessibleObject getStaticPart();
}
3.3 Invocation
不同的静态连接点上发生的事件,有不同的叫法。对于方法和构造器来说,叫做调用(invocation),对于字段来说,叫做访问(access)。Invocation
接口就是用来处理方法和构造器的,getArguments
方法的作用是获取参数值,而调用方法和构造器是可以传递参数的。
public interface Invocation extends Joinpoint {
Object[] getArguments();
}
Incovation
接口有两个子接口,分别对应方法和构造器。getMethod
和 getConstructor
方法实际上是 getStaticPart
方法的具体化。对于方法调用来说,静态部分就是 Method
,对于构造器来说,静态部分则是 Constructor
。我们可以把构造器看成是特殊的方法,只要是方法就会有参数,因此 getArgument
方法被定义在父接口中。
public interface MethodInvocation extends Invocation {
Method getMethod();
}
public interface ConstructorInvocation extends Invocation {
Constructor getConstructor();
}
3.4 事件三要素
在 Joinpoint
接口的定义中提到,运行时连接点是指在静态连接点上发生的事件,这句话该如何理解?在程序运行时,方法和构造器被调用,或者字段被取值或赋值,这些行为的发生被认为是一次事件。单就解释来说仍有点抽象,我们以 HTML 页面为例说明。(当然也可以换成桌面窗口程序或手机 App)
一个页面上可能有多个控件,比如按钮、输入框等。当鼠标单击按钮时,会生成一个 click 事件;当文本输入框中的内容改变时,会生成 key-down 等事件。既然是事件,就要满足三个要素。一是事件源,也就是产生事件的载体。二是事件携带的数据,比如文本框的内容。三是处理事件的逻辑,比如在 JavaScript 代码中添加一个监听器。我们可以从 Jointpoint
和 Invocation
接口定义的方法中找到对应的三要素,如下所示:
getStaticPart
方法:返回静态连接点,也就是事件源getArguments
方法:当方法和构造器被调用时,传入的参数就是事件携带的数据proceed
方法:调用方法或构造器时,执行增强逻辑,实际上完成了对事件的处理
4. 通知
4.1 概述
当我们把类看作一个页面,其中的方法相当于一个按钮。点击按钮时会生成一个事件,然后由监听器来处理。同理,当调用某个方法时,也会产生一个事件。区别在于,监听器会将事件消费掉,而拦截器在调用目标方法的同时,还执行了增强的逻辑。Joinpoint
接口只规定了事件的三要素,现在还需要一个增强逻辑,Advice
接口充当了这一角色。
Advice
是一个标记接口,具体实现可以是任意类型的增强,比如拦截器。Interceptor
也是一个标记接口,其定义如下:
A generic interceptor can intercept runtime events that occur within a base program. Those events are materialized by (reified in) joinpoints.
Interceptor
的作用是拦截源程序上发生的运行时事件,这些事件由若干连接点所产生。
源程序(base program)是指业务代码,调用目标方法就是所谓的运行时事件。这些事件由拦截器来处理,实际上提供了增强的语义。若干连接点(joinpoints)的潜含义是拦截器可以应用于多个方法或构造器,如果把一个方法或构造器视为一个点,那么多个方法或构造器就构成了一个切面。
4.2 方法与构造器拦截
根据连接点的不同,Interceptor
也有相应的子接口。MethodInterceptor
接口的作用是对方法调用进行拦截,ConstructorInterceptor
接口的作用是对构造器调用进行拦截。
public interface MethodInterceptor extends Interceptor {
Object invoke(MethodInvocation invocation) throws Throwable;
}
public interface ConstructorInterceptor extends Interceptor {
Object construct(ConstructorInvocation invocation) throws Throwable;
}
4.3 字段访问
AOP 规范虽然没有定义访问字段的组件,但明里暗里透露了不少信息。事实上,在 Interceptor
接口的注释中,我们找到了一些蛛丝马迹。
根据以上的示例代码,我们可以依照调用方法和构造器的思路,予以实现。首先是 FieldAccess
接口,定义了访问字段的行为。
getField
方法表示具体的静态部分,同样是getThis
方法的具体化。getValueToSet
方法表示赋给字段的值,相当于Invocation
接口的getArguments
方法,同样表示事件携带的数据。
public interface FieldAccess extends Joinpoint {
Field getField();
Object getValueToSet();
}
其次是 FieldInterceptor
接口,访问字段有两种操作(方法和构造器只有调用这一种操作),get
方法的作用是获取字段的值,set
方法的作用是为字段赋值。
public interface FieldInterceptor extends Interceptor {
Object get(FieldAccess fa) throws Throwable;
Object set(FieldAccess fa) throws Throwable;
}
5. AOP 实现
AOP 规范是高度抽象的,没有明确指出 AOP 实现的路线图。但我们可以从接口和方法的定义中获取信息,总的来说,AOP 的实现需要满足三要素,我们将在接下来的内容详细讨论。简单介绍如下:
- 增强:在业务代码之外执行额外的增强逻辑,这是对被分离的关注点的抽象。
- 切面:增强逻辑可以应用在若干连接点上,这些连接点构成了一个切面。换句话说,切面是由需求驱动的,只要有需要就能享受到某项公共服务。
- 代理:方法、构造器和字段是静态连接点,它们必定属于某一个对象。将增强逻辑应用于对象的过程称为织入,一般通过代理机制来实现。
6. 总结
本节介绍了面向切面编程的基本概念,aopalliance 包对面向切面编程进行了高度的抽象。AOP 框架的顶级接口有两个,分别是 Joinpoint
和 Advice
。其中,Advice
接口表示增强逻辑,Joinpoint
接口表示程序执行中的一个点,比如调用方法或访问字段。增强逻辑通过连接点介入到业务代码之中,这是 AOP 最基本的内涵。
连接点是一个复合概念,包括连接点的静态部分和在静态部分上发生的事件。所谓连接点的静态部分是指可访问的对象,分别是方法、构造器和字段。因此,连接点上发生的事件也分为三种,即方法调用、构造器调用和字段访问。需要注意的是,连接点与通知是一一对应的,比如方法调用对应方法拦截器,字段访问对应字段拦截器。
AOP 规范将连接点的调用或访问抽象成事件,既然如此,就需要满足事件的三要素。首先是事件源,也就是静态连接点,即方法、构造器和字段。其次是数据,当方法和构造器被调用时,需要传递参数。最后是事件处理逻辑,在调用方法或构造器的同时,还执行了额外的增强逻辑。
面向切面编程定义了很多概念,但 AOP 实现最终落实在三个方面,即增强、切面和代理。我们将在接下来的内容中着重介绍这三个核心要素,所谓纲举目张,就是抓住主要矛盾来分析和解决问题。
欢迎关注公众号【Java编程探微】,加群一起讨论。