声明通知

通知(Advice)与切点表达式相关联,并在切点匹配的方法执行之前、之后或周围运行。 切点表达式既可以是_内联切点_,也可以是对 命名切点 的引用。

前置通知

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

下例使用内联切点表达式。

  • Java

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;

@Aspect
public class BeforeExample {

  @Before("execution(* com.xyz.dao.*.*(..))")
  public void doAccessCheck() {
    // ...
  }
}

如果我们使用 命名切点,我们可以将前面的例子重写如下:

  • Java

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;

@Aspect
public class BeforeExample {

  @Before("com.xyz.CommonPointcuts.dataAccessOperation()")
  public void doAccessCheck() {
    // ...
  }
}

返回后通知

返回后通知(After returning advice)在匹配的方法执行正常返回时运行。 你可以使用 @AfterReturning 注解来声明它。

  • Java

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.AfterReturning;

@Aspect
public class AfterReturningExample {

  @AfterReturning("execution(* com.xyz.dao.*.*(..))")
  public void doAccessCheck() {
    // ...
  }
}
你可以在同一个切面中拥有多个通知声明(以及其他成员)。 在这些示例中,我们只展示单个通知声明,以集中展示每个通知的效果。

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

  • Java

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 属性中使用的名称必须与通知方法中的参数名称相对应。 当方法执行返回时,返回值将作为相应的参数值传递给通知方法。 returning 子句还限制匹配,使其仅匹配那些返回指定类型值的方法执行 (在本例中为 Object,它匹配任何返回值)。

请注意,使用返回后通知时,不可能返回一个完全不同的引用。

抛出异常后通知

抛出异常后通知(After throwing advice)在匹配的方法执行通过抛出异常退出时运行。 你可以使用 @AfterThrowing 注解来声明它,如下例所示:

  • Java

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.AfterThrowing;

@Aspect
public class AfterThrowingExample {

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

通常,你希望通知仅在抛出给定类型的异常时运行,并且你通常还需要在通知体中访问抛出的异常。 你可以使用 throwing 属性来限制匹配(如果需要——否则使用 Throwable 作为异常类型) 并将抛出的异常绑定到通知参数。下例展示了如何做到这一点:

  • Java

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 属性中使用的名称必须与通知方法中的参数名称相对应。 当方法执行通过抛出异常退出时,该异常将作为相应的参数值传递给通知方法。 throwing 子句还限制匹配,使其仅匹配那些抛出指定类型异常的方法执行 (在本例中为 DataAccessException)。

请注意,@AfterThrowing 并不表示通用的异常处理回调。 具体来说,@AfterThrowing 通知方法只应该接收来自连接点(用户声明的目标方法)本身的异常, 而不应该接收来自伴随的 @After/@AfterReturning 方法的异常。

后置(最终)通知

后置(最终)通知(After (finally) advice)在匹配的方法执行退出时运行。 它通过使用 @After 注解来声明。后置通知必须准备好处理正常返回和异常返回的情况。 它通常用于释放资源和类似目的。下例展示了如何使用后置(最终)通知:

  • Java

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.After;

@Aspect
public class AfterFinallyExample {

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

请注意,AspectJ 中的 @After 通知被定义为“后置最终通知”,类似于 try-catch 语句中的 finally 块。 它将在任何结果(正常返回或从连接点(用户声明的目标方法)抛出异常)下被调用, 这与 @AfterReturning 相反,后者仅适用于成功的正常返回。

环绕通知

最后一种通知是_环绕_通知(Around advice)。环绕通知在匹配的方法执行“周围”运行。 它有机会在方法运行之前和之后进行工作,并确定方法何时、如何甚至是否真正运行。 如果你需要以线程安全的方式在方法执行之前和之后共享状态(例如,启动和停止计时器), 通常会使用环绕通知。

始终使用满足你要求的功能最弱的通知形式。

例如,如果_前置_通知足以满足你的需求,请不要使用_环绕_通知。

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

当使用 Object[] 调用 proceed 时,其行为与 AspectJ 编译器编译的环绕通知的 proceed 行为略有不同。 对于使用传统 AspectJ 语言编写的环绕通知,传递给 proceed 的参数数量必须与传递给环绕通知的参数数量匹配 (而不是底层连接点接受的参数数量),并且在给定参数位置传递给 proceed 的值会取代该值绑定到的实体的连接点处的原始值 (如果现在不明白也不用担心)。

Infra 采用的方法更简单,并且更符合其基于代理的、仅执行的语义。 只有当你编译为 Infra 编写的 @AspectJ 切面并使用 AspectJ 编译器和织入器对带参数的 proceed 进行处理时, 你才需要注意这种差异。有一种方法可以编写这样的切面,使其在 Infra AOP 和 AspectJ 之间 100% 兼容, 这将在 下一节关于通知参数的内容 中讨论。

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

如果你将环绕通知方法的返回类型声明为 void,则始终会向调用者返回 null, 从而有效地忽略任何 proceed() 调用的结果。因此,建议环绕通知方法声明 Object 返回类型。 通知方法通常应返回 proceed() 调用返回的值,即使底层方法具有 void 返回类型也是如此。 但是,根据用例,通知可以根据需要返回缓存值、包装值或其他值。

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

  • Java

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.ProceedingJoinPoint;

@Aspect
public class AroundExample {

  @Around("execution(* com.xyz..service.*.*(..))")
  public Object doBasicProfiling(ProceedingJoinPoint pjp) throws Throwable {
    // 启动计时器
    Object retVal = pjp.proceed();
    // 停止计时器
    return retVal;
  }
}

通知参数

Infra 提供了完全类型的通知,这意味着你在通知签名中声明所需的参数(如我们之前在返回和抛出示例中看到的那样), 而不是一直使用 Object[] 数组。我们将要在本节稍后部分看到如何使参数和其他上下文值对通知体可用。 首先,我们来看看如何编写通用的通知,以便了解通知当前正在通知的方法。

访问当前 JoinPoint

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

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

  • getArgs(): 返回方法参数。

  • getThis(): 返回代理对象。

  • getTarget(): 返回目标对象。

  • getSignature(): 返回正在被通知的方法的描述。

  • toString(): 打印正在被通知的方法的有用描述。

有关更多详细信息,请参阅 javadoc

将参数传递给通知

我们已经看到了如何绑定返回值或异常值(使用返回后和抛出异常后通知)。 要使参数值对通知体可用,可以使用 args 的绑定形式。 如果在 args 表达式中使用参数名称代替类型名称,则在调用通知时,相应参数的值将作为参数值传递。 一个例子应该会让这一点更清楚。 假设你想通知将 Account 对象作为第一个参数的 DAO 操作的执行,并且你需要在这个通知体中访问该账户。 你可以这样写:

  • Java

@Before("execution(* com.xyz.dao.*.*(..)) && args(account,..)")
public void validateAccount(Account account) {
  // ...
}

切点表达式的 args(account,..) 部分有两个作用。首先,它限制匹配,使其仅匹配那些方法至少接受一个参数, 并且传递给该参数的实参是 Account 实例的方法执行。 其次,它通过 account 参数使实际的 Account 对象对通知可用。

编写此代码的另一种方法是声明一个切点,当它匹配连接点时“提供” Account 对象值, 然后从通知中引用该命名切点。如下所示:

  • Java

@Pointcut("execution(* com.xyz.dao.*.*(..)) && args(account,..)")
private void accountDataAccessOperation(Account account) {}

@Before("accountDataAccessOperation(account)")
public void validateAccount(Account account) {
  // ...
}

有关更多详细信息,请参阅 AspectJ 编程指南。

代理对象 (this)、目标对象 (target) 和注解 (@within@target@annotation@args) 都可以以类似的方式绑定。下一组示例展示了如何匹配带有 @Auditable 注解的方法执行并提取审计代码:

以下显示了 @Auditable 注解的定义:

  • Java

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Auditable {
  AuditCode value();
}

以下显示了匹配 @Auditable 方法执行的通知:

  • Java

@Before("com.xyz.Pointcuts.publicMethod() && @annotation(auditable)") (1)
public void audit(Auditable auditable) {
  AuditCode code = auditable.value();
  // ... 使用 code
}
1 引用在 组合切点表达式 中定义的 publicMethod 命名切点。

通知参数与泛型

Infra AOP 可以处理类声明和方法参数中使用的泛型。 假设你有如下泛型类型:

  • Java

public interface Sample<T> {
  void sampleGenericMethod(T param);
  void sampleGenericCollectionMethod(Collection<T> param);
}

你可以通过将通知参数绑定到要拦截该方法的参数类型,将方法类型的拦截限制为某些参数类型:

  • Java

@Before("execution(* ..Sample+.sampleGenericMethod(*)) && args(param)")
public void beforeSampleMethod(MyType param) {
  // 通知实现
}

这种方法不适用于泛型集合。所以你不能像下面这样定义切点:

  • Java

@Before("execution(* ..Sample+.sampleGenericCollectionMethod(*)) && args(param)")
public void beforeSampleMethod(Collection<MyType> param) {
  // 通知实现
}

要使其工作,我们需要检查集合的每个元素,这是不合理的,因为我们也无法决定通常如何处理 null 值。 要实现类似的功能,你必须将参数类型定义为 Collection<?> 并手动检查元素的类型。

确定参数名称

通知调用中的参数绑定依赖于将切点表达式中使用的名称与通知和切点方法签名中声明的参数名称进行匹配。

本节互换使用术语_实参(argument)和_形参(parameter),因为 AspectJ API 将形参名称称为实参名称。

Infra AOP 使用以下 ParameterNameDiscoverer 实现来确定参数名称。 每个发现者都有机会发现参数名称,第一个成功的发现者获胜。 如果没有任何已注册的发现者能够确定参数名称,则会抛出异常。

AspectJAnnotationParameterNameDiscoverer

使用用户通过相应通知或切点注解中的 argNames 属性显式指定的参数名称。 有关详细信息,请参阅 显式参数名称

KotlinReflectionParameterNameDiscoverer

使用 Kotlin 反射 API 确定参数名称。 仅当类路径上存在此类 API 时才使用此发现者。

StandardReflectionParameterNameDiscoverer

使用标准 java.lang.reflect.Parameter API 确定参数名称。 要求使用 javac-parameters 标志编译代码。Java 8+ 上的推荐方法。

AspectJAdviceParameterNameDiscoverer

从切点表达式、returningthrowing 子句中推断参数名称。 有关所用算法的详细信息,请参阅 javadoc

显式参数名称

@AspectJ 通知和切点注解具有一个可选的 argNames 属性,你可以使用它来指定带注解方法的参数名称。

如果 @AspectJ 切面已由 AspectJ 编译器 (ajc) 编译(即使没有调试信息),则无需添加 argNames 属性, 因为编译器保留了所需的信息。

同样,如果 @AspectJ 切面已使用 javac-parameters 标志编译,则无需添加 argNames 属性, 因为编译器保留了所需的信息。

下例展示了如何使用 argNames 属性:

  • Java

@Before(value = "com.xyz.Pointcuts.publicMethod() && target(bean) && @annotation(auditable)", (1)
  argNames = "bean,auditable") (2)
public void audit(Object bean, Auditable auditable) {
  AuditCode code = auditable.value();
  // ... 使用 code 和 bean
}
1 引用在 组合切点表达式 中定义的 publicMethod 命名切点。
2 声明 beanauditable 作为参数名称。

如果第一个参数的类型是 JoinPointProceedingJoinPointJoinPoint.StaticPart, 你可以从 argNames 属性的值中省略该参数的名称。 例如,如果你修改前面的通知以接收连接点对象,则 argNames 属性不需要包含它:

  • Java

@Before(value = "com.xyz.Pointcuts.publicMethod() && target(bean) && @annotation(auditable)", (1)
  argNames = "bean,auditable") (2)
public void audit(JoinPoint jp, Object bean, Auditable auditable) {
  AuditCode code = auditable.value();
  // ... 使用 code, bean, 和 jp
}
1 引用在 组合切点表达式 中定义的 publicMethod 命名切点。
2 声明 beanauditable 作为参数名称。

给予 JoinPointProceedingJoinPointJoinPoint.StaticPart 类型的第一个参数的特殊处理 对于不收集任何其他连接点上下文的通知方法特别方便。在这种情况下,你可以省略 argNames 属性。 例如,以下通知不需要声明 argNames 属性:

  • Java

@Before("com.xyz.Pointcuts.publicMethod()") (1)
public void audit(JoinPoint jp) {
  // ... 使用 jp
}
1 引用在 组合切点表达式 中定义的 publicMethod 命名切点。

带参数执行

我们之前提到过,我们将描述如何编写在 Infra AOP 和 AspectJ 之间一致工作的带参数的 proceed 调用。 解决方案是确保通知签名按顺序绑定每个方法参数。 下例展示了如何做到这一点:

  • Java

@Around("execution(List<Account> find*(..)) && " +
    "com.xyz.CommonPointcuts.inDataAccessLayer() && " +
    "args(accountHolderNamePattern)") (1)
public Object preProcessQueryPattern(ProceedingJoinPoint pjp,
    String accountHolderNamePattern) throws Throwable {
  String newPattern = preProcess(accountHolderNamePattern);
  return pjp.proceed(new Object[] {newPattern});
}
1 引用在 共享命名切点定义 中定义的 inDataAccessLayer 命名切点。

在许多情况下,无论如何你都会进行这种绑定(如前面的例子所示)。

通知顺序

当多条通知都想在同一个连接点运行时会发生什么? Infra AOP 遵循与 AspectJ 相同的优先级规则来确定通知执行的顺序。 优先级最高的通知在“进入时”首先运行(因此,给定两个前置通知,优先级高的那个先运行)。 从连接点“退出时”,优先级最高的通知最后运行(因此,给定两个后置通知,优先级高的那个后运行)。

当定义在不同切面中的两条通知都需要在同一个连接点运行时,除非你另行指定,否则执行顺序是未定义的。 你可以通过指定优先级来控制执行顺序。这可以通过在切面类中实现 infra.core.Ordered 接口 或用 @Order 注解标注它来以常规的 Infra 方式完成。给定两个切面, 从 Ordered.getOrder() 返回较低值(或注解值)的切面具有较高的优先级。

特定切面的每种不同通知类型在概念上都意味着直接应用于连接点。 因此,@AfterThrowing 通知方法不应该接收来自伴随的 @After/@AfterReturning 方法的异常。

在同一个 @Aspect 类中定义的、需要在同一个连接点运行的通知方法,根据其通知类型按以下顺序(从高到低)分配优先级: @Around, @Before, @After, @AfterReturning, @AfterThrowing。 但请注意,@After 通知方法实际上将在同一个切面中的任何 @AfterReturning@AfterThrowing 通知方法之后被调用, 遵循 AspectJ 对 @After 的“后置最终通知”语义。

当在同一个 @Aspect 类中定义的同一类型的两条通知(例如,两个 @After 通知方法)都需要在同一个连接点运行时, 顺序是未定义的(因为没有办法通过反射检索 javac 编译类的源代码声明顺序)。 考虑将这些通知方法折叠为每个 @Aspect 类中每个连接点一个通知方法, 或者将通知重构为单独的 @Aspect 类,你可以通过 Ordered@Order 在切面级别对其进行排序。