使用注解创建切面

2020/10/30 50

Spring

使用注解来创建切面是 AspectJ 5 所引入的关键特性。AspectJ 5 之前,编写 AspectJ 切面需要学习一种 Java 语言的扩展,但是 AspectJ 面向注解的模型可以非常简便的通过少量注解把任意类型转变为切面。

定义切面

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

package concert;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.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 类使用 @Aspect 注解进行了标注,表明 Audience 不仅使一个 POJO,还是一个切面。Audience 类中的方法都使用注解来定义切面的具体行为。

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

AspectJ 提供了五个注解来定义通知

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

Audience 使用到了前面五个注解中的三个,所有这些注解都给定了一个切点表达式,它们都是相同的,这可真不是什么光彩的事。如果我们只定义这个切点一次,然后每次需要的时候引用它,那就好了。幸好 @Pointcut 注解能够在一个 @Aspect 切面内定义可重用的切点。

package concert;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;

@Aspect
public class Audience {

    // 定义命名的切点
    @Pointcut("execution(** 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");
    }
}

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

需要注意的是,Audience 只是一个 Java 类,只不过它通过注解表明会作为切面使用而已。像其他 Java 类一样,它可以装配为 Spring 中的 bean:

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

如果就此止步的话, Audience 只会是 Spring 容器中的一个 bean,即便使用了 AspectJ 的注解,它并不会被视为切面。如果你使用 JavaConfig 的话,可以在配置类的类级别上通过使用 EnableAspectJAutoProxy 注解启用自动代理功能。

package concert;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;

@Configuration
@EnableAspectJAutoProxy
@ComponentScan
public class ConcertConfig {
    @Bean
    public Audience audience() {
        return new Audience();
    }
}

如果要在 XML 中配置的话,使用 <aop:aspectj-autoproxy />

我们要记住的是,Spring 的 AspectJ 自动代理仅仅使用 @AspectJ 作为创建切面的指导,在本质上,它依然是 Spring 基于代理的切面。

环绕通知

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

package concert;
import org.aspectj.lang.annotation.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Pointcut;

@Aspect
public class Audience {

    // 定义命名的切点
    @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");
        }
    }
}

在这里,@Around 注解表明 watchPerformance() 方法会作为 performance() 切点的环绕通知。在这个通知中,观众在演出之前会将手机调制静音并就坐,演出结束后会👏喝彩。如果演出失败,观众会要求退款。

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

新通知方法,接受一个 ProceedingJoinPoint 对象作为参数,当要将控制权交给被通知的方法时,需要调用 proceed(),如果不调用这个方法,会阻塞对被通知方法的调用,你也可以对它进行多次调用,在重试场景下使用。

处理通知中的参数

如果切面所通知的方法方法有参数怎么办?切面能访问传递给通知方法的参数吗?

package soundsystem;
import java.util.HashMap;
import java.util.Map;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;

@Aspect
public class TrackCounter {
    private Map<Integer, Integer> trackCounts = new HashMap<Integer, Integer>();

    @Pointcut("execution(* soundsystem.CompactDisc.playTrack(int)) && args(trackNumber)")
    public void trackPlayed(int 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 也与切点方法前面中的参数相匹配。

这个参数会传递到通知方法中,@Before 和命名切点 trackPlayed(trackNumber) 参数名称是一样的,这样就完成了从命名切点到通知方法的参数转移。