《Spring 实战》笔记4:面向切面的 Spring

在软件开发中,散布于应用中多处的功能被称为横切关注点(cross-cutting concern)。通常来讲,这些横切关注点从概念上是与应用的业务逻辑相分离的(但是往往会直接嵌入到应用的业务逻辑之中)。把这些横切关注点与业务逻辑相分离正是面向切面编程(AOP)所要解决的问题

在第2章,我们介绍了如何使用依赖注入(DI)管理和配置我们的应用对象。DI 有助于应用对象之间的解耦,而 AOP 可以实现横切关注点与它们所影响的对象之间的解耦。

日志是应用切面的常见范例,但它并不是切面适用的唯一场景。通览本书,我们还会看到切面所适用的多个场景,包括声明式事务、安全和缓存。

什么是面向切面编程

切面能够帮我们模块化横切关注点。简而言之,横切关注点可以被描述为影响应用多处的功能。例如 安全就是一个横切关注点,应用中的许多方法都会涉及到安全规则。

如果要重用对象的话,最常见的面向对象技术是继承、委托、组合。但是,如果整个应用中都使用相同的基类,继承往往会导致一个脆弱的对象体系。而使用委托可能需要委托对象进行复杂的调用。

切面提供了取代继承和委托的另一种可选方案。在使用面向切面编程时,我们仍然在一个地方定义通知功能,而无需修改受影响的类。横切关注点可以被模块化为特殊的类,这些类被称为切面(aspect)。 这样做带来两个好处:每个关注点都集中到一个地方,而不是分散到多处代码中;其次,服务模块更简洁,因为它只包含了主要关注点(核心功能)的代码。而次要关注的代码被移到切面中了。

定义 AOP 术语

通知(advice)

切面也有目标——它必须要完成的工作。在 AOP 术语中,切面的工作被称为通知

通知定义了切面是什么以及何时使用。除了描述切面要完成的工作外,通知还解决了何时执行这个工作问题。它应该在某个方法被调用之前?之后?之前和之后都调用?还是只在方法抛出异常时调用?

Spring 切面可以应用 5 中类型的通知:

  • 前置通知(Before):在目标方法被调用之前调用通知功能。
  • 后置通知(After):在目标方法完成之后调用通知
  • 返回通知(After-returning):在目标方法成功执行之后调用通知
  • 异常通知(After-throwing):在目标方法抛出异常后调用通知
  • 环绕通知(Around):在被通知方法调用之前和调用之后执行自定义的行为

连接点(Join point)

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

切点(Pointcut)

切入点是一个或一组连接点,通知将在这些位置执行。可以通过表达式或匹配的方式指明切入点。

切面(Aspect)

切面是通知和切点的结合。通知和切点通过定义了切面的全部内容——它是什么,在什么时候和在哪里完成其功能。

引入(Introduction)

引入允许我们向现有的类添加新的方法或者属性。

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

织入(Weaving)

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

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

通知包含了需要用于多个应用对象的横切行为;连接点是程序执行过程中能够应用通知的所有点;切点定义了通知被应用的具体位置(在哪些连接点)。其中关键的概念是切点定义了哪些连接点会得到通知。

Spring 对 AOP 的支持

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

  • 基于代理的经典 Spring AOP;非常笨重和过于复杂
  • 纯 POJO 切面;需要 XML 配置,但这的确是声明式地将对象转换为切面的简便方式。
  • @AspectJ 注解驱动的切面;它依然是 Spring 基于代理的 AOP,但是编程模型几乎与编写成熟的 AspectJ 注解切面完全一致。这种 AOP 风格的好处在于能够不使用 XML 来完成功能。
  • 注入式 AspectJ 切面(适用于 Spring 各版本)。如果你的 AOP 需求超过了简单的方法调用(如构造器或属性拦截),那么你需要考虑使用 AspectJ 来实现切面。能够帮助你将值注入到 AspectJ 驱动的切面中。

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

Spring 通知是 Java 编写的

Spring 所创建的通知都是用标准的 Java 类编写的,定义通知所应用的切点通常会使用注解或在 Spring 配置文件里采用 XML 来编写。

AspectJ 与之相反。虽然 AspectJ 现在支持基于注解的切面,但 AspectJ 最初是以 Java 语言扩展的方式实现的。这种方式有优点也有缺点。通过特有的 AOP 语言,我们可以获得更强大和细粒度的控制,以及更丰富的 AOP 工具集,但是我们需要额外学习新的工具和语法。

Spring 在运行时通知对象

通过在代理类中包裹切面,Spring 在运行期把切面织入到 Spring 管理的 bean 中。代理类封装了目标类,并拦截被通知方法的调用,再把调用转发给真正的目标 bean。当代理拦截到方法调用时,在调用目标 bean 方法之前,会执行切面逻辑。

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

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

正如前面所探讨过的,通过使用各种 AOP 方案可以支持多种连接点模型。因为 Spring 基于动态代理,所以 Spring 只支持方法连接点。这与一些其他的 AOP 框架是不同的,例如 AspectJJBoss,除了方法切点,它们还提供了字段和构造器接入点。Spring 缺少对字段连接点的支持,无法让我们创建细粒度的通知,例如拦截对象字段的修改。而且它不支持构造器连接点,我们就无法在 bean 创建时应用通知。

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

通过切点来选择连接点

切点用于准确定位应该在什么地方应用切面的通知。通知和切点是切面的最基本元素。

Spring 仅支持 AspectJ 切点指示器(pointcut designator)的一个子集。Spring 是基于代理的,而某些切点表达式是与基于代理的 AOP 无关的。

Spring AOP 所支持的 AspectJ 切点指示器

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

注意只有 execution 指示器是实际执行匹配的,而其他的指示器都是用来限制匹配的。这说明 execution 指示器是我们在编写切点定义时最主要使用的指示器。在此基础上,我们使用其他指示器来限制所匹配的切点。

编写切点

为了阐述 Spring 中的切面,我们需要有个主题来定义切面的切点。为此,我们定义一个 Performance 接口:

public interface Performance {
    public void perform();
}

下图展现了一个切点表达式,这个表达式能够设置当 perform() 方法执行时触发通知的调用。

使用 AspectJ 切点表达式来选择 Performance 的 perform() 方法

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

使用 within() 指示器限制切点范围

请注意我们使用了 && 操作符把 execution()within() 指示器连接在一起形成与(and)关系(切点必须匹配所有的指示器)。类似地,我们可以使用 || 操作符来标识或(or)关系,而使用 ! 操作符来标识非(not)操作。

因为 & 在XML中有特殊含义,所以在 Spring 的 XML 配置里面描述切点时,我们可以使用 and
来代替 && 。同样,or 和 not可以分别用来代替 ||!

在切点中选择 bean

Spring 引入了一个新的 bean() 指示器,它允许我们在切点表达式中使用 bean 的ID来标识 bean。bean() 使用 bean ID 或 bean 名称作为参数来限制切点只匹配特定的 bean。

// 在执行 Performance 的 perform() 方法时应用通知,但限定 bean 的 ID 为 woodstock。
execution(* concert.Performance.perform()) and bean("woodsotck")
// 切面的通知会被编织到所有 ID 不为 woodstock 的 bean 中
execution(* concert.Performance.perform()) and !bean("woodsotck")

使用注解创建切面

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

我们已经定义了 Performance 接口,它是切面中切点的目标对象。现在,让我们使用 AspectJ 注解来定义切面。

定义切面

@Aspect  // 使用 @Aspect 注解进行了标注,表明是一个切面
public class Audience {
    // 表演之前
    @Before("execution(** springaction04.concert.Performance.perform(..))")
    public void silenceCellPhones() {
        System.out.println("Silencing cell phones");
    }
    // 表演之前
    @Before("execution(** springaction04.concert.Performance.perform(..))")
    public void takeSeats() {
        System.out.println("Taking seats");
    }

    // 表演之后
    @AfterReturning("execution(** springaction04.concert.Performance.perform(..))")
    public void applause() {
        System.out.println("CLAP CLAP CLAP!!!");
    }

    // 表演失败之后
    @AfterThrowing("execution(** springaction04.concert.Performance.perform(..))")
    public void demandRefund() {
        System.out.println("Demanding a refund");
    }
}

两个 * 在 IDEA 中会提示 ( expectedWhy use two stars in point cut expression to match return type?

Spring 使用 AspectJ 注解来声明通知方法

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

@Pointcut

@Pointcut 注解能够在一个 @AspectJ 切面内定义可重用的切点。

@Aspect
public class Audience {
    /**
    * 定义命名的切点
    * performance() 方法的实际内容并不重要,在这里它实际上应该是空的。
    * 其实该方法本身只是一个标识,供 @Pointcut 注解依附。
    * 如果方法中存在内容,IDEA 会提示 Pointcut methods should have empty body
    */
    @Pointcut("execution(** springaction04.concert.Performance.perform(..))")
    public void performance() {}

    @Before("performance()")
    public void silenceCellPhones() {
        System.out.println("Silencing cell phones");
    }
    @Before("performance()")
    public void takeSeats() {
        System.out.println("Taking seats");
    }

    @AfterReturning("performance()")
    public void applause() {
        System.out.println("CLAP CLAP CLAP!!!");
    }

    @AfterThrowing("performance()")
    public void demandRefund() {
        System.out.println("Demanding a refund");
    }
}

在 JavaConfig 中启用 AspectJ 注解的自动代理

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

    @Bean
    public Audience audience() {  // 声明 Audience bean
        return new Audience();
    }
}

在 XML 中启用 AspectJ 自动代理

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:context="http://www.springframework.org/schema/context"
    xmlns:aop="http://www.springframework.org/schema/aop"
    xsi:schemaLocation="http://www.springframework.org/schema/aop
        http://www.springframework.org/schema/aop/spring-aop.xsd
        http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context
        http://www.springframework.org/schema/context/spring-context.xsd">

    <context:component-scan base-package="springaction04.concert" />
    <!-- 启用 AspectJ 自动代理 -->
    <aop:aspectj-autoproxy />
    <bean class="concert.Audience" />
</beans>

如果想利用 AspectJ 的所有能力,我们必须在运行时使用 AspectJ 并且不依赖 Spring 来创建基于代理的切面。

创建环绕通知

环绕通知是最为强大的通知类型。它能够让你所编写的逻辑将被通知的目标方法完全包装起来。实际上就像在一个通知方法中同时编写前置通知和后置通知。

// 使用环绕通知重新实现 Audience 切面
@Aspect
public class Audience3 {
    @Pointcut("execution(** concert.Performance.perform(..))")
    public void performance() {}

    @Around("performance()")
    public void watchPerformance(ProceedingJoinPoint jp) {
        try {
            System.out.println("Silencing cell phones");
            System.out.println("Taking seats");
            jp.proceed();
            System.out.println("CLAP CLAP CLAP!!!");
        } catch (Throwable e) {
            System.out.println("Demanding a refund");
        }
    }
}

可以看到,这个通知所达到的效果与之前的前置通知和后置通知是一样的。但是,现在它们位于同一个方法中,不像之前那样分散在四个不同的通知方法里面。

处理通知中的参数

// 使用参数化的通知来记录磁道播放的次数
@Aspect
public class TrackCounter {
    private Map<Integer, Integer> trackCounts = new HashMap<>();
    @Pointcut(
            "execution(* soundsystem.CompactDisc.playTrack(int)) " +
                    "&& args(trackNumber)")
    public void trackPlayed(int trackNumber) {}

    // IDEA 提示:argNames attribute isn't defined,建议修改如下:
    // @Before(value = "trackPlayed(trackNumber)", argNames = "trackNumber")
    @Before("trackPlayed(trackNumber)")
    public void countTrack(int trackNumber) {
        int currentCount = getPlayCount(trackNumber);
        trackCounts.put(trackNumber, currentCount + 1);
    }

    public int getPlayCount(int trackNumber) {
        return trackCounts.containsKey(trackNumber)
                ? trackCounts.get(trackNumber) : 0;
    }
}

在切点表达式中声明参数,这个参数传入到通知方法中

需要关注的是切点表达式中的 args(trackNumber) 限定符。它表明传递给 playTrack() 方法的 int 类型参数也会传递到通知中去。参数的名称 trackNumber 也与切点方法签名中的参数相匹配。

通过注解引入新功能

使用 Spring AOP,我们可以为 bean 引入新的方法。代理拦截调用并委托给实现该方法的其他对象

为示例中的所有的 Performance 实现引入下面的 Encoreable 接口:

public interface Encoreable {
    void performEncore();
}

我们需要有一种方式将这个接口应用到 Performance 实现中。我们现在假设你能够访问 Performance 的所有实现,并对其进行修改,让它们都实现 Encoreable 接口。但是,从设计的角度来看,这并不是最好的做法,并不是所有的 Performance 都是具有 Encoreable 特性的。另外一方面,有可能无法修改所有的 Performance 实现,当使用第三方实现并且没有源码的时候更是如此。

值得庆幸的是,借助于 AOP 的引入功能,我们可以不必在设计上妥协或者侵入性地改变现有的实现。为了实现该功能,我们要创建一个新的切面:

@Aspect
public class EncoreableIntroducer {
    @DeclareParents(value="concert.Performance+", defaultImpl=DefaultEncoreable.class)
    public static Encoreable encoreable;
}

EncoreableIntroducer 是一个切面。但是,它与我们之前所创建的切面不同,它并没有提供前置、后置或环绕通知,而是通过 @DeclareParents 注解,将 Encoreable 接口引入到 Performance bean 中。

@DeclareParents 注解由三部分组成:

  • value 属性指定了哪种类型的 bean 要引入该接口。在本例中,也就是所有实现 Performance 的类型。(标记符后面的加号表示是 Performance 的所有子类型,而不是 Performance 本身。)
  • defaultImpl 属性指定了为引入功能提供实现的类。在这里,我们指定的是 Default`Encoreable 提供实现。
  • @DeclareParents 注解所标注的静态属性指明了要引入了接口。在这里,我们所引入的是 Encoreable 接口。

和其他的切面一样,我们需要在 Spring 应用中将 EncoreableIntroducer 声明为一个 bean:

<bean class="concert.EncoreableIntroducer" />

在 Spring 中,注解和自动代理提供了一种很便利的方式来创建切面。它非常简单,并且只涉及到最少的Spring配置。但是,面向注解的切面声明有一个明显的劣势:你必须能够为通知类添加注解。为了做到这一点,必须要有源码

如果你没有源码的话,或者不想将 AspectJ 注解放到你的代码之中,Spring 为切面提供了另外一种可选方案。让我们看一下如何在 Spring XML 配置文件中声明切面。

在 XML 中声明切面

Sprin 的 AOP 配置元素能够以非侵入性的方式声明切面

AOP配置元素 用  途
<aop:advisor> 定义 AOP 通知器
<aop:after> 定义 AOP 后置通知(不管被通知的方法是否执行成功)
<aop:after-returning> 定义 AOP 返回通知
<aop:after-throwing> 定义 AOP 异常通知
<aop:around> 定义 AOP 环绕通知
<aop:aspect> 定义一个切面
<aop:aspectj-autoproxy> 启用 @AspectJ 注解驱动的切面
<aop:before> 定义一个 AOP 前置通知
<aop:config> 顶层的 AOP 配置元素。大多数的 <aop:*> 元素必须包含在 <aop:config> 元素内
<aop:declare-parents> 以透明的方式为被通知的对象引入额外的接口
<aop:pointcut> 定义一个切点

将 Audience 类所有的 AspectJ 注解全部移除掉:

public class Audience {
    public void silenceCellPhones() {
        System.out.println("Silencing cell phones");
    }
    public void takeSeats() {
        System.out.println("Taking seats");
    }
    public void applause() {
        System.out.println("CLAP CLAP CLAP!!!");
    }
    public void demandRefund() {
        System.out.println("Demanding a refund");
    }
}

声明前置和后置通知

<aop:config>  <!-- 大多数的 AOP 配置元素必须在 <aop:config> 元素的上下文内使用。 -->
    <aop:aspect ref="audience">  <!-- 引用 audience bean -->

        <aop:before
            pointcut="execution(** concert.Performance.perform(..))"
            method="silenceCellPhones"/>
        <aop:before
            pointcut="execution(** concert.Performance.perform(..))"
            method="takeSeats"/>

        <aop:after-returning
            pointcut="execution(** concert.Performance.perform(..))"
            method="applause"/>

        <aop:after-throwing
            pointcut="execution(** concert.Performance.perform(..))"
            method="demandRefund"/>
    </aop:aspect>
</aop:config>

使用 <aop:pointcut> 定义命名切点

<aop:config>
    <aop:aspect ref="audience">
        <!-- 将通用的切点表达式抽取到一个切点声明中 -->
        <aop:pointcut id="performance"
            expression="execution(** concert.Performance.perform(..))" />

        <aop:before pointcut-ref="performance" method="silenceCellPhones"/>
        <aop:before pointcut-ref="performance" method="takeSeats"/>
        <aop:after-returning pointcut-ref="performance" method="applause"/>
        <aop:after-throwing pointcut-ref="performance" method="demandRefund"/>
    </aop:aspect>
</aop:config>

如果想让定义的切点能够在多个切面使用,我们可以把 <aop:pointcut> 元素放在 <aop:config> 元素的范围内。

声明环绕通知

前置通知和后置通知有一些限制。如果不使用成员变量存储信息的话,在前置通知和后置通知之间共享信息非常麻烦。

使用环绕通知,我们可以完成前置通知和后置通知所实现的相同功能,而且只需要在一个方法中实现。因为整个通知逻辑是在一个方法内实现的,所以不需要使用成员变量保存状态。

// 新 Audience 类的 watchPerformance() 方法,没有使用任何的注解。
public class Audience {
    public void watchPerformance(ProceedingJoinPoint jp) {
        try {
            System.out.println("Silencing cell phones");
            System.out.println("Taking seats");
            jp.proceed();
            System.out.println("CLAP CLAP CLAP!!!");
        } catch (Throwable e) {
            System.out.println("Demanding a refund");
        }
    }
}

在 XML 中使用 <aop:around> 元素声明环绕通知

<aop:config>
    <aop:aspect ref="audience">
        <aop:pointcut
            id="performance"
            expression="execution(** concert.Performance.perform(..))" />

        <aop:around pointcut-ref="performance" method="watchPerformance"/>
    </aop:aspect>
</aop:config>

为通知传递参数

// 移除掉 TrackCounter 上所有的 @AspectJ 注解
public class TrackCounter {
    private Map<Integer, Integer> trackCounts = new HashMap<>();

    public void countTrack(int trackNumber) {
        int currentCount = getPlayCount(trackNumber);
        trackCounts.put(trackNumber, currentCount + 1);
    }

    public int getPlayCount(int trackNumber) {
        return trackCounts.containsKey(trackNumber)
                ? trackCounts.get(trackNumber) : 0;
    }
}
<bean id="trackCounter" class="soundsystem.TrackCounter" />
<bean id="cd" class="soundsystem.BlankDisc">
    <property name="title" value="Sgt. Pepper's Lonely Hearts Club Band" />
    <property name="artist" value="The Beatles" />
    <property name="tracks">
        <list>
            <value>Sgt. Pepper's Lonely Hearts Club Band</value>
            <value>With a Little Help from My Friends</value>
            <value>Lucy in the Sky with Diamonds</value>
            <value>Getting Better</value>
            <value>Fixing a Hole</value>
            <!-- ...other tracks omitted for brevity... -->
        </list>
    </property>
</bean>

<aop:config>
    <aop:aspect ref="trackCounter">
        <aop:pointcut id="trackPlayed" expression=
            "execution(* soundsystem.CompactDisc.playTrack(int)) and args(trackNumber)" />
        <aop:before pointcut-ref="trackPlayed" method="countTrack"/>
    </aop:aspect>
</aop:config>

我们使用了和前面相同的 aop 命名空间 XML 元素,它们会将 POJO 声明为切面。唯一明显的差别在于切点表达式中包含了一个参数,这个参数会传递到通知方法中。

通过切面引入新的功能

借助 AspectJ@DeclareParents 注解为被通知的方法神奇地引入新的方法。但是 AOP 引入并不是 AspectJ 特有的。使用 Spring aop 命名空间中的 <aop:declare-parents> 元素,我们可以实现相同的功能。

<aop:aspect>
    <aop:declare-parents
        types-matching="concert.Performance+"
        implement-interface="concert.Encoreable"
        default-impl="concert.DefaultEncoreable"
        />
</aop:aspect>

这里有两种方式标识所引入接口的实现。在本例中,我们使用 default-impl 属性用全限定类名来显式指定 Encoreable 的实现。或者,我们还可以使用 delegate-ref 属性来标识。

<bean id="encoreableDelegate" class="concert.DefaultEncoreable" />

<aop:aspect>
    <aop:declare-parents
        types-matching="concert.Performance+"
        implement-interface="concert.Encoreable"
        delegate-ref="encoreableDelegate"
        />
</aop:aspect>

使用 default-impl 来直接标识委托和间接使用 delegate-ref 的区别在于后者是 Spring bean,它本身可以被注入、通知或使用其他的 Spring 配置。

注入 AspectJ 切面

虽然 Spring AOP 能够满足许多应用的切面需求,但是与 AspectJ 相比,Spring AOP 是一个功能比较弱的 AOP 解决方案。AspectJ 提供了 Spring AOP 所不能支持的许多类型的切点。例如,构造器切点。

我们可以借助 Spring 的依赖注入把 bean 装配进 AspectJ 切面中。

我们为上面的演出创建一个新切面。具体来讲,我们以切面的方式创建一个评论员的角色,他会观看演出并且会在演出之后提供一些批评意见。下面的 CriticAspect 就是一个这样的切面。

// 使用 AspectJ 实现表演的评论员
public aspect CriticAspect {
    public CriticAspect() {}

    pointcut performance() : execution(* perform(..));

    afterReturning() : performance() {
        System.out.println(criticismEngine.getCriticism());
    }

    private CriticismEngine criticismEngine;
    public void setCriticismEngine(CriticismEngine criticismEngine) {
        this.criticismEngine = criticismEngine;
    }
}

CriticAspect 的主要职责是在表演结束后为表演发表评论。程序中的 performance() 切点匹配 perform() 方法。当它与 afterReturning() 通知一起配合使用时,我们可以让该切面在表演结束时起作用。

CriticAspect 与一个 CriticismEngine 对象相协作,在表演结束时,调用该对象的 getCriticism() 方法来发表一个苛刻的评论。下图展示了此关系。

切面也需要注入。像其他的 bean 一样,Spring 可以为 AspectJ 切面注入依赖

public interface CriticismEngine {
    public String getCriticism();
}

public class CriticismEngineImpl implements CriticismEngine {
    public CriticismEngineImpl() {}
    public String getCriticism() {
        int i = (int) (Math.random() * criticismPool.length);
        return criticismPool[i];
    }
    // injected
    private String[] criticismPool;
    public void setCriticismPool(String[] criticismPool) {
        this.criticismPool = criticismPool;
    }
}

CriticismEngineImpl 可以使用如下的 XML 声明为一个 Spring bean

<bean id="criticismEngine"
        class="com.springinaction.springidol.CriticismEngineImpl">
    <property name="criticisms">
        <list>
            <value>Worst performance ever!</value>
            <value>I laughed, I cried, then I realized I was at the wrong show.</value>
            <value>A must see show!</value>
        </list>
    </property>
</bean>

criticismEngine bean 注入到 CriticAspect 中:

<bean class="com.springinaction.springidol.CriticAspect"
        factory-method="aspectOf">
    <property name="criticismEngine" ref="criticismEngine" />
</bean>

使用了 factory-method 属性。通常情况下,AspectJ 切面是由 AspectJ 在运行期创建的。

Spring 不能负责创建 CriticAspect,但所有的 AspectJ 切面都提供了一个静态的 aspectOf() 方法,该方法返回切面的一个单例。所以为了获得切面的实例,我们必须使用 factory-method 来调用 asepctOf() 方法而不是调用 CriticAspect 的构造器方法。


转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 bin07280@qq.com

文章标题:《Spring 实战》笔记4:面向切面的 Spring

文章字数:6.7k

本文作者:Bin

发布时间:2019-07-04, 20:09:37

最后更新:2019-12-10, 22:22:38

原始链接:http://coolview.github.io/2019/07/04/Spring/%E3%80%8ASpring%20%E5%AE%9E%E6%88%98%E3%80%8B%E7%AC%94%E8%AE%B04%EF%BC%9A%E9%9D%A2%E5%90%91%E5%88%87%E9%9D%A2%E7%9A%84%20Spring/

版权声明: "署名-非商用-相同方式共享 4.0" 转载请保留原文链接及作者。

目录