11、面向切面的Spring(1)(spring笔记)

在生活中,监控用电量是一个很重要的功能,但并不是大多数家庭重点关注的问题。软件系统的一些功能就像家里的电表一样,这些功能需要用到应用程序的多个地方,但是我们又不想在每个点都明确调用它们。日志、安全和事务管理的确都很重要,但它们是否为应用对象主动参与的行为呢?在软件开发中,散布于应用中多处的功能被称为横切关注点(cross-cutting concern)。通常来讲,这些横切关注点从概念上是与应用的业务逻辑相分离的。

一、什么是面向切面编程

图中展现了一个被划分为模块的典型应用。每个模块的核心功能都是为特定业务领域提供服务,但是这些模块都需要类似的辅助功能,如安全和事务管理。


1
  • 如果要重用通用的功能的话,最常见的面向对象技术是继承(inheritance)或委托(delegation)。但是,如果在整个应用中都使用相同的基类,继承往往会导致一个脆弱的对象体系;而使用委托可能需要对委托对象进行复杂的调用。

  • 切面提供了取代继承和委托的另一种可选方案。在使用面向切面编程时,我们仍然在一个地方定义通用功能,但是可以通过声明的方式定义这个功能要以何种方式在何处应用,而无须修改受影响的类。横切关注点可以被模块化为特殊的类,这些类被称为切面(aspect)。

1.1 定义AOP术语

描述切面术语有通知(advice)、切点(pointcut)和连接点(join point),如图所示:

2

1.1.1 通知(advice)

电表抄表员需要统计每家每户的用电量,而记录用电量是其主要的工作。类似的,切面也有目标——它必须要完成的工作。在AOP术语中,切面的工作被称为通知。其实就是整个程序执行过程中要插入的具体的功能,如日志等。
Spring切面中,可以应用五种类型的通知:

  • 前置通知(Before):在目标方法被调用之前调用通知功能;
  • 后置通知(After):在目标方法完成之后调用通知,此时不会关心方法的输出是什么;
  • 返回通知(After-returning):在目标方法成功执行之后调用通知;
  • 异常通知(After-throwing):在目标方法抛出异常后调用通知;
  • 环绕通知(Around):通知包裹了被通知的方法,在被通知的方法调用之前和调用之后执行自定义的行为。

1.1.2 连接点(Join point)

通知可能需要在程序的很多时机应用。这些时机被称为连接点。连接点是应用执行过程中能够插入切面的一个点。这个点可以是调用方法时、抛出异常时、甚至修改一个字段时。切面代码可以利用这些点插入到应用的正常流程之中,并添加新的行为。

1.1.3 切点(Pointcut)

如果说通知定义了切面的“什么”和“何时”的话,那么切点就定义了“何处”。切点的定义会匹配通知所要织入的一个或多个连接点。一般使用明确的类和方法名称或是利用正则表达式定义所匹配的类和方法名称来指定这些切点。

1.1.4 切面(Aspect)

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

1.1.5 引入(Introduction)

引入允许我们向现有的类添加新方法或属性。如我们可以创建一个Auditable通知类,该类记录了对象最后一次修改时的状态。这很简单,只需一个方法setLastModified(Data)和一个实例变量来保存这个状态。然后,这个新方法和实例变量就可以被引入到现有的类中,从而可以在无需修改这些现有的类的情况下,让它们具有新的行为和状态。

1.1.6 织入(Weaving)

织入是把切面应用到目标对象并创建新的代理对象的过程。切面在指定的连接点被织入到目标对象中。在目标对象的声明周期里有多个点可以进行织入:

  • 编译期:切面在目标类编译时被织入。这种方式需要特殊的编译器。AspectJ的织入编译器就是以这种方式织入切面的。
  • 类加载期:切面在目标类加载到JVM时被织入。这种方式需要特殊的类加载器(ClassLoader),它可以在目标类被引入引用之前增强该目标类的字节码。AspectJ 5的加载时织入(load-time weaving,LTW)就支持以这种方式织入切面。
  • 运行期:切面在应用运行的某个时刻被织入。一般情况下,在织入切面时,AOP容器会为目标对象动态地创建一个代理对象。Spring AOP就是以这种方式织入切面的。

总结:如日志记录、安全性检查等这些问题,我们可以称作横切性关注点,而将这个横切性的问题模块化为一个类,这个类就叫做切面(Aspect);切面中有很多内容,其中有一个内容就是实际处理如日志记录的方法,这个方法就叫做通知(Advice);而这个方法要应用在那些地方,这需要规定一个范围,这个范围就是切点(Pointcut);而这个范围中有很多连接点(Join Point),即应用在哪些方法或类上(spring只支持方法);而通知(或切面)应用的过程就叫织入(Weave)。

1.2 Spring对AOP的支持

Spring中提供了四种类型的AOP支持(其中,前三种都是Spring AOP实现的变体,Spring AOP构建在动态代理基础上,因此,SpringAOP的支持局限于方法拦截):

  • 基于代理的经典Spring AOP
  • POJO切面
  • @AspectJ注解驱动的切面
  • 注入式AspectJ切面(适用于Spring各版本)

由于Spring经典的AOP不怎么样,这里就不再细说。借助Springaop命名空间,我们可以将纯POJO转换为切面。实际上,这些POJO只是提供了满足切点条件时所要调用的方法。遗憾的是,这种技术需要XML配置,但是这的确是声明式地将对象转换为切面的简便方式。

Spring借鉴了AspectJ的切面,以提供注解驱动的AOP。本质上,它依然是Spring基于代理的AOP,但是编程模型几乎与编写成熟的AspectJ注解切面完全一致。这种方式不需要使用XML配置。

1.2.1 Spring 通知是 Java 编写的

Spring所创建的通知都是标准的Java类编写的。而AspectJ最初是以Java语言扩展的方式实现的,这种方式有优点也有缺点。通过特有的AOP语言,我们可以获得更强大和细粒度的控制,以及更丰富的AOP工具集。

1.2.2 Spring在运行时通知对象

3

通过在代理类中包裹切面,Spring在运行期把切面织入到Springbean中。如图所示,代理类封装了目标类,并拦截被通知方法的调用,再把调用转发给真正的目标bean。当拦截到方法调用时,在调用目标bean方法之前,会执行切面逻辑。直到应用需要被代理的bean时,才会创建代理对象。如果使用的是ApplicationContext的话,在ApplicationContextBeanFactory中加载所有bean的时候,Spring才会创建被代理的对象。因为Spring运行时才创建代理对象,所以我们不需要特殊的编译器来织入Spring AOP的切面。

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

因为Spring基于动态代理,所以只支持方法连接点。而像AspectJ,除了方法切点,还提供了字段和构造器连接点。Spring缺少对字段连接点的支持,无法让我们创建细粒度的通知,例如拦截对象字段的修改。而且它还不支持构造器连接点,我们就无法在bean创建时应用通知。

但是方法拦截可以满足绝大部分需求。如果需要方法拦截之外的连接点拦截功能,那么我们可以利用AspectJ来补充Spring AOP功能。

二、通过切点来选择连接点

Spring AOP中,要使用AspectJ的切点表达式语言来定义切点。关于Spring AOPAspectJ切点,最重要的一点就是Spring仅支持AspectJ切点指示器的一个子集。下表是Spring AOP所支持的AspectJ切点指示器。

AspectJ指示器 描述
arg() 限制连接点匹配参数为指定类型的执行方法
@args() 限制连接点匹配参数由指定注解标注的执行方法
execution() 用于匹配是连接点的执行方法
this() 限制连接点匹配AOP代理的bean引用为指定类型的类
target 限制连接点匹配目标对象为指定类型的类
@target() 限制连接点匹配特定的执行对象,这些对象对应的类要具有指定类的注解
within() 限制连接点匹配的类型
@within() 限制连接点匹配指定注解所标注的类型(当使用Spring AOP时,方法定义在由指定的注解所标注的类里)
@annotation 限定匹配带有指定注解的连接点

Spring中尝试使用AspectJ其他指示器时,将会抛出IllegalArgumentException异常。其中只有execution指示器是实际执行匹配的,而其他的指示器都是用来限制匹配的,这表示execution是我们在编写切点定义时最主要使用的指示器,在此基础上,我们使用其他指示器显示所匹配的切点。

2.1 编写切点

为了阐述Spring中的切面,举例说明。

package concert;
public interface Performance{
    public void perform();
}

说明:这个接口可以表示任何类型的现场表演,如果我们想编写Performanceperform()方法出发的通知,如图所示:

4

这里使用execution()指示器选择Performanceperform()方法。方法表达式以"*"号开始,表明了我们不关心方法返回值的类型。然后指定了全限定类名和方法名。对于方法参数列表,我们使用两个点号(..)标明切点要选择任意的perform()方法,无论该方法的入参是什么。

现在假设需要配置的切点仅匹配concert包。在此场景下,可以使用within()指示器来限定匹配,如图所示。

5

还可以使用"||"操作符表示或(or)关系,而使用"!"操作符来标识非(not)操作。因为"&"XML中有特殊的含义,所以在XML中配置时需要使用and来替代"&&",同样,ornot可以分别用来替代"||""!"

2.2 在切点中选择bean

可以使用Springbean()指示器,它允许我们在切点表达式中使用beanID来标识bean。如下:

execution( * concert.Performance.perform(..)) and bean('woodstock')
execution( * concert.Performance.perform(..)) and !bean('woodstock')

说明:第一种表示在应用通知时,限定beanIDwoodstock。第二种表示可以限定为除了特定ID以外的其他bean引用通知。

三、使用注解创建切面

3.1 定义切面

使用注解来创建切面是AspectJ 5所引入的关键特性。

6

说明:从上图中可以清晰看到如何使用注解来声明通知方法(注意:图中execution右边应该是一个星号,书中可能有误)。Spring使用AspectJ注解来声明通知方法有几种方式:

注解 通知
@After 通知方法会在目标方法返回或抛出异常后调用
@AfterReturning 通知方法会在目标方法返回后调用
@AfterThrowing 通知方法会在目标方法抛出异常后调用
@Around 通知方法会将目标方法封装起来
@Before 通知方法会在目标方法调用之前执行

可能会注意到,所有的这些注解都给定了一个切点表达式作为它的值,同时,这四个方法的切点表达式都是相同的。其实它们可以设置成不同的切点表达式,但是此处却是一样的。相同的切点表达式重复多次可不是很好,这里我们可以使用@Pointcut注解定义一个重用的切点。

7

说明:首先定义了一个切点,这样就可以在任何切点表达式中使用performance()方法了,此方法的实际内容并不重要,只是供@Pointcut注解依附。需要注意的是,除了注解和没有实际操作的performance()方法,Audience类依然是一个POJO,可以像使用其他的Java类那样调用它的方法,也能够独立的进行测试(注意:图中execution右边应该是一个星号,书中可能有误)。还可以装配为Spring中的bean

@Bean
public Audience audience(){
  return new Audience();
}

但是如果就此止步的话,Audience只会是Spring容器中的一个bean,即便使用了AspectJ注解,但并不会被视为切面。如果使用JavaConfig的话,需要使用@EnableAspectJAutoProxy注解启用自动代理功能。

package concert;
import org.springframework.context.annotation.*;

@Configuration
@EnableAspectJAutoProxy/*启用AspectJ自动代理*/
@ComponentScan
public class ConcertConfig {

    @Bean
    public Audience andience(){
        return new Audience();
    }
}

而如果是在XML中配置,则如下:

<context:component-scan base-package="concert" />
<aop:aspectj-autoproxy />
<bean class="concert.Audience" />

说明:SpringAspectJ自动代理仅仅使用@AspectJ作为创建切面的指导,切面依然是基于代理的。本质上,它依然是Spring基于代理的切面。

推荐阅读更多精彩内容