使用 ProxyFactoryBean 建立 AOP 代理

如果您為您的業務物件使用 Spring IoC 容器(ApplicationContextBeanFactory)(而且您應該這樣做!),您會想要使用 Spring 的 AOP FactoryBean 實作之一。(請記住,factory bean 引入了一個間接層,使其可以建立不同類型的物件。)

Spring AOP 支援在底層也使用了 factory bean。

在 Spring 中建立 AOP 代理的基本方法是使用 org.springframework.aop.framework.ProxyFactoryBean。這讓您可以完全控制切入點、任何適用的 advice 及其順序。但是,如果您不需要這種程度的控制,則有更簡單且更佳的選項。

基礎

ProxyFactoryBean,如同其他 Spring FactoryBean 實作,引入了一個間接層級。如果您定義一個名為 fooProxyFactoryBean,參考 foo 的物件不會看到 ProxyFactoryBean 實例本身,而是由 ProxyFactoryBeangetObject() 方法的實作所建立的物件。此方法會建立一個 AOP 代理,包裝目標物件。

使用 ProxyFactoryBean 或其他 IoC 感知類別來建立 AOP 代理,最重要的好處之一是 advice 和切入點也可以由 IoC 管理。這是一個強大的功能,可以實現某些其他 AOP 框架難以實現的方法。例如,advice 本身可以參考應用程式物件(除了目標物件,目標物件在任何 AOP 框架中都應該可用),從依賴注入提供的所有可插拔性中受益。

JavaBean 屬性

與 Spring 提供的多數 FactoryBean 實作相同,ProxyFactoryBean 類別本身也是一個 JavaBean。其屬性用於:

一些關鍵屬性繼承自 org.springframework.aop.framework.ProxyConfig(Spring 中所有 AOP 代理工廠的父類別)。這些關鍵屬性包括:

  • proxyTargetClass:如果應代理目標類別,而不是目標類別的介面,則為 true。如果此屬性值設定為 true,則會建立 CGLIB 代理(但另請參閱 基於 JDK 和 CGLIB 的代理)。

  • optimize:控制是否對透過 CGLIB 建立的代理應用積極的最佳化。除非您完全了解相關的 AOP 代理如何處理最佳化,否則不應輕易使用此設定。目前僅用於 CGLIB 代理。它對 JDK 動態代理沒有任何作用。

  • frozen:如果代理組態為 frozen,則不再允許變更組態。這既可用作輕微的最佳化,也適用於您不希望呼叫者在建立代理後(透過 Advised 介面)操作代理的情況。此屬性的預設值為 false,因此允許變更(例如新增額外的 advice)。

  • exposeProxy:決定是否應在 ThreadLocal 中公開目前的代理,以便目標可以存取它。如果目標需要取得代理且 exposeProxy 屬性設定為 true,則目標可以使用 AopContext.currentProxy() 方法。

ProxyFactoryBean 特有的其他屬性包括:

  • proxyInterfacesString 介面名稱陣列。如果未提供此項,則會使用目標類別的 CGLIB 代理(但另請參閱 基於 JDK 和 CGLIB 的代理)。

  • interceptorNames:要套用的 Advisor、攔截器或其他 advice 名稱的 String 陣列。順序很重要,先到先服務。也就是說,列表中的第一個攔截器是第一個能夠攔截調用的攔截器。

    這些名稱是目前工廠中的 Bean 名稱,包括來自祖先工廠的 Bean 名稱。您不能在此處提及 Bean 參考,因為這樣做會導致 ProxyFactoryBean 忽略 advice 的單例設定。

    您可以使用星號 (*) 附加攔截器名稱。這樣做會導致套用名稱以星號之前的部分開頭的所有 advisor Bean。您可以在 使用「全域」Advisor 中找到使用此功能的範例。

  • singleton:工廠是否應傳回單一物件,無論 getObject() 方法被呼叫多少次。多個 FactoryBean 實作提供此類方法。預設值為 true。如果您想要使用具狀態的 advice(例如,用於具狀態的 mixin),請將原型 advice 與 false 的單例值一起使用。

基於 JDK 和 CGLIB 的代理

本節作為關於 ProxyFactoryBean 如何選擇為特定目標物件(要代理的物件)建立基於 JDK 的代理或基於 CGLIB 的代理的權威文件。

ProxyFactoryBean 在 Spring 的 1.2.x 和 2.0 版本之間,在建立基於 JDK 或 CGLIB 的代理方面的行為發生了變化。ProxyFactoryBean 現在在自動偵測介面方面表現出與 TransactionProxyFactoryBean 類別相似的語意。

如果要代理的目標物件的類別(以下簡稱為目標類別)未實作任何介面,則會建立基於 CGLIB 的代理。這是最簡單的情況,因為 JDK 代理是基於介面的,而沒有介面意味著 JDK 代理甚至不可能。您可以插入目標 Bean,並透過設定 interceptorNames 屬性來指定攔截器列表。請注意,即使 ProxyFactoryBeanproxyTargetClass 屬性已設定為 false,也會建立基於 CGLIB 的代理。(這樣做沒有意義,最好從 Bean 定義中移除,因為它充其量是多餘的,最壞的情況是令人困惑。)

如果目標類別實作了一個(或多個)介面,則建立的代理類型取決於 ProxyFactoryBean 的組態。

如果 ProxyFactoryBeanproxyTargetClass 屬性已設定為 true,則會建立基於 CGLIB 的代理。這是合理的,並且符合最小驚奇原則。即使 ProxyFactoryBeanproxyInterfaces 屬性已設定為一個或多個完整限定的介面名稱,proxyTargetClass 屬性設定為 true 的事實也會導致基於 CGLIB 的代理生效。

如果 ProxyFactoryBeanproxyInterfaces 屬性已設定為一個或多個完整限定的介面名稱,則會建立基於 JDK 的代理。建立的代理實作了 proxyInterfaces 屬性中指定的所有介面。如果目標類別恰好實作了比 proxyInterfaces 屬性中指定的介面多得多的介面,那也很好,但傳回的代理不會實作這些額外的介面。

如果 ProxyFactoryBeanproxyInterfaces 屬性尚未設定,但目標類別確實實作了一個(或多個)介面,則 ProxyFactoryBean 會自動偵測到目標類別實際上至少實作了一個介面,並建立基於 JDK 的代理。實際代理的介面是目標類別實作的所有介面。實際上,這與將目標類別實作的每個介面列表提供給 proxyInterfaces 屬性相同。但是,這樣做的工作量明顯較少,而且不太容易出現印刷錯誤。

代理介面

考量 ProxyFactoryBean 運作的簡單範例。此範例包含:

  • 要代理的目標 Bean。這是範例中的 personTarget Bean 定義。

  • 用於提供 advice 的 AdvisorInterceptor

  • AOP 代理 Bean 定義,用於指定目標物件 (personTarget Bean)、要代理的介面以及要套用的 advice。

以下列表顯示了範例:

<bean id="personTarget" class="com.mycompany.PersonImpl">
	<property name="name" value="Tony"/>
	<property name="age" value="51"/>
</bean>

<bean id="myAdvisor" class="com.mycompany.MyAdvisor">
	<property name="someProperty" value="Custom string property value"/>
</bean>

<bean id="debugInterceptor" class="org.springframework.aop.interceptor.DebugInterceptor">
</bean>

<bean id="person"
	class="org.springframework.aop.framework.ProxyFactoryBean">
	<property name="proxyInterfaces" value="com.mycompany.Person"/>

	<property name="target" ref="personTarget"/>
	<property name="interceptorNames">
		<list>
			<value>myAdvisor</value>
			<value>debugInterceptor</value>
		</list>
	</property>
</bean>

請注意,interceptorNames 屬性採用 String 列表,其中包含目前工廠中攔截器或 advisor 的 Bean 名稱。您可以使用 advisor、攔截器、before、after returning 和 throws advice 物件。advisor 的順序很重要。

您可能想知道為什麼列表不包含 Bean 參考。原因是,如果 ProxyFactoryBean 的 singleton 屬性設定為 false,則它必須能夠傳回獨立的代理實例。如果任何 advisor 本身都是原型,則需要傳回獨立的實例,因此必須能夠從工廠取得原型的實例。持有參考是不夠的。

先前顯示的 person Bean 定義可以用來代替 Person 實作,如下所示:

  • Java

  • Kotlin

Person person = (Person) factory.getBean("person");
val person = factory.getBean("person") as Person

同一個 IoC 容器中的其他 Bean 可以表示對它的強型別依賴性,就像使用普通的 Java 物件一樣。以下範例顯示了如何執行此操作:

<bean id="personUser" class="com.mycompany.PersonUser">
	<property name="person"><ref bean="person"/></property>
</bean>

此範例中的 PersonUser 類別公開了 Person 型別的屬性。就其而言,AOP 代理可以透明地代替「真實」的 person 實作使用。但是,它的類別將是動態代理類別。可以將其轉換為 Advised 介面(稍後討論)。

您可以使用匿名內部 Bean 來隱藏目標和代理之間的區別。只有 ProxyFactoryBean 定義不同。advice 僅為了完整性而包含。以下範例顯示了如何使用匿名內部 Bean:

<bean id="myAdvisor" class="com.mycompany.MyAdvisor">
	<property name="someProperty" value="Custom string property value"/>
</bean>

<bean id="debugInterceptor" class="org.springframework.aop.interceptor.DebugInterceptor"/>

<bean id="person" class="org.springframework.aop.framework.ProxyFactoryBean">
	<property name="proxyInterfaces" value="com.mycompany.Person"/>
	<!-- Use inner bean, not local reference to target -->
	<property name="target">
		<bean class="com.mycompany.PersonImpl">
			<property name="name" value="Tony"/>
			<property name="age" value="51"/>
		</bean>
	</property>
	<property name="interceptorNames">
		<list>
			<value>myAdvisor</value>
			<value>debugInterceptor</value>
		</list>
	</property>
</bean>

使用匿名內部 Bean 的優點是只有一個 Person 型別的物件。如果我們想要防止應用程式容器的使用者取得未通知的物件的參考,或者需要避免 Spring IoC 自動裝配的任何歧義,這會很有用。可以說,ProxyFactoryBean 定義是自包含的,這也是一個優點。但是,在某些情況下(例如,在某些測試情境中),能夠從工廠取得未通知的目標實際上可能是一個優點。

代理類別

如果您需要代理類別,而不是一個或多個介面,該怎麼辦?

假設在我們先前的範例中,沒有 Person 介面。我們需要通知一個名為 Person 的類別,該類別未實作任何業務介面。在這種情況下,您可以將 Spring 組態為使用 CGLIB 代理,而不是動態代理。為此,請將先前顯示的 ProxyFactoryBean 上的 proxyTargetClass 屬性設定為 true。雖然最好針對介面而不是類別進行程式設計,但在處理舊版程式碼時,通知未實作介面的類別的能力可能很有用。(一般來說,Spring 並非規範性的。雖然它使應用良好實務變得容易,但它避免強制採用特定的方法。)

如果您願意,您可以強制在任何情況下都使用 CGLIB,即使您有介面。

CGLIB 代理的工作方式是在執行時期產生目標類別的子類別。Spring 組態此產生的子類別,以將方法調用委派給原始目標。子類別用於實作裝飾器模式,編織 advice。

CGLIB 代理通常對使用者是透明的。但是,有一些問題需要考量:

  • final 類別無法代理,因為它們無法擴展。

  • final 方法無法通知,因為它們無法覆寫。

  • private 方法無法通知,因為它們無法覆寫。

  • 不可見的方法(通常是來自不同套件的父類別中的套件私有方法)無法通知,因為它們實際上是私有的。

無需將 CGLIB 新增至您的類別路徑。CGLIB 已重新封裝並包含在 spring-core JAR 中。換句話說,基於 CGLIB 的 AOP 可以「開箱即用」,JDK 動態代理也是如此。

CGLIB 代理和動態代理之間的效能差異很小。在這種情況下,效能不應成為決定性的考量因素。

使用「全域」Advisor

透過將星號附加到攔截器名稱,所有 Bean 名稱與星號之前的部分匹配的 advisor 都會新增至 advisor 鏈。如果您需要新增一組標準的「全域」advisor,這可能會派上用場。以下範例定義了兩個全域 advisor:

<bean id="proxy" class="org.springframework.aop.framework.ProxyFactoryBean">
	<property name="target" ref="service"/>
	<property name="interceptorNames">
		<list>
			<value>global*</value>
		</list>
	</property>
</bean>

<bean id="global_debug" class="org.springframework.aop.interceptor.DebugInterceptor"/>
<bean id="global_performance" class="org.springframework.aop.interceptor.PerformanceMonitorInterceptor"/>