对于最新稳定版本,请使用 Spring Framework 7.0.6spring-doc.cadn.net.cn

Spring 中的 Advice API

现在我们可以研究 Spring AOP 是如何处理通知(advice)的。spring-doc.cadn.net.cn

通知生命周期

每条通知(advice)都是一个 Spring bean。一个通知实例可以被所有被通知的对象共享,也可以对每个被通知的对象保持唯一。这分别对应于按类(per-class)或按实例(per-instance)的通知。spring-doc.cadn.net.cn

最常用的是基于类的增强(advice)。它适用于通用型增强,例如事务增强器(transaction advisors)。这类增强不依赖于被代理对象的状态,也不会添加新的状态,而仅作用于方法及其参数。spring-doc.cadn.net.cn

每实例的增强(advice)适用于引入(introductions),以支持混入(mixins)。在这种情况下,增强会为被代理对象添加状态。spring-doc.cadn.net.cn

你可以在同一个 AOP 代理中混合使用共享的和每个实例的增强(advice)。spring-doc.cadn.net.cn

Spring 中的通知类型

Spring 提供了多种通知(advice)类型,并且可扩展以支持任意的通知类型。本节将介绍基本概念和标准的通知类型。spring-doc.cadn.net.cn

环绕拦截通知

Spring 中最基本的通知类型是环绕通知。spring-doc.cadn.net.cn

Spring 遵循 AOP Alliance 接口规范,用于实现基于方法拦截的环绕通知(around advice)。实现 MethodInterceptor 并提供环绕通知的类还应实现以下接口:spring-doc.cadn.net.cn

public interface MethodInterceptor extends Interceptor {

	Object invoke(MethodInvocation invocation) throws Throwable;
}

传递给 MethodInvocation 方法的 invoke() 参数暴露了正在被调用的方法、目标连接点、AOP 代理以及该方法的参数。invoke() 方法应返回本次调用的结果:即连接点的返回值。spring-doc.cadn.net.cn

以下示例展示了一个简单的 MethodInterceptor 实现:spring-doc.cadn.net.cn

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

注意对 proceed()MethodInvocation 方法的调用。该调用会沿着拦截器链继续向下执行,直至到达连接点(join point)。大多数拦截器都会调用此方法并返回其返回值。然而,MethodInterceptor 与任何环绕通知(around advice)一样,也可以选择返回一个不同的值,或者抛出异常,而不调用 proceed 方法。不过,除非有充分的理由,否则你不应这样做。spring-doc.cadn.net.cn

MethodInterceptor 的实现提供了与其他符合 AOP Alliance 规范的 AOP 实现之间的互操作性。本节其余部分讨论的其他通知类型实现了通用的 AOP 概念,但采用了 Spring 特有的方式。尽管使用最具体的通知类型具有一定优势,但如果你希望将来在其他 AOP 框架中运行该切面,则应坚持使用基于 MethodInterceptor 的环绕通知。请注意,目前各框架之间的切入点(pointcut)并不具备互操作性,而且 AOP Alliance 目前也未定义切入点接口。

前置通知

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

前置通知(before advice)的主要优点是无需调用 proceed() 方法,因此也就不会意外地忘记继续执行拦截器链。spring-doc.cadn.net.cn

以下代码清单展示了 MethodBeforeAdvice 接口:spring-doc.cadn.net.cn

public interface MethodBeforeAdvice extends BeforeAdvice {

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

(Spring 的 API 设计虽然允许在字段访问前加入通知(before advice),但通常的对象适用于字段拦截,而且 Spring 很可能永远不会实现这一功能。)spring-doc.cadn.net.cn

请注意,返回类型为 void。前置通知(before advice)可以在连接点执行之前插入自定义行为,但不能更改返回值。如果前置通知抛出异常,它将停止拦截器链的进一步执行。该异常会沿着拦截器链向上传播。如果该异常是未检查异常(unchecked exception),或者出现在被调用方法的签名中,则会直接传递给客户端;否则,AOP 代理会将其包装在一个未检查异常中。spring-doc.cadn.net.cn

以下示例展示了 Spring 中的一个前置通知(before advice),用于统计所有方法调用次数:spring-doc.cadn.net.cn

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
	}
}
前置通知(Before advice)可以与任意切入点(pointcut)一起使用。

抛出建议

异常通知(Throws advice)在连接点抛出异常后、连接点返回时被调用。Spring 提供了类型化的异常通知。请注意,这意味着 org.springframework.aop.ThrowsAdvice 接口不包含任何方法。它是一个标记接口,用于标识给定对象实现了一个或多个类型化的异常通知方法。这些方法应采用以下形式:spring-doc.cadn.net.cn

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

仅最后一个参数是必需的。方法签名可以包含一个或四个参数,具体取决于通知方法是否需要访问目标方法及其参数。接下来的两个代码清单展示了作为异常通知示例的类。spring-doc.cadn.net.cn

如果抛出 RemoteException(包括其子类)时,将调用以下通知:spring-doc.cadn.net.cn

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 时会被调用:spring-doc.cadn.net.cn

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。任意数量的异常通知(throws advice)方法都可以组合在同一个类中。以下代码清单展示了最后一个示例:spring-doc.cadn.net.cn

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 方法抛出了一个受检异常(checked exception),则该异常必须与目标方法声明的异常相匹配,因此在某种程度上会与特定的目标方法签名耦合。切勿抛出与目标方法签名不兼容的未声明受检异常!
异常通知(Throws advice)可以与任意切入点(pointcut)一起使用。

返回后通知

Spring 中的后置返回通知(after returning advice)必须实现 org.springframework.aop.AfterReturningAdvice 接口,如下所示:spring-doc.cadn.net.cn

public interface AfterReturningAdvice extends Advice {

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

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

以下的后置返回通知(after returning advice)会统计所有未抛出异常的成功方法调用次数:spring-doc.cadn.net.cn

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-doc.cadn.net.cn

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

介绍建议

Spring 将引入型通知(introduction advice)视为一种特殊的拦截通知(interception advice)。spring-doc.cadn.net.cn

引入(Introduction)需要一个 IntroductionAdvisor 和一个实现以下接口的 IntroductionInterceptorspring-doc.cadn.net.cn

public interface IntroductionInterceptor extends MethodInterceptor {

	boolean implementsInterface(Class intf);
}

从 AOP Alliance 的 invoke() 接口继承而来的 MethodInterceptor 方法必须实现引介(introduction)。也就是说,如果被调用的方法属于一个被引介的接口,则该引介拦截器负责处理该方法调用——它不能调用 proceed()spring-doc.cadn.net.cn

引介(Introduction)通知不能与任意切入点(pointcut)一起使用,因为它仅作用于类级别,而非方法级别。你只能将引介通知与IntroductionAdvisor一起使用,该接口包含以下方法:spring-doc.cadn.net.cn

public interface IntroductionAdvisor extends Advisor, IntroductionInfo {

	ClassFilter getClassFilter();

	void validateInterfaces() throws IllegalArgumentException;
}

public interface IntroductionInfo {

	Class<?>[] getInterfaces();
}

没有与引入型通知(introduction advice)关联的 MethodMatcher,因此也没有 Pointcut。仅类过滤是合理的。spring-doc.cadn.net.cn

getInterfaces() 方法返回此通知器所引入的接口。spring-doc.cadn.net.cn

validateInterfaces() 方法在内部用于检查所引入的接口是否可以由配置的 IntroductionInterceptor 实现。spring-doc.cadn.net.cn

考虑一个来自 Spring 测试套件的示例,假设我们希望向一个或多个对象引入以下接口:spring-doc.cadn.net.cn

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 异常。因此,我们可以添加一个切面(aspect),在对象本身对此一无所知的情况下,赋予它们不可变的能力:这是面向切面编程(AOP)的一个绝佳示例。spring-doc.cadn.net.cn

首先,我们需要一个IntroductionInterceptor来完成繁重的工作。在此例中,我们扩展了org.springframework.aop.support.DelegatingIntroductionInterceptor这个便利类。我们也可以直接实现IntroductionInterceptor接口,但在大多数情况下,使用DelegatingIntroductionInterceptor是最佳选择。spring-doc.cadn.net.cn

DelegatingIntroductionInterceptor 旨在将引入委托给所引入接口的实际实现,从而隐藏用于实现此目的的拦截机制。您可以使用构造函数参数将委托设置为任何对象。默认委托(当使用无参构造函数时)是 this。因此,在下一个示例中,委托是 DelegatingIntroductionInterceptorLockMixin 子类。 给定一个委托(默认为其自身),DelegatingIntroductionInterceptor 实例会查找委托实现的所有接口(IntroductionInterceptor 除外),并支持针对其中任何接口的引入。LockMixin 等子类可以调用 suppressInterface(Class intf) 方法来抑制不应暴露的接口。然而,无论 IntroductionInterceptor 准备支持多少个接口,实际使用的 IntroductionAdvisor 都会控制最终暴露哪些接口。被引入的接口会隐藏目标对象对同一接口的任何实现。spring-doc.cadn.net.cn

因此,LockMixin 继承了 DelegatingIntroductionInterceptor 并自身实现了 Lockable 接口。其父类会自动识别出 Lockable 可以被用于引入(introduction),因此我们无需显式指定这一点。通过这种方式,我们可以引入任意数量的接口。spring-doc.cadn.net.cn

注意对 locked 实例变量的使用。这实际上为目标对象中已有的状态额外添加了新的状态。spring-doc.cadn.net.cn

以下示例展示了 LockMixin 类的示例:spring-doc.cadn.net.cn

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 方法。spring-doc.cadn.net.cn

所需的引入只需持有一个唯一的 LockMixin 实例,并指定被引入的接口(在此例中,仅为 Lockable)。一个更复杂的示例可能会引用引入拦截器(该拦截器将被定义为原型作用域)。在本例中,LockMixin 没有任何相关的配置,因此我们直接使用 new 来创建它。以下示例展示了我们的 LockMixinAdvisor 类:spring-doc.cadn.net.cn

public class LockMixinAdvisor extends DefaultIntroductionAdvisor {

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

我们可以非常简单地应用这个通知器(advisor),因为它不需要任何配置。(然而,不使用 IntroductionInterceptor 就无法使用 IntroductionAdvisor。)与通常的引入(introduction)一样,该通知器必须是每个实例独立的,因为它是有状态的。对于每个被通知的对象,我们都需要一个不同的 LockMixinAdvisor 实例,因此也需要一个不同的 LockMixin 实例。该通知器构成了被通知对象状态的一部分。spring-doc.cadn.net.cn

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