使用 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 例項。