对于最新的稳定版本,请使用 Spring Framework 7.0.6!spring-doc.cadn.net.cn

声明通知

通知与切入点表达式相关联,在由切入点匹配的方法执行之前、之后或环绕运行。切入点表达式可以是内联切入点,也可以是引用命名切入点spring-doc.cadn.net.cn

前置通知

您可以通过使用@Before注解在切面中声明前置通知。spring-doc.cadn.net.cn

以下示例使用内联切入点表达式。spring-doc.cadn.net.cn

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

如果我们使用命名切点,可以重写前面的示例如下:spring-doc.cadn.net.cn

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 注解来声明它。spring-doc.cadn.net.cn

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来获取该访问权限,如下例所示:spring-doc.cadn.net.cn

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,这匹配任何返回值)。spring-doc.cadn.net.cn

请注意,在使用返回后通知时,不可能返回一个完全不同的引用。spring-doc.cadn.net.cn

在抛出建议后

在抛出通知会在匹配的方法执行退出并抛出异常时运行。你可以通过使用@注解来声明它,如下例所示:spring-doc.cadn.net.cn

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作为异常类型)并将抛出的异常绑定到建议参数。以下示例展示了如何做到这一点:spring-doc.cadn.net.cn

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)的方法执行。spring-doc.cadn.net.cn

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

最后通知

在匹配的方法执行退出时,后置(finally)通知会运行。它通过使用@注解来声明。后置通知必须准备好处理正常和异常返回条件。它通常用于释放资源等类似目的。以下示例展示了如何使用后置finally通知:spring-doc.cadn.net.cn

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 advice”,类似于try-catch语句中的finally块。无论结果如何,无论是正常返回还是从连接点(用户声明的目标方法)抛出异常,它都将被调用,与之相对的是@AfterReturning,仅适用于成功的正常返回。spring-doc.cadn.net.cn

环绕通知

最后一种建议是“环绕”建议。环绕建议会在匹配方法执行的“周围”运行。它有机会在方法执行前后执行操作,并决定方法是否实际运行、何时以及如何运行。如果需要以线程安全的方式在方法执行前后共享状态,通常会使用环绕建议——例如,启动和停止计时器。spring-doc.cadn.net.cn

始终使用满足您需求的最弱形式的建议。spring-doc.cadn.net.cn

例如,如果您的需求只需要之前通知,则不要在周围使用通知。spring-doc.cadn.net.cn

环绕通知通过使用@Around注解标注方法来声明。该方法应将Object作为返回类型,方法的第一个参数必须是ProceedingJoinPoint类型。在通知方法的主体中,必须对ProceedingJoinPoint调用proceed(),以便底层方法能够运行。不带参数调用proceed()将会在调用底层方法时将调用者的原始参数传递给该方法。对于高级用例,proceed()方法有一个重载变体,它接受一个参数数组(Object[])。数组中的值将在调用底层方法时作为参数使用。spring-doc.cadn.net.cn

当使用 proceed 时,其行为与使用 Object[] 的行为略有不同, 由 AspectJ 编译器编译的环绕通知的 proceed。对于使用传统 AspectJ 语言编写的环绕通知, 传递给 proceed 的参数数量必须与传递给环绕通知的参数数量匹配(不是底层连接点的参数数量), 并且在给定参数位置上传递给 proceed 的值将取代连接点上该值所绑定实体的原始值(如果现在不太明白也没关系)。spring-doc.cadn.net.cn

Spring采用的方法更为简单,并且更符合其基于代理、仅执行语义的特性。只有在您为Spring编译@AspectJ方面,并使用proceed与参数一起与AspectJ编译器和织入器一起使用时,才需要了解这一差异。有一种编写此类方面的方式可以在Spring AOP和AspectJ之间完全兼容,这将在后面的“建议参数”部分进行讨论。spring-doc.cadn.net.cn

环绕通知返回的值是调用方法的调用者看到的返回值。例如,一个简单的缓存方面可以在有缓存值时返回该值,或者在没有缓存值时调用 proceed()(并返回该值)。请注意,在环绕通知的主体内,proceed 可能被调用一次、多次或根本不调用。所有这些情况都是合法的。spring-doc.cadn.net.cn

如果你将环绕通知方法的返回类型声明为 voidnull 会始终返回给调用者,从而忽略对 proceed() 的任何调用结果。因此,建议环绕通知方法将返回类型声明为 Object。该通知方法通常应返回对 proceed() 的调用返回的值,即使底层方法具有 void 返回类型。 但是,根据使用情况,通知也可以选择返回缓存的值、包装后的值或其他值。

以下示例展示了如何使用环绕通知:spring-doc.cadn.net.cn

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[] 数组。在本节稍后部分,我们将介绍如何使参数和其他上下文值对通知体可用。首先,我们来看看如何编写通用的通知,以了解当前正在通知的方法。spring-doc.cadn.net.cn

访问当前的JoinPoint

任何建议方法都可以将其第一个参数声明为类型 org.aspectj.lang.JoinPoint。请注意,围绕建议需要将第一个参数声明为类型 ProceedingJoinPoint,这是类型 JoinPoint 的子类。spring-doc.cadn.net.cn

The JoinPoint interface provides a number of useful methods:spring-doc.cadn.net.cn

请参阅javadoc以获取更多详细信息。spring-doc.cadn.net.cn

将参数传递给通知

我们已经了解了如何绑定返回值或异常值(使用 after returning 和 after throwing 通知)。要使参数值对通知体可用,可以使用 args 的绑定形式。如果在 args 表达式中使用参数名代替类型名,那么在调用通知时,对应参数的值将作为参数值传递。一个例子会使这一点更清晰。假设您希望通知以 Account 对象作为第一个参数执行的 DAO 操作,并且需要在通知体中访问账户,您可以编写以下内容:spring-doc.cadn.net.cn

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

The args(account,..) 部分的切入点表达式有两个目的。首先,它限制匹配仅限于那些方法执行,这些方法至少接受一个参数,并且传递给该参数的参数是一个 Account 的实例。其次,它通过 account 参数使实际的 Account 对象对通知可用。spring-doc.cadn.net.cn

另一种写法是声明一个切入点,在匹配连接点时“提供”Account对象值,然后从通知中引用命名的切入点。如下所示:spring-doc.cadn.net.cn

@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 编程指南以了解更多信息。spring-doc.cadn.net.cn

代理对象(this)、目标对象(target)和注解(@within@target@annotation 以及 @args)都可以通过类似方式进行绑定。接下来 这组示例展示了如何匹配带有 @Auditable 注解的方法执行并提取审计代码:spring-doc.cadn.net.cn

以下展示了 @Auditable 注解的定义:spring-doc.cadn.net.cn

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

以下展示了与@Auditable个方法执行相匹配的通知:spring-doc.cadn.net.cn

@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 可以处理类声明和方法参数中使用的泛型。假设你有如下所示的泛型类型:spring-doc.cadn.net.cn

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

你可以通过将通知参数绑定到想要拦截方法的参数类型,来限制对方法类型的拦截:spring-doc.cadn.net.cn

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

这种方法不适用于泛型集合。因此,您不能如下定义切入点:spring-doc.cadn.net.cn

@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<?>,并手动检查元素的类型。spring-doc.cadn.net.cn

确定参数名称

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

本节中,参数参数这两个术语可以互换使用,因为 AspectJ API 将参数名称称为参数名称。

Spring AOP 使用以下 ParameterNameDiscoverer 种实现来确定参数名称。每个发现者都将有机会发现参数名称,第一个成功的发现者将获胜。如果所有已注册的发现者都无法确定参数名称,将抛出异常。spring-doc.cadn.net.cn

AspectJAnnotationParameterNameDiscoverer

使用通过相应 advice 或 pointcut 注解中的 argNames 属性显式指定的参数名称。有关详细信息,请参见 显式参数名称spring-doc.cadn.net.cn

KotlinReflectionParameterNameDiscoverer

使用 Kotlin 反射 API 来确定参数名称。仅当类路径上存在此类 API 时,才会使用此发现器。spring-doc.cadn.net.cn

StandardReflectionParameterNameDiscoverer

使用标准 java.lang.reflect.Parameter API 来确定参数名称。需要在编译代码时使用 -parameters 标志 for javac。推荐在 Java 8+ 上使用此方法。spring-doc.cadn.net.cn

LocalVariableTableParameterNameDiscoverer

分析通知类字节码中的可用局部变量表,从调试信息中确定参数名称。 要求代码在编译时包含调试符号(至少需要-g:vars级别)。自Spring Framework 6.0起已弃用, 计划在Spring Framework 6.1中移除,建议改用-parameters级别编译代码。在GraalVM原生镜像中不受支持。spring-doc.cadn.net.cn

AspectJAdviceParameterNameDiscoverer

从切点表达式、returningthrowing 子句中推断参数名称。有关所使用算法的详细信息,请参见 javadocspring-doc.cadn.net.cn

显式参数名称

@AspectJ 通知和切入点注解有一个可选的 argNames 属性,您可以使用它来指定带注解方法的参数名称。spring-doc.cadn.net.cn

如果一个 @AspectJ 切面已由 AspectJ 编译器编译(ajc),即使没有调试信息,您也不需要添加 argNames 属性,因为编译器会保留所需的信息。spring-doc.cadn.net.cn

同样,如果一个 @AspectJ 切面是使用 javac 选项编译的,则不需要添加 argNames 属性,因为编译器会保留所需的信息。spring-doc.cadn.net.cn

以下示例显示了如何使用 argNames 属性:spring-doc.cadn.net.cn

@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 属性不需要包含它:spring-doc.cadn.net.cn

@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 属性:spring-doc.cadn.net.cn

@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中都能一致工作。解决方案是确保建议签名按顺序绑定每个方法参数。以下示例展示了如何做到这一点:spring-doc.cadn.net.cn

@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-doc.cadn.net.cn

建议排序

当多个切面建议都希望在同一个连接点运行时会发生什么? Spring AOP遵循与AspectJ相同的优先级规则来确定建议执行的顺序。最高优先级的建议首先在“进入”时运行(因此,如果有两个前置建议,最高优先级的建议将首先运行)。在从一个连接点“退出”时,最高优先级的建议最后运行(因此,如果有两个后置建议,最高优先级的建议将第二个运行)。spring-doc.cadn.net.cn

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

每个特定切面的不同通知类型在概念上都旨在直接应用于连接点。因此,一个@AfterThrowing通知方法不应该接收来自伴随的@After/@AfterReturning方法的异常。spring-doc.cadn.net.cn

从Spring Framework 5.2.7开始,定义在同一个@Aspect类中的需要在同一连接点运行的建议方法根据其建议类型按以下顺序分配优先级,从最高到最低:@Around@Before@After@AfterReturning@AfterThrowing。但是,请注意,@After建议方法实际上会在同一切面中的任何@AfterReturning@AfterThrowing建议方法之后被调用,遵循AspectJ的“after finally advice”语义进行@Afterspring-doc.cadn.net.cn

当同一类型的两个通知(例如,两个@After通知方法)在同一@Aspect类中定义并且都需要在同一个连接点运行时,顺序是未定义的(因为无法通过反射获取javac编译类的源代码声明顺序)。考虑将此类通知方法合并为每个@Aspect类中的一个通知方法,或者将通知方法重构为单独的@Aspect类,这样你可以在切面级别通过Ordered@Order对它们进行排序。spring-doc.cadn.net.cn