Spring 中的通知 API

现在,我们可以研究 Spring AOP 如何处理建议。

建议生命周期

每个建议都是一个 Spring bean。一个建议实例可以在所有被建议的对象之间共享,也可以对每个被建议的对象是唯一的。这对应于按类或按实例的建议。

按类建议使用得最多。它适用于通用建议,例如事务顾问。这些不依赖于代理对象的状态或添加新状态。它们只是作用于方法和参数。

按实例建议适用于引入,以支持混合。在这种情况下,建议会向代理对象添加状态。

你可以在同一个 AOP 代理中使用共享和按实例建议的混合。

Spring 中的通知类型

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

环绕通知拦截

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

Spring 符合使用方法拦截的环绕通知的 AOP Alliance 接口。实现 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 rval = invocation.proceed();
		System.out.println("Invocation returned");
		return rval;
	}
}
class DebugInterceptor : MethodInterceptor {

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

请注意对 MethodInvocationproceed() 方法的调用。这会沿着拦截器链向下进行,朝向连接点。大多数拦截器调用此方法并返回其返回值。但是,MethodInterceptor(如任何环绕通知)可以返回不同的值或引发异常,而不是调用 proceed 方法。但是,如果没有充分的理由,您不希望这样做。

MethodInterceptor 实现提供与其他 AOP Alliance 兼容的 AOP 实现的互操作性。本节其余部分讨论的其他通知类型以 Spring 特定的方式实现了常见的 AOP 概念。虽然使用最具体的通知类型有优势,但如果您可能希望在另一个 AOP 框架中运行切面,请坚持使用 MethodInterceptor 环绕通知。请注意,切入点当前无法在框架之间互操作,并且 AOP Alliance 当前未定义切入点接口。

前置通知

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

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

以下清单显示了 MethodBeforeAdvice 接口

public interface MethodBeforeAdvice extends BeforeAdvice {

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

(Spring 的 API 设计允许字段前置通知,尽管通常的对象适用于字段拦截,但 Spring 不太可能实现它。)

请注意,返回类型为 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();
}

没有 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
}

这说明了一个 mixin。我们希望能够将建议的对象强制转换为 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 配置中以编程方式应用此顾问,就像任何其他顾问一样。下面讨论的所有代理创建选项,包括“自动代理创建器”,都能正确处理引入和有状态的混合。