使用 ProxyFactoryBean 建立 AOP 代理

如果你使用 Spring IoC 容器(ApplicationContextBeanFactory)來管理你的業務物件(並且你應該這樣做!),你會希望使用 Spring 的一種 AOP FactoryBean 實現。(記住,工廠 bean 引入了一個間接層,允許它建立不同型別的物件。)

Spring AOP 支援在內部也使用了工廠 bean。

在 Spring 中建立 AOP 代理的基本方法是使用 org.springframework.aop.framework.ProxyFactoryBean。這提供了對切點、應用的任何通知及其順序的完全控制。然而,如果你不需要這樣的控制,還有更簡單的選項更可取。

基礎

與其他 Spring FactoryBean 實現一樣,ProxyFactoryBean 引入了一層間接性。如果你定義了一個名為 fooProxyFactoryBean,引用 foo 的物件看到的不是 ProxyFactoryBean 例項本身,而是由 ProxyFactoryBeangetObject() 方法實現建立的物件。這個方法建立一個包裝了目標物件的 AOP 代理。

使用 ProxyFactoryBean 或其他 IoC 感知類來建立 AOP 代理的最重要好處之一是,通知和切點也可以由 IoC 管理。這是一個強大的特性,使得其他 AOP 框架難以實現的一些方法成為可能。例如,通知本身可以引用應用程式物件(除了目標物件,目標物件在任何 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,因此允許更改(例如新增額外通知)。

  • exposeProxy:確定當前代理是否應暴露在 ThreadLocal 中,以便目標物件可以訪問它。如果目標物件需要獲取代理且 exposeProxy 屬性設為 true,則目標物件可以使用 AopContext.currentProxy() 方法。

ProxyFactoryBean 特有的其他屬性包括以下內容

  • proxyInterfaces:一個包含 String 介面名稱的陣列。如果未提供此屬性,則使用目標類的 CGLIB 代理(另請參見 基於 JDK 和基於 CGLIB 的代理)。

  • interceptorNames:一個包含 Advisor、攔截器或其他通知名稱的 String 陣列,用於應用。順序很重要,遵循先到先得的原則。也就是說,列表中的第一個攔截器是第一個能夠攔截呼叫的。

    這些名稱是當前工廠中的 bean 名稱,包括祖先工廠中的 bean 名稱。這裡不能提及 bean 引用,因為這樣做會導致 ProxyFactoryBean 忽略通知的 singleton 設定。

    你可以在攔截器名稱後附加一個星號 (*)。這樣做會導致應用所有名稱以星號之前的部開頭的 advisor bean。你可以在 使用“全域性”Advisor 中找到使用此功能的示例。

  • singleton:工廠是否應返回單個物件,無論 getObject() 方法被呼叫多少次。一些 FactoryBean 實現提供了此方法。預設值為 true。如果你想使用有狀態的通知(例如,用於有狀態的 mixin),請使用 prototype 通知並設定 singleton 值為 false

基於 JDK 和基於 CGLIB 的代理

本節作為權威文件,說明了 ProxyFactoryBean 如何為一個特定目標物件(即將被代理的物件)選擇建立基於 JDK 的代理還是基於 CGLIB 的代理。

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

如果要被代理的目標物件的類(下文簡稱目標類)沒有實現任何介面,則建立基於 CGLIB 的代理。這是最簡單的情況,因為 JDK 代理是基於介面的,沒有介面意味著甚至無法進行 JDK 代理。你可以透過設定 interceptorNames 屬性來指定目標 bean 和攔截器列表。請注意,即使 ProxyFactoryBeanproxyTargetClass 屬性已設定為 false,也會建立基於 CGLIB 的代理。(這樣做沒有意義,最好從 bean 定義中刪除,因為它充其量是多餘的,最糟糕是令人困惑的。)

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

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

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

如果 ProxyFactoryBeanproxyInterfaces 屬性未設定,但目標類確實實現了一個(或多個)介面,則 ProxyFactoryBean 會自動檢測到目標類實際上實現了至少一個介面的事實,並建立一個基於 JDK 的代理。實際被代理的介面是目標類實現的所有介面。實際上,這與將目標類實現的每個介面都提供給 proxyInterfaces 屬性相同。然而,這種方法的工作量顯著減少,並且不太容易出現拼寫錯誤。

代理介面

考慮一個 ProxyFactoryBean 實際應用的簡單示例。此示例涉及

  • 一個被代理的目標 bean。在示例中,這就是 personTarget bean 的定義。

  • 用於提供通知的 AdvisorInterceptor

  • 一個 AOP 代理 bean 定義,用於指定目標物件(personTarget bean)、要代理的介面和要應用的通知。

以下列表顯示了示例

<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、攔截器、前置、後置返回和丟擲異常通知物件。advisor 的順序很重要。

你可能想知道為什麼列表不包含 bean 引用。原因是,如果 ProxyFactoryBean 的 singleton 屬性設定為 false,它必須能夠返回獨立的代理例項。如果任何 advisor 本身是 prototype,則需要返回一個獨立的例項,因此必須能夠從工廠中獲取 prototype 的例項。持有引用是不夠的。

前面顯示的 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 定義不同。通知僅為了完整性而包含。以下示例顯示瞭如何使用匿名內部 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 配置這個生成的子類,將方法呼叫委託給原始目標。這個子類用於實現裝飾器模式,並織入通知。

CGLIB 代理通常對使用者來說應該是透明的。然而,有一些問題需要考慮

  • final 類不能被代理,因為它們不能被繼承。

  • final 方法不能被通知,因為它們不能被覆蓋。

  • private 方法不能被通知,因為它們不能被覆蓋。

  • 不可見的方法,通常是不同包中父類裡的 package private 方法,不能被通知,因為它們實際上是 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"/>