Spring中的Advice API

现在我们可以看一下Spring AOP如何处理advice。

Advice生命周期

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

每个类的advice通常被最常用。它适用于通用的advice,比如事务advisor。这些不依赖于代理对象的状态或添加新的状态。它们仅仅作用于方法和参数。

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

您可以在同一个AOP代理中混合使用共享和每个实例的advice。

Spring中的通知类型

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

环绕通知

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

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实现的互操作性。本节其余部分讨论的其他advice类型以Spring特定的方式实现了常见的AOP概念。虽然使用最具体的advice类型有优势,但如果可能要在另一个AOP框架中运行aspect,则应坚持使用MethodInterceptor环绕通知。请注意,切入点目前在框架之间不具有互操作性,并且AOP Alliance目前未定义切入点接口。

前置通知

一个更简单的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
	}
}
前置通知可以与任何切入点一起使用。

异常抛出通知

如果连接点抛出异常,则在连接点返回后调用异常抛出通知。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 {
		// 处理远程异常
	}
}
class RemoteThrowsAdvice : ThrowsAdvice {

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

与前面的通知不同,下一个示例声明了四个参数,以便可以访问调用的方法、方法参数和目标对象。如果抛出ServletException,则调用以下通知:

  • Java

  • Kotlin

public class ServletThrowsAdviceWithArguments implements ThrowsAdvice {

	public void afterThrowing(Method m, Object[] args, Object target, ServletException ex) {
		// 处理所有参数
	}
}
class ServletThrowsAdviceWithArguments : ThrowsAdvice {

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

最后一个示例说明了如何在单个类中处理RemoteExceptionServletException的两种方法。任意数量的异常抛出通知方法可以组合在单个类中。以下示例显示了最终示例:

  • Java

  • Kotlin

public static class CombinedThrowsAdvice implements ThrowsAdvice {

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

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

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

	fun afterThrowing(m: Method, args: Array<Any>, target: Any, ex: ServletException) {
		// 处理所有参数
	}
}
如果异常抛出通知方法本身抛出异常,则会覆盖原始异常(即,将抛出给用户的异常更改)。覆盖的异常通常是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联盟继承的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()方法,我们希望所有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配置中,像任何其他顾问一样。下面讨论的所有代理创建选择,包括“自动代理创建者”,都正确处理介绍和有状态的混入。