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 接口兼容,用于采用方法拦截的环绕通知。因此,实现环绕通知的类应实现以下来自 org.aopalliance.intercept 包的 MethodInterceptor 接口:spring-doc.cadn.net.cn

public interface MethodInterceptor extends Interceptor {

	Object invoke(MethodInvocation invocation) throws Throwable;
}

invoke()方法传递的MethodInvocation参数暴露了正在调用的方法、目标连接点、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 result = invocation.proceed();
		System.out.println("Invocation returned");
		return result;
	}
}
class DebugInterceptor : MethodInterceptor {

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

请注意对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;
}

请注意返回类型是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提供了类型化的异常通知。请注意,这意味着 org.springframework.aop.ThrowsAdvice 接口不包含任何方法。它是一个标记接口,用于标识给定对象实现了一个或多个类型化的异常通知方法。这些方法应采用以下形式:spring-doc.cadn.net.cn

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

仅最后一个参数是必需的。方法签名可能有一个或四个参数,这取决于通知方法是否对方法和参数感兴趣。接下来的两个列表显示了作为抛出异常通知示例的类。spring-doc.cadn.net.cn

如果抛出RemoteException(包括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中,一个后置返回通知必须实现 Ordered接口,如下代码所示: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