Spring学习笔记(八) --- 面向切面的Spring

本系列博客为spring In Action 这本书的学习笔记

早在第一篇Spring的博客里面就提到过, Spring采取了四种策略来简化Java开发, 前几篇博客着重讲了其中的一种策略: 依赖注入(DI). 那么这篇博客就要学习Spring的另一个简化Java开发的策略: 面向切面编程(AOP). DI为了使应用对象之间解耦, 而AOP则是为了使横切关注点与它们所影响的业务逻辑之间解耦.


一. 什么是面向切面编程?

在之前的Java开发中, 如果想要重用通用功能的话, 我们会使用面向对象技术中的继承或委托. 但是如果整个应用中都使用相同的基类, 继承往往会导致一个比较脆弱的对象体系, 委托可能需要对委托对象进行复杂的调用.
现在, 切面提供了取代继承和委托的另一种方案, 就是面向切面编程.

先来看一下我们在第一篇Spring博客中对面向切面编程的介绍:

面向切面编程能将遍布应用各处的功能分离出来形成可重用的组件. 我们可以这样理解面向切面编程: 面向对象编程(OOP)的思想主要处理的是对象从上到下的关系, 那么AOP处理的就是一种从左到右的关系.比如说, 我们现在有好几种业务流程从上到下, 但是每种流程都包含了部分的日志处理, 那么我们就可以将日志处理看作是一个切面, 一个横切所有流程的切面, 因为在每个流程的执行过程中, 遇到需要进行日志记录的部分都会转去进行日志处理, 这就是一个切面. 我们可以把切面想象为覆盖在很多组件之上的一个外壳.

在软件开发中, 散布于应用中多处的功能被称为横切关注点. 通常来讲, 这些横切关注点从概念上来讲和业务逻辑是分开的, 而将这些横切关注点和应用的业务逻辑分离开就是面向切面编程要做的.

横切关注点可以被模块化为特殊的类, 这些类就被称为切面.

二. 关于AOP的术语

在使用AOP之前, 我们首先要了解它的一些重要术语, 如果对这些概念了解不是很清楚的话将会给后面的使用带来很大的麻烦.

描述切面的常用术语有: 通知, 连接点, 切点和切面.
它们之间的大致关系如下:
Spring学习笔记(八) --- 面向切面的Spring

而关于AOP操作的术语还有引入和织入.

下面我们就来详细了解一下这些概念吧.

1. 通知

通俗一点来讲, 通知就是切面的功能, 就是切面要完成的工作. 通知定义了切面是什么以及在什么时候使用.

通知定义了切面在什么时候使用, 是在应用的某个方法被调用之前? 之后? 之前和之后都调用? 还是只在方法抛出异常的时候使用?
针对这些不同的使用时机, Spring切面可以应用5种类型的通知:

  • 前置通知: 在目标方法被调用之前调用通知功能;
  • 后置通知: 在目标方法被调用之后调用通知功能;
  • 返回通知: 在目标方法成功执行之后调用通知;
  • 异常通知: 在目标方法抛出异常后调用通知;
  • 环绕通知: 环绕通知包裹了被通知的方法, 在被通知的方法调用之前和之后执行自定义的行为.

2. 连接点

连接点是在应用执行过程中能够插入切面的一个点. 这个点可以是调用方法时, 也可以是抛出异常时, 甚至可以是更改一个字段时. 切面代码可以利用这些点插入到应用的正常行为中, 并添加新的行为.

3. 切点

如果说通知定义了切面的”什么”和”何时”的话, 那么切点就定义了”何处”.

切点和连接点之间的关系是: 切点的定义会匹配通知的一个或多个连接点. 也就是切点定义了通知被应用的位置, 即连接点. 我们通常会使用明确的类和方法名称, 或者是利用正则表达式定义所匹配的类和方法名称来指定这些切点.

4. 切面

切面是通知和切点的结合. 通知和切点共同定义了切面的全部内容. 它是什么, 在何时和何处完成功能.

在前面也提到过, 切面就是横切关注点被模块化的特殊的类, 所以去掉注解, 切面还是一个普通的POJO.

5. 引入

引入允许我们通过切面向现有的Spring Bean添加新方法或者新属性.

简单来说就是, 现在我们可以通过AOP的引入机制, 为当前的类引入新的属性或者方法, 而这些新的属性和方法由是不属于原有类的属性和方法, 它们不必在原有类中存在和实现就能被原有类使用. 这可能听起来有些神奇和不可思议, 但是它确实是可以实现的. 我们将在后面详细说明并举例.

6. 织入

织入就是把切面应用到目标对象中, 并且创建新的代理对象的过程.
在后面的学习当中, 我们会感受到面向切面编程中有一个很重要的机制, 就是代理机制. 关于织入的概念我们在后面也会详细介绍和举例.


如果你跟我一样是第一次接触面向切面编程, 那么这些概念你很难一下子弄懂它, 如果实在读不明白也没关系, 这些概念在后面我们会用一个小例子来把这些概念都串起来, 这样你就能更好地理解它们了.

三. Spring中的AOP

1. Spring对AOP的支持

Spring提供了四种类型的AOP支持:

  • 基于代理的经典Spring AOP;
  • 纯POJO切面;
  • @AspectJ(一个面向切面的框架)注解驱动的切面;
  • 注入式AspectJ切面(适用于Spring各版本).

前三种都是Spring AOP实现的变体, 也就是构建在动态代理之上的, 因此Spring对AOP的支持局限于方法拦截.

第一种经典的Spring AOP的编程模型在现在看来过于复杂, 所以不推荐使用. 第二种纯POJO虽然简便, 但是这种技术需要依赖XML配置. 第三种*Spring借鉴了AspectJ的切面, 以提供注解驱动的AOP, 本质上, 它仍然是Spring基于代理的AOP, 但是编程模型几乎与编写成熟的ASpectJ注解切面完全一致, 并且不需要XML来完成功能. 第四种*就是如果你的AOP需求超过了简单的方法调用(如构造器或者属性拦截), 那么就需要使用AspectJ来实现切面了.

在后面的例子种, 我们会使用第三种类型的AOP, 并且补充一点关于AspectJ的使用方式.

2. Spring的通知

Spring所创建的通知都是使用标准的Java类编写的. 这样使得我们可以使用与普通Java开发一样的IDE来开发切面. 定义通知所应用的切点通常会使用注解或者XML来编写.

而AspectJ与之相反, 虽然AspectJ现在也支持基于注解的切面, 但是AspectJ最初是以Java语言扩展的方式实现的, 也就是有特定的AOP语言. 这种方式有好也有坏. 好处就是我们可以使用AOP工具集来简化AOP开发, 但是坏处就是需要额外学习AOP语言.

3. Spring在运行时通知对象

通过在代理类中包裹切面, Spring在运行期间把切面织入到Spring管理的Bean中. 如下图所示:
Spring学习笔记(八) --- 面向切面的Spring

Spring的切面由包裹了目标对象(目标Bean)的代理类实现. 代理类封装了目标对象, 并拦截被通知方法的调用, 执行额外的切面逻辑, 再把调用转发给真正的目标对象.

需要注意的是, 直到使用需要被的代理的Bean的时候, Spring才会创佳年代理对象. 如果使用的是ApplicationContext的话, 在ApplicationContext从BeanFactory中加载所有Bean的时候, Spring才会创建被代理的对象. 因为Spring运行时才创建代理对象, 所以我们不需要特殊的编译器来织入Spring AOP的切面.

4. Spring只支持方法级别的连接点

Spring基于动态代理, 所以Spring值支持方法连接点. 这与其它的一些AOP框架是不同的, 比如AspectJ, 除了方法切点, 还提供了字段和构造器接入点. 但是方法拦截可以满足大多数的需求, 如果需要方法拦截之外的连接点拦截功能, 我们可以使用AspectJ来补充Spring AOP的功能.


现在我们对AOP的一些概念和Spring是如何支持AOP的已经由了一些大致的了解, 那么在下一篇博客中, 我们会用一个个小例子来学习如何在Spring中创建切面.