跳到主要内容
版本:7.0.3

Spring中的Advice API

Hunyuan 7b 中英对照 Advice API in Spring

现在我们可以研究Spring AOP是如何处理通知(advice)的。

建议生命周期

每个建议(advice)都相当于一个Spring Bean。一个建议实例可以在所有被建议的对象(advised objects)之间共享,也可以为每个被建议的对象单独设置。这分别对应于“针对每个类”的(per-class)和“针对每个实例”的(per-instance)建议。

最常使用的是针对每个类的建议(per-class advice)。这类建议适用于通用场景,例如事务顾问(transaction advisors)。它们不依赖于被代理对象的状态,也不会添加新的状态。它们仅仅是作用于方法及其参数上。

针对每个实例的建议适用于引入(introductions),以支持混入(mixins)。在这种情况下,该建议会为被代理对象(proxied object)添加状态(state)。

你可以在同一个AOP代理中同时使用共享建议(shared advice)和每个实例特有的建议(per-instance advice)。

Spring中的建议类型

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

围绕建议的拦截

在Spring中,最基础的建议(advice)类型是围绕建议的拦截(interception around advice)

Spring遵循AOP Alliance接口中关于使用方法拦截(method interception)的规范。因此,实现围绕通知(around advice)的类应实现org.aopalliance.intercept包中的以下MethodInterceptor接口:

public interface MethodInterceptor extends Interceptor {

Object invoke(MethodInvocation invocation) throws Throwable;
}

invoke() 方法的 MethodInvocation 参数会暴露被调用的方法、目标连接点(join point)、AOP 代理以及该方法的参数。invoke() 方法应当返回调用结果:通常是连接点的返回值。

以下示例展示了一个简单的 MethodInterceptor 实现:

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

请注意这里调用了MethodInvocationproceed()方法。该方法会沿着拦截器链向下传递,直至到达连接点(join point)。大多数拦截器都会调用此方法并返回其返回值。然而,像MethodInterceptor这样的拦截器(就像其他类型的环绕建议(around advice)一样),可以返回一个不同的值或抛出异常,而无需调用proceed方法。不过,如果没有充分的理由,你不应该这样做。

备注

MethodInterceptor 的实现提供了与其他符合 AOP Alliance 标准的 AOP 实现的互操作性。本节余下部分讨论的其他通知(advice)类型也实现了常见的 AOP 概念,但采用的是 Spring 特有的方式。虽然使用最具体的通知类型有一定的优势,但如果你可能希望在其他 AOP 框架中运行该切面(aspect),那么还是建议继续使用 MethodInterceptor。需要注意的是,目前不同框架之间的切入点(pointcuts)并不具备互操作性,而且 AOP Alliance 也尚未定义切入点接口。

使用前的建议

一种更简单的建议类型是“方法执行前建议”(before advice)。这种建议不需要MethodInvocation对象,因为它仅在进入方法之前被调用。

在事前建议(before advice)中的主要优势在于,无需调用proceed()方法,因此也就不存在无意中未能继续执行拦截器链(interceptor chain)的情况。

以下列表显示了MethodBeforeAdvice接口:

public interface MethodBeforeAdvice extends BeforeAdvice {

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

请注意,返回类型是void。在连接点(join point)执行之前,前置顾问(before advice)可以插入自定义行为,但无法更改返回值。如果前置顾问抛出异常,将停止拦截器链(interceptor chain)的进一步执行。该异常会沿拦截器链向上传播。如果该异常未被捕获(unchecked),或者发生在被调用方法的签名部分(signature of the invoked method),则会直接传递给客户端。否则,AOP代理会将该异常包装成一个未被捕获的异常(unchecked exception)。

以下示例展示了Spring中的一个“建议”(before advice),该“建议”会统计所有的方法调用次数:

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;
}
}
提示

在任何切点(pointcut)中使用建议(advice)之前,请先进行相关配置。

抛出异常建议

如果在连接点(join point)执行过程中抛出了异常,在该连接点返回后会调用ThrowsAdvice。Spring提供了类型化的throws建议(typed throws advice)。需要注意的是,这意味着org.springframework.aop.ThrowsAdvice接口不包含任何方法。它只是一个标记接口,用于标识给定的对象实现了一个或多个类型化的throws建议方法。这些方法的格式应如下:

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

只需要最后一个参数。方法签名可以有一个或四个参数,这取决于建议方法(advice method)是否关心该方法及其参数。接下来的两个示例展示了属于“throws建议”(throws advice)的类示例。

如果抛出RemoteException(包括RemoteException的子类),则会采用以下建议:

public class RemoteThrowsAdvice implements ThrowsAdvice {

public void afterThrowing(RemoteException ex) throws Throwable {
// Do something with remote exception
}
}

与前一条建议不同,下一个示例声明了四个参数,这样它就可以访问被调用的方法、方法参数以及目标对象。如果抛出了ServletException,则会执行以下提示:

public class ServletThrowsAdviceWithArguments implements ThrowsAdvice {

public void afterThrowing(Method m, Object[] args, Object target, ServletException ex) {
// Do something with all arguments
}
}

最后一个示例说明了如何在一个类中同时使用这两种方法来处理RemoteExceptionServletException。可以在一个类中组合任意数量的抛出建议(throws advice)方法。以下是该示例的代码:

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

如果一个提供“throws”建议的方法本身抛出了异常,那么它就覆盖了原始的异常(即,它改变了向用户抛出的异常类型)。这种被覆盖的异常通常是一个RuntimeException,它可以与任何方法签名兼容。然而,如果一个提供“throws”建议的方法抛出了一个受检异常(checked exception),那么这个异常必须与目标方法声明的异常类型相匹配,因此在某种程度上,该方法会依赖于特定的目标方法签名。切勿抛出与目标方法签名不兼容的未声明受检异常!

提示

Throws advice 可以与任何切点(pointcut)一起使用。

返回后的建议

在Spring中,一个*返回后通知(after returning advice)*必须实现org.springframework.aop.AfterReturningAdvice接口,以下示例展示了这一点:

public interface AfterReturningAdvice extends Advice {

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

返回后的代码可以访问返回值(但不能对其进行修改)、被调用的方法、方法的参数以及目标对象。

以下返回后的建议统计了所有没有抛出异常的成功方法调用:

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

这条建议不会改变执行路径。如果它抛出异常,那么异常会沿着拦截器链向上抛出,而不是作为返回值被传递。

提示

返回后的建议可以用于任何切点(pointcut)。

介绍建议

Spring将*引入建议(introduction advice)*视为一种特殊的拦截建议(interception advice)。

介绍功能需要一个IntroductionAdvisor和一个IntroductionInterceptor,它们需要实现以下接口:

public interface IntroductionInterceptor extends MethodInterceptor {

boolean implementsInterface(Class intf);
}

从AOP Alliance的MethodInterceptor接口继承的invoke()方法必须实现引入(introduction)功能。也就是说,如果被调用的方法位于一个被引入的接口上,那么引入拦截器(introduction interceptor)负责处理该方法调用——它不能调用proceed()方法。

引入建议(Introduction advice)不能与任何切点(pointcut)一起使用,因为它仅适用于类级别,而不适用于方法级别。你只能与IntroductionAdvisor一起使用引入建议,IntroductionAdvisor具有以下方法:

public interface IntroductionAdvisor extends Advisor, IntroductionInfo {

ClassFilter getClassFilter();

void validateInterfaces() throws IllegalArgumentException;
}

public interface IntroductionInfo {

Class<?>[] getInterfaces();
}

没有与“引入建议”(introduction advice)相关联的MethodMatcher,因此也就没有与之关联的Pointcut。只有类过滤(class filtering)是合理的。

getInterfaces() 方法返回此顾问引入的接口。

validateInterfaces() 方法在内部被用来检查引入的接口是否能够被配置好的 IntroductionInterceptor 实现。

考虑一个来自Spring测试套件的例子,假设我们想向一个或多个对象引入以下接口:

public interface Lockable {
void lock();
void unlock();
boolean locked();
}

这说明了什么是混入(mixin)。我们希望能够将任何类型的对象转换为Lockable类型,并调用它们的锁定(lock)和解锁(unlock)方法。如果我们调用lock()方法,我们希望所有的设置器(setter)方法都能抛出LockedException异常。因此,我们可以添加一个方面(aspect),使得对象能够在不知道这一点的情形下变得不可修改:这是面向切面编程(AOP)的一个很好的例子。

首先,我们需要一个IntroductionInterceptor来承担主要的任务。在这种情况下,我们继承了org.springframework.aop.support.DelegatingIntroductionInterceptor这个便捷类。我们也可以直接实现IntroductionInterceptor,但在大多数情况下,使用DelegatingIntroductionInterceptor更为合适。

DelegatingIntroductionInterceptor 的设计目的是将接口的引入任务委托给实际实现该接口的对象,从而隐藏了使用拦截(interception)来实现这一功能的过程。您可以通过构造函数参数来设置委托对象。当使用无参数构造函数时,默认的委托对象是 this。因此,在下一个示例中,委托对象就是 DelegatingIntroductionInterceptor 的子类 LockMixin。给定一个委托对象(默认情况下就是 this),DelegatingIntroductionInterceptor 实例会查找该委托对象实现的所有接口(除了 IntroductionInterceptor 本身),并支持对这些接口的引入操作。像 LockMixin 这样的子类可以调用 suppressInterface(Class intf) 方法来抑制不应被暴露的接口。然而,无论 IntroductionInterceptor 准备支持多少个接口,实际哪些接口会被暴露还是由所使用的 IntroductionAdvisor 来控制。被引入的接口会隐藏目标对象中对该相同接口的任何实现。

因此,LockMixin 继承了 DelegatingIntroductionInterceptor 并且自身实现了 Lockable 接口。超类会自动识别出该对象支持 Lockable,所以我们无需额外指定这一点。我们可以通过这种方式引入任意数量的接口。

请注意locked实例变量的使用。这实际上为目标对象所持有的状态添加了额外的状态信息。

以下示例展示了LockMixin类的用法:

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

通常,你无需覆盖invoke()方法。DelegatingIntroductionInterceptor的实现(如果该方法被引入,则调用delegate方法,否则直接进入连接点)通常就足够了。在当前情况下,我们需要添加一个检查:如果处于锁定模式,就不能调用任何setter方法。

所需的引入口只需要持有一个唯一的LockMixin实例,并指定引入的接口(在这个例子中,只有Lockable)。一个更复杂的例子可能需要引用引入口拦截器(该拦截器将被定义为原型)。在这个例子中,没有与LockMixin相关的配置,所以我们使用new来创建它。以下示例展示了我们的LockMixinAdvisor类:

public class LockMixinAdvisor extends DefaultIntroductionAdvisor {

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

我们可以非常简单地应用这个顾问(advisor),因为它不需要任何配置。(不过,如果没有IntroductionAdvisor,就无法使用IntroductionInterceptor。)与通常的引入(introduction)机制一样,由于该顾问具有状态性(stateful),因此必须为每个实例(instance)单独配置该顾问。对于每个需要被管理的对象(advised object),我们都需要一个不同的LockMixinAdvisor实例,进而也需要一个不同的LockMixin实例。这个顾问实际上是该被管理对象状态(state)的一部分。

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