5.4.4. 声明切点

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

前置通知

您可以使用注解@Before在切面声明之前的通知:

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

@Aspect
public class BeforeExample {

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

如果我们使用就地切入点表达式,我们可以将前面的示例重写为以下示例:

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

@Aspect
public class BeforeExample {

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

返回通知

返回通知后,当匹配的方法执行正常返回时运行。您可以使用@AfterReturning注解来声明它:

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

@Aspect
public class AfterReturningExample {

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

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

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

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

@Aspect
public class AfterReturningExample {

    @AfterReturning(
        pointcut="com.xyz.myapp.CommonPointcuts.dataAccessOperation()",
        returning="retVal")
    public void doAccessCheck(Object retVal) {
        // ...
    }
}

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

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

异常通知

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

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

@Aspect
public class AfterThrowingExample {

    @AfterThrowing("com.xyz.myapp.CommonPointcuts.dataAccessOperation()")
    public void doRecoveryActions() {
        // ...
    }
}

通常,您希望通知仅在引发给定类型的异常时运行,并且您还经常需要访问通知正文中引发的异常。您可以使用throwing属性来限制匹配(如果需要 -否则用Throwable 作异常类型)并将抛出的异常绑定到通知参数。以下示例显示了如何执行此操作:

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

@Aspect
public class AfterThrowingExample {

    @AfterThrowing(
        pointcut="com.xyz.myapp.CommonPointcuts.dataAccessOperation()",
        throwing="ex")
    public void doRecoveryActions(DataAccessException ex) {
        // ...
    }
}

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

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

(最终)后置通知

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

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

@Aspect
public class AfterFinallyExample {

    @After("com.xyz.myapp.CommonPointcuts.dataAccessOperation()")
    public void doReleaseLock() {
        // ...
    }
}

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

环绕通知

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

始终使用满足您要求的最不强大的通知形式。例如,如果之前的通知足以满足您的需求,请不要使用环绕通知。

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

使用proceed 调用Object[]时的行为与 AspectJ 编译器编译的proceed 焕荣通知的行为略有不同。对于使用传统 AspectJ 语言编写的环绕通知,传递给proceed的参数 数量必须与传递给环绕通知的参数数量相匹配(而不是底层连接点采用的参数数量),并且传递给给定继续进行的值参数位置替换值绑定到的实体的连接点处的原始值(如果现在没有意义,请不要担心)。Spring 采用的方法更简单,更符合其基于代理的、仅执行的语义。如果您编译为 Spring 编写的@AspectJ切面和与 AspectJ 编译器和编织器一起使用proceed参数,您只需要注意这种差异 。有一种方法可以编写跨 Spring AOP 和 AspectJ 100% 兼容的切面,这将在 下一节有关通知参数的部分中讨论。

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

如果您将环绕通知方法的返回类型声明为void,将始终返回给调用者null ,有效地忽略任何调用proceed()的结果。因此,使用环绕通知方法声明返回类型为Object. 通知方法通常应该返回调用proceed()返回的值,即使底层方法具有void返回类型。但是,根据用例,通知可以选择返回缓存值、包装值或其他值。

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

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

@Aspect
public class AroundExample {

    @Around("com.xyz.myapp.CommonPointcuts.businessService()")
    public Object doBasicProfiling(ProceedingJoinPoint pjp) throws Throwable {
        // start stopwatch
        Object retVal = pjp.proceed();
        // stop stopwatch
        return retVal;
    }
}

通知参数

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

访问当前JoinPoint

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

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

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

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

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

  • getSignature():返回所通知方法的描述。

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

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

将参数传递给 Advice

我们已经看到了如何绑定返回值或异常值(在返回和抛出通知之后使用)。要使参数值可用于通知正文,您可以使用args. 如果在args表达式中使用参数名称代替类型名称,则在调用通知时相应参数的值将作为参数值传递。一个例子应该更清楚地说明这一点。假设您要通知执行以Account 对象为第一个参数的 DAO 操作,并且您需要访问通知正文中的帐户。您可以编写以下内容:

@Before("com.xyz.myapp.CommonPointcuts.dataAccessOperation() && args(account,..)")
public void validateAccount(Account account) {
    // ...
}

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

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

@Pointcut("com.xyz.myapp.CommonPointcuts.dataAccessOperation() && args(account,..)")
private void accountDataAccessOperation(Account account) {}

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

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

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

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

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

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

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

通知参数和泛型

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

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

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

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

这种方法不适用于泛型集合。所以你不能定义一个切入点如下:

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

为了完成这项工作,我们必须检查集合的每个元素,这是不合理的,因为我们也无法决定如何处理null一般的值。要实现类似的效果,您必须键入参数Collection<?>并手动检查元素的类型。

确定参数名称

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

  • 如果用户已明确指定参数名称,则使用指定的参数名称。通知和切入点注解都有一个可选argNames属性,您可以使用它来指定带注解的方法的参数名称。这些参数名称在运行时可用。以下示例显示了如何使用该argNames属性:

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

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

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

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

@Before("com.xyz.lib.Pointcuts.anyPublicMethod()")
public void audit(JoinPoint jp) {
    // ... use jp
}
  • 使用argNames属性有点笨拙,所以如果没有指定argNames属性,Spring AOP 会查看类的调试信息并尝试从局部变量表中确定参数名称。只要使用调试信息(至少)编译了类,就会出现此信息。使用此标志进行编译的后果是:(1)您的代码更容易理解(逆向工程),(2)类文件大小稍微大一点(通常无关紧要),(3)优化以删除未使用的本地您的编译器未应用变量。换句话说,打开此标志进行构建应该不会遇到任何困难。

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

  • 如果在没有必要调试信息的情况下编译了代码,Spring AOP 会尝试推断绑定变量与参数的配对(例如,如果切入点表达式中只绑定了一个变量,并且advice 方法只接受一个参数,则配对很明显)。如果给定可用信息,变量的绑定不明确,则抛出AmbiguousBindingException

  • 如果上述所有策略均失败,则抛出 IllegalArgumentException

参数处理

我们之前提到过,我们将描述如何编写一个带有在 Spring AOP 和 AspectJ 中一致工作的参数的proceed调用。解决方案是确保通知签名按顺序绑定每个方法参数。以下示例显示了如何执行此操作:

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

在许多情况下,无论如何都要执行此绑定(如前面的示例中所示)。

通知优先级

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

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

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

从 Spring Framework 5.2.7 开始,需要在同一连接点运行的同一 @Aspect 类中定义的通知方法将根据其通知类型按以下顺序分配优先级,从最高优先级到最低优先级:@Around、@Before 、@After、@AfterReturning、@AfterThrowing。但请注意,@After 通知方法将在同一方面中的任何 @AfterReturning 或 @AfterThrowing 通知方法之后有效地调用,遵循 AspectJ 的 @After 的“after finally 通知”语义。

当在同一个 @Aspect 类中定义的两个相同类型的通知(例如,两个 @After 通知方法)都需要在同一连接点运行时,顺序是未定义的(因为无法检索源)通过 javac 编译类的反射来声明代码顺序)。考虑将此类通知方法折叠为每个 @Aspect 类中每个连接点的一个通知方法,或者将通知片段重构为单独的 @Aspect 类,您可以通过 Ordered 或 @Order 在方面级别订购这些类。

最后更新于