8.5.使用ProxyFactoryBean创建AOP代理

优质
小牛编辑
131浏览
2023-12-01

8.5. 使用ProxyFactoryBean创建AOP代理

如果你正在使用Spring IoC容器(即ApplicationContext或BeanFactory)来管理你的业务对象--这正是你应该做的--你也许会想要使用Spring中关于AOP的FactoryBean。(记住使用工厂bean引入一个间接层之后,我们就可以创建不同类型的对象了)。

Spring 2.0的AOP支持也在底层使用工厂bean。

在Spring里创建一个AOP代理的基本方法是使用org.springframework.aop.framework.ProxyFactoryBean。这个类对应用的切入点和通知提供了完整的控制能力(包括它们的应用顺序)。然而如果你不需要这种控制,你会喜欢更简单的方式。

8.5.1. 基础

像其它的FactoryBean实现一样,ProxyFactoryBean引入了一个间接层。如果你定义一个名为fooProxyFactoryBean, 引用foo的对象看到的将不是ProxyFactoryBean实例本身,而是一个ProxyFactoryBean实现里getObject()方法所创建的对象。 这个方法将创建一个AOP代理,它包装了一个目标对象。

使用ProxyFactoryBean或者其它IoC相关类带来的最重要的好处之一就是创建AOP代理,这意味着通知和切入点也可以由IoC来管理。这是一个强大的功能并使得某些特定的解决方案成为可能, 而这些用其它AOP框架很难做到。例如,一个通知也许本身也要引用应用程序对象(不仅仅是其它AOP框架中也可以访问的目标对象),这令你可以从依赖注射的可拔插特性中获益。

8.5.2. JavaBean属性

通常情况下Spring提供了大多数的FactoryBean实现,ProxyFactoryBean类本身也是一个JavaBean。它的属性被用来:

  • 指定你希望代理的目标对象

  • 指定是否使用CGLIB(查看下面叫做第 8.5.3 节 “基于JDK和CGLIB的代理”的小节)

一些主要属性从org.springframework.aop.framework.ProxyConfig里继承下来(这个类是Spring里所有AOP代理工厂的父类)。这些主要属性包括:

  • proxyTargetClass:这个属性为true时,目标类本身被代理而不是目标类的接口。如果这个属性值被设为true,CGLIB代理将被创建(可以参看下面名为第 8.5.3 节 “基于JDK和CGLIB的代理”的章节)。

  • optimize:用来控制通过CGLIB创建的代理是否使用激进的优化策略。除非完全了解AOP代理如何处理优化,否则不推荐用户使用这个设置。目前这个属性仅用于CGLIB代理;对于JDK动态代理(缺省代理)无效。

  • frozen:用来控制代理工厂被配置之后,是否还允许修改通知。缺省值为false(即在代理被配置之后,不允许修改代理的配置)。

  • exposeProxy:决定当前代理是否被保存在一个ThreadLocal中以便被目标对象访问。(目标对象本身可以通过MethodInvocation来访问,因此不需要ThreadLocal。) 如果个目标对象需要获取代理而且exposeProxy属性被设为true,目标对象可以使用AopContext.currentProxy()方法。

  • aopProxyFactory:使用AopProxyFactory的实现。这提供了一种方法来自定义是否使用动态代理,CGLIB或其它代理策略。 缺省实现将根据情况选择动态代理或者CGLIB。一般情况下应该没有使用这个属性的需要;它是被设计来在Spring 1.1中添加新的代理类型的。

ProxyFactoryBean中需要说明的其它属性包括:

  • proxyInterfaces:需要代理的接口名的字符串数组。如果没有提供,将为目标类使用一个CGLIB代理(也可以查看下面名为第 8.5.3 节 “基于JDK和CGLIB的代理”的章节)。

  • interceptorNamesAdvisor的字符串数组,可以包括拦截器或其它通知的名字。顺序是很重要的,排在前面的将被优先服务。就是说列表里的第一个拦截器将能够第一个拦截调用。

    这里的名字是当前工厂中bean的名字,包括父工厂中bean的名字。这里你不能使用bean的引用因为这会导致ProxyFactoryBean忽略通知的单例设置。

    你可以把一个拦截器的名字加上一个星号作为后缀(*)。这将导致这个应用程序里所有名字以星号之前部分开头的advisor都被应用。你可以在下面发现一个使用这个特性的例子。

  • 单例:工厂是否应该返回同一个对象,不论方法getObject()被调用的多频繁。多个FactoryBean实现都提供了这个方法。缺省值是true。如果你希望使用有状态的通知--例如,有状态的mixin--可以把单例属性的值设置为false来使用原型通知。

8.5.3. 基于JDK和CGLIB的代理

这个小节作为说明性文档,解释了对于一个目标对象(需要被代理的对象),ProxyFactryBean是如何决定究竟创建一个基于JDK还是CGLIB的代理的。

注意

ProxyFactoryBean需要创建基于JDK还是CGLIB代理的具体行为在版本1.2.x和2.0中有所不同。现在ProxyFactoryBean在关于自动检测接口方面使用了与TransactionProxyFactoryBean相似的语义。

如果一个需要被代理的目标对象的类(后面将简单地称它为目标类)没有实现任何接口,那么一个基于CGLIB的代理将被创建。这是最简单的场景,因为JDK代理是基于接口的,没有接口意味着没有使用JDK进行代理的可能。 在目标bean里将被插入探测代码,通过interceptorNames属性给出了拦截器的列表。注意一个基于CGLIB的代理将被创建即使ProxyFactoryBeanproxyTargetClass属性被设置为false。 (很明显这种情况下对这个属性进行设置是没有意义的,最好把它从bean的定义中移除,因为虽然这只是个多余的属性,但在许多情况下会引起混淆。)

如果目标类实现了一个(或者更多)接口,那么创建代理的类型将根据ProxyFactoryBean的配置来决定。

如果ProxyFactoryBeanproxyTargetClass属性被设为true,那么一个基于CGLIB的代理将创建。这样的规定是有意义的,遵循了最小惊讶法则(保证了设定的一致性)。 甚至当ProxyFactoryBeanproxyInterfaces属性被设置为一个或者多个全限定接口名,而proxyTargetClass属性被设置为true仍然实际使用基于CGLIB的代理。

如果ProxyFactoryBeanproxyInterfaces属性被设置为一个或者多个全限定接口名,一个基于JDK的代理将被创建。被创建的代理将实现所有在proxyInterfaces属性里被说明的接口;如果目标类实现了全部在proxyInterfaces属性里说明的接口以及一些额外接口,返回的代理将只实现说明的接口而不会实现那些额外接口。

如果ProxyFactoryBeanproxyInterfaces属性没有被设置,但是目标类实现了一个(或者更多)接口,那么ProxyFactoryBean将自动检测到这个目标类已经实现了至少一个接口, 一个基于JDK的代理将被创建。被实际代理的接口将是目标类所实现的全部接口;实际上,这和在proxyInterfaces属性中列出目标类实现的每个接口的情况是一样的。然而,这将显着地减少工作量以及输入错误的可能性。

8.5.4. 对接口进行代理

让我们看一个关于ProxyFactoryBean的简单例子。这个例子涉及:

  • 一个将被代理的目标bean。在下面的例子里这个bean是“personTarget”。

  • 被用来提供通知的一个advisor和一个拦截器。

  • 一个AOP代理bean的定义,它说明了目标对象(personTarget bean)以及需要代理的接口,还包括需要被应用的通知。

<bean id="personTarget">
    <property name="name"><value>Tony</value></property>
    <property name="age"><value>51</value></property>
</bean>

<bean id="myAdvisor">
    <property name="someProperty"><value>Custom string property value</value></property>
</bean>

<bean id="debugInterceptor">
</bean>

<bean id="person" 
   >
    <property name="proxyInterfaces"><value>com.mycompany.Person</value></property>

    <property name="target"><ref local="personTarget"/></property>
    <property name="interceptorNames">
  <list>
<value>myAdvisor</value>
<value>debugInterceptor</value>
  </list>
    </property>
</bean>

注意interceptorNames属性接受一组字符串:当前工厂中拦截器或advisorbean的名字。拦截器,advisor,前置, 后置和异常通知对象都可以在这里被使用。这里advisor的顺序是很重要的。

你也许很奇怪为什么这个列表不保存bean的引用。理由是如果ProxyFactoryBean的singleton属性被设置为false,它必须返回独立的代理实例。如果任何advisor本身是一个原型,则每次都返回一个独立实例,因此它必须能够从工厂里获得原型的一个实例;保存一个引用是不够的。

上面“person” bean的定义可以被用来取代一个Person接口的实现,就像下面这样:

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

在同一个IoC上下文中其它的bean可以对这个bean有基于类型的依赖,就像对一个普通的Java对象那样:

<bean id="personUser">
  <property name="person"><ref local="person" /></property>
</bean>

这个例子里的PersonUser类将暴露一个类型为Person的属性。就像我们关心的那样,AOP代理可以透明地取代一个“真实”的person接口实现。然而,它的类将是一个动态代理类。 它可以被转型成Advised接口(将在下面讨论)。

就像下面这样,你可以使用一个匿名内部bean来隐藏目标和代理之间的区别。仅仅ProxyFactoryBean的定义有所不同;通知的定义只是由于完整性的原因而被包括进来:

<bean id="myAdvisor">
  <property name="someProperty"><value>Custom string property value</value></property>
</bean>

<bean id="debugInterceptor"/>

<bean id="person">
  <property name="proxyInterfaces"><value>com.mycompany.Person</value></property>
  <!-- Use inner bean, not local reference to target -->
  <property name="target">
    <bean>
<property name="name"><value>Tony</value></property>
<property name="age"><value>51</value></property>
    </bean>
  </property>
  <property name="interceptorNames">
    <list>
<value>myAdvisor</value>
<value>debugInterceptor</value>
    </list>
  </property>
</bean>

对于只需要一个Person类型对象的情况,这是有好处的:如果你希望阻止应用程序上下文的用户获取一个指向未通知对象的引用或者希望避免使用Spring IoC 自动织入 时的混淆。 按理说ProxyFactoryBean定义还有一个优点是它是自包含的。然而,有时能够从工厂里获取未通知的目标也是一个优点:例如,在某些测试场景里。

8.5.5. 对类进行代理

如果你需要代理一个类而不是代理一个或是更多接口,那么情况将是怎样?

想象在我们上面的例子里,不存在Person接口:我们需要通知一个叫做Person的类,它没有实现任何业务接口。在这种情况下,你可以配置Spring使用CGLIB代理,而不是动态代理。 这只需简单地把上面ProxyFactoryBean的proxyTargetClass属性设为true。虽然最佳方案是面向接口编程而不是类,但在与遗留代码一起工作时,通知没有实现接口的类的能力是非常有用的。(通常情况下,Spring没有任何规定。它只是让你很容易根据实际情况选择最好的解决方案,避免强迫使用特定方式)。

也许你希望你能够在任何情况下都强制使用CGLIB,甚至在你使用接口的时候也这样做。

CGLIB通过在运行时生成一个目标类的子类来进行代理工作。Spring配置这个生成的子类对原始目标对象的方法调用进行托管:子类实现了装饰器(Decorator)模式,把通知织入。

CGLIB的代理活动应当对用户是透明的。然而,有一些问题需要被考虑:

  • Final方法不可以被通知,因为它们不能被覆盖。

  • 你需要在你的类路径里有CGLIB 2的库;使用动态代理的话只需要JDK。

在CGLIB代理和动态代理之间的速度差别是很小的。在Spring 1.0中,动态代理会快一点点。但这点可能在将来被改变。这种情况下,选择使用何种代理时速度不应该成为决定性的理由。

8.5.6. 使用“全局”advisor

通过在一个拦截器名后添加一个星号,所有bean名字与星号之前部分相匹配的通知都将被加入到advisor链中。这让你很容易添加一组标准的“全局”advisor:

<bean id="proxy">
  <property name="target" ref="service"/>
  <property name="interceptorNames">
    <list>
<value>globa *</value>
    </list>
  </property>
</bean>

<bean id="global_debug"/>
<bean id="global_performance"/>