Spring 中的 Advice API

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

通知生命周期

每个通知都是一个 Spring bean。一个通知实例可以被所有通知对象共享,也可以是每个通知对象独有的。这对应于每个类或每个实例的通知。

每个类的通知使用最频繁。它适用于通用通知,例如事务通知器。这些不依赖于代理对象的状态或添加新状态。它们仅作用于方法和参数。

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

你可以在同一个 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(包括 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 将 引入通知 视为一种特殊类型的拦截通知。

引入需要一个 IntroductionAdvisor 和一个实现以下接口的 IntroductionInterceptor

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

没有 MethodMatcher,因此也没有与引入通知关联的 Pointcut。只有类过滤是逻辑的。

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

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

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

  • Java

  • Kotlin

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

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

首先,我们需要一个执行繁重工作的 IntroductionInterceptor。在这种情况下,我们扩展 org.springframework.aop.support.DelegatingIntroductionInterceptor 便利类。我们可以直接实现 IntroductionInterceptor,但在大多数情况下使用 DelegatingIntroductionInterceptor 是最好的。

DelegatingIntroductionInterceptor 旨在将引入委托给引入接口的实际实现,从而隐藏使用拦截来实现这一点的细节。你可以使用构造函数参数将委托设置为任何对象。默认委托(当使用无参数构造函数时)是 this。因此,在下一个示例中,委托是 DelegatingIntroductionInterceptorLockMixin 子类。给定一个委托(默认情况下是它自己),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。)像往常一样,对于引入,通知器必须是每个实例的,因为它是有状态的。对于每个通知对象,我们需要一个不同的 LockMixinAdvisor 实例,因此也需要一个不同的 LockMixin 实例。通知器构成了通知对象状态的一部分。

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