使用 TargetSource 实现

Spring 提供了 `TargetSource` 的概念,在 `org.springframework.aop.TargetSource` 接口中表达。此接口负责返回实现连接点的“目标对象”。每次 AOP 代理处理方法调用时,都会向 `TargetSource` 实现请求目标实例。

使用 Spring AOP 的开发者通常不需要直接使用 `TargetSource` 实现,但这提供了一种强大的方法来支持池化、热插拔和其他复杂的 target。例如,池化 `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 引用的客户端不知道此更改,但会立即开始命中新目标。

尽管此示例没有添加任何 advice(使用 `TargetSource` 不需要添加 advice),但任何 `TargetSource` 都可以与任意 advice 一起使用。

池化目标源

使用池化目标源提供了与无状态会话 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 上下文中定义的拦截器的名称。但是,您不需要指定拦截器来使用池化。如果您只想池化而不需要其他 advice,则根本不要设置 `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 实例,而不是没有其他适当处理代码的情况。