4.3.1 定义切面

如果一场演出没有观众的话,那不能称之为演出。对不对?从演出的角度来看,观众是非常重要的,但是对演出本身的功能来讲,它并不是核心,这是一个单独的关注点。因此,将观众定义为一个切面,并将其应用到演出上就是较为明智的做法。

程序清单 4.1展现了 Audience 类,它定义了我们所需的一个切面。

程序清单 4.1 Audience 类:观看演出的切面
package concert;

import org.aspect.lang.annotation.AfterReturning;
import org.aspect.lang.annotation.AfterThrowing;
import org.aspect.lang.annotation.Aspect;
import org.aspect.lang.annotation.Before;

@Aspect
public class Audience {

  @Before("execution(** concert.Performance.perform(..))")
  public void silenceCellPhones() {
    System.out.println("Silencing cell phones");
  }
  
  @Before("execution(** concert.Performance.perform(..))")
  public void takeSeats() {
    System.out.println("Taking seats");
  }
  
  @AfterReturning("execution(** concert.Performance.perform(..))")
  public void applause() {
    System.out.println("CLAP CLAP CLAP!!!");
  }
  
  @AfterThrowing("execution(** concert.Performance.perform(..))")
  public void demandRefund() {
    System.out.println("Demanding a refund");
  }
}

Audience 类使用 @AspectJ 注解进行了标注。该注解表明 Audience 不仅仅是一个 POJO,还是一个切面。Audience 类中的方法都使用注解来定义切面的具体行为。

Audience 有四个方法,定义了一个观众在观看演出时可能会做的事情。在演出之前,观众要就坐 takeSeats() 并将手机调至静音状态 silenceCellPhones()。如果演出很精彩的话,观众应该会鼓掌喝彩 applause()。不过,如果演出没有达到观众预期的话,观众会要求退款 demandRefund()。

可以看到,这些方法都使用了通知注解来表明它们应该在什么时候调用。AspectJ 提供了五个注解来定义通知,如表 4.2 所示。

Audience 使用到了前面五个注解中的三个。takeSeats() 和 silenceCellPhones() 方法都用到了 @Before 注解,表明它们应该在演出开始之前调用。applause() 方法使用了 @AfterReturning 注解,它会在演出成功返回后调用。demand-Refund() 方法上添加了 @AfterThrowing 注解,这表明它会在抛出异常以后执行。

你可能已经注意到了,所有的这些注解都给定了一个切点表达式作为它的值,同时,这四个方法的切点表达式都是相同的。其实,它们可以设置成不同的切点表达式,但是在这里,这个切点表达式就能满足所有通知方法的需求。让我们近距离看一下这个设置给通知注解的切点表达式,我们发现它会在 Performance 的 perform() 方法执行时触发。

相同的切点表达式我们重复了四遍,这可真不是什么光彩的事情。这样的重复让人感觉有些不对劲。如果我们只定义这个切点一次,然后每次需要的时候引用它,那么这会是一个很好的方案。

幸好,我们完全可以这样做:@Pointcut 注解能够在一个 @AspectJ 切面内定义可重用的切点。接下来的以下程序展现了新的 Audience,现在它使用了@Pointcut。

程序清单 4.2 通过 @Pointcut 注解声明频繁使用的切点表达式
package concert;

import org.aspect.lang.annotation.AfterReturning;
import org.aspect.lang.annotation.AfterThrowing;
import org.aspect.lang.annotation.Aspect;
import org.aspect.lang.annotation.Before;
import org.aspect.lang.annotation.Pointcut;

@Aspect
public class Audience {

  @Pointcut("execution(** concert.Performance.perform(..))")
  public void performce() { }

  @Before("performce()")
  public void silenceCellPhones() {
    System.out.println("Silencing cell phones");
  }
  
  @Before("performce()")
  public void takeSeats() {
    System.out.println("Taking seats");
  }
  
  @AfterReturning("performce()")
  public void applause() {
    System.out.println("CLAP CLAP CLAP!!!");
  }
  
  @AfterThrowing("performce()")
  public void demandRefund() {
    System.out.println("Demanding a refund");
  }
}

在 Audience 中,performance() 方法使用了 @Pointcut 注解。为 @Pointcut 注解设置的值是一个切点表达式,就像之前在通知注解上所设置的那样。通过在 performance() 方法上添加 @Pointcut 注解,我们实际上扩展了切点表达式语言,这样就可以在任何的切点表达式中使用 performance() 了,如果不这样做的话,你需要在这些地方使用那个更长的切点表达式。我们现在把所有通知注解中的长表达式都替换成了 performance()。

performance() 方法的实际内容并不重要,在这里它实际上应该是空的。其实该方法本身只是一个标识,供 @Pointcut 注解依附。

需要注意的是,除了注解和没有实际操作的 performance() 方法,Audience 类依然是一个 POJO。我们能够像使用其他的 Java 类那样调用它的方法,它的方法也能够独立地进行单元测试,这与其他的 Java 类并没有什么区别。Audience 只是一个 Java 类,只不过它通过注解表明会作为切面使用而已。

像其他的 Java 类一样,它可以装配为 Spring 中的 bean:

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

如果你就此止步的话,Audience 只会是 Spring 容器中的一个 bean。即便使用了 AspectJ 注解,但它并不会被视为切面,这些注解不会解析,也不会创建将其转换为切面的代理。

如果你使用 JavaConfig 的话,可以在配置类的类级别上通过使用 EnableAspectJAutoProxy 注解启用自动代理功能。程序清单 4.3 展现了如何在 JavaConfig 中启用自动代理。

程序清单 4.3 在 JavaConfig 中启用 AspectJ 注解的自动代理。
package concert;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Component;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;

@Configuration
@EnableAspectJAutoProxy
@Component
public class ConcertConfig {

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

假如你在 Spring 中要使用 XML 来装配 bean 的话,那么需要使用 Spring aop 命名空间中的 <aop:aspectj-autoproxy> 元素。下面的 XML 配置展现了如何完成该功能。

程序清单 4.4 在 XML 中,通过 Spring 的 aop 命名空间启用 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:context="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="context" />
  
  <aop:aspectj-autoproxy />
  
  <bean class="concert.Audience" />

</beans>

不管你是使用 JavaConfig 还是 XML,AspectJ 自动代理都会为使用 @Aspect 注解的 bean 创建一个代理,这个代理会围绕着所有该切面的切点所匹配的 bean。在这种情况下,将会为 Concert bean 创建一个代理,Audience 类中的通知方法将会在 perform() 调用前后执行。

我们需要记住的是,Spring 的 AspectJ 自动代理仅仅使用 @AspectJ 作为创建切面的指导,切面依然是基于代理的。在本质上,它依然是 Spring 基于代理的切面。这一点非常重要,因为这意味着尽管使用的是 @AspectJ 注解,但我们仍然限于代理方法的调用。如果想利用 AspectJ 的所有能力,我们必须在运行时使用 AspectJ 并且不依赖 Spring 来创建基于代理的切面。

到现在为止,我们的切面在定义时,使用了不同的通知方法来实现前置通知和后置通知。但是表 4.2 还提到了另外的一种通知:环绕通知 (around advice)。环绕通知与其他类型的通知有所不同,因此值得花点时间来介绍如何进行编写。

Last updated