7.3. Spring的通知API

现在让我们看一下SPring AOP是怎样处理通知的。

7.3.1. 通知的生命周期

每个通知都是一个Spring bean。一个通知实例既可以被所有被通知的对象共享,也可以被每个被通知对象独占。 这根据设置类共享(per-class)基于实例(per-instance)的参数来决定。

类共享通知经常会被用到。它很适合用作通用的通知例如事务advisor。这些advisor不依赖于代理对象的状态也不会向代理对象添加新的状态;它们仅仅在方法和参数上起作用。

基于实例的通知很适合用作导入器来支持混合类型。在这种情况下,通知向代理对象添加状态。

在同一个AOP代理里混合使用类共享和基于实例的通知是可能的。

7.3.2. Spring里的通知类型

Spring提供了多种开箱即用的通知类型,而且它们也可以被扩展来支持任何通知类型。让我们先看看基本概念和标准的通知类型。

7.3.2.1. 拦截around通知

在Spring中最基础的通知类型是拦截around通知

Spring里使用方法拦截的around通知兼容AOP联盟接口。实现around通知的MethodInterceptor应当实现下面的接口:

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

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

一个简单的MethodInterceptor实现看起来像下面这样:

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

注意对MethodInvocation中proceed()方法的调用。这个方法 继续运行指向连接点的拦截器链并返回proceed()的结果。然而,一个类似任何环绕通知的MethodInterceptor, 可以返回一个不同的值或者抛出一个异常而不是调用proceed方法。但除非你有很好的理由,否则不要考虑这样做!

注意

MethodInterceptor提供了与其它AOP联盟兼容实现的互操作性。本节的剩下部分将讨论其它的通知类型,它们实现了通用的AOP概念, 但是以一种Spring风格的方式来实现的。使用最通用的通知类型还有一个好处,固定使用MethodInterceptor 环绕通知可以让你在其它的AOP框架里 运行你所定制的方面。要注意现在切入点还不能和其它框架进行互操作,AOP联盟目前还没有定义切入点接口。

7.3.2.2. 前置通知

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

前置通知(before advice)的一个主要优点是它不需要调用proceed() 方法,因此就不会发生 无意间运行拦截器链失败的情况。

MethodBeforeAdvice 接口被显示在下面。(Spring的API设计能够为类中的成员变量提供前置通知,虽然这可以把通用对象应用到成员变量拦截上,但看起来Spring并不打算实现这个功能。)

public interface MethodBeforeAdvice extends BeforeAdvice {

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

注意返回值的类型是void。前置通知可以在连接点执行之前插入自定义行为,但是不能修改连接点的返回值。如果一个前置通知抛出异常,这将中止拦截器链的进一步执行。 异常将沿着拦截器链向回传播。如果异常是非强制检查的(unchecked)或者已经被包含在被调用方法的签名中(译者:即出现在方法声明的throws子句中),它将被直接返回给客户端; 否则它将由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; 
    }
}

提示

前置通知可以和任何切入点一起使用。

7.3.2.3. 异常通知

如果连接点抛出异常,异常通知(throws advice)将在连接点返回后被调用。 Spring提供类型检查的异常通知,这意味着org.springframework.aop.ThrowsAdvice接口不包含任何方法:它只是一个标记接口用来标识 所给对象实现了一个或者多个针对特定类型的异常通知方法。这些方法应当满足下面的格式

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

只有最后一个参数是必须的。因此异常通知方法对方法及参数的需求,方法的签名将从一到四个参数之间变化。 下面是一些throws通知的例子。

当一个RemoteException(包括它的子类)被抛出时,下面的通知会被调用:

public class RemoteThrowsAdvice implements ThrowsAdvice {

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

当一个ServletException被抛出,下面的通知将被调用。 和上面的通知不同,它声明了4个参数,因此它可以访问被调用的方法,方法的参数以及目标对象:

public class ServletThrowsAdviceWithArguments implements ThrowsAdvice {

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

最后一个例子说明怎样在同一个类里使用两个方法来处理 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 will all arguments
    }
}

提示

异常通知可以和任何切入点一起使用。

7.3.2.4. 后置通知

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

这个通知不改变执行路线。如果通知抛出异常,异常将沿着拦截器链返回(抛出)而不是返回被调用方法的执行结果。

提示

后置通知可以和任何切入点一起使用。

7.3.2.5. 引入通知

Spring 把引入通知(introduction advice)作为一种特殊的拦截通知进行处理。

引入通知需要一个IntroductionAdvisor, 和一个IntroductionInterceptor, 后者实现下面的接口:

public interface IntroductionInterceptor extends MethodInterceptor {

    boolean implementsInterface(Class intf);
}

invoke() 方法,继承了AOP联盟MethodInterceptor 接口,必须确保实现引入: 这里的意思是说,如果被调用的方法位于一个已经被引入接口里,这个引入拦截器将负责完成对这个方法的调用--因为后者不能调用proceed()方法。

引入通知不能和任何切入点一起使用,因为它是应用在类级别而不是方法级别。 你可以通过IntroductionAdvisor来使用引入通知,这个接口包括下面的方法:

public interface IntroductionAdvisor extends Advisor, IntroductionInfo {

	ClassFilter getClassFilter();

	void validateInterfaces() throws IllegalArgumentException;
}

public interface IntroductionInfo {

	Class[] getInterfaces();
}

这里没有MethodMatcher接口,因此也就没有 Pointcut接口与引入通知相关联。这里只进行类过滤。

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

validateInterfaces()方法将被内部使用来查看被引入的接口是否能够由配置的IntroductionInterceptor来实现。

让我们看看从Spring测试集里拿来的一个简单例子。让我们假设我们希望把下面的接口引入给一个或者多个对象:

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

这里描述了一个混合类型。我们希望不论原本对象是什么类型,都把这个被通知对象转换为Lockable接口并可以调用lock 和unlock 方法。 如果我们调用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接口并提供引入支持,因此我们不需要配置它。用这种方法我们能够介绍任意数量的接口。

注意locked实例变量的用法。这有效地向目标对象增加了额外状态。

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里面已经包含了一个实现--如果一个方法被引入,这个实现将调用实际的托管方法,否则它将直接处理连接点--通常这已经足够了。在当前这个例子里,我们需要增加一个检查:如果处于加锁(locked)状态,没有setter方法可以被调用。

引入处理器的要求是很简单的。它的全部要求只是保持一个特定的LockMixin实例, 并说明被通知的接口--在这个例子里,只有一个Lockable接口。 一个更复杂的例子也许会获取一个介绍拦截器的引用(后者可以被定义为一个prototype): 在这种情况下,不需要对LockMixin进行相关配置,因此我们可以简单的用new关键字来创建它。

public class LockMixinAdvisor extends DefaultIntroductionAdvisor {

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

我们可以很容易应用这个advisor:它不需要配置。(然而,下面必须记住的:不可以在没有IntroductionAdvisor的情况下使用IntroductionInterceptor。) 对于通常的引入advisor必须是基于实例的,因为它是有状态的。因此,对于每个被通知对象我们需要一个不同 实例的LockMixinAdvisorLockMixin。这种情况下advisor保存了被通知对象的部分状态。

我们能够通过使用Advised.addAdvisor() 的编程方式来应用advisor,或者像其它advisor那样(也是推荐的方式)在XML里进行配置。全部的代理创建选择(包括“自动代理创建器”)将在下面进行讨论, 看看如何正确地处理introduction和有状态混合类型。