|
对于最新稳定版本,请使用 Spring Framework 7.0.6! |
使用 ProxyFactoryBean 创建 AOP 代理
如果你为你的业务对象使用 Spring IoC 容器(ApplicationContext 或 BeanFactory)(而且你应该这样做!),那么你应当使用 Spring AOP 的某个 FactoryBean 实现。(请记住,工厂 bean 引入了一层间接性,使其能够创建不同类型的对象。)
| Spring AOP 支持在底层也使用了工厂 Bean。 |
在 Spring 中创建 AOP 代理的基本方法是使用
org.springframework.aop.framework.ProxyFactoryBean。这可以完全控制
切入点(pointcuts)、所应用的通知(advice)以及它们的执行顺序。然而,如果你不需要如此精细的控制,
还有更简单的选项可供选择。
基础
ProxyFactoryBean 与其他 Spring FactoryBean 实现一样,引入了一层间接性。如果你定义了一个名为 ProxyFactoryBean 的 foo,那么引用 foo 的对象并不会看到 ProxyFactoryBean 实例本身,而是看到由 getObject() 中 ProxyFactoryBean 方法实现所创建的对象。该方法会创建一个 AOP 代理,用以包装目标对象。
使用 ProxyFactoryBean 或其他感知 IoC 的类来创建 AOP 代理,其中一个最重要的好处是通知(advice)和切入点(pointcut)也可以由 IoC 容器进行管理。这是一个强大的特性,使得某些在其他 AOP 框架中难以实现的方法成为可能。例如,一个通知本身可以引用应用程序对象(除了目标对象之外,而目标对象在任何 AOP 框架中都应该是可用的),从而充分利用依赖注入所提供的全部可插拔性。
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、拦截器或其他通知(advice)的名称。顺序很重要,遵循先到先服务的原则。也就是说,列表中的第一个拦截器将最先有机会拦截方法调用。这些名称是当前工厂中的 bean 名称,包括从父级工厂继承而来的 bean 名称。此处不能引用 bean 引用,因为这样做会导致
ProxyFactoryBean忽略通知(advice)的单例(singleton)设置。您可以在拦截器名称后附加一个星号(
*)。这样会将所有名称以星号前部分开头的 Advisor Bean 应用到目标上。您可以在使用“全局”Advisor一节中找到使用此功能的示例。 -
singleton:工厂是否应始终返回同一个对象,无论
getObject()方法被调用多少次。多个FactoryBean实现都提供了这样的方法。默认值为true。如果你希望使用有状态的通知(advice)——例如用于有状态的混入(mixins)——请将原型(prototype)通知与false的 singleton 值一起使用。
基于 JDK 和 CGLIB 的代理
本节作为关于ProxyFactoryBean如何为特定目标对象(即需要被代理的对象)选择创建基于JDK的代理还是基于CGLIB的代理的权威文档。
ProxyFactoryBean 在创建基于 JDK 或 CGLIB 的代理方面的行为,在 Spring 1.2.x 版本和 2.0 版本之间发生了变化。ProxyFactoryBean 现在在自动检测接口方面的语义与 TransactionProxyFactoryBean 类的行为类似。 |
如果要被代理的目标对象的类(以下简称目标类)未实现任何接口,则会创建一个基于 CGLIB 的代理。这是最简单的情况,因为 JDK 代理是基于接口的,而没有接口就意味着根本无法使用 JDK 进行代理。您可以通过设置 interceptorNames 属性来注入目标 bean 并指定拦截器列表。请注意,即使将 proxyTargetClass 的 ProxyFactoryBean 属性设置为 false,也会创建基于 CGLIB 的代理。(这样做毫无意义,最好从 bean 定义中移除该设置,因为它至多是多余的,最坏的情况下还会引起混淆。)
如果目标类实现了一个(或多个)接口,则所创建的代理类型取决于 ProxyFactoryBean 的配置。
如果 proxyTargetClass 的 ProxyFactoryBean 属性被设置为 true,
则会创建一个基于 CGLIB 的代理。这种行为是合理的,并且符合“最小意外原则”(principle of least surprise)。
即使 proxyInterfaces 的 ProxyFactoryBean 属性已被设置为一个或多个完全限定的接口名称,
但由于 proxyTargetClass 属性被设为 true,仍然会启用基于 CGLIB 的代理。
如果 proxyInterfaces 的 ProxyFactoryBean 属性被设置为一个或多个
完全限定的接口名称,则会创建一个基于 JDK 的代理。所创建的
代理将实现 proxyInterfaces 属性中指定的所有接口。
如果目标类恰好实现了比 proxyInterfaces 属性中指定的更多的接口,
这完全没有问题,但这些额外的接口不会被返回的代理所实现。
如果未设置 proxyInterfaces 的 ProxyFactoryBean 属性,但目标类确实实现了一个(或多个)接口,则 ProxyFactoryBean 会自动检测到目标类实际上至少实现了一个接口,并创建一个基于 JDK 的代理。实际被代理的接口是目标类所实现的所有接口。实际上,这等同于将目标类实现的每一个接口都显式提供给 proxyInterfaces 属性。然而,这种方式工作量显著减少,且不易出现拼写错误。
代理接口
考虑一个正在运行的简单 ProxyFactoryBean 示例。该示例包含:
-
一个被代理的目标 bean。这是示例中的
personTargetbean 定义。 -
一个用于提供通知的
Advisor和一个Interceptor。 -
一个 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 类型的列表,其中包含当前工厂中拦截器或通知器(advisor)的 bean 名称。您可以使用通知器(advisor)、拦截器(interceptor)、前置通知(before)、后置返回通知(after returning)以及异常抛出通知(throws advice)对象。通知器的顺序非常重要。
你可能会疑惑为什么这个列表不保存 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 代理可以透明地替代“真实”的 Person 实现。然而,其实际类将是一个动态代理类,因此可以将其转换(cast)为 Advised 接口(稍后讨论)。
你可以通过使用匿名内部 bean 来隐藏目标对象与代理之间的区别。只有 ProxyFactoryBean 的定义有所不同。此处包含通知(advice)仅为了完整性。以下示例展示了如何使用匿名内部 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 类型的对象只有一个。如果我们希望防止应用程序上下文的用户获取未被代理(un-advised)对象的引用,或者需要避免与 Spring IoC 自动装配产生任何歧义,这一点非常有用。此外,可以说还有一个优势,即 ProxyFactoryBean 的定义是自包含的。然而,在某些情况下,能够从工厂中获取未被代理的目标对象实际上可能是一种优势(例如,在某些测试场景中)。
代理类
如果你需要代理一个类,而不是一个或多个接口,该怎么办?
假设在我们之前的示例中,并不存在 Person 接口。我们需要对一个名为 Person 的类进行增强,而该类并未实现任何业务接口。在这种情况下,您可以配置 Spring 使用 CGLIB 代理,而不是动态代理。为此,请将前面所示的 proxyTargetClass 的 ProxyFactoryBean 属性设置为 true。尽管最好面向接口而非类进行编程,但在处理遗留代码时,能够对未实现接口的类进行增强的功能仍然非常有用。(总体而言,Spring 并不强制推行某种特定方式。虽然它使应用良好实践变得容易,但不会强制要求采用某种特定方法。)
如果需要,即使存在接口,你也可以强制在任何情况下使用 CGLIB。
CGLIB 代理通过在运行时生成目标类的子类来实现。Spring 将此生成的子类配置为将方法调用委托给原始目标对象。该子类用于实现装饰器(Decorator)模式,将通知(advice)织入其中。
CGLIB 代理对用户来说通常是透明的。然而,有一些问题需要注意:
-
final类无法被代理,因为它们不能被继承。 -
final方法无法被通知(advised),因为它们不能被重写(overridden)。 -
private方法无法被通知(advised),因为它们不能被重写(overridden)。
无需将 CGLIB 添加到您的类路径中。CGLIB 已被重新打包并包含在 spring-core JAR 文件中。换句话说,基于 CGLIB 的 AOP 和 JDK 动态代理一样,开箱即用。 |
CGLIB 代理和动态代理之间的性能差异很小。 在这种情况下,性能不应该是决定性的考虑因素。
使用“全局”顾问
通过在拦截器名称后附加一个星号(*),所有 bean 名称与星号前部分匹配的增强器(advisor)都会被添加到增强器链中。当你需要添加一组标准的“全局”增强器时,这一功能非常有用。以下示例定义了两个全局增强器:
<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"/>