跳到主要内容

Spring 中的建议 API

ChatGPT-4o-mini 中英对照 Advice API in Spring

现在我们可以研究 Spring AOP 如何处理通知。

建议生命周期

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

每类建议最常被使用。它适用于通用建议,例如事务顾问。这些不依赖于代理对象的状态,也不添加新的状态。它们仅仅作用于方法和参数。

每个实例的建议适用于引入,以支持混入。在这种情况下,建议为代理对象添加状态。

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

Spring 中的通知类型

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

环绕通知

在 Spring 中,最基本的建议类型是 环绕通知

Spring 遵循 AOP Alliance 接口,用于使用方法拦截的环绕通知。因此,实现环绕通知的类应实现以下来自 org.aopalliance.intercept 包的 MethodInterceptor 接口:

public interface MethodInterceptor extends Interceptor {

Object invoke(MethodInvocation invocation) throws Throwable;
}
java

invoke() 方法的 MethodInvocation 参数暴露了被调用的方法、目标连接点、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;
}
}
java

注意对 MethodInvocationproceed() 方法的调用。这会沿着拦截器链向下推进,直到连接点。大多数拦截器都会调用这个方法并返回其返回值。然而,像任何环绕通知一样,MethodInterceptor 可以返回一个不同的值或抛出一个异常,而不是调用 proceed 方法。然而,您不想在没有充分理由的情况下这样做。

备注

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

前置通知

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

before advice 的主要优点是无需调用 proceed() 方法,因此没有意外未能继续执行拦截器链的可能性。

以下列表显示了 MethodBeforeAdvice 接口:

public interface MethodBeforeAdvice extends BeforeAdvice {

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

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

以下示例展示了 Spring 中的一个前置通知,它统计所有方法调用:

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

在任何切点之前可以使用建议。

Throws Advice

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

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

只有最后一个参数是必需的。方法签名可以有一个或四个参数,这取决于建议方法是否对方法和参数感兴趣。接下来的两个列表展示了作为 throws 建议的类的示例。

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

public class RemoteThrowsAdvice implements ThrowsAdvice {

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

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

public class ServletThrowsAdviceWithArguments implements ThrowsAdvice {

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

最后的示例说明了这两种方法如何在一个处理 RemoteExceptionServletException 的单一类中使用。可以在一个类中组合任意数量的抛出建议方法。以下列表展示了最终示例:

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

如果一个 throws-advice 方法自身抛出异常,它会覆盖原始异常(即,它会改变抛给用户的异常)。覆盖的异常通常是 RuntimeException,这与任何方法签名都是兼容的。然而,如果一个 throws-advice 方法抛出一个已检查异常,它必须与目标方法声明的异常匹配,因此在某种程度上与特定的目标方法签名耦合。不要抛出与目标方法签名不兼容的未声明已检查异常!

提示

抛出建议可以与任何切入点一起使用。

返回后通知

在 Spring 中,一个 后置返回通知 必须实现 org.springframework.aop.AfterReturningAdvice 接口,以下列表展示了这一点:

public interface AfterReturningAdvice extends Advice {

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

一个返回后的建议可以访问返回值(它无法修改)、被调用的方法、方法的参数和目标。

以下内容在返回建议后计算所有未抛出异常的成功方法调用次数:

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

这个建议不会改变执行路径。如果抛出异常,它会沿着拦截器链向上抛出,而不是返回值。

提示

返回的建议可以与任何切入点一起使用。

引言建议

Spring 将 引介建议 视为一种特殊类型的拦截建议。

引言需要一个 IntroductionAdvisor 和一个 IntroductionInterceptor,它们实现以下接口:

public interface IntroductionInterceptor extends MethodInterceptor {

boolean implementsInterface(Class intf);
}
java

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

引介通知不能与任何切入点一起使用,因为它仅适用于类级别,而不是方法级别。您只能使用引介通知与 IntroductionAdvisor,它具有以下方法:

public interface IntroductionAdvisor extends Advisor, IntroductionInfo {

ClassFilter getClassFilter();

void validateInterfaces() throws IllegalArgumentException;
}

public interface IntroductionInfo {

Class<?>[] getInterfaces();
}
java

没有 MethodMatcher,因此也没有与引介通知相关的 Pointcut。仅类过滤是合乎逻辑的。

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

validateInterfaces() 方法在内部用于检查引入的接口是否可以被配置的 IntroductionInterceptor 实现。

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

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

这说明了一个混入。我们希望能够将被建议的对象转换为 Lockable,无论它们的类型如何,并调用锁定和解锁方法。如果我们调用 lock() 方法,我们希望所有的设置方法抛出 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 类的示例:

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

}
java

通常,您无需重写 invoke() 方法。DelegatingIntroductionInterceptor 实现(如果方法被引入则调用 delegate 方法,否则继续向连接点推进)通常足够。在当前情况下,我们需要添加一个检查:如果处于锁定模式,则不能调用任何 setter 方法。

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

public class LockMixinAdvisor extends DefaultIntroductionAdvisor {

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

我们可以非常简单地应用这个顾问,因为它不需要任何配置。 (然而,使用 IntroductionInterceptor 是不可能的,除非有 IntroductionAdvisor。) 与引入一样,顾问必须是每个实例的,因为它是有状态的。我们需要为每个被建议的对象提供一个不同的 LockMixinAdvisor 实例,因此也需要不同的 LockMixin 实例。顾问构成了被建议对象状态的一部分。

我们可以通过使用 Advised.addAdvisor() 方法或(推荐的方式)在 XML 配置中以任何其他顾问的方式程序化地应用此顾问。下面讨论的所有代理创建选择,包括“自动代理创建者”,都正确处理引入和有状态混合。