使用 ProxyFactoryBean
建立 AOP 代理
如果你使用 Spring IoC 容器(ApplicationContext
或 BeanFactory
)來管理你的業務物件(並且你應該這樣做!),你會希望使用 Spring 的一種 AOP FactoryBean
實現。(記住,工廠 bean 引入了一個間接層,允許它建立不同型別的物件。)
Spring AOP 支援在內部也使用了工廠 bean。 |
在 Spring 中建立 AOP 代理的基本方法是使用 org.springframework.aop.framework.ProxyFactoryBean
。這提供了對切點、應用的任何通知及其順序的完全控制。然而,如果你不需要這樣的控制,還有更簡單的選項更可取。
基礎
與其他 Spring FactoryBean
實現一樣,ProxyFactoryBean
引入了一層間接性。如果你定義了一個名為 foo
的 ProxyFactoryBean
,引用 foo
的物件看到的不是 ProxyFactoryBean
例項本身,而是由 ProxyFactoryBean
的 getObject()
方法實現建立的物件。這個方法建立一個包裝了目標物件的 AOP 代理。
使用 ProxyFactoryBean
或其他 IoC 感知類來建立 AOP 代理的最重要好處之一是,通知和切點也可以由 IoC 管理。這是一個強大的特性,使得其他 AOP 框架難以實現的一些方法成為可能。例如,通知本身可以引用應用程式物件(除了目標物件,目標物件在任何 AOP 框架中都應該可用),從而受益於依賴注入提供的所有可插拔性。
JavaBean 屬性
與 Spring 提供的多數 FactoryBean
實現一樣,ProxyFactoryBean
類本身是一個 JavaBean。它的屬性用於
-
指定要代理的目標。
-
指定是否使用 CGLIB(後面會描述,另請參見 基於 JDK 和基於 CGLIB 的代理)。
一些關鍵屬性繼承自 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 和攔截器列表。請注意,即使 ProxyFactoryBean
的 proxyTargetClass
屬性已設定為 false
,也會建立基於 CGLIB 的代理。(這樣做沒有意義,最好從 bean 定義中刪除,因為它充其量是多餘的,最糟糕是令人困惑的。)
如果目標類實現了一個(或多個)介面,則建立的代理型別取決於 ProxyFactoryBean
的配置。
如果 ProxyFactoryBean
的 proxyTargetClass
屬性已設定為 true
,則建立基於 CGLIB 的代理。這符合常理並與最小意外原則一致。即使 ProxyFactoryBean
的 proxyInterfaces
屬性已設定為一個或多個完全限定的介面名稱,但 proxyTargetClass
屬性設定為 true
的事實會導致 CGLIB 代理生效。
如果 ProxyFactoryBean
的 proxyInterfaces
屬性已設定為一個或多個完全限定的介面名稱,則建立基於 JDK 的代理。建立的代理實現了 proxyInterfaces
屬性中指定的所有介面。如果目標類碰巧實現了比 proxyInterfaces
屬性中指定的介面更多的介面,那也很好,但這些額外的介面不會由返回的代理實現。
如果 ProxyFactoryBean
的 proxyInterfaces
屬性未設定,但目標類確實實現了一個(或多個)介面,則 ProxyFactoryBean
會自動檢測到目標類實際上實現了至少一個介面的事實,並建立一個基於 JDK 的代理。實際被代理的介面是目標類實現的所有介面。實際上,這與將目標類實現的每個介面都提供給 proxyInterfaces
屬性相同。然而,這種方法的工作量顯著減少,並且不太容易出現拼寫錯誤。
代理介面
考慮一個 ProxyFactoryBean
實際應用的簡單示例。此示例涉及
-
一個被代理的目標 bean。在示例中,這就是
personTarget
bean 的定義。 -
用於提供通知的
Advisor
和Interceptor
。 -
一個 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"/>