Skip to content

声明通知 (Declaring Advice)

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

前置通知 (Before Advice)

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

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

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

@Aspect
public class BeforeExample {

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

@Aspect
class BeforeExample {

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

如果使用命名切入点,可以将上述示例重写如下:

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

@Aspect
public class BeforeExample {

	@Before("com.xyz.CommonPointcuts.dataAccessOperation()")
	public void doAccessCheck() {
		// ...
	}
}
kotlin
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
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.AfterReturning;

@Aspect
public class AfterReturningExample {

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

@Aspect
class AfterReturningExample {

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

有时,你需要在通知正文中访问实际返回的值。你可以使用绑定返回值的 @AfterReturning 形式:

java
@Aspect
public class AfterReturningExample {

	@AfterReturning(
		pointcut="execution(* com.xyz.dao.*.*(..))",
		returning="retVal")
	public void doAccessCheck(Object retVal) {
		// ... 使用 retVal
	}
}
kotlin
@Aspect
class AfterReturningExample {

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

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

注意,使用后置返回通知时,无法返回完全不同的引用

异常通知 (After Throwing Advice)

异常通知在匹配的方法执行通过抛出异常退出时运行。你可以使用 @AfterThrowing 注解来声明:

java
@Aspect
public class AfterThrowingExample {

	@AfterThrowing("execution(* com.xyz.dao.*.*(..))")
	public void doRecoveryActions() {
		// ...
	}
}
kotlin
@Aspect
class AfterThrowingExample {

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

通常,你希望通知仅在抛出给定类型的异常时运行,并且需要在通知正文中访问抛出的异常。使用 throwing 属性可以限制匹配并绑定异常参数:

java
@Aspect
public class AfterThrowingExample {

	@AfterThrowing(
		pointcut="execution(* com.xyz.dao.*.*(..))",
		throwing="ex")
	public void doRecoveryActions(DataAccessException ex) {
		// ...
	}
}
kotlin
@Aspect
class AfterThrowingExample {

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

throwing 子句还将匹配限制为仅抛出指定类型(本例中为 DataAccessException)异常的方法执行。@AfterThrowing 仅接收来自连接点(目标方法)本身的异常,而不处理来自同一切面的其他通知(如 @After)的异常。

后置最终通知 (After (Finally) Advice)

后置(最终)通知在匹配的方法执行退出(无论是正常返回还是抛出异常)时运行。使用 @After 注解声明。此通知必须准备好处理正常和异常返回条件,通常用于释放资源。

java
@Aspect
public class AfterFinallyExample {

	@After("execution(* com.xyz.dao.*.*(..))")
	public void doReleaseLock() {
		// ...
	}
}
kotlin
@Aspect
class AfterFinallyExample {

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

注意,@After 类似于 try-catch 语句中的 finally 块。

环绕通知 (Around Advice)

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

建议

请始终使用满足你要求的最小强度的通知形式。例如,如果前置通知就足够了,请不要使用环绕通知。

环绕通知通过 @Around 注解声明。方法应声明 Object 作为其返回类型,并且第一个参数必须是 ProceedingJoinPoint 类型。在通知正文中,必须调用 ProceedingJoinPointproceed() 方法,底层方法才会运行。

java
@Aspect
public class AroundExample {

	@Around("execution(* com.xyz..service.*.*(..))")
	public Object doBasicProfiling(ProceedingJoinPoint pjp) throws Throwable {
		// 执行前逻辑(如记录开始时间)
		Object retVal = pjp.proceed();
		// 执行后逻辑(如记录结束时间)
		return retVal;
	}
}
kotlin
@Aspect
class AroundExample {

	@Around("execution(* com.xyz..service.*.*(..))")
	fun doBasicProfiling(pjp: ProceedingJoinPoint): Any? {
		// 执行前逻辑
		val retVal = pjp.proceed()
		// 执行后逻辑
		return retVal
	}
}

⚠️ 重要

如果你将环绕通知方法的返回类型声明为 void,调用者将始终收到 null,从而忽略 proceed() 的任何结果。因此,建议环绕通知始终返回 Object 并在通常情况下返回 pjp.proceed() 的结果。

通知参数 (Advice Parameters)

访问当前 JoinPoint

任何通知方法都可以将 org.aspectj.lang.JoinPoint 声明为其第一个参数(环绕通知则是 ProceedingJoinPoint)。JoinPoint 提供了一些有用的方法:

  • getArgs(): 返回方法参数。
  • getThis(): 返回代理对象。
  • getTarget(): 返回目标对象。
  • getSignature(): 返回被通知方法的描述。
  • toString(): 打印被通知方法的有用描述。

将参数传递给通知

除了绑定返回值和异常,你还可以绑定方法参数。在 args 表达式中使用参数名代替类型名时,对应参数的值将在通知调用时传递。

java
@Before("execution(* com.xyz.dao.*.*(..)) && args(account,..)")
public void validateAccount(Account account) {
	// ... 在此直接使用 account 对象
}
kotlin
@Before("execution(* com.xyz.dao.*.*(..)) && args(account,..)")
fun validateAccount(account: Account) {
	// ...
}

通过这种方式,args(account,..) 既起到了匹配限制的作用(要求第一个参数必须是 Account 实例),又起到了参数绑定的作用。

同样,也可以为注解绑定参数:

java
@Before("com.xyz.Pointcuts.publicMethod() && @annotation(auditable)")
public void audit(Auditable auditable) {
	AuditCode code = auditable.value();
	// ...
}
kotlin
@Before("com.xyz.Pointcuts.publicMethod() && @annotation(auditable)")
fun audit(auditable: Auditable) {
	val code = auditable.value()
	// ...
}

参数名确定机制

Spring AOP 使用以下顺序来确定参数名称:

  1. argNames 属性:显式通过注解指定。
  2. Kotlin 反射:如果是 Kotlin 代码。
  3. 标准 Java 反射:如果编译时开启了 -parameters 标志(Java 8+ 推荐)。
  4. 切点推导:根据表达式推断参数名。

通知顺序 (Advice Ordering)

当多个通知要在同一个连接点运行时,Spring AOP 遵循与 AspectJ 相同的优先级规则。

  • “进入”连接点时,优先级最高的通知先运行(例如两个 @Before,高优先级先运行)。
  • “退出”连接点时,优先级最高的通知最后运行(例如两个 @After,高优先级后运行)。

如何控制优先级?

  • 不同切面之间:让切面实现 Ordered 接口或标注 @Order 注解。值越小,优先级越高
  • 同一切面内部:优先级顺序固定为:@Around, @Before, @After, @AfterReturning, @AfterThrowing。但请注意,@After 的行为类似于 finally,实际上它会在同一切面的返回或异常通知之后执行。

补充教学

1. 核心五大通知执行顺序图 (Spring 5.x+)

理解通知的层级关系对调试至关重要:

text
Around (前半段开始)
  Before
    Target Method Execution
  AfterReturning / AfterThrowing
After (Finally)
Around (后半段结束)

2. ProceedingJoinPoint.proceed() 的进阶用法

在环绕通知中,你可以修改传递给目标方法的参数:

java
@Around("execution(* serialize(..))")
public Object tweakArguments(ProceedingJoinPoint pjp) throws Throwable {
    Object[] args = pjp.getArgs();
    if (args.length > 0 && args[0] instanceof String) {
        args[0] = ((String) args[0]).trim(); // 去除首尾空格
    }
    return pjp.proceed(args); // 带有修改后参数的调用
}

3. 切记:不要拦截“核心基础设施”

避免编写拦截 Spring 核心 Bean(如 BeanPostProcessor 或 AOP 切面本身)的切入点。这可能导致死循环或 Bean 实例化过早的警告。切面的目标应该是你的业务 ServiceDAO

4. 异常处理的最佳实践

AfterThrowing 中,如果你捕获了异常并记录了日志,请记住 AOP 默认不会阻止异常继续向上抛出。如果你想在 AOP 层“吃掉”异常并返回默认值,必须使用 Around 通报,并手动 try-catchpjp.proceed()

5. JoinPoint vs ProceedingJoinPoint

  • JoinPoint:只读。可以查看方法名、参数,但无法控制执行流程。适用于 Before, After 等。
  • ProceedingJoinPoint:可控。只能在 Around 中使用。它拥有 proceed() 方法,就像手中握着目标方法的“点火钥匙”。

Based on Spring Framework.