Advice API in Spring

  • 环绕通知(Around Advice): 环绕整个方法调用,允许定制输入和输出。

  • 前置通知(Before Advice): 在方法调用之前执行,用于在调用之前执行自定义操作。

  • 抛出通知(Throws Advice): 在方法抛出异常时执行,用于处理或记录异常。

  • 返回后通知(After Returning Advice): 在方法顺利返回时执行,用于处理或记录返回值。

  • 切入通知(Introduction Advice): 将新接口和行为引入目标对象,扩展其功能,而无需修改其源代码。

现在我们可以检查 Spring AOP 如何处理建议。

Advice Lifecycles

每条建议都是一个 Spring bean。建议实例可以在所有建议对象中共享,也可以是特定于每个建议对象的。这对应于按类或按实例的建议。

按类建议使用最频繁。它适用于通用的建议,比如事务顾问。这些不依赖代理对象的当前状态或不添加新状态。它们仅对方法和参数起作用。

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

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

Advice Types in Spring

Spring 提供了几种建议类型并可以通过扩展支持任意建议类型。本节对基本概念和标准建议类型进行了描述。

Interception Around Advice

Spring 中最基础的建议类型是在周围拦截的建议。

Spring 符合 Alliance AOP 接口,用于使用方法拦截的环绕通知。实现 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 实现的互操作性。本节其余部分中讨论的其他建议类型实现了通用 AOP 概念,但以 Spring 特有的方式。尽管使用最具体建议类型有优势,如果你可能希望在其他 AOP 框架中运行方面,请使用 MethodInterceptor 建议。注意,切入点当前无法在框架之间互操作,并且 AOP Alliance 当前未定义切入点接口。

Before Advice

一种更简单的通知类型是前置通知。这不需要 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
	}
}

在使用任何接入点之前可以使用建议。

Throws Advice

如果连接点抛出一个异常,则会在连接点返回后调用抛出通知。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
	}
}

如果 throws-advice 方法抛出自身的一个异常,那么它将覆盖原始的异常(也就是说,它将抛出的异常更改为用户)。覆盖的异常通常是 RuntimeException,它与任何方法签名兼容。然而,如果 throws-advice 方法抛出一个已检查的异常,那么它必须匹配目标方法的已声明异常,进而或多或少与特定的目标方法签名耦合。Do not throw an undeclared checked exception that is incompatible with the target method’s signature!

抛出建议可用于任何接入点。

After Returning Advice

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

此通知不会更改执行路径。如果它抛出一个异常,该异常将沿拦截器链向上抛出,而不是返回的值。

在返回建议后可用于任何接入点。

Introduction Advice

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() 方法,我们希望所有设置器方法都抛出一个 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 方法;否则将继续进行连接点)通常就足够了。在本例中,我们需要添加一个检查:在锁定模式下不得调用任何设置器方法。

所需的切入只需要包含一个不同的 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 配置(与任何其他建议一样)以编程方式应用此建议。下面讨论的所有代理创建选项,包括“自动代理创建程序”,都能正确处理切入和有状态混合。