Skip to content

Spring 中的通知 (Advice) API

现在我们可以考察 Spring AOP 是如何处理通知(Advice)的。

通知的生命周期

每个通知都是一个 Spring Bean。一个通知实例可以被所有被通知(advised)对象共享,也可以对每个被通知对象是唯一的。这分别对应于类级别(per-class)实例级别(per-instance)通知。

类级别通知最常被使用。它适用于通用的通知,例如事务 Advisor。这些通知不依赖于被代理对象的状态,也不添加新的状态;它们仅作用于方法和参数。

实例级别通知适用于“引入(Introductions)”,用于支持混入(Mixins)。在这种情况下,通知会向被代理对象添加状态。

你可以在同一个 AOP 代理中混合使用共享通知和实例级别通知。

Spring 中的通知类型

Spring 提供了几种通知类型,并且是可扩展的,以支持任意通知类型。本节描述基本概念和标准通知类型。

环绕拦截通知 (Interception Around Advice)

Spring 中最基本的通知类型是环绕拦截通知

Spring 遵循 AOP Alliance 接口标准,通过方法拦截(method interception)实现环绕通知。因此,实现环绕通知的类应实现 org.aopalliance.intercept.MethodInterceptor 接口:

java
public interface MethodInterceptor extends Interceptor {

	Object invoke(MethodInvocation invocation) throws Throwable;
}

invoke() 方法的 MethodInvocation 参数公开了正在被调用的方法、目标连接点、AOP 代理以及方法的参数。invoke() 方法应返回调用结果:通常是连接点的返回值。

以下示例显示了一个简单的 MethodInterceptor 实现:

java
public class DebugInterceptor implements MethodInterceptor {

	public Object invoke(MethodInvocation invocation) throws Throwable {
		System.out.println("Before: invocation=[" + invocation + "]");
		Object result = invocation.proceed();
		System.out.println("Invocation returned");
		return result;
	}
}
kotlin
class DebugInterceptor : MethodInterceptor {

	override fun invoke(invocation: MethodInvocation): Any? {
		println("Before: invocation=[$invocation]")
		val result = invocation.proceed()
		println("Invocation returned")
		return result
	}
}

注意对 MethodInvocationproceed() 方法的调用。这会沿着拦截器链向连接点推进。大多数拦截器会调用此方法并返回其返回值。然而,与任何环绕通知一样,MethodInterceptor 可以返回不同的值或抛出异常,而不是调用 proceed 方法。但是,如果没有充分的原因,请不要这样做。

TIP

MethodInterceptor 实现提供了与其他兼容 AOP Alliance 的 AOP 实现的互操作性。本节其余部分讨论的其他通知类型实现了通用的 AOP 概念,但以 Spring 特有的方式。虽然使用更具体的通知类型有其优势,但如果你可能希望在另一个 AOP 框架中运行该切面,请坚持使用 MethodInterceptor 环绕通知。

前置通知 (Before Advice)

一种更简单的通知类型是前置通知。这不需要 MethodInvocation 对象,因为它仅在进入方法之前被调用。

前置通知的主要优点是无需调用 proceed() 方法,因此不存在意外导致拦截器链无法推进的可能性。

以下显示了 MethodBeforeAdvice 接口:

java
public interface MethodBeforeAdvice extends BeforeAdvice {

	void before(Method m, Object[] args, Object target) throws Throwable;
}

注意返回值类型是 void。前置通知可以在连接点运行前插入自定义行为,但不能更改返回值。如果前置通知抛出异常,它将停止拦截器链的进一步执行。异常将沿着拦截器链向上传播。

以下示例显示了 Spring 中的前置通知,它计算所有方法调用的次数:

java
public class CountingBeforeAdvice implements MethodBeforeAdvice {

	private int count;

	public void before(Method m, Object[] args, Object target) throws Throwable {
		++count;
	}

	public int getCount() {
		return count;
	}
}
kotlin
class CountingBeforeAdvice : MethodBeforeAdvice {

	var count: Int = 0

	override fun before(m: Method, args: Array<Any>, target: Any?) {
		++count
	}
}

异常通知 (Throws Advice)

如果在连接点抛出异常,则在连接点返回后调用异常通知。Spring 提供类型化的异常通知。注意,这意味着 org.springframework.aop.ThrowsAdvice 接口不包含任何方法。它是一个标记接口,标识给定对象实现了一个或多个类型化的异常通知方法。这些方法应采用以下形式:

java
afterThrowing([Method, args, target], subclassOfThrowable)

只有最后一个参数是必需的。方法签名可以有一个或四个参数,具体取决于通知方法是否对方法和参数感兴趣。

以下示例是异常通知的类:

java
public class RemoteThrowsAdvice implements ThrowsAdvice {

	public void afterThrowing(RemoteException ex) throws Throwable {
		// 处理远程异常
	}
}
kotlin
class RemoteThrowsAdvice : ThrowsAdvice {

	fun afterThrowing(ex: RemoteException) {
		// 处理远程异常
	}
}

下面的示例声明了四个参数,因此它可以访问被调用的方法、方法参数和目标对象。如果抛出 ServletException,则调用此通知:

java
public class ServletThrowsAdviceWithArguments implements ThrowsAdvice {

	public void afterThrowing(Method m, Object[] args, Object target, ServletException ex) {
		// 使用所有参数执行某些操作
	}
}
kotlin
class ServletThrowsAdviceWithArguments : ThrowsAdvice {

	fun afterThrowing(m: Method, args: Array<Any>, target: Any, ex: ServletException) {
		// 使用所有参数执行某些操作
	}
}

最后一个例子说明了如何在一个类中组合使用这两个方法,处理 RemoteExceptionServletException

java
public static class CombinedThrowsAdvice implements ThrowsAdvice {

	public void afterThrowing(RemoteException ex) throws Throwable {
		// 处理远程异常
	}

	public void afterThrowing(Method m, Object[] args, Object target, ServletException ex) {
		// 处理 ServletException
	}
}
kotlin
class CombinedThrowsAdvice : ThrowsAdvice {

	fun afterThrowing(ex: RemoteException) {
		// 处理远程异常
	}

	fun afterThrowing(m: Method, args: Array<Any>, target: Any, ex: ServletException) {
		// 处理 ServletException
	}
}

TIP

如果异常通知方法本身抛出异常,它将覆盖原始异常(即更改抛给用户的异常)。如果抛出受检异常,它必须与目标方法的声明异常匹配。不要抛出与目标方法签名不兼容的未声明受检异常!

后置返回通知 (After Returning Advice)

Spring 中的后置返回通知必须实现 org.springframework.aop.AfterReturningAdvice 接口:

java
public interface AfterReturningAdvice extends Advice {

	void afterReturning(Object returnValue, Method m, Object[] args, Object target)
			throws Throwable;
}

后置返回通知可以访问返回值(但不能修改它)、被调用的方法、方法的参数和目标对象。

以下是一个统计成功调用的后置返回通知:

java
public class CountingAfterReturningAdvice implements AfterReturningAdvice {

	private int count;

	public void afterReturning(Object returnValue, Method m, Object[] args, Object target)
			throws Throwable {
		++count;
	}

	public int getCount() {
		return count;
	}
}
kotlin
class CountingAfterReturningAdvice : AfterReturningAdvice {

	var count: Int = 0
		private set

	override fun afterReturning(returnValue: Any?, m: Method, args: Array<Any>, target: Any?) {
		++count
	}
}

引入通知 (Introduction Advice)

Spring 将引入通知视为一种特殊的拦截通知。引入需要 IntroductionAdvisorIntroductionInterceptor

引入通知不能与任何切入点一起使用,因为它仅在类级别(而非方法级别)应用。你只能将引入通知与 IntroductionAdvisor 一起使用。

假设我们要向一个或多个对象引入以下接口:

java
public interface Lockable {
	void lock();
	void unlock();
	boolean locked();
}
kotlin
interface Lockable {
	fun lock()
	fun unlock()
	fun locked(): Boolean
}

这是一个混入(Mixin)的例子。我们希望能够将任何被通知对象转换为 Lockable,并调用其方法。

为此,我们通常扩展 org.springframework.aop.support.DelegatingIntroductionInterceptor 类。

java
public class LockMixin extends DelegatingIntroductionInterceptor implements Lockable {

	private boolean locked;

	public void lock() {
		this.locked = true;
	}

	public void unlock() {
		this.locked = false;
	}

	public boolean locked() {
		return this.locked;
	}

	public Object invoke(MethodInvocation invocation) throws Throwable {
		if (locked() && invocation.getMethod().getName().indexOf("set") == 0) {
			throw new LockedException();
		}
		return super.invoke(invocation);
	}
}
kotlin
class LockMixin : DelegatingIntroductionInterceptor(), Lockable {

	private var locked: Boolean = false

	override fun lock() {
		this.locked = true
	}

	override fun unlock() {
		this.locked = false
	}

	override fun locked(): Boolean {
		return this.locked
	}

	override fun invoke(invocation: MethodInvocation): Any? {
		if (locked() && invocation.method.name.indexOf("set") == 0) {
			throw LockedException()
		}
		return super.invoke(invocation)
	}
}

补充教学

1. 深度解析:为什么 ThrowsAdvice 是标记接口?

你会发现 ThrowsAdvice 里面一个方法都没有,这在 Java API 中很不常见(通常直接用反射或者 Functional Interface)。

  • 灵活性:如果定义了固定接口(例如 void afterThrowing(Exception ex)),你就只能拦截所有的 Exception。
  • 多态分发:Spring 的 ThrowsAdviceInterceptor 在底层会使用反射寻找名为 afterThrowing 且参数匹配的方法。这允许你像上面的例子一样,在一个类中定义针对 RemoteExceptionServletException 的不同处理逻辑,而不需要写一堆 instanceof

2. MethodInterceptor vs. 其他特定通知

  • 什么时候用特定通知(Before/After)?
    • 如果你只需要在开始或结束时做点事(如单纯的计数、审计日志),请优先使用特定的前置/后置通知。它们的语义更明确,且不需要显式调用 proceed(),不容易出错。
  • 什么时候用 MethodInterceptor?
    • 如果你需要改变返回值、动态决定是否执行目标方法(比如权限检查未通过时拦截)、或者需要在返回后根据业务结果做清理,必须使用 MethodInterceptor

3. 引入(Introduction)—— 给 Java 插上“翅膀”

引入通知是 Spring AOP 中最强大的功能之一。它允许你为一个现有的类在运行时动态实现一个新接口

  • 使用场景
    • 对象锁定:如文中例子,让普通 Bean 具备“只读/锁定”状态。
    • 审计/状态标记:让原本没有 ModifiedAware 接口的 Bean 自动具备记录上次修改时间的能力。
  • 注意:因为引入修改了代理对象的类型(增加了接口),所以必须使用 实例级别(Per-instance)。这意味着每个被代理对象都要对应一个独立的通知实例,否则大家的状态就混到一起了。

Based on Spring Framework.