使用 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 特有的其他属性包括:

  • proxyInterfacesString 接口名称数组。如果未提供此项,则使用目标类的 CGLIB 代理(但另请参阅 基于 JDK 和基于 CGLIB 的代理)。

  • interceptorNames:一个 String 数组,包含要应用的 Advisor、拦截器或其他通知的名称。顺序很重要,先到先得。也就是说,列表中的第一个拦截器是第一个能够拦截调用的拦截器。这些名称是当前工厂中的 bean 名称,包括来自祖先工厂的 bean 名称。你不能在此处提及 bean 引用,因为这样做会导致 ProxyFactoryBean 忽略通知的单例设置。 你可以在拦截器名称后附加一个星号(*)。这样做会导致应用所有名称以星号前部分开头的 Advisor bean。你可以在 使用“全局”Advisor 中找到使用此功能的示例。

  • singleton:无论 getObject() 方法被调用多少次,工厂是否都应返回单个对象。几个 FactoryBean 实现提供了这种方法。默认值为 true。如果你想使用有状态通知(例如,用于有状态 mixin),请使用原型通知以及 false 的单例值。

基于 JDK 和基于 CGLIB 的代理

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

ProxyFactoryBean 在创建基于 JDK 或基于 CGLIB 的代理方面的行为在 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 列表,其中包含当前工厂中拦截器或 Advisor 的 bean 名称。你可以使用 Advisor、拦截器、前置、后置返回和抛出通知对象。Advisor 的顺序很重要。

你可能想知道为什么列表不包含 bean 引用。原因在于,如果 ProxyFactoryBeansingleton 属性设置为 false,它必须能够返回独立的代理实例。如果任何 Advisor 本身是原型,则需要返回一个独立的实例,因此必须能够从工厂获取原型的实例。持有引用是不够的。

前面显示的 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 定义是自包含的,这也是一个优点。然而,有时能够从工厂获取未通知的目标实际上可能是一个优点(例如,在某些测试场景中)。

代理类

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

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

如果你愿意,即使有接口,也可以强制使用 CGLIB。

CGLIB 代理通过在运行时生成目标类的子类来工作。Spring 配置这个生成的子类以将方法调用委托给原始目标。子类用于实现 Decorator 模式,将通知编织进去。

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

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

  • final 方法不能被通知,因为它们不能被覆盖。

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