声明通知

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

前置通知

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

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

  • Java

  • Kotlin

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

@Aspect
public class BeforeExample {

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

@Aspect
class BeforeExample {

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

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

  • Java

  • Kotlin

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

@Aspect
public class BeforeExample {

	@Before("com.xyz.CommonPointcuts.dataAccessOperation()")
	public void doAccessCheck() {
		// ...
	}
}
import org.aspectj.lang.annotation.Aspect
import org.aspectj.lang.annotation.Before

@Aspect
class BeforeExample {

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

后置返回通知

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

  • Java

  • Kotlin

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

@Aspect
public class AfterReturningExample {

	@AfterReturning("execution(* com.xyz.dao.*.*(..))")
	public void doAccessCheck() {
		// ...
	}
}
import org.aspectj.lang.annotation.Aspect
import org.aspectj.lang.annotation.AfterReturning

@Aspect
class AfterReturningExample {

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

有时,您需要在通知体中访问实际返回的值。您可以使用绑定返回值形式的 @AfterReturning 来获得这种访问权,如下面示例所示

  • Java

  • Kotlin

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) {
		// ...
	}
}
import org.aspectj.lang.annotation.Aspect
import org.aspectj.lang.annotation.AfterReturning

@Aspect
class AfterReturningExample {

	@AfterReturning(
		pointcut = "execution(* com.xyz.dao.*.*(..))",
		returning = "retVal")
	fun doAccessCheck(retVal: Any?) {
		// ...
	}
}

returning 属性中使用的名称必须与通知方法中参数的名称一致。当方法执行返回时,返回值会作为相应的参数值传递给通知方法。returning 子句还会限制匹配,只匹配返回指定类型值(在本例中是 Object,匹配任何返回值)的方法执行。

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

后置异常通知

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

  • Java

  • Kotlin

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

@Aspect
public class AfterThrowingExample {

	@AfterThrowing("execution(* com.xyz.dao.*.*(..))")
	public void doRecoveryActions() {
		// ...
	}
}
import org.aspectj.lang.annotation.Aspect
import org.aspectj.lang.annotation.AfterThrowing

@Aspect
class AfterThrowingExample {

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

通常,您希望通知只在抛出给定类型的异常时运行,而且通常需要在通知体中访问抛出的异常。您可以使用 throwing 属性,既可以限制匹配(如果需要,否则使用 Throwable 作为异常类型),也可以将抛出的异常绑定到通知参数。下面示例展示了如何实现

  • Java

  • Kotlin

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) {
		// ...
	}
}
import org.aspectj.lang.annotation.Aspect
import org.aspectj.lang.annotation.AfterThrowing

@Aspect
class AfterThrowingExample {

	@AfterThrowing(
		pointcut = "execution(* com.xyz.dao.*.*(..))",
		throwing = "ex")
	fun doRecoveryActions(ex: DataAccessException) {
		// ...
	}
}

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

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

后置(最终)通知

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

  • Java

  • Kotlin

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

@Aspect
public class AfterFinallyExample {

	@After("execution(* com.xyz.dao.*.*(..))")
	public void doReleaseLock() {
		// ...
	}
}
import org.aspectj.lang.annotation.Aspect
import org.aspectj.lang.annotation.After

@Aspect
class AfterFinallyExample {

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

注意,AspectJ 中的 @After 通知被定义为“后置最终通知”,类似于 try-catch 语句中的 finally 块。它会在任何结果下被调用,无论是正常返回还是从连接点(用户声明的目标方法)抛出异常,这与 @AfterReturning 只应用于成功的正常返回不同。

环绕通知

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

始终使用满足您需求的最低能力的通知形式。

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

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

使用 Object[] 调用 proceed 的行为与 AspectJ 编译器编译的环绕通知的 proceed 行为略有不同。对于使用传统 AspectJ 语言编写的环绕通知,传递给 proceed 的参数数量必须与传递给环绕通知的参数数量匹配(而不是底层连接点接受的参数数量),并且在给定参数位置传递给 proceed 的值将替换连接点处该值所绑定的实体的原始值(如果这暂时难以理解,请勿担心)。

Spring 采用的方法更简单,并且更匹配其基于代理、仅执行的语义。您只需要在通过 AspectJ 编译器和织入器编译为 Spring 编写的 @AspectJ 切面并使用带参数的 proceed 时了解此差异。有一种编写此类切面的方法,可以在 Spring AOP 和 AspectJ 之间实现 100% 兼容,这将在下一节关于通知参数中讨论。

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

如果您将环绕通知方法的返回类型声明为 void,将始终向调用者返回 null,实际上忽略了对 proceed() 的任何调用的结果。因此建议环绕通知方法声明 Object 作为返回类型。通知方法通常应返回调用 proceed() 返回的值,即使底层方法的返回类型是 void。然而,根据用例,通知可以选择返回缓存值、包装值或某些其他值。

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

  • Java

  • Kotlin

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 {
		// start stopwatch
		Object retVal = pjp.proceed();
		// stop stopwatch
		return retVal;
	}
}
import org.aspectj.lang.annotation.Aspect
import org.aspectj.lang.annotation.Around
import org.aspectj.lang.ProceedingJoinPoint

@Aspect
class AroundExample {

	@Around("execution(* com.xyz..service.*.*(..))")
	fun doBasicProfiling(pjp: ProceedingJoinPoint): Any? {
		// start stopwatch
		val retVal = pjp.proceed()
		// stop stopwatch
		return retVal
	}
}

通知参数

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

访问当前 JoinPoint

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

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

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

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

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

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

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

更多详细信息请参阅javadoc

向通知传递参数

我们已经了解了如何绑定返回值或异常值(使用后置返回和后置异常通知)。为了使参数值在通知体中可用,您可以使用 args 的绑定形式。如果您在 args 表达式中用参数名称代替类型名称,调用通知时,相应参数的值将作为参数值传递。一个示例应该能更清楚地说明这一点。假设您想通知以 Account 对象作为第一个参数的 DAO 操作的执行,并且您需要在通知体中访问该帐户。您可以编写如下代码

  • Java

  • Kotlin

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

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

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

  • Java

  • Kotlin

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

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

@Before("accountDataAccessOperation(account)")
fun validateAccount(account: Account) {
	// ...
}

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

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

下面展示了 @Auditable 注解的定义

  • Java

  • Kotlin

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Auditable {
	AuditCode value();
}
@Retention(AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.FUNCTION)
annotation class Auditable(val value: AuditCode)

下面展示了匹配 @Auditable 方法执行的通知

  • Java

  • Kotlin

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

通知参数和泛型

Spring AOP 可以处理类声明和方法参数中使用的泛型。假设您有一个如下所示的泛型类型

  • Java

  • Kotlin

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

您可以通过将通知参数绑定到您想要拦截方法的参数类型来限制对方法类型的拦截

  • Java

  • Kotlin

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

这种方法不适用于泛型集合。因此您不能按如下方式定义切点

  • Java

  • Kotlin

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

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

确定参数名称

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

本节交替使用术语 argumentparameter,因为 AspectJ API 将参数名称称为实参名称。

Spring 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

  • Kotlin

@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();
	// ... use code and bean
}
1 引用了组合切点表达式中定义的 publicMethod 命名切点。
2 beanauditable 声明为参数名称。
@Before(
	value = "com.xyz.Pointcuts.publicMethod() && target(bean) && @annotation(auditable)", (1)
	argNames = "bean,auditable") (2)
fun audit(bean: Any, auditable: Auditable) {
	val code = auditable.value()
	// ... use code and bean
}
1 引用了组合切点表达式中定义的 publicMethod 命名切点。
2 beanauditable 声明为参数名称。

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

  • Java

  • Kotlin

@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();
	// ... use code, bean, and jp
}
1 引用了组合切点表达式中定义的 publicMethod 命名切点。
2 beanauditable 声明为参数名称。
@Before(
	value = "com.xyz.Pointcuts.publicMethod() && target(bean) && @annotation(auditable)", (1)
	argNames = "bean,auditable") (2)
fun audit(jp: JoinPoint, bean: Any, auditable: Auditable) {
	val code = auditable.value()
	// ... use code, bean, and jp
}
1 引用了组合切点表达式中定义的 publicMethod 命名切点。
2 beanauditable 声明为参数名称。

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

  • Java

  • Kotlin

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

带参数的执行

我们之前提到过,我们将描述如何编写一个带参数的 proceed 调用,使其在 Spring AOP 和 AspectJ 中保持一致。解决方案是确保通知签名按顺序绑定每个方法参数。以下示例展示了如何实现这一点

  • Java

  • Kotlin

@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 的切入点。
@Around("execution(List<Account> find*(..)) && " +
		"com.xyz.CommonPointcuts.inDataAccessLayer() && " +
		"args(accountHolderNamePattern)") (1)
fun preProcessQueryPattern(pjp: ProceedingJoinPoint,
						accountHolderNamePattern: String): Any? {
	val newPattern = preProcess(accountHolderNamePattern)
	return pjp.proceed(arrayOf<Any>(newPattern))
}
1 引用 共享命名切入点定义 中定义的名为 inDataAccessLayer 的切入点。

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

通知排序

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

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

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

在同一个 @Aspect 类中定义、需要在同一个连接点运行的通知方法根据其通知类型分配优先级,顺序从高到低依次为:@Around, @Before, @After, @AfterReturning, @AfterThrowing。但请注意,根据 AspectJ 对 @After 的“after finally advice”语义,同一个切面中的 @After 通知方法将在任何 @AfterReturning@AfterThrowing 通知方法之后有效调用。

当定义在同一个 @Aspect 类中的两个相同类型的通知(例如,两个 @After 通知方法)都需要在同一个连接点运行时,其顺序是未定义的(因为对于 javac 编译的类,无法通过反射获取源代码声明顺序)。考虑将此类通知方法合并到每个 @Aspect 类中每个连接点的一个通知方法中,或者将这些通知重构到独立的 @Aspect 类中,您可以通过 Ordered@Order 在切面级别进行排序。