Advice 与一个切点表达式相关联,在切点匹配的方法执行之前、之后或周围运行。该切点表达式可以是对一个命名的切点的简单引用,也可以是一个就地声明的切点表达式。

Before Advice / 方法执行前

你可以通过使用 @Before注解在一个切面方法执行前的 advice:

  1. import org.aspectj.lang.annotation.Aspect;
  2. import org.aspectj.lang.annotation.Before;
  3. @Aspect
  4. public class BeforeExample {
  5. // 注意这里引用的是 另外一个公共类中声明的切点
  6. @Before("com.xyz.myapp.CommonPointcuts.dataAccessOperation()")
  7. public void doAccessCheck() {
  8. // ...
  9. }
  10. }

如果就地声明的话,就得重写为下面这种

  1. import org.aspectj.lang.annotation.Aspect;
  2. import org.aspectj.lang.annotation.Before;
  3. @Aspect
  4. public class BeforeExample {
  5. @Before("execution(* com.xyz.myapp.dao.*.*(..))")
  6. public void doAccessCheck() {
  7. // ...
  8. }
  9. }

After Returning Advice / 方法执行正常返回后

当一个匹配的方法执行正常返回时,返回后 advice 运行。你可以通过使用 @AfterReturning注解来声明它。

  1. import org.aspectj.lang.annotation.Aspect;
  2. import org.aspectj.lang.annotation.AfterReturning;
  3. @Aspect
  4. public class AfterReturningExample {
  5. @AfterReturning("com.xyz.myapp.CommonPointcuts.dataAccessOperation()")
  6. public void doAccessCheck() {
  7. // ...
  8. }
  9. }

:::info 你可以有多个 advice 声明(也可以有其他成员),都在同一个 切面。我们在这些例子中只展示了一个 advice 声明,以集中展示每个 advice 的效果。 :::

有时,你需要在 advcie 正文中访问被返回的实际值。你可以使用绑定返回值的 @AfterReturning的形式来获得这种访问权,正如下面的例子所示:

  1. import org.aspectj.lang.annotation.Aspect;
  2. import org.aspectj.lang.annotation.AfterReturning;
  3. @Aspect
  4. public class AfterReturningExample {
  5. @AfterReturning(
  6. pointcut="com.xyz.myapp.CommonPointcuts.dataAccessOperation()",
  7. returning="retVal") // 绑定返回值参数名称
  8. public void doAccessCheck(Object retVal) {
  9. // ...
  10. }
  11. }

返回属性中使用的名称必须与 advice 方法中的参数名称相对应。当一个方法执行返回时,返回值会作为相应的参数值传递给 advice 方法。returning 子句也限制了匹配,只匹配那些返回指定类型的值的方法执行(在这种情况下是 Object,它匹配任何返回值)。

请注意,在返回 advice 后使用时,不可能返回一个完全不同的参考。

After Throwing Advice / 抛出异常后

当匹配的方法执行通过抛出异常退出时。您可以使用 @AfterThrowing 注解来声明它,如以下示例所示:

  1. import org.aspectj.lang.annotation.Aspect;
  2. import org.aspectj.lang.annotation.AfterThrowing;
  3. @Aspect
  4. public class AfterThrowingExample {
  5. @AfterThrowing("com.xyz.myapp.CommonPointcuts.dataAccessOperation()")
  6. public void doRecoveryActions() {
  7. // ...
  8. }
  9. }

通常情况下,你希望建议只在给定类型的异常被抛出时运行,而且你也经常需要在 advice 主体中访问被抛出的异常。你可以使用 throwing 属性来限制匹配(如果需要的话 — 否则使用 Throwable 作为异常类型),并将抛出的异常绑定到 advice 参数上。下面的例子展示了如何做到这一点:

  1. import org.aspectj.lang.annotation.Aspect;
  2. import org.aspectj.lang.annotation.AfterThrowing;
  3. @Aspect
  4. public class AfterThrowingExample {
  5. @AfterThrowing(
  6. pointcut="com.xyz.myapp.CommonPointcuts.dataAccessOperation()",
  7. throwing="ex") // 绑定到参数
  8. public void doRecoveryActions(DataAccessException ex) { // 限制只匹配 DataAccessException 的异常
  9. // ...
  10. }
  11. }

在 throwing 属性中使用的名称必须与 advice 方法中的参数名称相对应。当一个方法的执行通过抛出一个异常退出时,该异常将作为相应的参数值传递给 advice 方法。throwing 子句也限制了匹配,只能匹配那些抛出指定类型的异常的方法执行(本例中是 DataAccessException)。

:::info 请注意,@AfterThrowing 并不表示一个一般的异常处理回调。具体来说,@AfterThrowing advice 法只应该接收来自连接点(用户声明的目标方法)本身的异常,而不是来自附带的 @After/@AfterReturning 方法。 :::

After (Finally) Advice / 方法执行后(Finally)

当一个匹配的方法执行退出时,After(Finally)advice 会运行。它是通过使用 @After 注解来声明的。After advice 必须准备好处理正常和异常的返回条件。它通常被用于释放资源和类似的目的。下面的例子展示了如何使用 after finally advice。

  1. @Aspect
  2. public class AfterFinallyExample {
  3. @After("com.xyz.myapp.CommonPointcuts.dataAccessOperation()")
  4. public void doReleaseLock() {
  5. // ...
  6. }
  7. }

:::info 请注意,AspectJ 中的 @After advice 被定义为 “after finally advice”,类似于 try-catch 语句中的 finally 块。它将对任何结果、正常返回或从连接点(用户声明的目标方法)抛出的异常进行调用,这与 @AfterReturning 不同,后者只适用于成功的正常返回。 :::

Around Advice / 环绕通知

最后一种建议是 环绕 Advice。 「环绕」一个匹配的方法的执行而运行。它有机会在方法运行之前和之后进行工作,并决定何时、如何、甚至是否真正运行该方法。如果你需要以线程安全的方式分享方法执行前后的状态(例如,启动和停止一个计时器),通常会使用 「环绕通知」。始终使用最不强大的 advice 形式来满足你的要求(也就是说,如果 before advice 也可以,就不要使用 around advice)。

环绕通知是通过使用 @Around注解来声明的。advice 方法的第一个参数必须是 ProceedingJoinPoint类型。在 advice 的正文中,对ProceedingJoinPoint调用 proceed()会导致底层方法的运行。proceed 方法也可以传入一个 Object[]。数组中的值将作为方法执行时的参数。

:::info 当用 Object[]调用时, proceed 的行为与 AspectJ 编译器编译的环绕通知的行为有些不同。对于使用传统 AspectJ 语言编写的环绕通知,传递给 Proceed 的参数数必须与传递给环绕通知的参数数相匹配(而不是底层连接点的参数数),并且在给定的参数位置传递给Proceed 的值会取代该值所绑定的实体在连接点上的原始值(如果这现在没有意义,请不要担心)。Spring 采取的方法更简单,也更符合其基于代理的、只执行的语义。只有当你编译为 Spring 编写的 @AspectJ 切面并使用 AspectJ 编译器和 weaver 的参数进行时,你才需要注意这个区别。有一种方法可以在 Spring AOP 和 AspectJ 之间100%兼容地编写这些切面,这将在下面关于 advice 参数的章节中讨论。 :::

下面的例子展示了如何使用 around 通知:

  1. import org.aspectj.lang.annotation.Aspect;
  2. import org.aspectj.lang.annotation.Around;
  3. import org.aspectj.lang.ProceedingJoinPoint;
  4. @Aspect
  5. public class AroundExample {
  6. @Around("com.xyz.myapp.CommonPointcuts.businessService()")
  7. public Object doBasicProfiling(ProceedingJoinPoint pjp) throws Throwable {
  8. // start stopwatch , 这里写你自己的逻辑,比如:写一个方法开始运行开始的计时操作
  9. Object retVal = pjp.proceed(); // 调用目标方法
  10. // stop stopwatch // 这里写你自己的逻辑,比如:写一个方法结束运行时的计时操作,并统计这个方法执行的耗时信息
  11. return retVal;
  12. }
  13. }

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

Advice Parameters / advice 的参数

Spring 提供了完全类型化的 advice,这意味着你可以在 advice 签名中声明你需要的参数(就像我们在前面看到的返回和抛出的例子一样),而不是一直用 Object[]数组工作。我们将在本节后面看到如何使参数和其他上下文值对 advice 主体可用。首先,我们看一下如何编写通用的 advice,它可以找出 advice 当前所 advice 的方法。

访问当前的 JoinPoint

任何 advice 方法都可以声明一个 org.aspectj.lang.JoinPoint类型的参数作为它的第一个参数(注意,Arount Advice 需要声明一个 ProceedingJoinPoint 类型的第一个参数,它是 JoinPoint 的一个子类。JoinPoint 接口提供了许多有用的方法:

  • getArgs():获取方法参数
  • getThis():获取代理对象
  • getTarget():获取目标对象
  • getSignature(): 获取方法的签名
  • toString(): 获取对 advice 方法有用的描述

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

将参数传递给 advice

我们已经看到了如何绑定返回值或异常值(使用返回后和抛出后通知)。为了使参数值对 advice 主体可用,你可以使用 args 的绑定形式。如果你在 args 表达式中使用参数名来代替类型名,那么当 advice 被调用时,相应参数的值将作为参数值被传递。一个例子可以让我们更清楚地了解这一点。假设你想 advice 执行将一个 account 对象作为第一个参数的 DAO 操作,并且你需要在 advice 正文中访问该 account。你可以写如下内容:

  1. // 这里使用了 逻辑操作符来限定参数,第一个参数必须是 account 名称,与下面声明的 Account 对应
  2. @Before("com.xyz.myapp.CommonPointcuts.dataAccessOperation() && args(account,..)")
  3. public void validateAccount(Account account) {
  4. // ...
  5. }

pointcut 表达式的 args(account,..)部分有两个目的:

  • 首先,它将匹配限制在方法的执行上,即方法至少需要一个参数,并且传递给该参数的参数是一个 Account 的实例。
  • 其次,它使实际的 Account 对象通过 account 参数对 advice 可用

另一种写法是声明一个 pointcut,当它与一个连接点匹配时 「提供 」Account 对象的值,然后从 advice 中引用命名的 pointcut。这看起来就像这样:

  1. @Pointcut("com.xyz.myapp.CommonPointcuts.dataAccessOperation() && args(account,..)")
  2. private void accountDataAccessOperation(Account account) {}
  3. @Before("accountDataAccessOperation(account)")
  4. public void validateAccount(Account account) {
  5. // ...
  6. }

更多细节请参见 AspectJ 编程指南。

代理对象(this)、目标对象(target)和注解(@within、@target、@annotation 和 @args)都可以用类似的方式绑定。接下来的两个例子展示了如何匹配带有 @Auditable注解的方法的执行并提取 AuditCode。

两个例子中的第一个显示了 @Auditable 注解的定义:

  1. @Retention(RetentionPolicy.RUNTIME)
  2. @Target(ElementType.METHOD)
  3. public @interface Auditable {
  4. AuditCode value();
  5. }

两个例子中的第二个显示了与执行 @Auditable方法相匹配的 advice :

  1. @Before("com.xyz.lib.Pointcuts.anyPublicMethod() && @annotation(auditable)")
  2. public void audit(Auditable auditable) {
  3. AuditCode code = auditable.value();
  4. // ...
  5. }

Advice 的参数和泛型

Spring AOP 可以处理类声明和方法参数中使用的泛型。假设你有一个像下面这样的泛型:

  1. public interface Sample<T> {
  2. void sampleGenericMethod(T param);
  3. void sampleGenericCollectionMethod(Collection<T> param);
  4. }

你可以将方法类型的拦截限制在某些参数类型上,方法是将 advice 参数打到你想拦截的参数类型上:

  1. @Before("execution(* ..Sample+.sampleGenericMethod(*)) && args(param)")
  2. public void beforeSampleMethod(MyType param) {
  3. // Advice implementation
  4. }

这种方法对通用集合不起作用。因此,你不能像下面这样定义一个切点:

  1. @Before("execution(* ..Sample+.sampleGenericCollectionMethod(*)) && args(param)")
  2. public void beforeSampleMethod(Collection<MyType> param) {
  3. // Advice implementation
  4. }

为了实现这一点,我们必须检查集合中的每一个元素,这是不太合理的,因为我们也无法决定如何处理一般的空值。为了实现与此类似的东西,你必须将参数输入到 Collection<?>中,并手动检查元素的类型。

确定参数名称

advice 调用中的参数绑定依赖于将 pointcut 表达式中使用的名称与 advice 和 pointcut 方法签名中声明的参数名称相匹配。参数名称无法通过 Java 反射获得,因此 Spring AOP 使用以下策略来确定参数名称:

如果用户已经明确指定了参数名,那么将使用指定的参数名。advice 和 pointcut 注解都有一个可选的 argNames 属性,你可以用它来指定被注解方法的参数名。这些参数名在运行时是可用的。下面的例子展示了如何使用 argNames 属性:

  1. @Before(value="com.xyz.lib.Pointcuts.anyPublicMethod() && target(bean) && @annotation(auditable)",
  2. argNames="bean,auditable")
  3. public void audit(Object bean, Auditable auditable) {
  4. AuditCode code = auditable.value();
  5. // ... use code and bean
  6. }

如果第一个参数是 JoinPoint、ProceedingJoinPoint 或 JoinPoint.StaticPart 类型,你可以从 argNames 属性的值中省略参数的名称。例如,如果你修改前面的 advice 以接收连接点对象,argNames 属性不需要包括它:

  1. @Before(value="com.xyz.lib.Pointcuts.anyPublicMethod() && target(bean) && @annotation(auditable)",
  2. argNames="bean,auditable")
  3. public void audit(JoinPoint jp, Object bean, Auditable auditable) {
  4. AuditCode code = auditable.value();
  5. // ... use code, bean, and jp
  6. }

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

  1. @Before("com.xyz.lib.Pointcuts.anyPublicMethod()")
  2. public void audit(JoinPoint jp) {
  3. // ... use jp
  4. }

使用 argNames属性有点笨拙,所以如果没有指定 argNames属性,Spring AOP 会查看类的调试信息并尝试从局部变量表中确定参数名称。只要类在编译时带有调试信息(至少是 -g:vars),这个信息就存在。使用这个标志进行编译的后果是。

  1. 你的代码稍微容易理解(逆向工程)
  2. 类文件的大小稍微大一点(通常不重要)
  3. 你的编译器不应用删除未使用的局部变量的优化。换句话说,使用这个标志进行构建,你应该不会遇到困难。

:::info 如果一个 @AspectJ 切面已经被 AspectJ 编译器(ajc)编译,即使没有调试信息,你也不需要添加 argNames 属性,因为编译器会保留需要的信息。 :::

如果代码在编译时没有必要的调试信息,Spring AOP 会尝试推断出绑定变量与参数的配对关系(例如,如果在 pointcut 表达式中只绑定了一个变量,而 advice 方法只接受一个参数,那么这种配对关系就很明显)。如果考虑到可用的信息,变量的绑定是不明确的,就会抛出一个 AmbiguousBindingException。

如果上述所有的策略都失败了,就会抛出一个 IllegalArgumentException。

参数的讨论

我们在前面说过,我们将描述如何编写一个带参数的程序调用,并在 Spring AOP 和 AspectJ 中一致运行。解决方案是确保建议签名按顺序绑定每个方法参数。下面的例子展示了如何做到这一点:

  1. @Around("execution(List<Account> find*(..)) && " +
  2. "com.xyz.myapp.CommonPointcuts.inDataAccessLayer() && " +
  3. "args(accountHolderNamePattern)")
  4. public Object preProcessQueryPattern(ProceedingJoinPoint pjp,
  5. String accountHolderNamePattern) throws Throwable {
  6. String newPattern = preProcess(accountHolderNamePattern);
  7. return pjp.proceed(new Object[] {newPattern});
  8. }

在许多情况下,你还是会做这种绑定(如前面的例子)。

Advice 顺序

当多个 advice 都想在同一个连接点上运行时会发生什么?Spring AOP 遵循与 AspectJ 相同的优先级规则来决定 advice 的执行顺序。优先级最高的 advice 在 「进入」时首先运行(因此,给定两片 advice 之前,优先级最高的那片先运行)。从一个连接点 「出来」时,优先级最高的 advice 最后运行(因此,给定两片后 advice,优先级最高的 advice 将第二运行)。

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

:::info 一个特定切面的每个不同的 advice 类型在概念上都是为了直接适用于连接点。因此,@AfterThrowing advoce 方法不应该从附带的@After/@AfterReturning 方法中接收异常。

从 Spring Framework 5.2.7 开始,在同一 @Aspect 类中定义的、需要在同一连接点运行的 advice 方法会根据其 advice 类型被分配优先权,优先权从高到低的顺序如下。@Around, @Before, @After, @AfterReturning, @AfterThrowing。但是请注意,@After advice 方法将有效地在同一切面的任何 @AfterReturning 或 @AfterThrowing advice 方法之后被调用,这符合 AspectJ 对 @After 的 「最后建议」语义。

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