使用 TargetSource 实现

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

通常,使用 Spring AOP 的开发者无需直接使用 TargetSource 实现,但这提供了一种强大的方式来支持池化、热插拔和其他复杂目标。例如,池化 TargetSource 可以通过使用池来管理实例,从而为每次调用返回不同的目标实例。

如果你没有指定 TargetSource,则会使用默认实现来包装本地对象。每次调用都返回同一个目标(正如你所期望的)。

本节的其余部分描述了 Spring 提供的标准目标源以及如何使用它们。

使用自定义目标源时,你的目标通常需要是原型 (prototype) Bean 定义,而不是单例 (singleton) 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 放在应用的 classpath 中才能使用此功能。你还可以继承 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)必须是原型 (prototype)。这使得 PoolingTargetSource 实现能够在必要时创建新的目标实例来扩大池。有关其属性的信息,请参阅 AbstractPoolingTargetSource 的 javadoc 以及你要使用的具体子类。maxSize 是最基本的属性,并且始终保证存在。

在这种情况下,myInterceptor 是需要在同一个 IoC 上下文中定义的拦截器的名称。但是,你不需要指定拦截器来使用池。如果你只想要池化而没有其他通知,则完全不要设置 interceptorNames 属性。

你可以配置 Spring 使任何池化对象都可以转换为 org.springframework.aop.target.PoolingConfig 接口,该接口通过一个引入暴露有关池配置和当前大小的信息。你需要定义一个类似于以下的 Advisor:

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

这个 Advisor 是通过调用 AbstractPoolingTargetSource 类上的一个便利方法获得的,因此使用了 MethodInvokingFactoryBean。该 Advisor 的名称(此处为 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 实例。