使用 ProxyFactoryBean
建立 AOP 代理
如果您為您的業務物件使用 Spring IoC 容器(ApplicationContext
或 BeanFactory
)(而且您應該這樣做!),您會想要使用 Spring 的 AOP FactoryBean
實作之一。(請記住,factory bean 引入了一個間接層,使其可以建立不同類型的物件。)
Spring AOP 支援在底層也使用了 factory bean。 |
在 Spring 中建立 AOP 代理的基本方法是使用 org.springframework.aop.framework.ProxyFactoryBean
。這讓您可以完全控制切入點、任何適用的 advice 及其順序。但是,如果您不需要這種程度的控制,則有更簡單且更佳的選項。
基礎
ProxyFactoryBean
,如同其他 Spring FactoryBean
實作,引入了一個間接層級。如果您定義一個名為 foo
的 ProxyFactoryBean
,參考 foo
的物件不會看到 ProxyFactoryBean
實例本身,而是由 ProxyFactoryBean
中 getObject()
方法的實作所建立的物件。此方法會建立一個 AOP 代理,包裝目標物件。
使用 ProxyFactoryBean
或其他 IoC 感知類別來建立 AOP 代理,最重要的好處之一是 advice 和切入點也可以由 IoC 管理。這是一個強大的功能,可以實現某些其他 AOP 框架難以實現的方法。例如,advice 本身可以參考應用程式物件(除了目標物件,目標物件在任何 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
,因此允許變更(例如新增額外的 advice)。 -
exposeProxy
:決定是否應在ThreadLocal
中公開目前的代理,以便目標可以存取它。如果目標需要取得代理且exposeProxy
屬性設定為true
,則目標可以使用AopContext.currentProxy()
方法。
ProxyFactoryBean
特有的其他屬性包括:
-
proxyInterfaces
:String
介面名稱陣列。如果未提供此項,則會使用目標類別的 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
屬性來指定攔截器列表。請注意,即使 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 定義。 -
用於提供 advice 的
Advisor
和Interceptor
。 -
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"/>