使用ProxyFactoryBean创建AOP代理
ProxyFactoryBean to Create AOP Proxies
如果你使用Spring IoC容器(ApplicationContext或BeanFactory)来管理你的业务对象(你确实应该这样做!),那么你应该使用Spring的AOP FactoryBean实现之一。(记住,工厂bean引入了一层间接性,它能够创建不同类型的对象。)
Spring AOP的实现也在底层使用了工厂Bean(factory beans)。
在Spring中创建AOP代理的基本方法是使用org.springframework.aop.framework-proxyFactoryBean。这种方式可以完全控制切点(pointcuts)、适用的增强(advises)以及它们的执行顺序。然而,如果你不需要这种程度的控制,还有更简单的方法可供选择。
基础知识
ProxyFactoryBean与其他Spring的FactoryBean实现一样,引入了一层间接性。如果你定义了一个名为foo的ProxyFactoryBean,那么引用foo的对象看到的并不是ProxyFactoryBean实例本身,而是由ProxyFactoryBean中getObject()方法的实现创建的对象。该方法会创建一个AOP代理,用来封装目标对象。
使用ProxyFactoryBean或其他支持IoC的类来创建AOP代理的最重要好处之一是,建议(advise)和切点(pointcuts)也可以由IoC进行管理。这是一个强大的特性,使得某些在其他AOP框架中难以实现的方法成为可能。例如,一个建议本身可能会引用应用程序对象(除了目标对象之外,而在任何AOP框架中目标对象都是可用的),从而能够充分利用依赖注入(Dependency Injection)所提供的可插拔性。
JavaBean 属性
与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: 如果代理配置被设置为frozen,则不再允许对其进行修改。这既是一种轻量级的优化手段,也适用于那些不希望调用者在代理创建后能够通过Advised接口来操作代理的情况。该属性的默认值为false,因此允许进行修改(例如添加额外的通知)。 -
exposeProxy: 决定当前代理是否应被暴露在ThreadLocal中,以便目标类可以访问它。如果目标类需要获取代理,并且exposeProxy属性被设置为true,那么目标类可以使用AopContext.currentProxy()方法来获取代理。
ProxyFactoryBean 具有的其他特定属性包括以下内容:
-
proxyInterfaces: 一个String接口名称的数组。如果未提供此参数,则会使用目标类的 CGLIB 代理(但请参阅 基于 JDK 和 CGLIB 的代理)。 -
interceptorNames: 一个String数组,其中包含要应用的Advisor、拦截器或其他建议的名称。这些名称的顺序很重要,按照“先来先服务”的原则进行处理。也就是说,列表中的第一个拦截器将最先能够拦截调用。
这些名称是当前工厂中的 bean 名称,包括来自祖先工厂的 bean 名称。在此处不能使用 bean 引用,因为这样做会导致 ProxyFactoryBean 忽略建议的单例设置。
可以在拦截器名称后添加一个星号(*)。这样,所有以星号前的部分作为名称的 advisor bean 都会被应用。您可以在 使用“全局”advisor 中找到使用此功能的示例。
singleton: 指工厂是否应始终返回单个对象,无论getObject()方法被调用多少次。一些FactoryBean实现提供了这样的方法。默认值为true。如果您想使用有状态的建议(例如,用于有状态的 mixin),则应使用原型建议,并将singleton值设置为false。
基于JDK和CGLIB的代理
本节提供了关于ProxyFactoryBean如何为特定的目标对象(即要被代理的对象)选择创建基于JDK的代理或基于CGLIB的代理的最终说明文档。
在Spring的1.2.x版本和2.0版本之间,ProxyFactoryBean在创建基于JDK或CGLIB的代理对象时的行为发生了变化。现在,ProxyFactoryBean在自动检测接口方面的语义与TransactionProxyFactoryBean类相似。
如果需要代理的目标对象的类(以下简称为目标类)没有实现任何接口,那么就会创建一个基于CGLIB的代理。这是最简单的情况,因为JDK代理是基于接口的,而如果没有接口的话,JDK代理就根本无法实现。你可以插入目标bean,并通过设置interceptorNames属性来指定拦截器的列表。需要注意的是,即使ProxyFactoryBean的proxyTargetClass属性被设置为false,也会创建一个基于CGLIB的代理。(这样做是没有意义的,最好从bean定义中移除这一设置,因为至少它是多余的,最坏的情况是还会造成混淆。)
如果目标类实现了一个(或多个)接口,那么创建的代理类型将取决于ProxyFactoryBean的配置。
如果 ProxyFactoryBean 的 proxyTargetClass 属性被设置为 true,则会创建一个基于 CGLIB 的代理。这是合理的,也符合“最小惊讶原则”(Principle of Least Surprise)。即使 ProxyFactoryBean 的 proxyInterfaces 属性被设置为一个或多个完整的接口名称,只要 proxyTargetClass 属性被设置为 true,就会启用基于 CGLIB 的代理机制。
如果ProxyFactoryBean的proxyInterfaces属性被设置为一个或多个完整的接口名称,那么就会创建一个基于JDK的代理。所创建的代理会实现proxyInterfaces属性中指定的所有接口。如果目标类恰好实现了比proxyInterfaces属性中指定的更多的接口,那也没有问题,但是返回的代理并不会实现这些额外的接口。
如果 ProxyFactoryBean 的 proxyInterfaces 属性没有被设置,但目标类确实实现了一个或多个接口,那么 ProxyFactoryBean 会自动检测到目标类实际上至少实现了一个接口,并创建一个基于 JDK 的代理。实际被代理的接口就是目标类所实现的所有接口。从效果上来说,这相当于将目标类实现的所有接口列表直接提供给 proxyInterfaces 属性。不过,这种方式的工作量要小得多,也更容易避免输入错误。
代理接口
考虑一个简单的ProxyFactoryBean使用示例。这个示例包括:
-
一个被代理的目标Bean。这就是示例中的
personTargetBean定义。 -
一个
Advisor和一个Interceptor,用于提供建议(advice)。 -
一个AOP代理Bean定义,用于指定目标对象(即
personTargetBean)、要代理的接口以及要应用的建议。
以下列表展示了示例:
<bean id="personTarget" class="com.mycompany.PersonImpl">
<property name="name" value="Tony"/>
<property name="age" value="51"/>
</bean>
<bean id="myAdvisor" class="com.mycompany.MyAdvisor">
<property name="someProperty" value="Custom string property value"/>
</bean>
<bean id="debugInterceptor" class="org.springframework.aop.interceptor.DebugInterceptor">
</bean>
<bean id="person"
class="org.springframework.aop.framework.ProxyFactoryBean">
<property name="proxyInterfaces" value="com.mycompany.Person"/>
<property name="target" ref="personTarget"/>
<property name="interceptorNames">
<list>
<value>myAdvisor</value>
<value>debugInterceptor</value>
</list>
</property>
</bean>
请注意,interceptorNames 属性接受一个 String 列表,其中包含当前工厂中的拦截器(interceptors)或顾问(advisors)的 bean 名称。你可以使用在方法执行前、执行后以及抛出异常时执行的顾问对象。顾问的顺序非常重要。
你可能会疑惑为什么这个列表不保存bean引用。原因在于,如果ProxyFactoryBean的singleton属性被设置为false,那么它就必须能够返回独立的代理实例。如果其中任何顾问(advisor)本身也是一个原型(prototype),那么就需要返回一个独立的实例,因此有必要能够从工厂中获取该原型的实例。仅仅保存一个引用是不够的。
前面展示的person Bean 定义可以用来替代 Person 的实现,如下所示:
- Java
- Kotlin
Person person = (Person) factory.getBean("person");
val person = factory.getBean("person") as Person
在同一IoC上下文中的其他bean也可以像对普通Java对象一样,对其表达强类型依赖。以下示例展示了如何实现这一点:
<bean id="personUser" class="com.mycompany.PersonUser">
<property name="person"><ref bean="person"/></property>
</bean>
在这个例子中,PersonUser 类暴露了一个类型为 Person 的属性。就其本身而言,AOP 代理可以透明地替代“真实”的人员实现。然而,它的类将是一个动态代理类。可以将它强制转换为 Advised 接口(稍后会讨论)。
你可以通过使用匿名内部bean来隐藏目标对象(target)和代理对象(proxy)之间的区别。只有ProxyFactoryBean的定义有所不同。加入这个示例只是为了完整性。以下示例展示了如何使用匿名内部bean:
<bean id="myAdvisor" class="com.mycompany.MyAdvisor">
<property name="someProperty" value="Custom string property value"/>
</bean>
<bean id="debugInterceptor" class="org.springframework.aop.interceptor.DebugInterceptor"/>
<bean id="person" class="org.springframework.aop.framework.ProxyFactoryBean">
<property name="proxyInterfaces" value="com.mycompany.Person"/>
<!-- Use inner bean, not local reference to target -->
<property name="target">
<bean class="com.mycompany.PersonImpl">
<property name="name" value="Tony"/>
<property name="age" value="51"/>
</bean>
</property>
<property name="interceptorNames">
<list>
<value>myAdvisor</value>
<value>debugInterceptor</value>
</list>
</property>
</bean>
使用匿名内部bean有一个优点,那就是只会创建一个Person类型的对象。如果我们想防止应用程序上下文的使用者获取到这个未被管理的对象的引用,或者需要避免与Spring IoC自动装配产生任何混淆,这就非常有用。此外,可以说ProxyFactoryBean的定义也是自包含的,这也是一个优势。然而,在某些情况下,能够从工厂中获取到这个未被管理的目标对象实际上也是一种优势(例如,在某些测试场景中)。
代理类
如果你需要代理的是一个类,而不仅仅是一个或多个接口,那该怎么办?
想象一下,在我们之前的例子中,并不存在Person接口。我们需要为一个没有实现任何业务接口的名为Person的类提供增强(advice)。在这种情况下,你可以配置Spring使用CGLIB代理而不是动态代理。为此,将前面展示的ProxyFactoryBean中的proxyTargetClass属性设置为true。虽然最好是根据接口来编程而不是根据类来编程,但在处理遗留代码时,能够为没有实现接口的类提供增强功能会非常有用。(一般来说,Spring并不强制要求使用某种特定的方式。虽然它使得应用最佳实践变得容易,但它并不会强迫使用某种特定的方法。)
如果你想的话,无论如何都可以强制使用CGLIB,即使你已经有了接口。
CGLIB代理机制通过在运行时生成目标类的子类来发挥作用。Spring会配置这个生成的子类,使其将方法调用委托给原始的目标对象。该子类用于实现装饰器模式(Decorator Pattern),从而将额外的逻辑(即“建议”或“advice”)编织(weave)到目标方法的执行过程中。
CGLIB代理功能通常应该对用户来说是透明的。然而,还是有一些问题需要考虑:
final类不能被代理,因为它们不能被继承。final方法不能被建议(即不能通过 AOP 对其进行修改),因为它们不能被重写。private方法不能被建议,因为它们不能被重写。- 那些不可见的方法(通常是指来自不同包的父类中的
package-private方法)也不能被建议,因为它们实际上属于私有的(private)。
无需将CGLIB添加到你的类路径中。CGLIB已经被重新打包并包含在spring-core JAR中。换句话说,基于CGLIB的AOP可以“开箱即用”,JDK动态代理也是如此。
CGLIB代理和动态代理之间的性能差异很小。在这种情况下,性能不应成为决定性的考虑因素。
使用“全局”顾问
通过在拦截器名称后添加一个星号(*),所有bean名称与该星号之前的部分相匹配的顾问(advisors)都会被加入到顾问链(advisor chain)中。如果需要添加一组标准的“全局”顾问(global advisors),这种方法会非常有用。以下示例定义了两个全局顾问:
<bean id="proxy" class="org.springframework.aop.framework.ProxyFactoryBean">
<property name="target" ref="service"/>
<property name="interceptorNames">
<list>
<value>global*</value>
</list>
</property>
</bean>
<bean id="global_debug" class="org.springframework.aop.interceptor.DebugInterceptor"/>
<bean id="global_performance" class="org.springframework.aop.interceptor.PerformanceMonitorInterceptor"/>