Spring AOP

87 阅读7分钟

概念和术语

  • 切面(Aspect):在 Spring AOP 中,切面就是标注了 @Aspect 注解的类
  • 连接点(Join Point):程序执行期间的一个点,比如方法的执行或异常的处理。在 Spring AOP 中,连接点代表被添加通知的方法
  • 切点(Pointcut):将通知和连接点关联起来
  • 通知(Advice):在特定连接点上采取的操作。包括前置通知、后置通知等。

通知

Spring AOP 包括以下类型的通知:

前置通知(Before advice)

在连接点之前执行的通知,但不能阻止执行流程继续到连接点(除非它抛出异常)。

你可以在一个切面中使用 @Before 注解声明一个前置通知。

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
​
@Aspect
public class BeforeExample {
​
    @Before("execution(* com.xyz.dao.*.*(..))")
    public void doAccessCheck() {
        // ...
    }
}

后置通知(After returning advice)

在连接点正常完成后执行的通知(方法正常返回而不抛出异常)。

在匹配的方法执行返回之后执行,我们可以通过 @AfterReturning 注解声明后置通知。

@Aspect
public class AfterReturningExample {
​
    @AfterReturning("execution(* com.xyz.dao.*.*(..))")
    public void doAccessCheck() {
        // ...
    }
}

有时,你需要在通知正文中访问返回的实际值。你可以使用 @AfterReturning 绑定返回值的形式来获取该访问权限,如下示例所示:

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.AfterReturning;
​
@Aspect
public class AfterReturningExample {
​
    @AfterReturning(
        pointcut="execution(* com.xyz.dao.*.*(..))",
        returning="retVal")
    public void doAccessCheck(Object retVal) {
        // ...
    }
}

属性 returning 中使用的名称必须与通知方法中的参数名称相对应。当方法执行返回时,返回值作为相应的参数值传递给通知方法。

异常通知(After throwing advice)

在方法通过抛出异常退出时执行的通知。

你可以使用 @AfterThrowing 注解来声明它,如以下示例所示:

@Aspect
public class AfterThrowingExample {
​
    @AfterThrowing("execution(* com.xyz.dao.*.*(..))")
    public void doRecoveryActions() {
        // ...
    }
}

通常,你希望仅在引发给定类型的异常时才进行通知,并且通常还需要访问连接点中引发的异常。你可以使用 throwing 属性来限制匹配并将抛出的异常绑定到通知参数。如下示例所示:

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.AfterThrowing;
​
@Aspect
public class AfterThrowingExample {
​
    @AfterThrowing(
        pointcut="execution(* com.xyz.dao.*.*(..))",
        throwing="ex")
    public void doRecoveryActions(DataAccessException ex) {
        // ...
    }
}

throwing 属性中使用的名称必须与通知方法中的参数名称相对应。当方法执行因抛出异常而退出时,异常将作为相应的参数值传递给通知方法。

最终通知(After throwing advice)

无论连接点以何种方式退出(正常返回或异常退出),都会执行的通知。

它是通过使用 @After 注解来声明的,最终通知必须准备好处理正常和异常返回情况,它通常用于释放资源和类似目的。以下示例展示了如何使用最终通知:

@Aspect
public class AfterFinallyExample {
​
    @After("execution(* com.xyz.dao.*.*(..))")
    public void doReleaseLock() {
        // ...
    }
}

环绕通知(Around advice)

这是最强大的通知类型,环绕通知可以在方法调用之前和之后执行自定义行为。它还负责选择是继续执行连接点方法还是通过返回自己的返回值或抛出异常来快速执行通知的方法。

环绕通知通过使用 @Around 注解声明,该方法应声明 Object 为其返回类型。并且该方法的第一个参数必须是 ProceedingJoinPoint。在通知方法的主体中,你必须调用 ProceedingJoinPoint.proceed() 方法使底层方法运行。不带参数的 proceed() 调用将导致调用者的原始参数在调用时提供给底层方法。对于高级用例,proceed() 方法有一个重载变体,它接收参数数组(Object[])。调用时,数组中的值将作为底层方法的参数。

环绕通知返回的值是方法调用者看到的返回值,例如,一个简单的缓存切面可以从缓存中返回一个值,或者调用 proceed()。请注意,proceed() 可以在环绕通知的主体内调用一次、多次或根本不调用。所有这些都是合法的。

以下示例展示了如何使用环绕通知:

@Aspect
public class AroundExample {
​
    @Around("execution(* com.xyz..service.*.*(..))")
    public Object doBasicProfiling(ProceedingJoinPoint pjp) throws Throwable {
        // start stopwatch
        Object retVal = pjp.proceed();
        // stop stopwatch
        return retVal;
    }
}

通知参数

任何通知方法都可以声明类型为 org.aspectj.lang.JoinPoint 作为其第一个参数。请注意,环绕通知的第一个参数是 ProceedingJoinPoint,它是 JoinPoint 的子类。

JoinPoint 接口提供了许多有用的方法:

  • getArgs():返回方法参数
  • getThis():返回代理对象
  • getTarget():返回目标对象
  • getSignature():返回对所增强方法的描述
  • toString():打印所增强方法的有用描述

切入点

切入点将连接点和通知组合在一起,Spring AOP 支持多种切入点表达式,常用的有:

execution

execution 是使用最多的一种 Pointcut 表达式,表示某个方法的执行,其标准语法如下:

execution(modifiers-pattern? ret-type-pattern declaring-type-pattern? name-pattern(param-pattern) throws-pattern?)
  • modifiers-pattern?:修饰符匹配
  • ret-type-pattern:返回值匹配
  • declaring-type-pattern?:类路径匹配
  • name-pattern:方法名匹配
  • param-pattern:方法参数匹配
  • throws-pattern?:抛出异常匹配
  • 其中后面跟着 ? 的是可选项
  • 可以使用 * 匹配所有

下面看几个例子:

//表示匹配所有方法  
1execution(* *(..))  
//表示匹配com.fsx.run.UserService中所有的公有方法  
2execution(public * com.fsx.run.UserService.*(..))  
//表示匹配com.fsx.run包及其子包下的所有方法
3execution(* com.fsx.run..*.*(..))  

Pointcut 定义时,还可以使用 &&、||、!这三个运算符进行逻辑运算

// 签名:消息发送切面
@Pointcut("execution(* com.fsx.run.MessageSender.*(..))")
private void logSender(){}
// 签名:消息接收切面
@Pointcut("execution(* com.fsx.run.MessageReceiver.*(..))")
private void logReceiver(){}
// 只有满足发送  或者  接收  这个切面都会切进去
@Pointcut("logSender() || logReceiver()")
private void logMessage(){}

这个例子中,logMessage() 将匹配任何 MessageSender 和 MessageReceiver 中的任何方法。

当我们的切面很多的时候,我们可以把所有的切面放到单独的一个类中,进行统一管理,比如下面:

//集中管理所有的切入点表达式
public class Pointcuts {
​
@Pointcut("execution(* *Message(..))")
public void logMessage(){}
​
@Pointcut("execution(* *Attachment(..))")
public void logAttachment(){}
​
@Pointcut("execution(* *Service.*(..))")
public void auth(){}
}

使用的时候,采用全限定类名+方法名的方式

@Before("com.fsx.run.Pointcuts.logMessage()")
public void before(JoinPoint joinPoint) {
    System.out.println("Logging before " +       joinPoint.getSignature().getName());
}

@annotation

@annotation 用于匹配方法上拥有指定注解的情况。

// 可以匹配所有方法上标有此注解的方法
@Pointcut("@annotation(com.fsx.run.anno.MyAnno)")
public void pointCut() {
}

示例

  1. 添加依赖
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-aspects</artifactId>
    <version>5.3.18</version>
</dependency>
  1. 开启 @AspectJ 支持
@ComponentScan("org.example.aop")
@EnableAspectJAutoProxy
public class MyConfig {
}
  1. 定义目标类
@Component
public class MyService {
​
    public String doSomeThing(String param){
        System.out.println("target method call...");
        return param;
    }
}
  1. 定义切面
@Aspect
@Component
public class MyAspect {
​
    @Pointcut("execution(* org.example.aop.MyService.*(..))")
    void pointcutMethod(){};
​
    /**
     * 前置通知,目标方法之前执行,异常结束不会执行目标方法
     */
    @Before("pointcutMethod()")
    public void doBefore(){
        System.out.println("doBefore call...");
    }
​
    /**
     * 后置通知,目标方法之后执行,目标方法异常结束则不会执行
     */
    @AfterReturning(pointcut = "pointcutMethod()", returning = "retVal")
    public void doAfterReturning(Object retVal){
        System.out.println("doAfterReturning call...   intercept retVal: " + retVal.toString());
    }
​
    /**
     * 异常通知,目标方法异常结束时调用,可以获取到抛出的异常,参数类型可以指定抛出哪些异常时调用异常通知
     * 下面的 Exception 表示任何异常
     * @param ex
     */
    @AfterThrowing(pointcut = "pointcutMethod()", throwing = "ex")
    public void doAfterThrowing(Exception ex){
        System.out.println("doAfterThrowing call...");
        ex.printStackTrace();
    }
​
    /**
     * 最终通知,目标方法异常或正常结束都会调用,但是不能获取目标方法返回值和异常对象
     */
    @After("pointcutMethod()")
    public void doAfter(){
        System.out.println("doAfter call...");
    }
​
    /**
     * 环绕通知,最强大的通知类型,返回值必须是 Object,第一个参数必须是 ProceedingJoinPoint
     * @return
     */
    @Around("pointcutMethod()")
    public Object doAround(ProceedingJoinPoint pjp){
        System.out.println("doAround call before...");
        Object proceed = null;
        try {
            proceed = pjp.proceed();
        } catch (Throwable throwable) {
            throwable.printStackTrace();
        }
        System.out.println("target method result: " + proceed.toString());
        System.out.println("doAround call after...");
        return proceed;
    }
​
​
}
  1. 测试
@Test
public void test(){
    AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(MyConfig.class);
    MyService service = context.getBean(MyService.class);
    System.out.println(service.getClass());
    service.doSomeThing("hello world!");
}

输出

class org.example.aop.MyService$$EnhancerBySpringCGLIB$$f9f629cf
doAround call before...
doBefore call...
target method call...
doAfterReturning call...   intercept retVal: hello world!
doAfter call...
target method result: hello world!
doAround call after...

测试结果中,首先可以看到我们得到是一个 CGLIB 代理类,然后可以知道,环绕通知是最先执行和最后执行的。先是环绕通知 -> 前置通知 -> 目标方法 -> 后置通知 -> 最终通知 -> 环绕通知。

如果方法中抛出异常

@Component
public class MyService {
​
    public String doSomeThing(String param){
        System.out.println("target method call...");
        int i = 1/0;
        return param;
    }
}

输出

class org.example.aop.MyService$$EnhancerBySpringCGLIB$$f9f629cf
doAround call before...
doBefore call...
target method call...
doAfterThrowing call...
java.lang.ArithmeticException: / by zero

后置通知不会调用,异常通知会调用。