声明通知

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

前置 Advice

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

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

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

后置 Advice

后置 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 声明(以及其他成员)。在这些示例中,我们只显示单个 Advice 声明,以突出显示每个 Advice 的效果。

有时,您需要在 Advice 主体中访问实际返回的值。您可以使用@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,它匹配任何返回值)。

请注意,使用after returning通知时,无法返回完全不同的引用。

After Throwing 通知

当匹配的方法执行通过抛出异常退出时,after throwing通知将运行。您可以使用@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)通知

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

  • 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通知被定义为“after finally 通知”,类似于 try-catch 语句中的 finally 块。它将针对任何结果(正常返回或从连接点(用户声明的目标方法)抛出的异常)被调用,与@AfterReturning形成对比,后者仅适用于成功的正常返回。

Around 通知

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

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

例如,如果before通知足以满足您的需求,请不要使用around通知。

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

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

Spring 采用的方法更简单,并且更符合其基于代理的、仅执行语义。只有在您编译为 Spring 编写的 @AspectJ 方面并使用带参数的 proceed 与 AspectJ 编译器和编织器一起使用时,您才需要了解这种差异。有一种方法可以编写这样的方面,使其在 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

将参数传递给通知

我们已经了解了如何绑定返回值或异常值(使用 after returning 和 after throwing 通知)。要将参数值提供给通知主体,可以使用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<?> 并手动检查元素的类型。

确定参数名称

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

本节交替使用术语“参数”和“参数”,因为 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声明为参数名称。

对类型为JoinPointProceedingJoinPointJoinPoint.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 命名的切入点。

处理参数

我们之前提到过,我们将描述如何在 Spring AOP 和 AspectJ 中编写一个始终有效的带参数的 proceed 调用。解决方案是确保建议签名按顺序绑定每个方法参数。以下示例展示了如何做到这一点

  • 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 相同的优先级规则来确定建议执行的顺序。优先级最高的建议首先在“进入时”运行(因此,给定两个 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 建议方法)都需要在同一个连接点运行时,顺序是未定义的(因为没有办法通过反射检索 javac 编译类的源代码声明顺序)。考虑将每个 @Aspect 类中的每个连接点的这些建议方法合并为一个建议方法,或者将建议部分重构为单独的 @Aspect 类,你可以通过 Ordered@Order 在方面级别对其进行排序。