Spring 提供了 TargetSource 的概念,用 org.springframework.aop.TargetSource
接口表示。这个接口负责返回实现 连接点的 「目标对象」。每次 AOP 代理处理方法调用时,都会要求 TargetSource 实现提供一个目标实例。
使用 Spring AOP 的开发者通常不需要直接使用 TargetSource 实现,但这提供了支持池、热插拔和其他复杂目标的强大手段。例如,通过使用池来管理实例,一个池化的 TargetSource 可以为每次调用返回不同的目标实例。
如果你没有指定 TargetSource,就会使用一个默认的实现来包装一个本地对象。每次调用都会返回同一个目标(正如你所期望的)。
本节的其余部分描述了 Spring 提供的标准目标源以及你如何使用它们。
:::tips 当使用自定义目标源时,你的目标通常需要是一个 多例,而不是一个单例 bean 定义。这允许 Spring 在需要时创建一个新的目标实例。 :::
可热插拔的目标源
org.springframework.aop.target.HotSwappableTargetSource
的存在是为了让 AOP 代理的目标被切换,同时让调用者保留他们对它的引用。
改变目标源的目标会立即生效。HotSwappableTargetSource 是线程安全的。
你可以通过使用 HotSwappableTargetSource 的 swap()
方法来改变目标,如下面的例子所示:
HotSwappableTargetSource swapper = (HotSwappableTargetSource) beanFactory.getBean("swapper");
Object 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>
<!-- 在给目标类定义代理的时候,使用 targetSource 属性 指向 热插播目标源-->
<bean id="swappable" class="org.springframework.aop.framework.ProxyFactoryBean">
<property name="targetSource" ref="swapper"/>
</bean>
前面的 swap()
调用改变了可交换 Bean 的目标。持有对该 Bean 的引用的客户端并不知道这一变化,而是立即开始攻击新的目标。
虽然这个例子没有添加任何 advice (使用 TargetSource 不一定要添加 advice),但任何 TargetSource 都可以与任意 advice 一起使用。
Pooling / 池 目标源
使用池化目标源提供了一个类似于无状态会话 EJB 的编程模型,在这个模型中,一个相同的实例池被维护,方法的调用将被用于池中的自由对象。
Spring 池 和 SLSB 池的一个重要区别是,Spring 池可以应用于任何 POJO。正如 Spring 一般,这种服务可以以非侵入性的方式应用。
Spring 提供了对 Commons Pool 2.2 的支持,它提供了一个相当高效的池化实现。你需要在你的应用程序的 classpath 上找到 commons-pool Jar 来使用这个功能。你也可以子类化 org.springframework.aop.target.AbstractPoolingTargetSource
,以支持任何其他池化 API。
:::info Commons Pool 1.5+也被支持,但从 Spring Framework 4.2 开始被废弃。 :::
下面的列表显示了一个配置的例子:
<!-- 业务对象,多例的,用于池中存放的对象 -->
<bean id="businessObjectTarget" class="com.mycompany.MyBusinessObject"
scope="prototype">
... properties omitted
</bean>
<!-- CommonsPool2 实现的池化目标源-->
<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 中的拦截器名称列表中。
定义如下:
PoolingConfig conf = (PoolingConfig) beanFactory.getBean("businessObject");
System.out.println("Max pool size is " + conf.getMaxSize());
:::info 通常没有必要对无状态服务对象进行池化。我们认为这不应该是默认的选择,因为大多数无状态对象自然是线程安全的,而且如果资源被缓存,实例池就会出现问题。 :::
通过使用自动代理,可以获得更简单的池子。你可以设置任何自动代理创建者所使用的 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>
:::info 在多线程和多类加载器环境中不正确地使用 ThreadLocal 实例会带来严重的问题(有可能导致内存泄露)。你应该总是考虑在其他类中包装一个线程局部,而不要直接使用线程局部本身(除了在包装类中)。此外,你应该始终记得正确地设置和取消设置(后者只需要调用ThreadLocal.set(null))线程的本地资源。在任何情况下都应该取消设置,因为不取消设置可能会导致有问题的行为。Spring 的 ThreadLocal 支持为你做到了这一点,在使用 ThreadLocal 实例时,应始终考虑到没有其他适当的处理代码。 :::