Using the ProxyFactoryBean to Create AOP Proxies

如果您将 Spring IoC 容器(ApplicationContextBeanFactory)用于您的业务对象(您应该这么做!),您应该使用 Spring 的 AOP FactoryBean 实现之一。(记住,一个工厂 Bean 引入了间接层,允许其创建不同类型的对象。)

Spring AOP 支持也在后台使用工厂 bean。

在 Spring 中创建 AOP 代理的基本方法是使用 org.springframework.aop.framework.ProxyFactoryBean。这给予了对切点、任何适用的建议及它们的顺序的完全控制权。然而,如果您不需要如此控制权,还有一些更简单的备选方法值得考虑。

Basics

ProxyFactoryBean,就像其他 Spring FactoryBean 实现一样,引入了间接层。如果您定义了一个名为 fooProxyFactoryBean,则引用 foo 的对象不会看到 ProxyFactoryBean 实例本身,而是看到由 ProxyFactoryBean 中的 getObject() 方法实现创建的对象。此方法创建一个包装了目标对象的一个 AOP 代理。

使用 ProxyFactoryBean 或另一个 IoC 感知类来创建 AOP 代理的最重要的优点之一是建议和切点也可以由 IoC 管理。这是一个强大的功能,使得通过其他 AOP 框架难以实现的某些方法成为可能。例如,建议本身可以引用应用程序对象(除了目标,目标在任何 AOP 框架中都应该可用),还可以利用依赖注入所提供的全部可连接性。

JavaBean Properties

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

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

  • proxyTargetClasstrue ,如果要代理目标类,而不是目标类的界面。如果将该属性值设置为 true ,则创建 CGLIB 代理(但参见 JDK- and CGLIB-based proxies )。

  • optimize:控制是否对通过 CGLIB 创建的代理应用激进优化。除非你完全理解相关的 AOP 代理如何处理优化,否则不应草率地使用此设置。目前仅用于 CGLIB 代理。这对 JDK 动态代理没有影响。

  • frozen:如果代理配置为 frozen ,则不再允许更改配置。这既作为轻微的优化又适用于你不想让调用者在创建代理后能够操作代理(通过 Advised 接口)的情况。此属性的默认值是 false ,因此允许更改(例如添加更多建议)。

  • exposeProxy:确定当前代理是否应公开在 ThreadLocal 中,以便目标可以访问它。如果目标需要获取代理,并且 exposeProxy 属性设置为 true ,则目标可以使用 AopContext.currentProxy() 方法。

专门属于 ProxyFactoryBean 的其他属性包括:

  • proxyInterfaces:一系列 String 接口名称。如果未提供此项内容,则会为目标类使用 CGLIB 代理(但还可以参见 JDK- and CGLIB-based proxies)。

  • interceptorNames:要应用的 String 数组,拦截器或其他建议的名称。顺序很重要,先到先得。也就是说,列表中的第一个拦截器是第一个能够拦截调用的拦截器。名称是当前工厂中的 Bean 名称,包括祖先工厂的 Bean 名称。您无法在此处提及 Bean 引用,因为这样做会导致 ProxyFactoryBean 忽略建议的单例设置。 您可以追加一个带有星号 (*) 的拦截器名称。这样做会将名称以星号前部分开头的所有 advisors bean 应用到要应用的 advisors bean。您可以在 Using “Global” Advisors中找到使用此功能的示例。

  • singleton:无论 getObject() 方法被调用多少次,工厂是否应该返回一个对象。一些 FactoryBean 实现提供了这种方法。默认值是 true。如果你想使用状态建议(例如,对于状态混入),请将原型建议与 false 的 singleton 值一起使用。

JDK- and CGLIB-based proxies

本节用作有关 ProxyFactoryBean 如何选择为某个目标对象(要被代理)创建基于 JDK 的代理或基于 CGLIB 的代理的权威文档。

ProxyFactoryBean 从 1.2.x 和 2.0 版本之间修改了创建基于 JDK 或 CGLIB 代理的行为。ProxyFactoryBean 现在表现出与 TransactionProxyFactoryBean 类的类似语义,自动检测接口。

如果要被代理的目标对象(以下简称目标类)的类未实现任何接口,则会创建一个基于 CGLIB 的代理。这是最简单的场景,因为基于 JDK 的代理是基于接口的,而没有接口则意味着基于 JDK 的代理甚至无法进行。您可以插入目标 Bean 并通过设置 interceptorNames 属性指定拦截器列表。请注意,即使 ProxyFactoryBeanproxyTargetClass 属性已设置为“false”,也会创建一个基于 CGLIB 的代理。(这样做毫无意义,最好从 Bean 定义中删除,因为它充其量是多余的,最坏的情况会造成混乱。)

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

如果 ProxyFactoryBeanproxyTargetClass 属性已设置为 true,则会创建一个基于 CGLIB 的代理。这是有道理的,也符合最少惊讶原则。即使 ProxyFactoryBeanproxyInterfaces 属性已设置为一个或多个完全限定的接口名称,但由于 proxyTargetClass 属性已设置为“true”,会导致基于 CGLIB 的代理生效。

如果 ProxyFactoryBeanproxyInterfaces 属性已设置为一个或多个完全限定的接口名称,则会创建一个基于 JDK 的代理。创建的代理实现了在 proxyInterfaces 属性中指定的所有接口。如果目标类恰好实现了比 proxyInterfaces 属性中指定的目标类额外多的接口,那很好,但返回的代理不会实现这些其他接口。

如果尚未设置 ProxyFactoryBeanproxyInterfaces 属性,但目标类确实实现了某个(或多个)接口,则 ProxyFactoryBean 会自动检测到目标类实际实现了一个以上接口,并且会创建一个基于 JDK 的代理。实际代理的接口是目标类实现的所有接口。实际上,这与向 proxyInterfaces 属性提供目标类实现的每个接口的列表相同。不过,其工作量小得多,也不太容易发生印刷错误。

Proxying Interfaces

思考一个 ProxyFactoryBean 工作的简单示例。此示例包含:

  • 已代理的目标 bean。这是示例中的 personTarget Bean 定义。

  • 用于提供建议的 AdvisorInterceptor

  • AOP 代理 bean 定义用于指定目标对象(personTarget bean)、要代理的接口以及要应用的建议。

以下清单显示了该示例:

<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 列表,其中包含当前工厂中拦截器的 bean 名称或建议。你可以使用建议、拦截器,以及返回前、返回后和抛出建议对象。建议的顺序很重要。

你可能在想为什么列表不保留 bean 引用。原因是,如果 ProxyFactoryBean 的 single 值属性设置为 false,它必须能够返回独立代理实例。如果任何一个顾问本身是一个原型,则需要返回一个独立实例,因此有必要能够从工厂获取原型实例。保存引用是不够的。

前面显示的 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 来隐藏目标和代理之间的区别。只有 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 定义是自包含的。不过,有时能够从工厂获取未建议的目标实际上是一种优势(例如,在某些测试场景中)。

Proxying Classes

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

想象在我们的早期示例中,没有 Person 接口。我们需要建议一个未实现任何业务接口的,名为 Person 的类。在这种情况下,你可以将 Spring 配置为使用 CGLIB 代理,而不是动态代理。要执行此操作,请将前面所示 ProxyFactoryBean 上的 proxyTargetClass 属性设置为 true。尽管最好对接口而非类进行编程,但当处理遗留代码时,建议不实现接口的类的功能非常有用。(总的来说,Spring 并不是规定性的。虽然它使得应用良好实践变得容易,但它避免强制采用特定方法。)

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

CGLIB 代理通过在运行时生成目标类的子类来工作。Spring 将此生成的子类配置为可将方法调用委托给原始目标。子类用于实现装饰器模式,实现建议。

通常,CGLIB 代理对于用户来说应该是透明的。不过,需要考虑以下问题:

  • final 类不能被代理,因为它们不能被扩展。

  • final 方法不能被建议,因为它们不能被重写。

  • private 方法不能被建议,因为它们不能被重写。

  • 通常是父类(位于不同程序包)中的程序包私有方法等,这些不可见方法不能被建议,因为这些方法实际上是私有的。

无需将 CGLIB 添加到你的类路径。CGLIB 已重新打包并包含在 spring-core JAR 中。换句话说,基于 CGLIB 的 AOP 的开箱即用状态与 JDK 动态代理相同。

CGLIB 代理和动态代理之间的性能差异很小。在此情况下,性能不应该是决定性因素。

Using “Global” Advisors

通过向拦截器名称追加星号,所有与星号前部分匹配的 bean 名称的建议都会添加到建议链。如果你需要添加一组标准“全局”建议,这会派上用场。以下示例定义了两个全局建议:

<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"/>