使用 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:要应用的 Advisor、拦截器或其他通知名称的 String 数组。顺序很重要,先到先得。也就是说,列表中的第一个拦截器是第一个能够拦截调用的拦截器。

    名称是当前工厂中的 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 代理甚至不可能。你可以通过设置 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 定义。

  • 一个用于提供通知的 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 名称。你可以使用通知器、拦截器、前置通知、后置返回通知和抛出通知对象。通知器的顺序很重要。

你可能想知道为什么列表不包含 bean 引用。原因在于,如果 ProxyFactoryBean 的单例属性设置为 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 的优点是只有一个人类型的对象。这对于我们希望阻止应用程序上下文的用户获取未通知对象的引用,或者需要避免 Spring IoC 自动装配的任何歧义时非常有用。还有一点可以说是一个优点,那就是 ProxyFactoryBean 定义是自包含的。然而,有时能够从工厂获取未通知的目标实际上可能是一个优点(例如,在某些测试场景中)。

代理类

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

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

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

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

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

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

  • final 方法不能被通知,因为它们不能被重写。

  • private 方法不能被通知,因为它们不能被重写。

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

无需将 CGLIB 添加到你的 classpath。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"/>
© . This site is unofficial and not affiliated with VMware.