使用 ProxyFactoryBean 创建 AOP 代理

如果你使用 Spring IoC 容器(ApplicationContextBeanFactory)来管理你的业务对象(并且你应该这样做!),你会希望使用 Spring 的一种 AOP FactoryBean 实现。(记住,工厂 bean 引入了一个间接层,允许它创建不同类型的对象。)

Spring AOP 支持在内部也使用了工厂 bean。

在 Spring 中创建 AOP 代理的基本方法是使用 org.springframework.aop.framework.ProxyFactoryBean。这提供了对切点、应用的任何通知及其顺序的完全控制。然而,如果你不需要这样的控制,还有更简单的选项更可取。

基础

与其他 Spring FactoryBean 实现一样,ProxyFactoryBean 引入了一层间接性。如果你定义了一个名为 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:一个包含 Advisor、拦截器或其他通知名称的 String 数组,用于应用。顺序很重要,遵循先到先得的原则。也就是说,列表中的第一个拦截器是第一个能够拦截调用的。

    这些名称是当前工厂中的 bean 名称,包括祖先工厂中的 bean 名称。这里不能提及 bean 引用,因为这样做会导致 ProxyFactoryBean 忽略通知的 singleton 设置。

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

  • singleton:工厂是否应返回单个对象,无论 getObject() 方法被调用多少次。一些 FactoryBean 实现提供了此方法。默认值为 true。如果你想使用有状态的通知(例如,用于有状态的 mixin),请使用 prototype 通知并设置 singleton 值为 false

基于 JDK 和基于 CGLIB 的代理

本节作为权威文档,说明了 ProxyFactoryBean 如何为一个特定目标对象(即将被代理的对象)选择创建基于 JDK 的代理还是基于 CGLIB 的代理。

ProxyFactoryBean 在创建基于 JDK 或基于 CGLIB 的代理方面的行为在 Spring 1.2.x 和 2.0 版本之间发生了变化。ProxyFactoryBean 现在在自动检测接口方面表现出与 TransactionProxyFactoryBean 类相似的语义。

如果要被代理的目标对象的类(下文简称目标类)没有实现任何接口,则创建基于 CGLIB 的代理。这是最简单的情况,因为 JDK 代理是基于接口的,没有接口意味着甚至无法进行 JDK 代理。你可以通过设置 interceptorNames 属性来指定目标 bean 和拦截器列表。请注意,即使 ProxyFactoryBeanproxyTargetClass 属性已设置为 false,也会创建基于 CGLIB 的代理。(这样做没有意义,最好从 bean 定义中删除,因为它充其量是多余的,最糟糕是令人困惑的。)

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

如果 ProxyFactoryBeanproxyTargetClass 属性已设置为 true,则创建基于 CGLIB 的代理。这符合常理并与最小意外原则一致。即使 ProxyFactoryBeanproxyInterfaces 属性已设置为一个或多个完全限定的接口名称,但 proxyTargetClass 属性设置为 true 的事实会导致 CGLIB 代理生效。

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

如果 ProxyFactoryBeanproxyInterfaces 属性未设置,但目标类确实实现了一个(或多个)接口,则 ProxyFactoryBean 会自动检测到目标类实际上实现了至少一个接口的事实,并创建一个基于 JDK 的代理。实际被代理的接口是目标类实现的所有接口。实际上,这与将目标类实现的每个接口都提供给 proxyInterfaces 属性相同。然而,这种方法的工作量显著减少,并且不太容易出现拼写错误。

代理接口

考虑一个 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 列表,其中包含当前工厂中的拦截器或 advisor 的 bean 名称。你可以使用 advisor、拦截器、前置、后置返回和抛出异常通知对象。advisor 的顺序很重要。

你可能想知道为什么列表不包含 bean 引用。原因是,如果 ProxyFactoryBean 的 singleton 属性设置为 false,它必须能够返回独立的代理实例。如果任何 advisor 本身是 prototype,则需要返回一个独立的实例,因此必须能够从工厂中获取 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 实现使用。然而,它的类将是一个动态代理类。可以将其转换为 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 方法不能被通知,因为它们不能被覆盖。

  • 不可见的方法,通常是不同包中父类里的 package private 方法,不能被通知,因为它们实际上是 private 的。

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

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

使用“全局”Advisor

在拦截器名称后附加一个星号,所有 bean 名称与星号前部分匹配的 advisor 都会被添加到 advisor 链中。如果你需要添加一组标准的“全局”advisor,这会非常有用。以下示例定义了两个全局 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"/>