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

Advice 生命周期

每个 advice 都是一个 Spring Bean。一个 advice 实例可以在所有 advice 对象中共享,也可以对每个 advice 对象是唯一的。这对应于每类或每实例的 advcie。

每个类的 advice:是最经常使用的。它适合于通用 advice,如事务管理。这些 advice 不依赖于被代理对象的状态或添加新的状态。他们只是对方法和参数采取行动。

每个实例 advice:适合于 introductions(引入?),以支持混搭。在这种情况下,建议将状态添加到被代理对象中。

你可以在同一个 AOP 代理中混合使用每类和每实例 advice。

Spring 的 Advice 类型

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

Spring 符合 AOP 联盟(Alliance )对使用方法拦截的 环绕 advice 的接口。实现 MethodInterceptor 和实现 环绕 advice 的类也应该实现以下接口:

  1. public interface MethodInterceptor extends Interceptor {
  2. Object invoke(MethodInvocation invocation) throws Throwable;
  3. }

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

下面的例子显示了一个简单的 MethodInterceptor 实现:

  1. public class DebugInterceptor implements MethodInterceptor {
  2. public Object invoke(MethodInvocation invocation) throws Throwable {
  3. System.out.println("Before: invocation=[" + invocation + "]");
  4. Object rval = invocation.proceed();
  5. System.out.println("Invocation returned");
  6. return rval;
  7. }
  8. }

注意对 MethodInvocation 的 proceed()方法的调用。这是在拦截器链中向连接点进行的。大多数拦截器调用这个方法并返回其返回值。然而,MethodInterceptor 和环绕的任何 advice 一样,可以返回一个不同的值或抛出一个异常,而不是调用 proceed 方法。然而,如果没有充分的理由,你不需要这样做。

:::info MethodInterceptor 的实现提供了与其他符合 AOP 联盟的 AOP 实现的互操作性。本节其余部分讨论的其他 advice 类型实现了常见的 AOP 概念,但以 Spring 特有的方式实现。虽然使用最具体的 advice 类型有优势,但如果你可能想在另一个 AOP 框架中运行该 aspect(切面),请坚持使用围绕 advice 的 MethodInterceptor。需要注意的是,目前各框架之间的 pointcuts 并不具有互操作性,而且 AOP 联盟目前也没有定义 pointcuts 接口。 :::

Before Advice / 前置通知

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

Before advice 的主要优点是不需要调用 proceed()方法,因此也就不可能在不经意的情况下无法顺着拦截器链进行。

下面是 MethodBeforeAdvice 接口:

  1. /**
  2. * 在调用方法之前调用的通知。此类 advice 不能阻止方法调用继续进行,除非它们抛出 Throwable。
  3. */
  4. public interface MethodBeforeAdvice extends BeforeAdvice {
  5. /**
  6. * 调用给定方法之前的回调
  7. * @param method 被调用的方法
  8. * @param args 方法的参数
  9. * @param target 方法调用的目标。可能为 null
  10. * @throws Throwable 如果此对象希望中止调用。如果方法签名允许,任何抛出的异常都将返回给调用者。否则异常将被包装为运行时异常。
  11. */
  12. void before(Method m, Object[] args, Object target) throws Throwable;
  13. }

(Spring 的 API 设计将允许字段在 advice 之前,尽管通常的对象适用于字段拦截,Spring 不太可能实现它)。

注意,返回类型是无效的。Before advice 可以在连接点运行前插入自定义行为,但不能改变返回值。如果一个 before advice 抛出一个异常,它就会停止拦截器链的进一步执行。异常会在拦截器链上向后传播。如果它没有被选中或在被调用方法的签名上,它将直接传递给客户端。否则,它将被 AOP 代理包裹在一个未检查的异常中。

下面的例子显示了 Spring 中的 before advice,它计算了所有方法的调用:

  1. public class CountingBeforeAdvice implements MethodBeforeAdvice {
  2. private int count;
  3. public void before(Method m, Object[] args, Object target) throws Throwable {
  4. ++count;
  5. }
  6. public int getCount() {
  7. return count;
  8. }
  9. }

:::tips before advice,可以与任何 pointcut 一起使用。 :::

Throws Advice / 异常通知

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

  1. 用于 throws advice 的标签接口。
  2. 此接口上没有任何方法,因为方法是通过反射调用的。实现类必须实现以下形式的方法:
  3. void afterThrowing([Method, args, target], ThrowableSubclass);
  4. 一些有效方法的例子是:
  5. public void afterThrowing(Exception ex)
  6. public void afterThrowing(RemoteException)
  7. public void afterThrowing(Method method, Object[] args, Object target, Exception ex)
  8. public void afterThrowing(Method method, Object[] args, Object target, ServletException ex)
  9. 前三个参数是可选的,只有在我们想要关于连接点的更多信息时才有用,如 AspectJ after-throwing advice
  10. 注意:如果 throws-advice 方法本身抛出异常,它将覆盖原始异常(即更改向用户抛出的异常)。覆盖异常通常是 RuntimeException
  11. 这与任何方法签名兼容。然而,如果 throws-advice 方法抛出一个检查异常,它必须匹配目标方法声明的异常,
  12. 因此在某种程度上耦合到特定的目标方法签名。不要抛出与目标方法的签名不兼容的未声明的检查异常!
  13. public interface ThrowsAdvice extends AfterAdvice {
  14. }

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

如果抛出一个 RemoteException(包括来自子类的),将调用以下 advice:

  1. public class RemoteThrowsAdvice implements ThrowsAdvice {
  2. public void afterThrowing(RemoteException ex) throws Throwable {
  3. // Do something with remote exception
  4. }
  5. }

与前面的 advice 不同,下一个例子声明了四个参数,这样它就可以访问被调用的方法、方法参数和目标对象。如果抛出一个ServletException,下面的 advice 会被调用:

  1. public class ServletThrowsAdviceWithArguments implements ThrowsAdvice {
  2. public void afterThrowing(Method m, Object[] args, Object target, ServletException ex) {
  3. // Do something with all arguments
  4. }
  5. }

最后一个例子说明了如何在一个同时处理 RemoteException 和 ServletException 的单一类中使用这两种方法。任何数量的 throws advice 方法都可以在一个单一的类中被组合。下面的列表显示了最后的例子:

  1. public static class CombinedThrowsAdvice implements ThrowsAdvice {
  2. public void afterThrowing(RemoteException ex) throws Throwable {
  3. // Do something with remote exception
  4. }
  5. public void afterThrowing(Method m, Object[] args, Object target, ServletException ex) {
  6. // Do something with all arguments
  7. }
  8. }

:::info 如果一个 throws-advice 方法自己抛出了一个异常,它就会覆盖原来的异常(也就是说,它改变了抛给用户的异常)。覆盖的异常通常是一个 RuntimeException,它与任何方法的签名兼容。然而,如果一个 throws-advice 方法抛出一个被检查的异常,它必须与目标方法的声明异常相匹配,因此,在某种程度上与特定的目标方法签名相耦合。不要抛出一个与目标方法签名不兼容的未声明的检查性异常! :::

:::tips throws advice 可以与任 pointcut 一起使用。 :::

After Returning Advice / 正常返回通知

方法执行完成后,正常返回,不是抛出异常返回

Spring 中的返回 advice 必须实现 org.springframework.aop.AfterReturningAdvice接口,下面的列表显示了这一点:

  1. public interface AfterReturningAdvice extends Advice {
  2. // returnValue 目标方法的返回值,如果有的话则会有值
  3. void afterReturning(Object returnValue, Method m, Object[] args, Object target)
  4. throws Throwable;
  5. }

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

下面的返回 advice 统计了所有没有抛出异常的成功方法调用:

  1. public class CountingAfterReturningAdvice implements AfterReturningAdvice {
  2. private int count;
  3. public void afterReturning(Object returnValue, Method m, Object[] args, Object target)
  4. throws Throwable {
  5. ++count;
  6. }
  7. public int getCount() {
  8. return count;
  9. }
  10. }

这个 advice 不会改变执行路径。如果它抛出一个异常,它将被抛上拦截器链,而不是返回值。

:::tips after returning advice 可以与任 pointcut 一起使用。 :::

Introduction Advice / 引入通知?

Spring 将引入 advice 视为一种特殊的拦截 advice 。

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

  1. public interface IntroductionInterceptor extends MethodInterceptor {
  2. boolean implementsInterface(Class intf);
  3. }

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

引入建议不能被用于任何点切,因为它只适用于类,而不是方法层面。你只能在 IntroductionAdvisor 中使用引入通知,它有以下方法:

  1. public interface IntroductionAdvisor extends Advisor, IntroductionInfo {
  2. ClassFilter getClassFilter();
  3. void validateInterfaces() throws IllegalArgumentException;
  4. }
  5. public interface IntroductionInfo {
  6. Class<?>[] getInterfaces();
  7. }

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

getInterfaces()方法返回这个顾问所引入的接口。

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

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

  1. public interface Lockable {
  2. void lock();
  3. void unlock();
  4. boolean locked();
  5. }

这说明了一个混合器。我们希望能够将通知的对象转为 Lockable,无论其类型如何,并调用锁定和解锁方法。如果我们调用 lock()方法,我们希望所有的 setter方法都抛出一个 LockedException。因此,我们可以添加一个切面,提供使对象不可变的能力,而他们对此一无所知:这是 AOP 的一个好例子。

首先,我们需要一个 IntroductionInterceptor 来完成繁重的工作。在这种情况下,我们扩展org.springframework.aop.support.DelegatingIntroductionInterceptor便利类。我们可以直接实现 IntroductionInterceptor,但在大多数情况下,使用DelegatingIntroductionInterceptor是最好的。

DelegatingIntroductionInterceptor 被设计成将介绍委托给所介绍的接口的实际实现,隐蔽地使用拦截来做到这一点。你可以使用构造函数参数将委托设置为任何对象。默认的委托(当使用无参数构造函数时)是这个。因此,在下一个例子中,委托是DelegatingIntroductionInterceptor 的 LockMixin 子类。给定一个委托(默认情况下是它自己),DelegatingIntroductionInterceptor 实例寻 找由委托实现的所有接口(除 IntroductionInterceptor 外),并支持针对其中任何接口的引入。像 LockMixin 这样的子类可以调用suppressInterface(Class intf) 方法来抑制那些不应该被暴露的接口。然而,不管一个 IntroductionInterceptor 准备支持多少个接口,所使用的IntroductionAdvisor 控制着哪些接口被实际暴露。一个引入的接口隐藏了目标对同一接口的任何实现。

因此,LockMixin 扩展了 DelegatingIntroductionInterceptor 并实现了 Lockable 本身。超类会自动捕捉到 Lockable 可以被支持用于引入,所以我们不需要指定。我们可以用这种方式引入任何数量的接口。

注意锁定实例变量的使用。这有效地增加了目标对象中持有的额外状态。

下面的例子显示了 LockMixin 类的例子:

  1. public class LockMixin extends DelegatingIntroductionInterceptor implements Lockable {
  2. private boolean locked;
  3. public void lock() {
  4. this.locked = true;
  5. }
  6. public void unlock() {
  7. this.locked = false;
  8. }
  9. public boolean locked() {
  10. return this.locked;
  11. }
  12. public Object invoke(MethodInvocation invocation) throws Throwable {
  13. if (locked() && invocation.getMethod().getName().indexOf("set") == 0) {
  14. throw new LockedException();
  15. }
  16. return super.invoke(invocation);
  17. }
  18. }

通常,你不需要覆盖 invoke() 方法。通常,DelegatingIntroductionInterceptor 实现(如果方法被引入,它就调用委托方法,否则就向连接点前进)就足够了。在本例中,我们需要添加一个检查:如果处于锁定模式,则不能调用任何 setter 方法。

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

  1. public class LockMixinAdvisor extends DefaultIntroductionAdvisor {
  2. public LockMixinAdvisor() {
  3. super(new LockMixin(), Lockable.class);
  4. }
  5. }

我们可以非常简单地应用这个顾问,因为它不需要配置。(然而,没有 IntroductionAdvisor 就不可能使用 IntroductionInterceptor)。像通常的介绍一样,顾问必须是按实例的,因为它是有状态的。我们需要一个不同的 LockMixinAdvisor 实例,从而为每个被通知的对象提供 LockMixin 。顾问包括被通知对象的部分状态。

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