使用 ProxyFactoryBean 创建 AOP 代理

如果您使用 Spring IoC 容器(ApplicationContextBeanFactory)来管理您的业务对象(您应该这样做!),那么您需要使用 Spring 的 AOP FactoryBean 实现之一。(请记住,工厂 Bean 引入了一层间接性,允许它创建不同类型的对象。)

Spring AOP 支持也在幕后使用工厂 Bean。

在 Spring 中创建 AOP 代理的基本方法是使用 org.springframework.aop.framework.ProxyFactoryBean。这可以完全控制切点、任何适用的建议以及它们的顺序。但是,如果您不需要这种控制,则有一些更简单的选项更可取。

基础知识

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

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

JavaBean 属性

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

一些关键属性继承自 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 忽略建议的单例设置。

    您可以将拦截器名称附加一个星号 (*)。这样做会导致将所有名称以星号之前的部分开头的顾问 Bean 应用。您可以在 使用“全局”顾问 中找到使用此功能的示例。

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

基于 JDK 和 CGLIB 的代理

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

关于创建基于 JDK 或基于 CGLIB 的代理,ProxyFactoryBean 的行为在 Spring 的 1.2.x 版和 2.0 版之间发生了变化。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属性提供目标类实现的每个接口的列表相同。但是,这工作量要少得多,并且不易出现打字错误。

代理接口

考虑一个ProxyFactoryBean运行的简单示例。此示例涉及

  • 一个被代理的目标 Bean。这是示例中personTarget Bean 定义。

  • 一个Advisor和一个Interceptor用于提供建议。

  • 一个 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 名称。您可以使用顾问、拦截器、before、after returning 和 throws advice 对象。顾问的排序顺序很重要。

您可能想知道为什么列表不包含 Bean 引用。原因是,如果ProxyFactoryBean的 singleton 属性设置为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 代理可以透明地代替“真实”的 Person 实现使用。但是,它的类将是一个动态代理类。可以将其强制转换为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定义是自包含的。但是,有时能够从工厂中获取未建议的目标实际上可能是一个优势(例如,在某些测试场景中)。

代理类

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

假设在我们之前的示例中,没有Person接口。我们需要建议一个名为Person的类,该类没有实现任何业务接口。在这种情况下,您可以配置 Spring 使用 CGLIB 代理而不是动态代理。为此,将前面显示的ProxyFactoryBean上的proxyTargetClass属性设置为true。虽然最好对接口而不是类进行编程,但在处理遗留代码时,建议不实现接口的类的能力可能很有用。(通常,Spring 并不具有规定性。虽然它使应用最佳实践变得容易,但它避免强制使用特定方法。)

如果需要,您可以在任何情况下强制使用 CGLIB,即使您确实有接口。

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

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

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

  • 不能建议final方法,因为它们不能被覆盖。

  • 不能建议private方法,因为它们不能被覆盖。

  • 不可见的那些方法,通常是来自不同包的父类中的包私有方法,不能被建议,因为它们实际上是私有的。

无需将 CGLIB 添加到类路径中。CGLIB 已重新打包并包含在spring-core JAR 中。换句话说,基于 CGLIB 的 AOP 可以“开箱即用”,就像 JDK 动态代理一样。

CGLIB 代理和动态代理之间的性能差异很小。在这种情况下,性能不应成为决定性因素。

使用“全局”顾问

通过将星号附加到拦截器名称,所有 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"/>