Spring 中的通知 API

现在我们可以研究 Spring AOP 如何处理通知。

通知生命周期

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

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

实例级别通知适用于引入(introductions),以支持 mixin。在这种情况下,通知会向代理对象添加状态。

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

Spring 中的通知类型

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

环绕拦截通知

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

Spring 遵循 AOP Alliance 的环绕通知接口,该接口使用方法拦截。因此,实现环绕通知的类应该实现 org.aopalliance.intercept 包中的以下 MethodInterceptor 接口

public interface MethodInterceptor extends Interceptor {

	Object invoke(MethodInvocation invocation) throws Throwable;
}

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

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

  • Java

  • Kotlin

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;
	}
}
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 方法。但是,如果没有充分的理由,不应该这样做。

MethodInterceptor 实现提供了与遵循 AOP Alliance 标准的其他 AOP 实现的互操作性。本节其余部分讨论的其他通知类型实现了常见的 AOP 概念,但采用了 Spring 特有的方式。虽然使用最具体的通知类型有好处,但如果你可能想在其他 AOP 框架中运行切面,最好坚持使用 MethodInterceptor 环绕通知。请注意,目前切入点在不同框架之间是不可互操作的,AOP Alliance 目前也没有定义切入点接口。

前置通知

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

前置通知的主要优点是无需调用 proceed() 方法,因此不可能意外地未能沿着拦截器链向下推进。

以下列表显示了 MethodBeforeAdvice 接口

public interface MethodBeforeAdvice extends BeforeAdvice {

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

注意返回类型是 void。前置通知可以在连接点运行之前插入自定义行为,但不能改变返回值。如果前置通知抛出异常,它会停止拦截器链的进一步执行。异常会沿着拦截器链向上传播。如果它是非受检异常或者在被调方法的签名中声明了该异常,则它会直接传递给客户端。否则,它会被 AOP 代理包装成一个非受检异常。

以下示例展示了 Spring 中的一个前置通知,它统计所有方法调用次数

  • Java

  • Kotlin

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;
	}
}
class CountingBeforeAdvice : MethodBeforeAdvice {

	var count: Int = 0

	override fun before(m: Method, args: Array<Any>, target: Any?) {
		++count
	}
}
前置通知可以与任何切入点一起使用。

抛出通知

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

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

只需要最后一个参数。方法签名可以有一个参数或四个参数,具体取决于通知方法是否关注方法和参数。接下来的两个列表展示了作为抛出通知示例的类。

以下通知会在抛出 RemoteException(包括其子类)时被调用

  • Java

  • Kotlin

public class RemoteThrowsAdvice implements ThrowsAdvice {

	public void afterThrowing(RemoteException ex) throws Throwable {
		// Do something with remote exception
	}
}
class RemoteThrowsAdvice : ThrowsAdvice {

	fun afterThrowing(ex: RemoteException) {
		// Do something with remote exception
	}
}

与前面的通知不同,下一个示例声明了四个参数,因此它可以访问被调用的方法、方法参数和目标对象。以下通知会在抛出 ServletException 时被调用

  • Java

  • Kotlin

public class ServletThrowsAdviceWithArguments implements ThrowsAdvice {

	public void afterThrowing(Method m, Object[] args, Object target, ServletException ex) {
		// Do something with all arguments
	}
}
class ServletThrowsAdviceWithArguments : ThrowsAdvice {

	fun afterThrowing(m: Method, args: Array<Any>, target: Any, ex: ServletException) {
		// Do something with all arguments
	}
}

最后一个示例展示了如何在一个类中组合使用这两个方法来处理 RemoteExceptionServletException。可以在一个类中组合任意数量的抛出通知方法。以下列表展示了最后一个示例

  • Java

  • Kotlin

public static class CombinedThrowsAdvice implements ThrowsAdvice {

	public void afterThrowing(RemoteException ex) throws Throwable {
		// Do something with remote exception
	}

	public void afterThrowing(Method m, Object[] args, Object target, ServletException ex) {
		// Do something with all arguments
	}
}
class CombinedThrowsAdvice : ThrowsAdvice {

	fun afterThrowing(ex: RemoteException) {
		// Do something with remote exception
	}

	fun afterThrowing(m: Method, args: Array<Any>, target: Any, ex: ServletException) {
		// Do something with all arguments
	}
}
如果一个抛出通知方法本身抛出了异常,它会覆盖原始异常(也就是说,它会改变抛给用户的异常)。覆盖异常通常是一个 RuntimeException,它与任何方法签名都兼容。然而,如果一个抛出通知方法抛出了一个受检异常,它必须与目标方法声明的异常匹配,因此,在某种程度上与特定的目标方法签名耦合。不要抛出与目标方法签名不兼容的、未声明的受检异常!
抛出通知可以与任何切入点一起使用。

返回后通知

Spring 中的返回后通知必须实现 org.springframework.aop.AfterReturningAdvice 接口,以下列表显示了该接口

public interface AfterReturningAdvice extends Advice {

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

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

以下返回后通知统计所有未抛出异常的成功方法调用

  • Java

  • Kotlin

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;
	}
}
class CountingAfterReturningAdvice : AfterReturningAdvice {

	var count: Int = 0
		private set

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

此通知不会改变执行路径。如果它抛出异常,异常会沿拦截器链向上传播,而不是返回返回值。

返回后通知可以与任何切入点一起使用。

引入通知

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

引入需要实现以下接口的 IntroductionAdvisorIntroductionInterceptor

public interface IntroductionInterceptor extends MethodInterceptor {

	boolean implementsInterface(Class intf);
}

继承自 AOP Alliance MethodInterceptor 接口的 invoke() 方法必须实现引入逻辑。也就是说,如果调用的方法属于引入的接口,则引入拦截器负责处理该方法调用——它不能调用 proceed()

引入通知不能与任何切入点一起使用,因为它只应用于类级别,而不是方法级别。你只能将引入通知与 IntroductionAdvisor 一起使用,该通知器具有以下方法

public interface IntroductionAdvisor extends Advisor, IntroductionInfo {

	ClassFilter getClassFilter();

	void validateInterfaces() throws IllegalArgumentException;
}

public interface IntroductionInfo {

	Class<?>[] getInterfaces();
}

引入通知没有关联的 MethodMatcherPointcut。只有类过滤是合理的。

getInterfaces() 方法返回此通知器引入的接口。

validateInterfaces() 方法在内部用于检查配置的 IntroductionInterceptor 是否可以实现引入的接口。

考虑 Spring 测试套件中的一个示例,假设我们想向一个或多个对象引入以下接口

  • Java

  • Kotlin

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

这说明了一个 mixin。我们希望能够将任何类型的被通知对象强制转换为 Lockable,并调用 lock 和 unlock 方法。如果我们调用 lock() 方法,我们希望所有 setter 方法都抛出 LockedException。因此,我们可以添加一个切面,使其能够使对象不可变,而无需对象本身知道这一点:这是 AOP 的一个很好的示例。

首先,我们需要一个负责繁重工作的 IntroductionInterceptor。在此示例中,我们扩展了便捷类 org.springframework.aop.support.DelegatingIntroductionInterceptor。我们可以直接实现 IntroductionInterceptor,但在大多数情况下,使用 DelegatingIntroductionInterceptor 是最佳选择。

DelegatingIntroductionInterceptor 的设计目的是将引入逻辑委托给引入接口的实际实现,同时隐藏使用拦截来实现这一目的。你可以使用构造函数参数将委托对象设置为任何对象。默认的委托对象(在使用无参构造函数时)是 this。因此,在下一个示例中,委托对象是 DelegatingIntroductionInterceptor 的子类 LockMixin。给定一个委托对象(默认情况下是其自身),DelegatingIntroductionInterceptor 实例会查找委托对象实现的所有接口(除了 IntroductionInterceptor),并支持对其中任何接口进行引入。像 LockMixin 这样的子类可以调用 suppressInterface(Class intf) 方法来抑制不应该暴露的接口。然而,无论 IntroductionInterceptor 准备支持多少接口,实际暴露哪些接口由使用的 IntroductionAdvisor 控制。引入的接口会隐藏目标对象对同一接口的任何实现。

因此,LockMixin 扩展了 DelegatingIntroductionInterceptor 并自身实现了 Lockable。超类会自动识别 Lockable 可以被引入支持,所以我们不需要额外指定。我们可以通过这种方式引入任意数量的接口。

注意使用了 locked 实例变量。这有效地为目标对象所持有的状态添加了额外状态。

以下示例展示了 LockMixin 示例类

  • Java

  • Kotlin

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

}
class LockMixin : DelegatingIntroductionInterceptor(), Lockable {

	private var locked: Boolean = false

	fun lock() {
		this.locked = true
	}

	fun unlock() {
		this.locked = false
	}

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

}

通常,你不需要覆盖 invoke() 方法。DelegatingIntroductionInterceptor 的实现(如果方法已被引入则调用 delegate 方法,否则继续向下执行到连接点)通常就足够了。在本例中,我们需要添加一个检查:如果在锁定模式下,任何 setter 方法都不能被调用。

所需的引入只需持有一个独立的 LockMixin 实例并指定引入的接口(在此例中,仅为 Lockable)。一个更复杂的示例可能会引用引入拦截器(该拦截器将被定义为原型)。在本例中,LockMixin 没有相关的配置,因此我们使用 new 来创建它。以下示例展示了我们的 LockMixinAdvisor

  • Java

  • Kotlin

public class LockMixinAdvisor extends DefaultIntroductionAdvisor {

	public LockMixinAdvisor() {
		super(new LockMixin(), Lockable.class);
	}
}
class LockMixinAdvisor : DefaultIntroductionAdvisor(LockMixin(), Lockable::class.java)

我们可以非常简单地应用这个通知器,因为它不需要配置。(然而,没有 IntroductionAdvisor 就不可能使用 IntroductionInterceptor。)与引入通知一样,通知器必须是实例级别的(per-instance),因为它是 estatales。每个被通知对象都需要一个不同的 LockMixinAdvisor 实例,也因此需要不同的 LockMixin 实例。通知器构成了被通知对象状态的一部分。

我们可以通过使用 Advised.addAdvisor() 方法以编程式方式应用此通知器,或者(推荐方式)像其他任何通知器一样在 XML 配置中应用。下面讨论的所有代理创建选项,包括“自动代理创建器”,都能正确处理引入通知和有状态的 mixin。