使用 ProxyFactoryBean 建立 AOP 代理
如果你將 Spring IoC 容器(一個 ApplicationContext 或 BeanFactory)用於你的業務物件(你應該這樣做!),你會想使用 Spring 的 AOP FactoryBean 實現之一。(請記住,工廠 Bean 引入了一個間接層,使其能夠建立不同型別的物件。)
| Spring AOP 支援也在底層使用工廠 Bean。 |
在 Spring 中建立 AOP 代理的基本方法是使用 org.springframework.aop.framework.ProxyFactoryBean。這提供了對切入點、任何適用的通知及其順序的完全控制。然而,如果你不需要這種控制,還有更簡單的選項是更可取的。
基本原理
ProxyFactoryBean,像其他 Spring FactoryBean 實現一樣,引入了一個間接層。如果你定義了一個名為 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忽略通知的單例設定。你可以在攔截器名稱後附加一個星號(
*)。這樣做會導致應用所有名稱以星號之前部分開頭的 Advisor bean。你可以在使用“全域性”Advisor中找到使用此功能的示例。 -
singleton:無論
getObject()方法呼叫多少次,工廠是否應返回單個物件。幾個FactoryBean實現提供了這樣的方法。預設值為true。如果你想使用有狀態通知(例如,用於有狀態 mixin),請使用原型通知以及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。這是例子中的
personTargetbean 定義。 -
一個用於提供通知的
Advisor和一個Interceptor。 -
一個 AOP 代理 bean 定義,用於指定目標物件(
personTargetbean)、要代理的介面和要應用的通知。
以下列表顯示了示例:
<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 列表,其中包含當前工廠中攔截器或通知器的 bean 名稱。你可以使用通知器、攔截器、前置通知、後置返回通知和丟擲通知物件。通知器的順序很重要。
你可能想知道為什麼列表不包含 bean 引用。原因在於,如果 ProxyFactoryBean 的單例屬性設定為 false,它必須能夠返回獨立的代理例項。如果任何一個通知器本身是原型,則需要返回一個獨立的例項,因此必須能夠從工廠獲取原型的例項。持有引用是不夠的。 |
前面顯示的 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 的優點是隻有一個人型別的物件。這對於我們希望阻止應用程式上下文的使用者獲取未通知物件的引用,或者需要避免 Spring IoC 自動裝配的任何歧義時非常有用。還有一點可以說是一個優點,那就是 ProxyFactoryBean 定義是自包含的。然而,有時能夠從工廠獲取未通知的目標實際上可能是一個優點(例如,在某些測試場景中)。
代理類
如果你需要代理一個類,而不是一個或多個介面,該怎麼辦?
想象一下,在我們前面的示例中,沒有 Person 介面。我們需要通知一個名為 Person 的類,它不實現任何業務介面。在這種情況下,你可以配置 Spring 使用 CGLIB 代理而不是動態代理。為此,將前面所示的 ProxyFactoryBean 上的 proxyTargetClass 屬性設定為 true。雖然最好是面向介面程式設計而不是面向類程式設計,但在處理遺留程式碼時,能夠通知不實現介面的類會很有用。(一般來說,Spring 不具有強制性。雖然它使得應用良好實踐變得容易,但它避免強制採用特定方法。)
如果你願意,即使有介面,你也可以強制使用 CGLIB。
CGLIB 代理透過在執行時生成目標類的子類來工作。Spring 配置這個生成的子類以將方法呼叫委託給原始目標。子類用於實現裝飾器模式,並織入通知。
CGLIB 代理通常對使用者來說應該是透明的。但是,有一些問題需要考慮:
-
final類不能被代理,因為它們不能被擴充套件。 -
final方法不能被通知,因為它們不能被重寫。 -
private方法不能被通知,因為它們不能被重寫。 -
不可見的方法,通常是父類中來自不同包的包私有方法,不能被通知,因為它們實際上是私有的。
無需將 CGLIB 新增到你的 classpath。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"/>