声明通知

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

前置通知

您可以通过使用@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子句还将匹配限制为仅那些抛出指定类型异常的方法执行(在本例中为 )。

请注意,@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()将导致在调用基础方法时提供调用者的原始参数。对于高级用例,还有一个重载的proceed()方法,它接受一个参数数组(Object[])。数组中的值将在调用基础方法时用作参数。

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

Spring采用的方法更简单,更符合其基于代理的、仅执行的语义。只有在为Spring编写的@AspectJ方面并使用带有AspectJ编译器和织入器的参数的proceed时,您才需要了解这种差异。有一种方法可以编写这样的方面,可以在Spring AOP和AspectJ之间完全兼容,这在关于通知参数的下一节中讨论

环绕通知返回的值是方法调用者看到的返回值。例如,一个简单的缓存方面可以从缓存中返回一个值,如果没有,则调用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提供了完全类型化的advice,这意味着您在advice签名中声明您需要的参数(就像我们之前在返回和抛出示例中看到的那样),而不是一直使用Object[]数组。我们将在本节稍后看到如何将参数和其他上下文值提供给advice主体。首先,让我们看看如何编写通用的advice,以便了解当前advice正在通知的方法。

访问当前的JoinPoint

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

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

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

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

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

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

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

查看javadoc以获取更多详细信息。

将参数传递给Advice

我们已经看到如何绑定返回值或异常值(使用after returning和after throwing advice)。要使参数值在advice主体中可用,您可以使用args的绑定形式。如果在args表达式中使用参数名代替类型名,那么在调用advice时,相应参数的值将作为参数值传递。举个例子来说明。假设您想要通知执行以Account对象作为第一个参数的DAO操作,并且您需要在advice主体中访问该账户。您可以编写如下内容:

  • 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对象在advice中可用。

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

  • 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方法的advice:

  • 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<?>,并手动检查元素的类型。

确定参数名称

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

本节中将术语参数参数互换使用,因为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();
	// ... 使用 code 和 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()
	// ... 使用 code 和 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();
	// ... 使用 code、bean 和 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()
	// ... 使用 code、bean 和 jp
}
1 引用了在组合切入点表达式中定义的publicMethod命名切入点。
2 声明beanauditable为参数名称。

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

  • Java

  • Kotlin

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

进行参数处理

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

  • 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注解来完成。给定两个切面,从Ordered.getOrder()(或注解值)返回较低值的切面具有更高的优先级。

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

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

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