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

Spring中的通知API

现在我们可以考察Spring AOP是如何处理通知的。spring-doc.cadn.net.cn

通知生命周期

每个通知都是一个Spring bean。一个通知实例可以在所有被通知的对象之间共享,也可以对每个被通知的对象唯一。这对应于类级别的通知或实例级别的通知。spring-doc.cadn.net.cn

每类建议最常使用。它适用于通用建议,例如事务顾问。这些建议不依赖于代理对象的状态,也不添加新的状态。它们只是对方法和参数进行操作。spring-doc.cadn.net.cn

实例级别的建议适用于引入,以支持混入。在这种情况下,建议会向代理对象添加状态。spring-doc.cadn.net.cn

你可以在同一个AOP代理中使用共享和每个实例的建议的组合。spring-doc.cadn.net.cn

Spring中的通知类型

Spring 提供了几种通知类型,并且可以扩展以支持任意的通知类型。本节描述了基本概念和标准通知类型。spring-doc.cadn.net.cn

拦截器周围的通知

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

Spring 符合使用方法拦截的 AOP Alliance 接口的环绕通知。实现 MethodInterceptor 并且实现环绕通知的类还应该实现以下接口:spring-doc.cadn.net.cn

public interface MethodInterceptor extends Interceptor {

	Object invoke(MethodInvocation invocation) throws Throwable;
}

The MethodInvocation argument to the invoke() method exposes the method being invoked, the target join point, the AOP proxy, and the arguments to the method. The invoke() method should return the invocation’s result: the return value of the join point。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
	}
}

请注意对MethodInvocationproceed()方法的调用。这会沿着拦截器链向下进行,直到连接点。大多数拦截器调用此方法并返回其返回值。但是,一个MethodInterceptor,像任何环绕通知一样,可以返回不同的值或抛出异常,而不是调用proceed方法。但是,除非有充分的理由,否则您不想这样做。spring-doc.cadn.net.cn

MethodInterceptor 个实现提供了与其他符合 AOP Alliance 标准的 AOP 实现之间的互操作性。本节后面讨论的其他通知类型实现了常见的 AOP 概念,但以 Spring 特定的方式。虽然使用最具体的通知类型有优势,但如果你可能希望在另一个 AOP 框架中运行切面,请坚持使用 MethodInterceptor 类型的通知。请注意,切点目前在不同框架之间是不可互操作的,并且 AOP Alliance 目前没有定义切点接口。

前置通知

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

在使用前置通知的主要优势是不需要调用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 设计允许在字段之前使用通知,尽管通常的对象适用于字段拦截,并且 Spring 很可能永远不会实现它。)spring-doc.cadn.net.cn

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

以下示例展示了 Spring 中的前置通知,它统计所有方法调用: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
	}
}
在任何切点使用前置通知之前。

Throws Advice

Throws advice 在连接点抛出异常后被调用。Spring 提供了类型化的 throws advice。请注意,这意味着 org.springframework.aop.ThrowsAdvice 接口不包含任何方法。它是一个标记接口,用于标识给定对象实现了一个或多个类型化的 throws advice 方法。这些方法应具有以下形式: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。可以在一个类中组合任意数量的抛出建议方法。以下代码段显示了最终示例: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方法抛出一个已检查异常,它必须与目标方法声明的异常匹配,并因此在某种程度上与特定的目标方法签名耦合。不要抛出与目标方法签名不兼容的未声明的已检查异常!
抛出通知可以与任何切入点一起使用。

返回后通知

Spring 中的后置通知必须实现 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

以下返回后通知统计了所有成功的方法调用,这些方法调用没有抛出异常: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 将引入通知视为一种特殊的拦截通知。spring-doc.cadn.net.cn

介绍需要一个IntroductionAdvisor和一个IntroductionInterceptor,它们实现了以下接口:spring-doc.cadn.net.cn

public interface IntroductionInterceptor extends MethodInterceptor {

	boolean implementsInterface(Class intf);
}

The invoke() 方法继承自 AOP Alliance MethodInterceptor 接口必须实现引入。也就是说,如果调用的方法是在一个引入的接口上,那么引入拦截器负责处理方法调用——它不能调用 proceed()spring-doc.cadn.net.cn

介绍性通知不能与任何切入点一起使用,因为它仅适用于类级别,而不是方法级别。您只能将介绍性通知与IntroductionAdvisor一起使用,它具有以下方法:spring-doc.cadn.net.cn

public interface IntroductionAdvisor extends Advisor, IntroductionInfo {

	ClassFilter getClassFilter();

	void validateInterfaces() throws IllegalArgumentException;
}

public interface IntroductionInfo {

	Class<?>[] getInterfaces();
}

没有 MethodMatcher 和,因此,没有 Pointcut 与介绍建议相关联。只有类过滤是合理的。spring-doc.cadn.net.cn

The getInterfaces() 方法返回由这个顾问引入的接口。spring-doc.cadn.net.cn

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

这说明了一个混入。我们希望能够将被建议的对象转换为Lockable, 无论它们的类型是什么,并调用锁定和解锁方法。如果我们调用lock()方法,我们 希望所有设置器方法都抛出一个LockedException。因此,我们可以添加一个切面, 提供使对象不可变的能力,而无需对象对此有任何了解: 这是AOP的一个很好的例子。spring-doc.cadn.net.cn

首先,我们需要一个 IntroductionInterceptor 来完成主要工作。在这种情况下,我们扩展了 org.springframework.aop.support.DelegatingIntroductionInterceptor 辅助类。我们可以直接实现 IntroductionInterceptor,但使用 DelegatingIntroductionInterceptor 对于大多数情况是最好的。spring-doc.cadn.net.cn

The DelegatingIntroductionInterceptor 是为了解决一个介绍到实际实现的引入接口的问题,隐藏了使用拦截来实现这一点。你可以通过构造函数参数设置委托对象。默认的委托(当使用无参构造函数时)是 this。因此,在下一个示例中,委托是 LockMixin 类的子类 DelegatingIntroductionInterceptor。 给定一个委托(默认情况下,它本身),一个 DelegatingIntroductionInterceptor 实例会查找委托实现的所有接口(除了 IntroductionInterceptor 之外)并支持针对其中任何一个接口的引入。像 LockMixin 这样的子类可以调用 suppressInterface(Class intf) 方法来抑制不应该暴露的接口。但是,无论一个 IntroductionInterceptor 准备支持多少个接口,IntroductionAdvisor 的使用控制了哪些接口实际上被暴露。引入的接口隐藏了目标对相同接口的任何实现。spring-doc.cadn.net.cn

因此,LockMixin 扩展了 DelegatingIntroductionInterceptor 并实现了 Lockable 本身。超类会自动接收 Lockable 可以被支持用于介绍,所以我们不需要指定这一点。我们可以通过这种方式引入任意数量的接口。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() 方法。The 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)

我们可以非常简单地应用这个通知者,因为它不需要任何配置。(然而,没有IntroductionAdvisor就不可能使用IntroductionInterceptor)。像往常一样,在介绍中,通知者必须是每个实例的,因为它是有状态的。我们需要为每个被通知的对象提供一个不同的LockMixinAdvisor实例,因此也需要不同的LockMixin。通知器构成了被通知对象状态的一部分。spring-doc.cadn.net.cn

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