使用 TargetSource 实现

Spring 提供了 TargetSource 的概念,它通过 org.springframework.aop.TargetSource 接口来表达。此接口负责返回实现连接点的“目标对象”。每次 AOP 代理处理方法调用时,都会向 TargetSource 实现请求一个目标实例。使用 Spring AOP 的开发者通常不需要直接使用 TargetSource 实现,但这提供了一种支持池化、热插拔和其他复杂目标的强大方式。例如,一个池化的 TargetSource 可以通过使用池来管理实例,为每次调用返回不同的目标实例。如果您没有指定 TargetSource,则会使用默认实现来包装一个本地对象。每次调用都会返回相同的目标(正如您所期望的)。本节的其余部分描述了 Spring 提供的标准目标源以及如何使用它们。

使用自定义目标源时,您的目标通常需要是原型而不是单例 bean 定义。这允许 Spring 在需要时创建新的目标实例。

热插拔目标源

org.springframework.aop.target.HotSwappableTargetSource 的存在是为了允许 AOP 代理的目标在调用者保持对其引用的同时进行切换。

更改目标源的目标会立即生效。HotSwappableTargetSource 是线程安全的。

您可以使用 HotSwappableTargetSource 上的 swap() 方法更改目标,示例如下:

  • Java

  • Kotlin

HotSwappableTargetSource swapper = (HotSwappableTargetSource) beanFactory.getBean("swapper");
Object oldTarget = swapper.swap(newTarget);
val swapper = beanFactory.getBean("swapper") as HotSwappableTargetSource
val oldTarget = swapper.swap(newTarget)

以下示例显示了所需的 XML 定义:

<bean id="initialTarget" class="mycompany.OldTarget"/>

<bean id="swapper" class="org.springframework.aop.target.HotSwappableTargetSource">
	<constructor-arg ref="initialTarget"/>
</bean>

<bean id="swappable" class="org.springframework.aop.framework.ProxyFactoryBean">
	<property name="targetSource" ref="swapper"/>
</bean>

前面的 swap() 调用会更改可插拔 bean 的目标。持有该 bean 引用的客户端不会意识到这一更改,但会立即开始访问新目标。

尽管此示例没有添加任何通知(使用 TargetSource 不需要添加通知),但任何 TargetSource 都可以与任意通知结合使用。

池化目标源

使用池化目标源提供了一种类似于无状态会话 EJB 的编程模型,其中维护一个相同实例的池,方法调用会发送给池中的空闲对象。

Spring 池化与 SLSB 池化之间的一个关键区别在于,Spring 池化可以应用于任何 POJO。与 Spring 通常一样,此服务可以以非侵入式方式应用。

Spring 支持 Commons Pool 2.2,它提供了一个相当高效的池化实现。您需要在应用程序的类路径中包含 commons-pool Jar 才能使用此功能。您还可以子类化 org.springframework.aop.target.AbstractPoolingTargetSource 以支持任何其他池化 API。

Commons Pool 1.5+ 也受支持,但自 Spring Framework 4.2 起已弃用。

以下清单显示了一个示例配置:

<bean id="businessObjectTarget" class="com.mycompany.MyBusinessObject"
		scope="prototype">
	... properties omitted
</bean>

<bean id="poolTargetSource" class="org.springframework.aop.target.CommonsPool2TargetSource">
	<property name="targetBeanName" value="businessObjectTarget"/>
	<property name="maxSize" value="25"/>
</bean>

<bean id="businessObject" class="org.springframework.aop.framework.ProxyFactoryBean">
	<property name="targetSource" ref="poolTargetSource"/>
	<property name="interceptorNames" value="myInterceptor"/>
</bean>

请注意,目标对象(在前面的示例中为 businessObjectTarget)必须是原型。这允许 PoolingTargetSource 实现根据需要创建目标的新实例以扩充池。有关其属性的信息,请参阅 AbstractPoolingTargetSource 的 javadoc 和您希望使用的具体子类。maxSize 是最基本的属性,并且始终保证存在。

在此示例中,myInterceptor 是一个拦截器的名称,需要在相同的 IoC 上下文中定义。但是,您不需要指定拦截器来使用池化。如果您只想要池化而没有其他通知,则根本不要设置 interceptorNames 属性。

您可以将 Spring 配置为能够将任何池化对象转换为 org.springframework.aop.target.PoolingConfig 接口,该接口通过引入公开有关池的配置和当前大小的信息。您需要定义一个类似于以下的切面:

<bean id="poolConfigAdvisor" class="org.springframework.beans.factory.config.MethodInvokingFactoryBean">
	<property name="targetObject" ref="poolTargetSource"/>
	<property name="targetMethod" value="getPoolingConfigMixin"/>
</bean>

此切面是通过调用 AbstractPoolingTargetSource 类上的便利方法获得的,因此使用了 MethodInvokingFactoryBean。此切面的名称(此处为 poolConfigAdvisor)必须在公开池化对象的 ProxyFactoryBean 的拦截器名称列表中。

转换定义如下:

  • Java

  • Kotlin

PoolingConfig conf = (PoolingConfig) beanFactory.getBean("businessObject");
System.out.println("Max pool size is " + conf.getMaxSize());
val conf = beanFactory.getBean("businessObject") as PoolingConfig
println("Max pool size is " + conf.maxSize)

池化无状态服务对象通常不是必需的。我们不认为它应该是默认选择,因为大多数无状态对象天生就是线程安全的,如果资源被缓存,实例池化就会出现问题。

通过使用自动代理,可以实现更简单的池化。您可以设置任何自动代理创建器使用的 TargetSource 实现。

原型目标源

设置“原型”目标源类似于设置池化 TargetSource。在这种情况下,每次方法调用都会创建一个新的目标实例。虽然在现代 JVM 中创建新对象的成本不高,但连接新对象(满足其 IoC 依赖项)的成本可能更高。因此,如果没有非常好的理由,您不应使用此方法。

为此,您可以修改前面显示的 poolTargetSource 定义,如下所示(为清楚起见,我们也更改了名称):

<bean id="prototypeTargetSource" class="org.springframework.aop.target.PrototypeTargetSource">
	<property name="targetBeanName" ref="businessObjectTarget"/>
</bean>

唯一的属性是目标 bean 的名称。TargetSource 实现中使用了继承来确保命名一致性。与池化目标源一样,目标 bean 必须是原型 bean 定义。

ThreadLocal 目标源

如果您需要为每个传入请求(即每个线程)创建一个对象,ThreadLocal 目标源会很有用。ThreadLocal 的概念提供了一个 JDK 范围的工具,可以透明地将资源与线程一起存储。设置 ThreadLocalTargetSource 与其他类型的目标源的解释大致相同,示例如下:

<bean id="threadlocalTargetSource" class="org.springframework.aop.target.ThreadLocalTargetSource">
	<property name="targetBeanName" value="businessObjectTarget"/>
</bean>

ThreadLocal 实例在多线程和多类加载器环境中错误使用时会带来严重问题(可能导致内存泄漏)。您应始终考虑将 ThreadLocal 包装在其他类中,并且永远不要直接使用 ThreadLocal 本身(包装类中除外)。此外,您应始终记住正确设置和取消设置(后者涉及调用 ThreadLocal.remove())线程本地的资源。无论如何都应取消设置,因为不取消设置可能导致问题行为。Spring 的 ThreadLocal 支持为您完成了这项工作,并且应始终优先于在没有其他适当处理代码的情况下使用 ThreadLocal 实例。