Declaring Advice

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

Before Advice

您可以使用 @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() {
		// ...
	}
}

如果我们使用 named pointcut,我们可以按如下方式重写前一个示例:

  • 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() {
		// ...
	}
}

After Returning Advice

当匹配方法执行正常返回时,返回通知运行。你可以通过使用 @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() {
		// ...
	}
}

您可以有多个advice声明(以及其他成员),全部在同一个aspect中。我们在这些示例中仅显示一个advice声明,以重点关注每个advice的效果。

有时,你需要在通知正文中访问实际的返回值。你可以使用 @AfterReturning 的形式将返回值与 getthataccess 绑定,如下面的示例所示:

  • 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 匹配任何返回值)的那些方法执行。

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

After Throwing Advice

当匹配的方法执行通过抛出一个异常退出时,抛出通知运行。你可以通过使用 @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 (Finally) Advice

当匹配的方法执行退出时,最终(finally)通知运行。它通过使用 @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 Advice

最后一种通知是 around 通知。Around 通知在匹配方法的执行周围运行。它有机会在方法运行之前和之后都进行工作,并确定方法何时、如何,甚至是否真正运行。如果您需要在多线程方式下在方法执行之前和之后共享状态(例如,启动和停止计时器),则经常使用 around 通知。

始终使用满足您要求中最不强大的通知形式。 例如,如果 before 通知足以满足您的需要,请勿使用 around 通知。

通过使用 @Around 注解为方法添加注释来声明周围建议。该方法应声明 Object 作为其返回类型,并且该方法的第一个参数的类型必须是 ProceedingJoinPoint。在建议方法的主体中,你必须在 ProceedingJoinPoint 上调用 proceed() 才能让底层方法运行。在没有参数的情况下调用 proceed() 将导致在调用底层方法时将调用者的原始参数提供给底层方法。对于高级用例,proceed() 方法有一个过载变量,它接受一个参数数组 (Object[])。数组中的值将在调用底层方法时用作底层方法的参数。

在使用 Object[] 调用时 proceed 的行为与 AspectJ 编译器编译后的连接点建议行为稍有不同。对于使用传统 AspectJ 语言编写的连接点建议,传递给 proceed 的参数数量必须与传递给连接点建议的参数数量相匹配(不是底层连接点获取的参数数量),并且在给定参数位置传递给 proceed 的值会取代连接点中与该值绑定的实体的原始值(现在如果这没有意义,不用担心)。 Spring 采取的方法更简单,并且更符合它基于代理,只执行语义的方式。如果你为 Spring 编写 "@1" 切面并且使用 "@2" 与 AspectJ 编译器和织入器一起用参数,你只需要知道这个差别。针对 Spring AOP 和 AspectJ,有一种方法可以编写 100% 兼容的切面,这个方法在 "@3" 中进行了讨论。

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

如果您将 around advice 方法的返回类型声明为 voidnull 总是会被返回给调用者,进而忽略了对 proceed() 的任何调用结果。因此,建议 around advice 方法声明 Object 的返回类型。advice 方法通常应该返回从对 proceed() 的调用中返回的值,即使底层方法具有 void 返回类型。然而,advice 可以根据用例有选择地返回缓存值、包装值或其他值。

以下示例演示如何使用环绕通知:

  • 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
	}
}

Advice Parameters

Spring 提供全类型建议,这意味着您在建议签名中声明所需的用于建议签名的参数(如我们之前看到的返回和抛出示例)而不是始终处理 Object[] 数组。我们之后在本文中了解如何将参数和其他上下文值提供给建议主体。首先,我们了解如何编写通用建议,以查找建议当前建议的方法。

Access to the Current JoinPoint

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

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

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

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

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

  • getSignature():返回经过建议的方法说明。

  • toString():打印经过建议的方法的有用说明。

请参阅 javadoc 了解更多详细信息。

Passing Parameters to Advice

我们已经了解了如何绑定返回的值或异常值(使用 afterreturning 和 after throwing 建议)。要使参数值可用于建议正文,您可以使用 args 的绑定形式。如果您在 args 表达式中使用参数名称来代替 atype 名称,那么在调用建议时将值与对应的参数一起传递。一个示例可以使这一点更清楚。假设您希望建议执行将 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
@Before("com.xyz.Pointcuts.publicMethod() && @annotation(auditable)") (1)
public void audit(Auditable auditable) {
	AuditCode code = auditable.value();
	// ...
}
1 引用 Combining Pointcut Expressions 中定义的 publicMethod 命名切入点。
Kotlin
@Before("com.xyz.Pointcuts.publicMethod() && @annotation(auditable)") (1)
fun audit(auditable: Auditable) {
	val code = auditable.value()
	// ...
}
2 引用 Combining Pointcut Expressions 中定义的 publicMethod 命名切入点。

Advice Parameters and Generics

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

Determining Argument Names

建议调用中的参数绑定依赖于将切入点表达式中使用的名称与建议和切入点方法签名中声明的参数名称相匹配。

本部分可互换地使用术语 argumentparameter,因为 AspectJ API 将参数名称为变量名。

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

AspectJAnnotationParameterNameDiscoverer

Uses parameter names that have been explicitly specified by the user via the argNames attribute in the corresponding advice or pointcut annotation. See Explicit Argument Names for details.

KotlinReflectionParameterNameDiscoverer

Uses Kotlin reflection APIs to determine parameter names. This discoverer is only used if such APIs are present on the classpath.

StandardReflectionParameterNameDiscoverer

Uses the standard java.lang.reflect.Parameter API to determine parameter names. Requires that code be compiled with the -parameters flag for javac. Recommended approach on Java 8+.

AspectJAdviceParameterNameDiscoverer

Deduces parameter names from the pointcut expression, returning, and throwing clauses. See the javadoc for details on the algorithm used.

Explicit Argument Names

@AspectJ 建议和切入点注释具有可选的 argNames 属性,可以用来指定带注释的方法的参数名称。

如果 @AspectJ 方面是由 AspectJ 编译器 (ajc) 编制的,即使没有调试信息,您也不必添加 argNames 属性,因为编译器会保留所需的信息。 类似地,如果一个 @AspectJ 方面是使用 -parameters 标志和 javac 编制的,则不必添加 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();
	// ... use code and bean
}
1 引用 Combining Pointcut Expressions 中定义的 publicMethod 命名切入点。
2 将 `bean`和 `auditable`声明为参数名称。
Kotlin
@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
}
3 引用 Combining Pointcut Expressions 中定义的 publicMethod 命名切入点。
4 将 `bean`和 `auditable`声明为参数名称。

如果第一个参数的类型是 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();
	// ... use code, bean, and jp
}
1 引用 Combining Pointcut Expressions 中定义的 publicMethod 命名切入点。
2 将 `bean`和 `auditable`声明为参数名称。
Kotlin
@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
}
3 引用 Combining Pointcut Expressions 中定义的 publicMethod 命名切入点。
4 将 `bean`和 `auditable`声明为参数名称。

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

Java
@Before("com.xyz.Pointcuts.publicMethod()") (1)
public void audit(JoinPoint jp) {
	// ... use jp
}
1 引用 Combining Pointcut Expressions 中定义的 publicMethod 命名切入点。
Kotlin
@Before("com.xyz.Pointcuts.publicMethod()") (1)
fun audit(jp: JoinPoint) {
	// ... use jp
}
2 引用 Combining Pointcut Expressions 中定义的 publicMethod 命名切入点。

Proceeding with Arguments

我们前面说过将描述如何编写 proceed 调用,它的参数可以在 Spring AOP 和 AspectJ 中保持一致。解决方案是确保建议签名按顺序绑定每个方法参数。以下示例展示了如何执行此操作:

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 引用 Sharing Named Pointcut Definitions中定义的 `inDataAccessLayer`命名切入点。
Kotlin
@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))
}
2 引用 Sharing Named Pointcut Definitions中定义的 `inDataAccessLayer`命名切入点。

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

Advice Ordering

当多条建议都希望在同一连接点运行时会发生什么?Spring AOP 遵循与 AspectJ 相同的优先级规则,以确定建议执行的顺序。优先级最高的建议在“进入”时首先运行(因此,给定两条 before 建议,优先级最高的建议首先运行)。从连接点“退出”时,优先级最高的建议最后执行(因此,给定两条 after 建议,优先级最高的建议将第二个运行)。

当定义在不同方面中的两条建议都需要在同一连接点运行时,除非你另有指定,否则执行顺序是不确定的。你可以通过指定优先级来控制执行顺序。这是通过正常 Spring 方式完成的,即在方面类中实现 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 finally 方面”语义。 当在同一 @Aspect 类中定义的两条相同类型建议(例如两条 @After 建议方法)都需要在同一连接点运行时,则顺序是不确定的(因为无法通过反射获取 java 编译类的源代码声明顺序)。考虑将此类建议方法合并为每个连接点一个建议方法,以便每个 @Aspect 类或将建议方法重构成单独的 @Aspect 类,这样你可以在方面级别通过 Ordered@Order 对其进行排列。