Spring AOP API详解

上一章介绍了Spring对AOP的支持,包括@AspectJ和基于schema的切面定义。在这一章中,我们将讨论低级别的Spring AOP API。对于普通的应用,我们推荐使用前一章中描述的带有AspectJ pointcuts 的Spring AOP。

6.1. Spring 中的 Pointcut API

这一节描述了Spring如何处理关键的 pointcut 概念。

6.1.1. 概念

Spring的pointcut模型使pointcut重用与advice类型无关。你可以用相同的pointcut来针对不同的advice。

org.springframework.aop.Pointcut 接口是中心接口,用于为特定的类和方法提供advice。完整的接口如下。

public interface Pointcut {

    ClassFilter getClassFilter();

    MethodMatcher getMethodMatcher();
}

将 Pointcut 接口分成两部分,允许重用类和方法匹配部分以及细粒度的组合操作(比如与另一个方法匹配器进行 "联合")。

ClassFilter 接口被用来将该pointcut限制在一组给定的目标类中。如果 matches() 方法总是返回 true,所有的目标类都被匹配。下面的列表显示了 ClassFilter 接口的定义。

public interface ClassFilter {

    boolean matches(Class clazz);
}

MethodMatcher 接口通常更为重要。完整的接口:

public interface MethodMatcher {

    boolean matches(Method m, Class targetClass);

    boolean isRuntime();

    boolean matches(Method m, Class targetClass, Object... args);
}

matches(Method, Class) 方法用来测试这个pointcut是否曾经匹配过目标类上的一个给定的方法。这个评估可以在创建AOP代理时进行,以避免在每个方法调用时进行测试。如果双参数 matches 方法对一个给定的方法返回 true,并且 MethodMatcher 的 isRuntime() 方法返回 true,那么三参数 matches 方法将在每个方法调用中被调用。这让一个pointcut在目标 advice 开始之前立即查看传递给方法调用的参数。

大多数 MethodMatcher 实现是静态的,这意味着它们的 isRuntime() 方法返回 false。在这种情况下,三参数 matches 方法从未被调用。

如果可能的话,尽量使 pointcuts 成为静态的,允许AOP框架在创建AOP代理时缓存 pointcuts 评估的结果。

6.1.2. 对 Pointcut 的操作

Spring支持对 pointcut 的操作(尤其是 union 和 intersection)。

Union指的是任何一个pointcut匹配的方法。Intersection指的是两个pointcut都匹配的方法。Union通常更有用。你可以通过使用 org.springframework.aop.support.Pointcuts 类中的静态方法或通过使用同一包中的 ComposablePointcut 类来组合pointcuts。然而,使用AspectJ的pointcut表达式通常是一种更简单的方法。

6.1.3. AspectJ 表达式的 Pointcuts

从2.0开始,Spring使用的最重要的pointcut类型是 org.springframework.aop.aspectj.AspectJExpressionPointcut。这是一个使用AspectJ提供的库来解析AspectJ pointcut表达式字符串的pointcut。

关于支持的 AspectJ pointcut primitives 的讨论见 前一章。

6.1.4. 方便的 Pointcut 实现

Spring提供了几种方便的pointcut实现。你可以直接使用其中的一些;其他的则打算在特定于应用的 pointcut 进行子类化。

静态 Pointcut

静态pointcut是基于方法和目标类的,不能考虑到方法的参数。对于大多数的使用来说,静态的pointcut已经足够了,而且是最好的。Spring只能在一个方法第一次被调用时评估一次静态的pointcut。在那之后,就不需要在每次调用方法时再次评估该pointcut了。

本节的其余部分描述了Spring中包含的一些静态pointcut实现。

正则表达式 Pointcut

一个明显的指定静态pointcut的方法是正则表达式。org.springframework.aop.support.JdkRegexpMethodPointcut 是一个通用的正则表达式 pointcut,它使用JDK中的正则表达式支持。

通过 JdkRegexpMethodPointcut 类,你可以提供一个pattern字符串的列表。如果其中任何一个是匹配的,那么这个pointcut就会被评估为 true。(因此,产生的 pointcut 实际上是指定 pattern 的 union。)

下面的例子显示了如何使用 JdkRegexpMethodPointcut。


    
        
            .*set.*
            .*absquatulate
        
    

Spring提供了一个名为 RegexpMethodPointcutAdvisor 的方便类,它让我们也可以引用一个 Advice(记住,一个 Advice 可以是一个拦截器、before advice、throws advice以及其他)。在幕后,Spring使用 JdkRegexpMethodPointcut。使用 RegexpMethodPointcutAdvisor 可以简化装配,因为这个bean同时封装了 Pointcut 和 Advice,正如下面的例子所示。


    
        
    
    
        
            .*set.*
            .*absquatulate
        
    

你可以将 RegexpMethodPointcutAdvisor 用于任何 Advice 类型。

属性驱动 Pointcuts

静态pointcut的一个重要类型是元数据驱动的pointcut。它使用元数据属性的值(通常是源级元数据)。

动态 pointcuts

动态 pointcut 比静态 pointcut 的评估成本更高。它们考虑到了方法的参数以及静态信息。这意味着它们必须在每次调用方法时被评估,而且结果不能被缓存,因为参数会变化。

主要的例子是 control flow pointcut。

Control Flow Pointcut

Spring control flow pointcut 在概念上与AspectJ的 cflow pointcut相似,但功能较弱。(目前还没有办法指定一个pointcut在另一个pointcut所匹配的连接点下面运行)。一个 control flow pointcut 与当前的调用栈相匹配。例如,如果连接点被 com.mycompany.web 包中的方法或 SomeCaller 类所调用,它就会启动。Control flow pointcut 是通过使用 org.springframework.aop.support.ControlFlowPointcut 类指定的。

Control flow pointcut 在运行时的评估成本甚至比其他动态pointcut高得多。在Java 1.4中,其成本大约是其他动态pointcut的五倍。

6.1.5. Pointcut 超类

Spring提供了有用的 pointcut 超类来帮助你实现你自己的pointcut。

因为静态pointcut是最有用的,所以你可能应该子类化 StaticMethodMatcherPointcut。这只需要实现一个抽象方法(尽管你可以覆盖其他方法来定制行为)。下面的例子展示了如何子类化 StaticMethodMatcherPointcut。

Java

class TestStaticPointcut extends StaticMethodMatcherPointcut {

    public boolean matches(Method m, Class targetClass) {
        // return true if custom criteria match
    }
}

也有一些超类用于动态的 pointcut。你可以在任何 advice 类型中使用自定义的 pointcut。

6.1.6. 自定义 Pointcut

因为Spring AOP中的 pointcut 是Java类,而不是语言特性(如AspectJ),所以你可以声明自定义pointcut,无论是静态还是动态。在Spring中,自定义的pointcut可以是任意复杂的。然而,如果可以的话,我们建议使用AspectJ的pointcut表达语言。

后来的Spring版本可能会提供对JAC所提供的 "语义 pointcut" 的支持—​例如,"所有改变目标对象中实例变量的方法"。

6.2. Spring 的 Advice API

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

6.2.1. Advice 生命周期

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

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

每个实例的advice适合于introduction,以支持混搭。在这种情况下,advice将状态添加到被代理的对象中。

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

6.2.2. Spring 的 Advice Type

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

Interception Around Advice

Spring中最基本的advice type 是 interception around advice。

Spring 符合 AOP Alliance 对使用方法拦截的 around advice 的接口。实现 MethodInterceptor 和实现 around advice 的类也应该实现以下接口。

public interface MethodInterceptor extends Interceptor {

    Object invoke(MethodInvocation invocation) throws Throwable;
}

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

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

Java

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() 方法的调用。这在拦截器链中向连接点进行。大多数拦截器调用这个方法并返回其返回值。然而,MethodInterceptor 和任何 around advice 一样,可以返回一个不同的值或抛出一个异常,而不是调用 proceed 方法。然而,如果没有充分的理由,你不希望这样做。

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

Before Advice

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

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

下面的列表显示了 MethodBeforeAdvice 接口。

public interface MethodBeforeAdvice extends BeforeAdvice {

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

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

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

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

Java

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

Before advice,可以与任何pointcut一起使用。

Throws Advice

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

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

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

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

Java

public class RemoteThrowsAdvice implements ThrowsAdvice {

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

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

Java

public class ServletThrowsAdviceWithArguments implements ThrowsAdvice {

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

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

Java

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

Throws advice 可以与任何 pointcut 一起使用。

After Returning Advice

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

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

以下是 after returning advice 对所有没有抛出异常的成功方法调用进行统计。

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

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

After returning advice 可以与任何 pointcut 一起使用。

Introduction Advice

Spring 把 interception advice 当作一种特殊的 introduction advice。

Introduction 需要一个 IntroductionAdvisor 和一个 IntroductionInterceptor 来实现以下接口。

public interface IntroductionInterceptor extends MethodInterceptor {

    boolean implementsInterface(Class intf);
}

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

Introduction advice 能被用于任何 pointcut,因为它只适用于类,而不是方法层面。你只能在 IntroductionAdvisor 中使用 introduction advice,它有以下方法。

public interface IntroductionAdvisor extends Advisor, IntroductionInfo {

    ClassFilter getClassFilter();

    void validateInterfaces() throws IllegalArgumentException;
}

public interface IntroductionInfo {

    Class[] getInterfaces();
}

没有 MethodMatcher,因此也没有与 introduction advice 相关的 Pointcut。只有类的过滤是合乎逻辑的。

getInterfaces() 方法返回该 advice 所引入的接口。

validateInterfaces() 方法在内部使用,以查看引入的接口是否能被配置的 IntroductionInterceptor 实现。

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

Java

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

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

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

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

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

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

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

Java

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方法。

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

Java

public class LockMixinAdvisor extends DefaultIntroductionAdvisor {

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

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

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

6.3. Spring 中的 Advisor API

在Spring中,Advisor 是一个切面,它只包含一个与 pointcut 表达式相关联的单个advice对象。

除了introduction的特殊情况外,任何 advisor 都可以与任何advice一起使用。org.springframework.aop.support.DefaultPointcutAdvisor 是最常用的advisor类。它可以与 MethodInterceptor、BeforeAdvice 或 ThrowsAdvice 一起使用。

在同一个AOP代理中,有可能混合advisor和advice类型。例如,你可以在一个代理配置中使用interception around advice,throws advice 以及 before advice。Spring会自动创建必要的拦截器链。

6.4. 使用ProxyFactoryBean创建AOP代理

如果你为你的业务对象使用Spring IoC容器(ApplicationContext 或 BeanFactory)(你应该这样做!),你想使用Spring的AOP FactoryBean 实现之一。(记住,factory Bean引入了一层中介,让它创建不同类型的对象)。

Spring的AOP支持也使用了 factory bean。

在Spring中创建AOP代理的基本方法是使用 org.springframework.aop.framework.ProxyFactoryBean。这样就可以完全控制pointcut、任何适用的advice以及它们的排序。然而,如果你不需要这样的控制,也有一些更简单的选项是更好的。

6.4.1. 基础知识

ProxyFactoryBean 和其他Spring FactoryBean 的实现一样,引入了一定程度的间接性。如果你定义了一个名为 foo 的 ProxyFactoryBean,那么引用 foo 的对象看到的不是 ProxyFactoryBean 实例本身,而是一个由 ProxyFactoryBean 中 getObject() 方法的实现所创建的对象。这个方法创建了一个AOP代理,包裹了一个目标对象。

使用 ProxyFactoryBean 或其他IoC感知(aware)类来创建AOP代理的一个最重要的好处是,advice和pointcuts也可以由IoC管理。这是一个强大的功能,可以实现其他AOP框架难以实现的某些方法。例如,advice本身可以引用应用对象(除了目标,任何AOP框架都应该有),受益于依赖注入提供的所有可插拔性。

6.4.2. JavaBean Properties

与Spring提供的大多数 FactoryBean 实现一样,ProxyFactoryBean 类本身就是一个JavaBean。它的属性被用来:

  • 指定你要代理的目标。
  • 指定是否使用CGLIB(稍后描述,另见 基于 JDK 和 CGLIB 的代理)。

一些关键属性是从 org.springframework.aop.framework.ProxyConfig(Spring中所有AOP代理工厂的超类)继承的。这些关键属性包括以下内容。

  • proxyTargetClass: 如果目标类要被代理,而不是目标类的接口,则为 true。如果这个属性值被设置为 true,那么就会创建CGLIB代理(但也请看 基于 JDK 和 CGLIB 的代理)。
  • optimize: 控制是否将积极的优化应用于通过CGLIB创建的代理。你不应该轻率地使用这个设置,除非你完全了解相关的AOP代理如何处理优化。目前这只用于CGLIB代理。它对JDK动态代理没有影响。
  • frozen: 如果一个proxy configuration 冻结(frozen),就不再允许对配置进行更改。这既是一种轻微的优化,也适用于那些不希望调用者在代理创建后能够操作代理(通过 Advised 接口)的情况。这个属性的默认值是 false,所以允许改变(比如添加额外的 advice)。
  • exposeProxy: 确定当前代理是否应在 ThreadLocal 中暴露,以便它可以被目标访问。如果目标需要获得代理,并且 exposeProxy 属性被设置为 true,那么目标可以使用 AopContext.currentProxy() 方法。

ProxyFactoryBean 的其他特定属性包括如下。

  • proxyInterfaces: 一个 String 接口名称的数组。如果不提供这个,就会使用目标类的CGLIB代理(但也请看 基于 JDK 和 CGLIB 的代理)。
  • interceptorNames: 一个由 Advisor、拦截器或其他advice名称组成的 String 数组,以便应用。排序是很重要的,以先来后到为原则。也就是说,列表中的第一个拦截器是第一个能够拦截调用的。
  • 这些名字是当前工厂的Bean名称,包括来自祖先工厂的Bean名称。你不能在这里提到Bean引用,因为这样做会导致 ProxyFactoryBean 忽略advice的singleton设置。
  • 你可以在拦截器的名字后面加上星号(*)。这样做的结果是应用所有advisor bean,其名称以星号前的部分开始,将被应用。你可以在 使用 “全局” (Global)Advisor 中找到使用这一功能的例子。
  • singleton: 无论 getObject() 方法被调用多少次,工厂是否应该返回一个单一对象。一些 FactoryBean 的实现提供了这样的方法。默认值是 true。如果你想使用有状态的advice—​例如,对于有状态的混合器(mixin)--请使用 prototype advice和 false 的 singleton 值。

6.4.3. 基于 JDK 和 CGLIB 的代理

本节是关于 ProxyFactoryBean 如何选择为特定目标对象(要代理的对象)创建基于JDK的代理或基于CGLIB的代理的权威文档。

ProxyFactoryBean 在创建基于JDK或CGLIB的代理方面的行为在Spring的1.2.x和2.0版本之间有所改变。现在 ProxyFactoryBean 在自动检测接口方面表现出与 TransactionProxyFactoryBean 类相似的语义。

如果要代理的目标对象的类(以下简称目标类)没有实现任何接口,就会创建一个基于 CGLIB 的代理。这是最简单的情况,因为JDK代理是基于接口的,而没有接口意味着JDK代理甚至不可能。你可以插入目标Bean,并通过设置 interceptorNames 属性指定拦截器列表。注意,即使 ProxyFactoryBean 的 proxyTargetClass 属性被设置为 false,也会创建基于CGLIB的代理。(这样做没有意义,最好从Bean 定义(definition)中删除,因为它最好是多余的,最坏的情况是令人困惑。)

如果目标类实现了一个(或多个)接口,创建的代理类型取决于 ProxyFactoryBean 的配置。

如果 ProxyFactoryBean 的 proxyTargetClass 属性被设置为 true,就会创建一个基于CGLIB的代理。这是有道理的,并且符合最小惊喜的原则。即使 ProxyFactoryBean 的 proxyInterfaces 属性被设置为一个或多个全路径的接口名称,proxyTargetClass 属性被设置为 true 这一事实也会导致基于CGLIB的代理的生效。

如果 ProxyFactoryBean 的 proxyInterfaces 属性被设置为一个或多个全路径的接口名称,就会创建一个基于 JDK 的代理。创建的代理实现了在 proxyInterfaces 属性中指定的所有接口。如果目标类恰好实现了比 proxyInterfaces 属性中指定的更多的接口,那就好办了,但这些额外的接口不会被返回的代理所实现。

如果 ProxyFactoryBean 的 proxyInterfaces 属性没有被设置,但目标类确实实现了一个(或多个)接口,ProxyFactoryBean 会自动检测目标类确实实现了至少一个接口这一事实,并创建一个基于JDK的代理。实际上被代理的接口是目标类实现的所有接口。实际上,这与向 proxyInterfaces 属性提供目标类实现的每一个接口的列表是一样的。然而,这明显减少了工作量,也不容易出现排版错误。

6.4.4. 代理接口

考虑一下 ProxyFactoryBean 在 action 中的一个简单例子。这个例子涉及到:

  • 一个被代理的目标Bean。这就是例子中的 personTarget Bean定义。
  • 一个 Advisor 和一个 Interceptor 用来提供 advice。
  • 一个AOP代理Bean的定义,用来指定目标对象(personTarget Bean),代理的接口,以及应用的advice。

下面的列表显示了这个例子。


    
    



    






    

    
    
        
            myAdvisor
            debugInterceptor
        
    

请注意,interceptorNames 属性接收一个 String 列表,其中保存了当前工厂中的拦截器或advisor的bean名称。你可以使用advisor、拦截器、before、after returning 和 throws 的advice对象。advisor 的排序是很重要的。

你可能想知道为什么列表中没有保留Bean的引用。原因是,如果 ProxyFactoryBean 的 singleton 属性被设置为 false,它必须能够返回独立的代理实例。如果任何一个 advisor 本身是一个prototype,就需要返回一个独立的实例,所以必须能够从工厂获得一个 prototype 的实例。持有一个引用是不够的。

前面显示的 person bean定义可以用来代替 Person 的实现,如下所示。

Java

Person person = (Person) factory.getBean("person");

同一IoC上下文中的其他Bean可以表达对它的强类型依赖,就像对普通Java对象一样。下面的例子展示了如何做到这一点。


    

本例中的 PersonUser 类暴露了一个 Person 类型的属性。就它而言,AOP代理可以透明地用来代替 "真正的" person 的实现。然而,它的类将是一个动态代理类。有可能把它强制转换为 Advised 接口(后面讨论)。

你可以通过使用一个匿名的内部Bean来掩盖目标和代理之间的区别。只有 ProxyFactoryBean 的定义是不同的。包括这个建议只是为了完整。下面的例子展示了如何使用一个匿名的内部Bean。


    





    
    
    
        
            
            
        
    
    
        
            myAdvisor
            debugInterceptor
        
    

使用匿名内部Bean的好处是只有一个 Person 类型的对象。如果我们想防止应用程序上下文的用户获得对未被 advice 的对象的引用,或者需要避免与Spring IoC autowiring 的任何歧义,这就很有用。可以说,还有一个好处是 ProxyFactoryBean 的定义是自成一体的。然而,有时能够从工厂中获得未被 advice 的目标实际上是一种优势(例如,在某些测试场景中)。

6.4.5. 代理类

如果你需要代理一个类,而不是一个或多个接口怎么办?

想象一下,在我们之前的例子中,并没有 Person 接口。我们需要建议一个名为 Person 的类,它没有实现任何业务接口。在这种情况下,你可以将Spring配置为使用CGLIB代理而不是动态代理。要做到这一点,将前面所示的 ProxyFactoryBean 上的 proxyTargetClass 属性设置为 true。虽然最好是对接口而不是类进行编程,但在处理遗留代码时,为没有实现接口的类提供 advice 的能力还是很有用的。(一般来说,Spring不是规定性的。虽然它使应用良好的实践变得容易,但它避免了强迫一种特定的方法)。

如果你愿意,你可以在任何情况下强制使用CGLIB,即使你确实有接口。

CGLIB代理的工作方式是在运行时生成一个目标类的子类。Spring对这个生成的子类进行配置,将方法调用委托给原始目标。这个子类被用来实现Decorator模式,在其中advice中织入。

CGLIB 代理通常对用户来说应该是透明的。然而,有一些问题需要考虑。

  • final 类不能被代理,因为它们不能被继承。
  • final 方法不能被adivce,因为它们不能被覆写。
  • private 方法不能被adivce,因为它们不能被覆写。

不需要将CGLIB添加到你的classpath中。CGLIB被重新打包并包含在 spring-core JAR中。换句话说,基于CGLIB的AOP "开箱即用",正如JDK动态代理一样。

CGLIB代理和动态代理之间的性能差异很小。在这种情况下,性能不应该是一个决定性的考虑。

6.4.6. 使用 “全局” (Global)Advisor

通过在拦截器名称上附加星号,所有bean名称与星号前的部分相匹配的 advisor 都会被添加到 advisor 链中。如果你需要添加一套标准的 "全局" advisor,这就很方便了。下面的例子定义了两个全局 advisor。


    
    
        
            global*
        
    



6.5. 简明的代理定义

特别是在定义事务代理时,你可能最终会有许多类似的代理定义。使用父bean和子bean的定义,以及内部bean的定义,可以使代理定义更干净、更简洁。

首先,我们为代理创建一个父类、模板、bean类定义,如下所示。


    
    
        
            PROPAGATION_REQUIRED
        
    

这本身永远不会被实例化,所以它实际上可以是不完整的。然后,每个需要创建的代理都是一个子Bean定义,它将代理的目标包裹成一个内部Bean定义,因为无论如何目标都不会单独使用。下面的例子展示了这样一个子Bean。


    
        
        
    

你可以覆写来自父模板的属性。在下面的例子中,我们覆盖了事务传播的设置。


    
        
        
    
    
        
            PROPAGATION_REQUIRED,readOnly
            PROPAGATION_REQUIRED,readOnly
            PROPAGATION_REQUIRED,readOnly
            PROPAGATION_REQUIRED
        
    

请注意,在父类Bean的例子中,我们通过将 abstract 属性设置为 true,明确地将父类Bean的定义标记为抽象,如 前 所述,因此它实际上可能永远不会被实例化。应用程序上下文(但不是简单的Bean工厂),默认情况下,会预先实例化所有的singletons。因此,重要的是(至少对于单子Bean),如果你有一个(父)Bean定义,你打算只作为模板使用,并且这个定义指定了一个类,你必须确保将 abstract 属性设置为 true。否则,应用程序上下文实际上会试图预先将其实体化。

6.6. 用ProxyFactory以编程方式创建AOP代理

用Spring以编程方式创建AOP代理很容易。这让你可以在不依赖Spring IoC的情况下使用Spring AOP。

目标对象实现的接口会自动被代理。下面的列表显示了为一个目标对象创建一个代理,有一个拦截器和一个advisor。

Java

ProxyFactory factory = new ProxyFactory(myBusinessInterfaceImpl);
factory.addAdvice(myMethodInterceptor);
factory.addAdvisor(myAdvisor);
MyBusinessInterface tb = (MyBusinessInterface) factory.getProxy();

第一步是构造一个 org.springframework.aop.framework.ProxyFactory 类型的对象。你可以像前面的例子那样用一个目标对象来创建它,或者在另一个构造函数中指定要代理的接口。

你可以添加advice(用拦截器作为一种专门的advice)、advisor或两者,并在 ProxyFactory 的生命中操纵它们。如果你添加一个 IntroductionInterceptionAroundAdvisor,你可以使代理实现额外的接口。

在 ProxyFactory 上还有一些方便的方法(继承自 AdvisedSupport),可以让你添加其他的建议类型,比如 before 和 throws advice。AdvisedSupport 是 ProxyFactory 和 ProxyFactoryBean 的超类。

在大多数应用中,将AOP代理的创建与IoC框架相结合是最佳实践。我们建议你用AOP将配置从Java代码中外部化,一般来说,你应该这样做。

6.7. 操作 advice 的对象

无论你如何创建AOP代理,你都可以通过使用 org.springframework.aop.framework.Advised 接口来操作它们。任何AOP代理都可以被强制转换为个接口,不管它实现了哪些其他接口。这个接口包括以下方法。

Java

Advisor[] getAdvisors();

void addAdvice(Advice advice) throws AopConfigException;

void addAdvice(int pos, Advice advice) throws AopConfigException;

void addAdvisor(Advisor advisor) throws AopConfigException;

void addAdvisor(int pos, Advisor advisor) throws AopConfigException;

int indexOf(Advisor advisor);

boolean removeAdvisor(Advisor advisor) throws AopConfigException;

void removeAdvisor(int index) throws AopConfigException;

boolean replaceAdvisor(Advisor a, Advisor b) throws AopConfigException;

boolean isFrozen();

getAdvisors() 方法为每个advisor、拦截器或其他已添加到工厂的advisor类型返回一个 Advisor。如果你添加了一个 Advisor,在这个索引处返回的advisor就是你添加的对象。如果你添加了一个拦截器或其他advice类型,Spring将其包装在一个advisor中,并有一个总是返回 true 值的指针。因此,如果你添加了一个 MethodInterceptor,这个索引返回的顾问是一个 DefaultPointcutAdvisor,它返回你的 MethodInterceptor 和一个匹配所有类和方法的 pointcut。

addAdvisor() 方法可以用来添加任何 Advisor。通常,持有pointcut和advice的advisor是通用的 DefaultPointcutAdvisor,你可以将其用于任何advice或pointcut(但不用于introduction)。

默认情况下,即使代理被创建后,也可以添加或删除advisor或拦截器。唯一的限制是不可能添加或删除引入advisor,因为现有的来自工厂的代理不显示接口的变化。(你可以从工厂获得一个新的代理来避免这个问题)。

下面的例子显示了将一个AOP代理强制转换为 Advised 接口,并检查和操作其advice。

Java

Advised advised = (Advised) myObject;
Advisor[] advisors = advised.getAdvisors();
int oldAdvisorCount = advisors.length;
System.out.println(oldAdvisorCount + " advisors");

// Add an advice like an interceptor without a pointcut
// Will match all proxied methods
// Can use for interceptors, before, after returning or throws advice
advised.addAdvice(new DebugInterceptor());

// Add selective advice using a pointcut
advised.addAdvisor(new DefaultPointcutAdvisor(mySpecialPointcut, myAdvice));

assertEquals("Added two advisors", oldAdvisorCount + 2, advised.getAdvisors().length);

在生产中修改业务对象的advice是否可取(无双关之意)是值得怀疑的,尽管毫无疑问有合法的使用案例。然而,它在开发中可能非常有用(例如,在测试中)。我们有时发现,能够以拦截器或其他advice的形式添加测试代码,进入我们想要测试的方法调用中,是非常有用的。(例如,advice可以进入为该方法创建的事务中,也许是运行SQL来检查数据库是否被正确更新,然后再标记事务进行回滚)。

根据你创建代理的方式,你通常可以设置一个 frozen 标志。在这种情况下,Advised isFrozen() 方法返回 true,任何试图通过添加或删除来修改 advice 的行为都会导致 AopConfigException。freeze advice 对象的状态的能力在某些情况下是有用的(例如,防止调用代码删除安全拦截器)。

6.8. 使用 "auto-proxy" (自动代理)设施

到目前为止,我们已经考虑了通过使用 ProxyFactoryBean 或类似的工厂Bean显式创建AOP代理。

Spring还让我们使用 "自动代理" 的Bean定义,它可以自动代理选定的Bean定义。这是建立在Spring的 "Bean Post Processor" 基础设施上的,它可以在容器加载时修改任何Bean定义。

在这种模式下,你在你的XML Bean定义文件中设置了一些特殊的Bean定义来配置自动代理的基础设施。这让你可以声明符合自动代理的目标。你不需要使用 ProxyFactoryBean。

有两种方法可以做到这一点。

  • 通过使用一个自动代理创建者(auto-proxy creator),在当前上下文中引用特定的bean。
  • 值得单独考虑的自动代理创建的一个特殊情况:由源级元数据属性驱动的自动代理创建。

6.8.1. Auto-proxy Bean 定义

本节涉及 org.springframework.aop.framework.autoproxy 包提供的自动代理创建者。

BeanNameAutoProxyCreator

BeanNameAutoProxyCreator 类是一个 BeanPostProcessor,它为名称与字面值或通配符相匹配的Bean自动创建AOP代理。下面的例子展示了如何创建一个 BeanNameAutoProxyCreator Bean。


    
    
        
            myInterceptor
        
    

与 ProxyFactoryBean 一样,有一个 interceptorNames 属性,而不是一个 interceptors 的列表,以允许prototype advisor 的正确行为。被命名的 “interceptors” 可以是advisor或任何advice类型。

与一般的自动代理一样,使用 BeanNameAutoProxyCreator 的主要意义在于将相同的配置一致地应用于多个对象,而配置量最小。它是将声明性事务应用于多个对象的一个流行选择。

名字匹配的Bean定义,比如前面例子中的 jdkMyBean 和 onlyJdk,都是目标类的普通Bean定义。一个AOP代理是由 BeanNameAutoProxyCreator 自动创建的。同样的advice被应用于所有匹配的Bean。请注意,如果使用advisor(而不是前面例子中的拦截器),那么对不同的Bean适用的 pointcuts 可能不同。

DefaultAdvisorAutoProxyCreator

一个更普遍和极其强大的自动代理创建者是 DefaultAdvisorAutoProxyCreator。它可以自动应用当前上下文中符合条件的advisor,而不需要在自动代理advisor的Bean定义中包含特定的Bean名称。它提供了与 BeanNameAutoProxyCreator 一样的一致配置和避免重复的优点。

使用这一机制涉及到:

  • 指定一个 DefaultAdvisorAutoProxyCreator bean 类定义。
  • 在相同或相关的 context 下指定任何数量的advisor。注意,这些必须是advisor,而不是拦截器或其他advice。这是必要的,因为必须有一个 pointcut 来评估,以检查每个 advice 对候选bean定义的资格。

DefaultAdvisorAutoProxyCreator 会自动评估每个advisor中包含的 pointcut,以查看它应该对每个业务对象(例如例子中的 businessObject1 和 businessObject2)应用什么(如果有的话)advice。

这意味着任何数量的advisor可以被自动应用到每个业务对象。如果任何advisor中的任何点都不匹配业务对象中的任何方法,那么该对象就不会被代理。当Bean定义被添加到新的业务对象时,如果有必要,它们会被自动代理。

一般来说,自动代理的好处是使调用者或依赖者不可能获得一个不被advice的对象。在这个 ApplicationContext 上调用 getBean("businessObject1") 会返回一个 AOP 代理,而不是目标业务对象。(前面显示的 “inner bean” 成语也提供了这个好处)。

下面的例子创建了一个 DefaultAdvisorAutoProxyCreator Bean和本节讨论的其他元素。




    





    


如果你想对许多业务对象一致地应用相同的advice,DefaultAdvisorAutoProxyCreator 就非常有用。一旦基础设施定义到位,你可以添加新的业务对象而不包括具体的代理配置。你也可以轻松地加入额外的切面(例如,跟踪或性能监控切面),只需对配置进行最小的改变。

DefaultAdvisorAutoProxyCreator 提供了对过滤(通过使用命名惯例,以便只对某些顾问进行评估,这允许在同一工厂中使用多个不同配置的 AdvisorAutoProxyCreator)和排序的支持。顾问可以实现 org.springframework.core.Ordered 接口,以确保正确的排序,如果这是一个问题。前面例子中使用的 TransactionAttributeSourceAdvisor 有一个可配置的顺序值。默认设置是无序的。

6.9. 使用TargetSource实现

Spring提供了 TargetSource 的概念,用 org.springframework.aop.TargetSource 接口表示。这个接口负责返回实现连接点的 “target object”。每次AOP代理处理方法调用时,都会要求 TargetSource 实现提供一个目标实例。

使用Spring AOP的开发者通常不需要直接使用 TargetSource 实现,但这提供了支持池、热插拔和其他复杂目标的强大手段。例如,通过使用一个池来管理实例,一个池化的 TargetSource 可以为每次调用返回不同的目标实例。

如果你没有指定 TargetSource,就会使用一个默认的实现来包装一个本地对象。每次调用都会返回同一个目标(正如你所期望的)。

本节的其余部分描述了Spring提供的标准目标源(arget source)以及你如何使用它们。

当使用自定义目标源时,你的目标通常需要是一个prototype,而不是一个singleton bean定义。这允许Spring在需要时创建一个新的目标实例。

6.9.1. 可热插拔的目标源

org.springframework.aop.target.HotSwappableTargetSource 的存在是为了让AOP代理的目标被切换,同时让调用者保留他们对它的引用。

改变目标源的目标会立即生效。HotSwappableTargetSource 是线程安全的。

你可以通过使用 HotSwappableTargetSource 的 swap() 方法来改变目标,如下面的例子所示。

Java

HotSwappableTargetSource swapper = (HotSwappableTargetSource) beanFactory.getBean("swapper");
Object oldTarget = swapper.swap(newTarget);

下面的例子显示了所需的XML定义。




    



    

前面的 swap() 调用改变了可交换Bean的目标。持有对该Bean的引用的客户端并不知道这一变化,而是立即开始命中新的目标。

虽然这个例子没有添加任何advice(使用 TargetSource 不一定要添加advice),但任何 TargetSource 都可以与任意advice一起使用。

6.9.2. 池化目标源

使用池化目标源提供了一个类似于无状态会话EJB的编程模型,在这个模型中,一个相同的实例池被维护,方法的调用会被送到池中的自由对象。

Spring池和SLSB池的一个重要区别是,Spring池可以应用于任何POJO。正如Spring一般,这种服务可以以非侵入性的方式应用。

Spring提供了对Commons Pool 2.2的支持,它提供了一个相当高效的池化实现。你需要在你的应用程序的classpath上安装 commons-poo`l Jar来使用这个功能。你也可以子类化 `org.springframework.aop.target.AbstractPoolingTargetSource,以支持任何其他池化API。

Commons Pool 1.5+也被支持,但从Spring Framework 4.2开始被废弃。

下面的列表显示了一个配置的例子。


    ... properties omitted



    
    



    
    

注意,目标对象(前面例子中的 businessObjectTarget)必须是一个prototype。这让 PoolingTargetSource 的实现可以在必要时创建新的目标实例以增长池。参见 AbstractPoolingTargetSource 的 javadoc 和你希望使用的具体子类,以了解其属性信息。maxSize 是最基本的,并且总是被保证存在。

在这种情况下,myInterceptor 是一个拦截器的名字,需要在同一个IoC上下文中定义。然而,你不需要指定拦截器来使用池化。如果你只想使用池,而没有其他建议,就根本不要设置 interceptorNames 属性。

你可以将Spring配置为能够将任何池的对象强制转换 org.springframework.aop.target.PoolingConfig 接口,该接口通过一个介绍暴露了池的配置和当前大小的信息。你需要定义一个类似于以下的advisor。


    
    

这个advisor是通过调用 AbstractPoolingTargetSource 类上的一个方便方法获得的,因此要使用 MethodInvokingFactoryBean。这个advisor的名字(这里是 poolConfigAdvisor)必须在暴露池对象的 ProxyFactoryBean 中的拦截器名称列表中。

cast 的定义如下。

Java

PoolingConfig conf = (PoolingConfig) beanFactory.getBean("businessObject");
System.out.println("Max pool size is " + conf.getMaxSize());

通常没有必要对无状态服务对象进行池化。我们认为这不应该是默认的选择,因为大多数无状态对象自然是线程安全的,而且如果资源被缓存,实例池就会出现问题。

通过使用自动代理,可以获得更简单的池。你可以设置任何自动代理创建者所使用的 TargetSource 实现。

6.9.3. Prototype 目标源

设置一个 “prototype” 目标源类似于设置一个池化的 TargetSource。在这种情况下,每个方法调用时都会创建一个新的目标实例。尽管在现代JVM中创建一个新对象的成本并不高,但为新对象装配(满足其IoC依赖性)的成本可能更高。因此,如果没有很好的理由,你不应该使用这种方法。

要做到这一点,你可以将前面显示的 poolTargetSource 定义修改如下(为了清楚起见,我们还改变了名称)。


    

唯一的属性是目标Bean的名字。继承在 TargetSource 的实现中使用,以确保命名的一致性。与池化目标源一样,目标Bean必须是一个 prototype Bean定义。

6.9.4.ThreadLocal目标源

如果你需要为每个传入的请求(即每个线程)创建一个对象,那么 ThreadLocal 目标源就很有用。ThreadLocal 的概念提供了一个JDK范围内的设施,可以透明地将资源与线程一起存储。设置 ThreadLocalTargetSource 的方法与其他类型的目标源的方法基本相同,正如下面的例子所示。


    

当在多线程和多类加载器环境中不正确地使用 ThreadLocal 实例时,会带来严重的问题(可能会导致内存泄露)。你应该总是考虑将一个 ThreadLocal 包装在其他类中,而不是直接使用 ThreadLocal 本身(除了在包装类中)。此外,你应该始终记得正确地设置和取消设置(后者只需要调用 ThreadLocal.set(null))线程的本地资源。在任何情况下都应该取消设置,因为不取消设置可能会导致有问题的行为。Spring的 ThreadLocal 支持为你做到了这一点,在使用 ThreadLocal 实例时,应始终考虑到没有其他适当的处理代码。 

6.10. 定义新的 Advice Type

Spring AOP被设计为可扩展的。虽然目前内部使用的是拦截实现策略,但除了around advice、before、throws advice 和 after returning advice 的拦截(interception)之外,还可以支持任意的 advice 类型。

org.springframework.aop.framework.adapter 包是一个SPI包,可以在不改变核心框架的情况下增加对新的自定义 advice 类型的支持。对自定义 Advice 类型的唯一限制是它必须实现 org.aopalliance.aop.Advice 标记接口。

你可能感兴趣的:(#,Spring,专栏,spring,sql,java)