Spring 核心(第四部分)

帶有AspectJ切入點的Spring AOP

Spring通過使用基於模式的方法 schema-based approach或@AspectJ註釋樣式@AspectJ annotation style提供了編寫定製方面的簡單而強大的方法。這兩種風格都提供了完全類型化的建議和使用AspectJ切入點語言,同時仍然使用Spring AOP進行編織。
本章討論了基於schema和@ aspectj的AOP支持。下一章將討論較低級別的AOP支持。

AOP在Spring框架中用於:
提供聲明性企業服務。此類服務中最重要的是聲明性事務管理。
讓用戶實現自定義方面,用AOP補充他們對OOP的使用。

注意:如果您只對通用聲明性服務或其他預打包的聲明性中間件服務(如池)感興趣,那麼您不需要直接使用Spring AOP,並且可以跳過本章的大部分內容。

= = AOP概念
讓我們從定義一些核心的AOP概念和術語開始。這些術語不是特定於spring的。不幸的是,AOP術語不是特別直觀。然而,如果Spring使用自己的術語,則會更加混亂。

  • Aspect:跨越多個類的關注的模塊化。事務管理是企業Java應用程序中橫切關注點的一個很好的例子。在Spring AOP中,方面是通過使用常規類(基於模式的方法)或使用@Aspect註釋的常規類(@AspectJ樣式)來實現的。
  • Join point:程序執行過程中的一個點,如方法的執行或異常的處理。在Spring AOP中,連接點總是表示方法執行。
  • Advice:方面在特定連接點上採取的操作。不同類型的建議包括“around”、“before”、“after”的advice。(稍後將討論通知類型。)許多AOP框架,包括Spring,將通知建模爲攔截器,並維護圍繞連接點的攔截器鏈。
  • Pointcut:匹配連接點的謂詞。通知與切入點表達式相關聯,並在與切入點匹配的任何連接點上運行(例如,執行具有特定名稱的方法)。連接點與切入點表達式匹配的概念是AOP的核心,Spring默認使用AspectJ切入點表達式語言。
  • Introduction:代表類型聲明其他方法或字段。Spring AOP允許您將新的接口(和相應的實現)引入任何被建議的對象。例如,您可以使用一個介紹來讓一個bean實現一個IsModified接口,以簡化緩存。(介紹在AspectJ社區中稱爲類型間聲明。)
  • Target object:被一個或多個方面通知的對象。也稱爲“被通知對象”。因爲Spring AOP是通過使用運行時代理來實現的,所以這個對象總是一個代理對象。
  • AOP proxy:AOP框架爲了實現方面契約(通知方法執行等等)而創建的對象。在Spring框架中,AOP代理是JDK動態代理或CGLIB代理。
  • Weaving:將方面與其他應用程序類型或對象鏈接,以創建建議的對象。這可以在編譯時(例如,使用AspectJ編譯器)、加載時或運行時完成。與其他純Java AOP框架一樣,Spring AOP在運行時執行編織。

Spring AOP包括以下類型的通知:

  • Before advice:在連接點之前運行的通知,但是不能阻止執行流繼續到連接點(除非拋出異常)。
  • After returning advice:在連接點正常完成後運行的通知(例如,如果一個方法沒有拋出異常返回)。
  • After throwing advice:如果方法通過拋出異常退出,則執行通知。
  • After (finally) advice:不管連接點以何種方式退出(正常或異常返回),都要執行的通知。
  • Around advice:圍繞連接點(如方法調用)的通知。這是最有力的建議。Around通知可以在方法調用前後執行自定義行爲。它還負責選擇是繼續到連接點,還是通過返回它自己的返回值或拋出異常來簡化建議的方法執行。

Around advice是最普遍的建議。因爲Spring AOP和AspectJ一樣,提供了各種各樣的通知類型,所以我們建議您使用最不強大的通知類型來實現所需的行爲。例如,如果只需要用方法的返回值更新緩存,則最好實現after return通知,而不是around通知,儘管around通知可以完成相同的工作。使用最特定的通知類型可以提供更簡單的編程模型,減少出錯的可能性。例如,您不需要在用於around通知的連接點上調用proceed()方法,因此,您不可能不調用它。

所有的通知參數都是靜態類型的,這樣您就可以使用適當類型的通知參數(例如方法執行返回值的類型),而不是Object數組。

由切入點匹配的連接點的概念是AOP的關鍵,它將AOP與只提供攔截的舊技術區分開來。切入點使通知能夠獨立於面向對象的層次結構。例如,可以將提供聲明性事務管理的around建議應用於一組跨多個對象的方法(例如服務層中的所有業務操作)。

== Spring AOP的功能和目標
Spring AOP是在純Java中實現的。不需要特殊的編譯過程。Spring AOP不需要控制類裝入器層次結構,因此適合在servlet容器或應用服務器中使用。

Spring AOP目前只支持方法執行連接點(建議在Spring bean上執行方法)。雖然可以在不破壞核心Spring AOP api的情況下添加對字段攔截的支持,但是沒有實現字段攔截。如果需要通知字段訪問和更新連接點,請考慮AspectJ之類的語言。

Spring AOP的AOP方法與大多數其他AOP框架不同。其目的不是提供最完整的AOP實現(儘管Spring AOP非常強大)。相反,其目標是提供AOP實現和Spring IoC之間的緊密集成,以幫助解決企業應用程序中的常見問題。

因此,例如,Spring框架的AOP功能通常與Spring IoC容器一起使用。方面是通過使用普通的bean定義語法來配置的(儘管這允許強大的“自動代理”功能)。這是與其他AOP實現的一個重要區別。使用Spring AOP不能輕鬆或有效地完成某些事情,比如通知非常細粒度的對象(通常是域對象)。在這種情況下,AspectJ是最佳選擇。然而,我們的經驗是,Spring AOP爲企業Java應用程序中大多數適合AOP的問題提供了一個優秀的解決方案。

Spring AOP從未試圖與AspectJ競爭來提供全面的AOP解決方案。我們認爲基於代理的框架(如Spring AOP)和成熟的框架(如AspectJ)都是有價值的,它們是互補的,而不是競爭的。

Spring將Spring AOP和IoC與AspectJ無縫集成,從而在一致的基於Spring的應用程序體系結構中支持AOP的所有使用。這種集成並不影響Spring AOP API或AOP Alliance API。Spring AOP保持向後兼容。有關Spring AOP api的討論,請參閱下一章。

Spring框架的核心原則之一是無創性。您不應該被迫將特定於框架的類和接口引入到您的業務或域模型中。然而,在某些地方,Spring框架確實爲您提供了將特定於Spring框架的依賴項引入代碼庫的選項。之所以提供這些選項,是因爲在某些場景中,以這種方式閱讀或編寫某些特定功能的代碼可能更容易。然而,Spring框架(幾乎)總是爲您提供這樣的選擇:您可以自由地做出明智的決定,即哪種選擇最適合您的特定用例或場景。

與本章相關的一個選擇是選擇哪種AOP框架(以及哪種AOP風格)。您可以選擇AspectJ、Spring AOP,或者兩者兼而有之。您還可以選擇@AspectJ註釋樣式方法或Spring XML配置樣式方法。本章選擇首先介紹@AspectJ風格的方法,這並不意味着Spring團隊更喜歡@AspectJ註釋風格的方法,而不是Spring XML配置風格。

關於每種風格的“爲什麼和爲什麼”的更完整的討論,請參見 [aop-choosing]

= = AOP代理
Spring AOP默認爲AOP代理使用標準JDK動態代理。這允許代理任何接口(或接口集)。
Spring AOP還可以使用CGLIB代理。這對於代理類而不是接口是必要的。默認情況下,如果業務對象沒有實現接口,則使用CGLIB。由於對接口而不是類進行編程是一種良好的實踐,所以業務類通常實現一個或多個業務接口。強制使用CGLIB( force the use of CGLIB)是可能的,在那些(希望很少)情況下,您需要建議一個沒有在接口上聲明的方法,或者需要將經過代理的對象作爲具體類型傳遞給方法。
理解Spring AOP是基於代理的這一事實是很重要的。參見 [aop-understanding-aop-proxies],以徹底檢查這個實現細節的實際含義。

= = @ aspectj的支持
@AspectJ指的是將方面聲明爲用註釋註釋的常規Java類的樣式。@AspectJ樣式是由AspectJ項目作爲AspectJ 5發行版的一部分引入的。Spring使用AspectJ提供的用於切入點解析和匹配的庫來解釋與AspectJ 5相同的註釋。但是AOP運行時仍然是純Spring AOP,並且不依賴於AspectJ編譯器或編織器。

注意:使用AspectJ編譯器和weaver可以使用完整的AspectJ語言, [aop-using-aspectj]中對此進行了討論。

===啓用@AspectJ支持
要在Spring配置中使用@AspectJ方面,您需要啓用Spring對基於@AspectJ方面配置Spring AOP的支持,並根據這些方面是否建議自動代理bean。通過自動代理,我們的意思是,如果Spring確定一個bean由一個或多個方面通知,它將自動爲該bean生成一個代理來攔截方法調用,並確保根據需要執行通知。

可以通過XML或java風格的配置啓用@AspectJ支持。在這兩種情況下,您還需要確保AspectJ的aspectjweaver.jar庫位於應用程序的類路徑上(版本1.8或更高)。這個庫可以從AspectJ發行版的lib目錄或Maven中央存儲庫中獲得。

===使用Java配置啓用@AspectJ支持
要使用Java @Configuration啓用@AspectJ支持,請添加@EnableAspectJAutoProxy註釋,如下面的示例所示:

@Configuration
@EnableAspectJAutoProxy
public class AppConfig {

}

===啓用@AspectJ支持XML配置
要使用基於xml的配置啓用@AspectJ支持,可以使用aop:aspectj-autoproxy元素,如下面的示例所示:

<aop:aspectj-autoproxy/>

這假設您使用 XML Schema-based configuration中描述的模式支持。有關如何導入AOP名稱空間中的標記,請參閱 the AOP schema

===聲明一個方面
啓用@AspectJ支持後,在應用程序上下文中定義的任何bean,只要類是@AspectJ方面(具有@Aspect註釋),就會被Spring自動檢測到,並用於配置Spring AOP。接下來的兩個例子展示了一個不太有用的方面所需要的最小定義。
這兩個示例中的第一個示例顯示了應用程序上下文中的一個常規bean定義,它指向一個具有@Aspect註釋的bean類:

<bean id="myAspect" class="org.xyz.NotVeryUsefulAspect">
    <!-- configure properties of the aspect here -->
</bean>

兩個示例中的第二個示例顯示NotVeryUsefulAspect類定義,它是用org.aspectj.lang.annotation註釋的。註釋方面;

package org.xyz;
import org.aspectj.lang.annotation.Aspect;

@Aspect
public class NotVeryUsefulAspect {

}

方面(使用@Aspect註釋的類)可以具有與任何其他類相同的方法和字段。它們還可以包含切入點、通知和介紹(類型間)聲明。

注意:通過組件掃描自動檢測方面
您可以將方面類註冊爲Spring XML配置中的常規bean,或者通過類路徑掃描自動檢測它們——與任何其他Spring管理的bean相同。但是,請注意@Aspect註釋對於類路徑中的自動檢測是不夠的。爲此,您需要添加一個單獨的@Component註釋(或者,根據Spring的組件掃描器的規則,一個定製的原型註釋)。

向其他方面提供建議?
在Spring AOP中,方面本身不能成爲來自其他方面的建議的目標。類上的@Aspect註釋將其標記爲方面,因此將其排除在自動代理之外。

===聲明一個切入點
切入點確定感興趣的連接點,從而使我們能夠控制何時執行通知。Spring AOP只支持Spring bean的方法執行連接點,因此可以將切入點看作是與Spring bean上的方法執行相匹配的。切入點聲明有兩部分:一個簽名,它包含名稱和任何參數;一個切入點表達式,它確定我們對哪個方法執行感興趣。在AOP的@AspectJ註釋風格中,切入點簽名由一個常規方法定義提供,切入點表達式通過使用@Pointcut註釋來表示(作爲切入點簽名的方法必須有一個void返回類型)。

一個示例可能有助於明確切入點簽名和切入點表達式之間的區別。下面的例子定義了一個名爲anyOldTransfer的切入點,它匹配任何名爲transfer的方法的執行:

@Pointcut("execution(* transfer(..))") // the pointcut expression
private void anyOldTransfer() {} // the pointcut signature

形成@Pointcut註釋值的切入點表達式是一個常規的AspectJ 5切入點表達式。關於AspectJ的切入點語言的完整討論,請參閱AspectJ編程指南 AspectJ Programming Guide(對於擴展,請參閱AspectJ 5開發人員的筆記本 AspectJ 5 Developer’s Notebook)或關於AspectJ的書籍(如Colyer等人的Eclipse AspectJ或Ramnivas Laddad的AspectJ in Action)。

===支持的切入點指示器
Spring AOP支持以下用於切入點表達式的AspectJ切入點指示器(PCD):

  • execution:用於匹配方法執行連接點。這是使用Spring AOP時要使用的主要切入點指示器。
  • within:限制對某些類型中的連接點的匹配(使用Spring AOP時在匹配類型中聲明的方法的執行)。
  • this:限制連接點的匹配(使用Spring AOP時方法的執行),其中bean引用(Spring AOP代理)是給定類型的實例。
  • target:限制對連接點(使用Spring AOP時方法的執行)的匹配,其中目標對象(代理的應用程序對象)是給定類型的實例。
  • args:限制連接點的匹配(使用Spring AOP時方法的執行),其中的參數是給定類型的實例。
  • @target:限制連接點的匹配(使用Spring AOP時方法的執行),其中執行對象的類具有給定類型的註釋。
  • @args:限制連接點的匹配(使用Spring AOP時方法的執行),其中實際傳遞的參數的運行時類型具有給定類型的註釋。
  • @within:限制對具有給定註釋的類型中的連接點的匹配(在使用Spring AOP時,使用給定註釋在類型中聲明的方法的執行)。
  • @annotation:限制對連接點的匹配,連接點的主體(在Spring AOP中執行的方法)具有給定的註釋。

其他類型的切入點

完整的AspectJ切入點語言支持Spring中不支持的其他切入點指示器:調用、獲取、設置、預初始化、靜態初始化、初始化、處理程序、adviceexecution、withincode、cflow、cflowbelow、if、@this和@withincode。在由Spring AOP解釋的切入點表達式中使用這些切入點指示符會導致拋出IllegalArgumentException。

在將來的版本中,Spring AOP支持的一組切入點設計器可以得到擴展,以支持更多的AspectJ切入點設計器。

因爲Spring AOP將匹配限制爲只匹配方法執行連接點,前面關於切入點設計器的討論給出了比AspectJ編程指南中更窄的定義。此外,AspectJ本身具有基於類型的語義,在執行連接點上,這個和目標都引用同一個對象:執行方法的對象。Spring AOP是一個基於代理的系統,它區別於代理對象本身(綁定到它)和代理背後的目標對象(綁定到目標)。

由於Spring的AOP框架具有基於代理的特性,目標對象中的調用根據定義不會被攔截。對於JDK代理,只能攔截代理上的公共接口方法調用。使用CGLIB,可以截獲代理上的public和protected方法調用(如果需要,甚至可以截獲包可見的方法)。然而,通過代理的公共交互應該始終通過公共簽名來設計。

注意,切入點定義通常與任何被攔截的方法相匹配。如果嚴格地說切入點是公共的,那麼即使在CGLIB代理場景中,通過代理進行潛在的非公共交互,也需要對它進行相應的定義。

如果您的攔截需要包括目標類中的方法調用甚至構造函數,那麼考慮使用Spring驅動的本機AspectJ編織,而不是Spring的基於代理的AOP框架。這構成了具有不同特徵的AOP使用的不同模式,所以在做決定之前一定要熟悉編織。

Spring AOP還支持另外一個名爲bean的PCD。此PCD允許您將連接點的匹配限制爲特定的命名Spring bean或一組命名Spring bean(在使用通配符時)。bean PCD的形式如下:

bean(idOrNameOfBean)

ideofbean令牌可以是任何Spring bean的名稱。提供了使用*字符的有限通配符支持,因此,如果您爲Spring bean建立了一些命名約定,您可以編寫一個bean PCD表達式來選擇它們。與其他切入點指示器一樣,bean PCD可以與&&(和)、||(或)和一起使用!也(否定)運營商。

bean PCD只在Spring AOP中受支持,而在本機AspectJ編織中不受支持。它是AspectJ定義的標準pcd的特定於spring的擴展,因此不能用於@Aspect模型中聲明的方面。

bean PCD在實例級別(基於Spring bean名稱概念構建)而不是僅在類型級別(基於編織的AOP僅限於此)上運行。基於實例的切入點設計器是Spring的基於代理的AOP框架的一種特殊功能,它與Spring bean工廠緊密集成,通過名稱識別特定的bean是很自然和直接的。

===組合切入點表達式
可以使用&&、||和!組合切入點表達式。您還可以通過名稱引用切入點表達式。下面的例子展示了三個切入點表達式:

@Pointcut("execution(public * (..))")
private void anyPublicOperation() {} //1

@Pointcut("within(com.xyz.someapp.trading..)")
private void inTrading() {} //2

@Pointcut("anyPublicOperation() && inTrading()")
private void tradingOperation() {} //3

//1 如果方法執行連接點表示任何公共方法的執行,則anyPublicOperation匹配。
//2 如果方法執行在交易模塊中,則在交易中匹配。
//3 如果方法執行代表交易模塊中的任何公共方法,則tradingOperation匹配。

最佳實踐是從較小的命名組件構建更復雜的切入點表達式,如前面所示。當通過名稱引用切入點時,應用普通的Java可見性規則(您可以在同一類型中看到私有切入點、層次結構中的受保護切入點、任何地方的公共切入點,等等)。可見性不影響切入點匹配。

===共享公共切入點定義
在使用企業應用程序時,開發人員通常希望從幾個方面引用應用程序的模塊和特定的操作集。我們建議定義一個“系統架構”方面,它可以捕獲用於此目的的公共切入點表達式。這樣一個方面通常類似於下面的例子:

package com.xyz.someapp;

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;

@Aspect
public class SystemArchitecture {

    /
     * A join point is in the web layer if the method is defined
     * in a type in the com.xyz.someapp.web package or any sub-package
     * under that.
     /
    @Pointcut("within(com.xyz.someapp.web..)")
    public void inWebLayer() {}

    /
     * A join point is in the service layer if the method is defined
     * in a type in the com.xyz.someapp.service package or any sub-package
     * under that.
     /
    @Pointcut("within(com.xyz.someapp.service..)")
    public void inServiceLayer() {}

    /
     * A join point is in the data access layer if the method is defined
     * in a type in the com.xyz.someapp.dao package or any sub-package
     * under that.
     /
    @Pointcut("within(com.xyz.someapp.dao..)")
    public void inDataAccessLayer() {}

    /
     * A business service is the execution of any method defined on a service
     * interface. This definition assumes that interfaces are placed in the
     * "service" package, and that implementation types are in sub-packages.
     *
     * If you group service interfaces by functional area (for example,
     * in packages com.xyz.someapp.abc.service and com.xyz.someapp.def.service) then
     * the pointcut expression "execution(* com.xyz.someapp..service..(..))"
     * could be used instead.
     *
     * Alternatively, you can write the expression using the 'bean'
     * PCD, like so "bean(Service)". (This assumes that you have
     * named your Spring service beans in a consistent fashion.)
     */
    @Pointcut("execution( com.xyz.someapp..service..(..))")
    public void businessService() {}

    /*
     * A data access operation is the execution of any method defined on a
     * dao interface. This definition assumes that interfaces are placed in the
     * "dao" package, and that implementation types are in sub-packages.
     */
    @Pointcut("execution( com.xyz.someapp.dao..(..))")
    public void dataAccessOperation() {}

}

您可以在任何需要切入點表達式的地方引用在這樣一個方面中定義的切入點。例如,要使服務層具有事務性,可以編寫以下代碼:

<aop:config>
    <aop:advisor
        pointcut="com.xyz.someapp.SystemArchitecture.businessService()"
        advice-ref="tx-advice"/>
</aop:config>

<tx:advice id="tx-advice">
    <tx:attributes>
        <tx:method name="*" propagation="REQUIRED"/>
    </tx:attributes>
</tx:advice>

<aop:config>和<aop:advisor>元素在[aop-schema]中討論。事務管理中討論事務元素。
= = = =的例子
Spring AOP用戶可能最經常使用執行切入點指示器。執行表達式的格式如下:

execution(modifiers-pattern? ret-type-pattern declaring-type-pattern?name-pattern(param-pattern)
                throws-pattern?)

除了返回類型模式(前面代碼段中的rt -type-pattern)、名稱模式和參數模式之外,其他所有部分都是可選的。返回類型模式確定要匹配連接點,方法的返回類型必須是什麼。*是最常用的返回類型模式。它匹配任何返回類型。完全限定類型名僅在方法返回給定類型時才匹配。名稱模式與方法名稱匹配。您可以使用*通配符作爲名稱模式的全部或部分。如果指定了聲明類型模式,請包含尾隨。將其連接到名稱模式組件。參數模式稍微複雜一點:()匹配不帶參數的方法,而(..)匹配任意數量(零或更多)的參數。(*)模式匹配接受任意類型參數的方法。(*,String)匹配一個帶有兩個參數的方法。第一個可以是任何類型,第二個必須是字符串。有關更多信息,請參閱AspectJ編程指南的語言語義部分。

下面的例子展示了一些常見的切入點表達式:
執行任何公用方法:

execution(public * *(..))

任何以set開頭的方法的執行:

execution(* set*(..))

執行AccountService接口定義的任何方法:

execution(* com.xyz.service.AccountService.*(..))

執行服務包中定義的任何方法:

 execution(* com.xyz.service.*.*(..))

執行服務包或其中一個子包中定義的任何方法:

execution(* com.xyz.service..*.*(..))

服務包內的任何連接點(僅在Spring AOP中執行方法):

 within(com.xyz.service.*)

服務包或其子包中的任何連接點(僅在Spring AOP中執行方法):

within(com.xyz.service..*)

代理實現AccountService接口的任何連接點(僅在Spring AOP中執行方法):

 this(com.xyz.service.AccountService)

注意:“this”更常用於綁定形式。有關如何使代理對象在通知正文中可用,請參閱 [aop-advice]一節。

目標對象實現AccountService接口的任何連接點(僅在Spring AOP中執行方法):

 target(com.xyz.service.AccountService)

注意:‘target’更常用於綁定形式。請參閱[aop-advice]一節,瞭解如何使目標對象在advice主體中可用。

任何接受單個參數的連接點(只有在Spring AOP中才執行方法),並且在運行時傳遞的參數是可序列化的:

args(java.io.Serializable)

注意:“args”通常用於綁定形式。請參閱[aop-advice]一節,瞭解如何使方法參數在advice主體中可用。

注意,本例中給出的切入點不同於執行(* *(java.io.Serializable))。如果在運行時傳遞的參數是可序列化的,則args版本匹配;如果方法簽名聲明一個可序列化類型的參數,則執行版本匹配。

目標對象具有@Transactional註釋的任何連接點(僅在Spring AOP中執行方法):

 @target(org.springframework.transaction.annotation.Transactional)

注意:您還可以在綁定表單中使用'@target'。請參閱[aop-advice]一節,瞭解如何使註釋對象在通知正文中可用。

目標對象的聲明類型具有@Transactional註釋的任何連接點(僅在Spring AOP中執行方法):@within(org.springframework.transaction.annotation.Transactional)

注意:您還可以在綁定表單中使用'@within'。請參閱[aop-advice]一節,瞭解如何使註釋對象在通知正文中可用。

任何連接點(僅在Spring AOP中執行方法),其中執行方法具有@Transactional註釋:

@annotation(org.springframework.transaction.annotation.Transactional)

任何接受單個參數的連接點(僅在Spring AOP中執行方法),其中傳遞的參數的運行時類型有@ classification註釋:
@args(com.xyz.security.Classified)

在名爲tradeService的Spring bean上的任何連接點(僅在Spring AOP中執行方法):

 bean(tradeService)

在具有匹配通配符表達式*Service的名稱的Spring bean上的任何連接點(僅在Spring AOP中執行方法):

bean(*Service)

===編寫好的切入點
在編譯期間,AspectJ處理切入點以優化匹配性能。檢查代碼並確定每個連接點是否與給定的切入點匹配(靜態或動態)是一個開銷很大的過程。(動態匹配意味着不能通過靜態分析完全確定匹配,在代碼中放置一個測試,以確定在代碼運行時是否存在實際匹配)。在第一次遇到切入點聲明時,AspectJ會將它重寫爲匹配過程的最佳形式。這是什麼意思?基本上,切入點是用DNF(析取範式)重寫的,切入點的組件是排序的,這樣可以先檢查那些比較便宜的組件。這意味着您不必擔心理解各種切入點指示器的性能,並且可以在切入點聲明中以任意順序提供它們。

然而,AspectJ只能處理它被告知的內容。爲了獲得最佳匹配性能,您應該考慮他們試圖實現什麼,並在定義中儘可能縮小匹配的搜索空間。現有的指示器可以自然地分爲三類:kinded、scoping和context:

  • Kinded指示符選擇特定類型的連接點:執行、獲取、設置、調用和處理程序。
  • 作用域指定符選擇一組感興趣的連接點(可能有多種):inside和withincode
  • 上下文指示符根據上下文(this、target和@annotation)匹配(也可以綁定)

編寫良好的切入點至少應該包括前兩種類型(類型和範圍)。您可以包含基於連接點上下文進行匹配的上下文指示符,或者將該上下文綁定在通知中使用。只提供一種類型的指示符或只提供上下文指示符,但由於需要額外的處理和分析,可能會影響編織性能(所使用的時間和內存)。作用域指示器匹配起來非常快,使用它們意味着AspectJ可以非常快地取消不應該進一步處理的連接點組。好的切入點應該儘可能包含一個切入點。

= = =聲明的Advice
通知與切入點表達式相關聯,並在切入點匹配的方法執行之前、之後或前後運行。切入點表達式可以是對指定切入點的簡單引用,也可以是在適當位置聲明的切入點表達式。

= = = =Before Advice
你可以使用@Before註釋在方面中聲明before通知:

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;

@Aspect
public class BeforeExample {

    @Before("com.xyz.myapp.SystemArchitecture.dataAccessOperation()")
    public void doAccessCheck() {
        // ...
    }

}

如果我們使用一個插入的切入點表達式,我們可以將前面的例子重寫爲下面的例子:

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;

@Aspect
public class BeforeExample {

    @Before("execution(* com.xyz.myapp.dao..(..))")
    public void doAccessCheck() {
        // ...
    }

}

====After Returning Advice
返回後,當匹配的方法執行正常返回時,將運行通知。你可以使用@AfterReturning註釋來聲明它:

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.AfterReturning;

@Aspect
public class AfterReturningExample {

    @AfterReturning("com.xyz.myapp.SystemArchitecture.dataAccessOperation()")
    public void doAccessCheck() {
        // ...
    }

}

注意:您可以有多個通知聲明(以及其他成員),它們都在同一個方面中。在這些示例中,我們只顯示一個通知聲明,以集中說明每個通知的效果。

有時,您需要在advice主體中訪問返回的實際值。您可以使用@ afterreturn的形式綁定返回值來獲得訪問權限,如下面的例子所示:

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.AfterReturning;

@Aspect
public class AfterReturningExample {

    @AfterReturning(
        pointcut="com.xyz.myapp.SystemArchitecture.dataAccessOperation()",
        returning="retVal")
    public void doAccessCheck(Object retVal) {
        // ...
    }

}

返回屬性中使用的名稱必須與advice方法中的參數名稱相對應。當方法執行返回時,返回值作爲相應的參數值傳遞給advice方法。return子句還將匹配限制爲只匹配那些返回指定類型值的方法執行(在本例中爲Object,它匹配任何返回值)。

請注意,在使用返回通知後返回一個完全不同的引用是不可能的。
====拋出建議後
拋出建議後,當匹配的方法執行通過拋出異常退出時運行。您可以使用@AfterThrowing註釋來聲明它,如下面的例子所示:

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.AfterThrowing;

@Aspect
public class AfterThrowingExample {

    @AfterThrowing("com.xyz.myapp.SystemArchitecture.dataAccessOperation()")
    public void doRecoveryActions() {
        // ...
    }

}

通常,您希望僅在拋出給定類型的異常時才運行通知,並且常常需要在通知正文中訪問拋出的異常。您可以使用拋出屬性來限制匹配(如果需要,可以使用Throwable作爲異常類型),並將拋出的異常綁定到一個advice參數。下面的例子演示瞭如何做到這一點:

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.AfterThrowing;

@Aspect
public class AfterThrowingExample {

    @AfterThrowing(
        pointcut="com.xyz.myapp.SystemArchitecture.dataAccessOperation()",
        throwing="ex")
    public void doRecoveryActions(DataAccessException ex) {
        // ...
    }

}

拋出屬性中使用的名稱必須與advice方法中的參數名稱相對應。當方法執行通過拋出異常退出時,異常將作爲相應的參數值傳遞給advice方法。拋出子句還將匹配限制爲僅拋出指定類型的異常的方法執行(本例中爲DataAccessException)。

====After (Finally) Advice
當匹配的方法執行退出時,將運行After (finally)通知。它是使用@After註釋聲明的。After通知必須準備好處理正常和異常返回條件。它通常用於釋放資源和類似的目的。下面的例子展示瞭如何使用after finally advice:

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.After;

@Aspect
public class AfterFinallyExample {

    @After("com.xyz.myapp.SystemArchitecture.dataAccessOperation()")
    public void doReleaseLock() {
        // ...
    }

}

= = = = Around Advice
最後一種建議是關於建議的。Around通知“繞過”一個匹配的方法的執行。它有機會在方法執行之前和之後執行工作,並確定何時、如何執行,甚至是否真正執行方法。如果需要以線程安全的方式(例如,啓動和停止計時器)共享方法執行前後的狀態,通常會使用Around通知。始終使用最不強大的建議形式來滿足您的需求(也就是說,如果before建議可以的話,就不要使用around建議)。

Around通知是使用@Around註釋聲明的。通知方法的第一個參數必須是類型爲procedure edingjoinpoint的。在通知的主體中,對過程ingjoinpoint調用proceed()會導致底層方法執行。proceed方法也可以傳遞一個對象[]。當方法執行時,數組中的值用作方法執行的參數。

用Object[]調用proceed時的行爲與用AspectJ編譯器編譯around通知時的行爲稍有不同。建議使用傳統的AspectJ語言編寫,左右進行傳遞的參數的數量必須匹配的參數傳遞到周圍的建議(不是參數由底層連接點的數量),並繼續在一個給定的參數傳遞的價值立場取代原來的價值實體價值的連接點是綁定到(不要擔心如果現在沒有意義)。Spring採用的方法更簡單,更適合其基於代理、僅執行的語義。只有在編譯爲Spring編寫的@AspectJ方面並使用AspectJ編譯器和weaver的參數時,才需要注意這種差異。有一種方法可以編寫這樣的方面,它在Spring AOP和AspectJ之間都是100%兼容的,下面關於通知參數的部分將對此進行討論。

下面的例子展示瞭如何使用around建議:

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.ProceedingJoinPoint;

@Aspect
public class AroundExample {

    @Around("com.xyz.myapp.SystemArchitecture.businessService()")
    public Object doBasicProfiling(ProceedingJoinPoint pjp) throws Throwable {
        // start stopwatch
        Object retVal = pjp.proceed();
        // stop stopwatch
        return retVal;
    }

}

around通知返回的值是方法調用者看到的返回值。例如,簡單的緩存方面可以從緩存中返回一個值(如果有的話),如果沒有就調用proceed()。請注意,proceed可能被調用一次、多次,或者根本不在around通知的正文中調用。所有這些都是合法的。

= = = =Advice Parameters
Spring提供了全類型的通知,這意味着您可以在通知簽名中聲明所需的參數(正如我們在前面看到的返回和拋出示例),而不是一直使用Object[]數組。在本節的後面部分,我們將看到如何將參數和其他上下文值提供給建議主體。首先,我們來看看如何編寫通用的建議,從而找出建議當前建議的方法。

====訪問當前連接點
任何通知方法都可以將org.aspectj.lang類型的參數聲明爲它的第一個參數。注意,需要around通知來聲明類型爲procedure edingjoinpoint的第一個參數,它是JoinPoint的子類。JoinPoint接口提供了許多有用的方法:

  • getArgs():返回方法參數。
  • getThis():返回代理對象。
  • getTarget():返回目標對象。
  • getSignature():返回被建議的方法的描述。
  • toString():打印建議的方法的有用描述。

有關更多細節,請參見 javadoc
====將參數傳遞給通知
我們已經看到了如何綁定返回值或異常值(在返回和拋出通知之後使用)。要使參數值對通知主體可用,可以使用args的綁定形式。如果在args表達式中使用參數名代替類型名,則在調用通知時將傳遞相應參數的值作爲參數值。舉個例子應該會更清楚。假設您希望通知以Account對象作爲第一個參數的DAO操作的執行,並且需要訪問advice主體中的Account。你可以這樣寫:

@Before("com.xyz.myapp.SystemArchitecture.dataAccessOperation() && args(account,..)")
public void validateAccount(Account account) {
    // ...
}

切入點表達式的args(account,..)部分有兩個用途。首先,它將匹配限制爲僅在方法至少接受一個參數且傳遞給該參數的參數是Account實例的情況下執行的方法。其次,它通過Account參數將實際的Account對象提供給通知。
另一種編寫方法是聲明一個切入點,該切入點在匹配連接點時“提供”Account對象值,然後從通知中引用指定的切入點。這將看起來如下:

@Pointcut("com.xyz.myapp.SystemArchitecture.dataAccessOperation() && args(account,..)")
private void accountDataAccessOperation(Account account) {}

@Before("accountDataAccessOperation(account)")
public void validateAccount(Account account) {
    // ...
}

有關更多細節,請參閱AspectJ編程指南。
代理對象(this)、目標對象(target)和註釋(@within、@target、@annotation和@args)都可以以類似的方式綁定。接下來的兩個例子展示瞭如何匹配使用@Auditable註釋的方法的執行,並提取審計代碼:
兩個示例中的第一個示例顯示了@Auditable註釋的定義:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Auditable {
    AuditCode value();
}

兩個示例中的第二個示例顯示了與執行@Auditable方法相匹配的建議:

@Before("com.xyz.lib.Pointcuts.anyPublicMethod() && @annotation(auditable)")
public void audit(Auditable auditable) {
    AuditCode code = auditable.value();
    // ...
}

====通知參數和泛型
Spring AOP可以處理類聲明和方法參數中使用的泛型。假設你有一個泛型類型如下:

public interface Sample<T> {
    void sampleGenericMethod(T param);
    void sampleGenericCollectionMethod(Collection<T> param);
}

您可以將方法類型的攔截限制爲特定的參數類型,方法是將advice參數鍵入要攔截方法的參數類型:

@Before("execution(* ..Sample+.sampleGenericMethod(*)) && args(param)")
public void beforeSampleMethod(MyType param) {
    // Advice implementation
}

這種方法不適用於泛型集合。所以你不能像下面這樣定義一個切入點:

@Before("execution(* ..Sample+.sampleGenericCollectionMethod(*)) && args(param)")
public void beforeSampleMethod(Collection<MyType> param) {
    // Advice implementation
}

爲了實現這一點,我們必須檢查集合的每個元素,這是不合理的,因爲我們也無法決定如何處理空值。要實現類似的功能,必須將參數類型設置爲Collection<?並手動檢查元素的類型。
====確定參數名

通知調用中的參數綁定依賴於切入點表達式中使用的名稱與通知和切入點方法簽名中聲明的參數名稱的匹配。參數名無法通過Java反射獲得,因此Spring AOP使用以下策略來確定參數名:

  • 如果用戶顯式地指定了參數名,則使用指定的參數名。通知和切入點註釋都有一個可選的argNames屬性,您可以使用它來指定帶註釋的方法的參數名。這些參數名在運行時可用。下面的例子展示瞭如何使用argNames屬性:
@Before(value="com.xyz.lib.Pointcuts.anyPublicMethod() && target(bean) && @annotation(auditable)",
        argNames="bean,auditable")
public void audit(Object bean, Auditable auditable) {
    AuditCode code = auditable.value();
    // ... use code and bean
}

如果第一個參數是連接點的,則處理ingjoinpoint或JoinPoint。您可以從argNames屬性的值中省略參數的名稱。例如,如果您修改前面的通知來接收連接點對象,argNames屬性不需要包含它:

@Before(value="com.xyz.lib.Pointcuts.anyPublicMethod() && target(bean) && @annotation(auditable)",
        argNames="bean,auditable")
public void audit(JoinPoint jp, Object bean, Auditable auditable) {
    AuditCode code = auditable.value();
    // ... use code, bean, and jp
}

對連接點、加工連接點和連接點的第一個參數進行了特殊處理。對於不收集任何其他連接點上下文的通知實例,StaticPart類型特別方便。在這種情況下,可以忽略argNames屬性。例如,下面的建議不需要聲明argNames屬性:

@Before("com.xyz.lib.Pointcuts.anyPublicMethod()")
public void audit(JoinPoint jp) {
    // ... use jp
}
  • 使用'argNames'屬性有點笨拙,因此如果沒有指定'argNames'屬性,Spring AOP將查看該類的調試信息,並嘗試從局部變量表中確定參數名。只要使用調試信息('-g:vars')編譯了類,就會出現此信息。打開這個標記進行編譯的結果是:(1)您的代碼稍微容易理解(反向工程),(2)類文件的大小稍微大一些(通常是無關緊要的),(3)編譯器沒有應用刪除未使用的局部變量的優化。換句話說,您在構建時不應該遇到任何困難。

注意:如果AspectJ編譯器(ajc)編譯了@AspectJ方面,即使沒有調試信息,也不需要添加argNames屬性,因爲編譯器保留了所需的信息。

  • 如果在沒有必要的調試信息的情況下編譯了代碼,那麼Spring AOP就會嘗試將綁定變量對推斷爲參數(例如,如果切入點表達式中只綁定了一個變量,而advice方法只接受一個參數,那麼這種配對是明顯的)。如果給定可用信息,變量的綁定是不明確的,則拋出一個AmbiguousBindingException。
  • 如果上述所有策略都失敗,則拋出IllegalArgumentException。

====繼續討論參數
我們在前面提到過,我們將描述如何使用在Spring AOP和AspectJ中一致工作的參數來編寫proceed調用。解決方案是確保通知簽名按順序綁定每個方法參數。下面的例子演示瞭如何做到這一點:

@Around("execution(List<Account> find*(..)) && " +
        "com.xyz.myapp.SystemArchitecture.inDataAccessLayer() && " +
        "args(accountHolderNamePattern)")
public Object preProcessQueryPattern(ProceedingJoinPoint pjp,
        String accountHolderNamePattern) throws Throwable {
    String newPattern = preProcess(accountHolderNamePattern);
    return pjp.proceed(new Object[] {newPattern});
}

在許多情況下,無論如何都要進行這種綁定(如前面的示例所示)。
= = = =Advice Ordering

當多個通知都希望在同一個連接點上運行時,會發生什麼情況?Spring AOP遵循與AspectJ相同的優先規則來確定通知執行的順序。最高優先級的通知將首先運行“on The way in”(因此,給定兩個before通知,優先級最高的通知將首先運行)。從連接點“退出”時,優先級最高的通知最後運行(因此,給定兩個after通知,優先級最高的通知將運行在第二位)。

當在不同方面定義的兩個通知都需要在同一個連接點上運行時,除非您另外指定,否則執行的順序是未定義的。您可以通過指定優先級來控制執行的順序。這是通過實現org.springframework.core來實現的。方面類中的有序接口或使用有序註釋對其進行註釋。給定兩個方面,從order . getvalue()(或註釋值)返回較低值的方面具有較高的優先級。

當在相同方面中定義的兩個通知都需要在相同的連接點上運行時,順序是未定義的(因爲無法通過反射爲javac編譯的類檢索聲明順序)。考慮將這些通知方法分解爲每個方面類中每個連接點的一個通知方法,或者將這些通知片段重構爲可以在方面級別排序的獨立方面類。

= = =介紹
引入(在AspectJ中稱爲類型間聲明)使方面能夠聲明被通知的對象實現給定的接口,並代表這些對象提供接口的實現。

您可以使用@DeclareParents註釋進行介紹。此註釋用於聲明匹配類型具有新的父類型(因此得名)。例如,給定一個名爲UsageTracked的接口和一個名爲DefaultUsageTracked接口的實現,以下方面聲明所有服務接口的實現者也實現了UsageTracked接口(例如通過JMX公開統計信息):

@Aspect
public class UsageTracking {

    @DeclareParents(value="com.xzy.myapp.service.*+", defaultImpl=DefaultUsageTracked.class)
    public static UsageTracked mixin;

    @Before("com.xyz.myapp.SystemArchitecture.businessService() && this(usageTracked)")
    public void recordUsage(UsageTracked usageTracked) {
        usageTracked.incrementUseCount();
    }

}

要實現的接口由帶註釋字段的類型決定。@DeclareParents註釋的值屬性是一個AspectJ類型模式。任何匹配類型的bean都實現UsageTracked接口。注意,在前面示例的before建議中,服務bean可以直接用作UsageTracked接口的實現。如果以編程方式訪問一個bean,您將編寫以下代碼:

UsageTracked usageTracked = (UsageTracked) context.getBean("myService");

=== Aspect Instantiation Models

注意:這是一個高級的主題。如果你剛開始使用AOP,你可以安全地跳過它,直到以後。

默認情況下,應用程序上下文中每個方面都有一個單獨的實例。AspectJ稱之爲單例實例化模型。定義具有交替生命週期的方面是可能的。Spring支持AspectJ的perthis和pertarget實例化模型(當前不支持percflow、percflowbelow和pertypewithin)。
您可以通過在@Aspect註釋中指定perthis子句來聲明perthis方面。考慮下面的例子:

@Aspect("perthis(com.xyz.myapp.SystemArchitecture.businessService())")
public class MyAspect {

    private int someState;

    @Before(com.xyz.myapp.SystemArchitecture.businessService())
    public void recordServiceUsage() {
        // ...
    }

}

在前面的示例中,“perthis”子句的作用是爲每個執行業務服務的惟一服務對象創建一個方面實例(在切入點表達式匹配的連接點上綁定到“this”的惟一對象)。方面實例是在服務對象上第一次調用方法時創建的。當服務對象超出範圍時,方面就超出範圍。在方面實例創建之前,它內的任何通知都不執行。一旦方面實例被創建,在其中聲明的通知就會在匹配的連接點上執行,但是隻有當服務對象與方面相關聯時纔會執行。有關per子句的更多信息,請參閱AspectJ編程指南。

pertarget實例化模型的工作方式與perthis完全相同,但它在匹配的連接點爲每個惟一的目標對象創建一個方面實例。
===一個AOP例子
既然您已經看到了所有組成部分是如何工作的,那麼我們可以將它們放在一起來做一些有用的事情。

由於併發性問題(例如死鎖失敗者),業務服務的執行有時會失敗。如果操作被重試,它很可能在下一次嘗試中成功。對於適合在這種情況下重試的業務服務(冪等操作不需要返回用戶以解決衝突),我們希望透明地重試操作,以避免客戶端看到一個悲觀的lockingfailureexception異常。這是一個明顯跨越服務層中的多個服務的需求,因此是通過方面實現的理想需求。

因爲我們想要重試操作,所以我們需要使用around通知,以便能夠多次調用proceed。下面的清單顯示了基本的方面實現:

@Aspect
public class ConcurrentOperationExecutor implements Ordered {

    private static final int DEFAULT_MAX_RETRIES = 2;

    private int maxRetries = DEFAULT_MAX_RETRIES;
    private int order = 1;

    public void setMaxRetries(int maxRetries) {
        this.maxRetries = maxRetries;
    }

    public int getOrder() {
        return this.order;
    }

    public void setOrder(int order) {
        this.order = order;
    }

    @Around("com.xyz.myapp.SystemArchitecture.businessService()")
    public Object doConcurrentOperation(ProceedingJoinPoint pjp) throws Throwable {
        int numAttempts = 0;
        PessimisticLockingFailureException lockFailureException;
        do {
            numAttempts++;
            try {
                return pjp.proceed();
            }
            catch(PessimisticLockingFailureException ex) {
                lockFailureException = ex;
            }
        } while(numAttempts <= this.maxRetries);
        throw lockFailureException;
    }

}

請注意,方面實現了有序接口,因此我們可以將方面的優先級設置爲高於事務通知的優先級(我們希望每次重試時都有一個新的事務)。maxretry和order屬性都是由Spring配置的。主要操作發生在圍繞通知的doConcurrentOperation中。請注意,目前我們將重試邏輯應用於每個businessService()。我們嘗試繼續,如果我們失敗了,並且出現了一個悲觀的lockingfailureexception異常,我們就會再次嘗試,除非我們已經用盡了所有的重試嘗試。

對應的彈簧配置如下:

<aop:aspectj-autoproxy/>

<bean id="concurrentOperationExecutor" class="com.xyz.myapp.service.impl.ConcurrentOperationExecutor">
    <property name="maxRetries" value="3"/>
    <property name="order" value="100"/>
</bean>

爲了改進方面,使它只重試冪等操作,我們可以定義以下冪等註釋:

@Retention(RetentionPolicy.RUNTIME)
public @interface Idempotent {
    // marker annotation
}

然後,我們可以使用註釋來註釋服務操作的實現。對方面的更改是隻重試冪等操作,這涉及到細化切入點表達式,以便只匹配@Idempotent等操作,如下所示:

@Around("com.xyz.myapp.SystemArchitecture.businessService() && " +
        "@annotation(com.xyz.myapp.service.Idempotent)")
public Object doConcurrentOperation(ProceedingJoinPoint pjp) throws Throwable {
    // ...
}

==基於模式的AOP支持
如果您喜歡基於xml的格式,Spring還提供了使用新的aop名稱空間標記定義方面的支持。支持與使用@AspectJ樣式時完全相同的切入點表達式和通知類型。因此,在本節中,我們將重點討論新的語法,並將讀者轉到前一節([aop-ataspectj])的討論中,以瞭解編寫切入點表達式和通知參數的綁定。

要使用本節中描述的aop名稱空間標記,您需要導入spring-aop模式,正如在 XML Schema-based configuration中所描述的那樣。有關如何導入AOP名稱空間中的標記,請 the AOP schema

在Spring配置中,所有方面和advisor元素都必須放在<aop:config>元素中(在應用程序上下文配置中可以有多個<aop:config>元素)。<aop:config>元素可以包含切入點、顧問和方面元素(注意,這些元素必須按順序聲明)。

<aop:config>配置風格大量使用了Spring的自動代理機制 auto-proxying。如果您已經通過使用BeanNameAutoProxyCreator或類似的東西使用了顯式的自動代理,那麼這可能會導致問題(比如通知沒有被編織)。推薦的使用模式是隻使用<aop:config>樣式或者只使用AutoProxyCreator樣式,並且永遠不要混合使用它們。

===聲明一個Aspect
當您使用模式支持時,方面是定義爲Spring應用程序上下文中的bean的常規Java對象。狀態和行爲在對象的字段和方法中捕獲,切入點和建議信息在XML中捕獲。

可以使用<aop:aspect>元素聲明一個方面,使用ref屬性引用支持bean,如下面的例子所示:

<aop:config>
    <aop:aspect id="myAspect" ref="aBean">
        ...
    </aop:aspect>
</aop:config>

<bean id="aBean" class="...">
    ...
</bean>

支持方面的bean(本例中是aBean)當然可以像其他任何Spring bean一樣配置和注入依賴項。
===聲明一個切入點
可以在<aop:config>元素中聲明一個指定的切入點,讓切入點定義在多個方面和顧問之間共享。
表示服務層中任何業務服務執行的切入點可以定義如下:

<aop:config>

    <aop:pointcut id="businessService"
        expression="execution(* com.xyz.myapp.service.*.*(..))"/>

</aop:config>

注意,切入點表達式本身使用的是與 [aop-ataspectj]中描述的相同的AspectJ切入點表達式語言。如果您使用基於模式的聲明樣式,您可以引用在切入點表達式的types (@Aspects)中定義的命名切入點。定義上述切入點的另一種方法是:

<aop:config>

    <aop:pointcut id="businessService"
        expression="com.xyz.myapp.SystemArchitecture.businessService()"/>

</aop:config>

假設您有一個在[aop-common-pointcuts]中描述的系統架構方面。
然後在一個方面中聲明一個切入點與聲明一個頂級切入點非常相似,如下面的例子所示:

<aop:config>

    <aop:aspect id="myAspect" ref="aBean">

        <aop:pointcut id="businessService"
            expression="execution(* com.xyz.myapp.service.*.*(..))"/>

        ...

    </aop:aspect>

</aop:config>

與@AspectJ方面非常相似,通過使用基於模式的定義樣式聲明的切入點可以收集連接點上下文。例如,下面的切入點將這個對象收集爲連接點上下文,並將其傳遞給通知:

<aop:config>

    <aop:aspect id="myAspect" ref="aBean">

        <aop:pointcut id="businessService"
            expression="execution(* com.xyz.myapp.service.*.*(..)) &amp;&amp; this(service)"/>

        <aop:before pointcut-ref="businessService" method="monitor"/>

        ...

    </aop:aspect>

</aop:config>

通知必須通過包含匹配名稱的參數來聲明以接收收集到的連接點上下文,如下所示:

public void monitor(Object service) {
    // ...
}

當在XML文檔中組合切入點子表達式時,&&很不方便,所以您可以分別使用and、or和not關鍵字來代替&&、||和!。例如,前面的切入點可以寫得更好:

<aop:config>

    <aop:aspect id="myAspect" ref="aBean">

        <aop:pointcut id="businessService"
            expression="execution(* com.xyz.myapp.service.*.*(..)) and this(service)"/>

        <aop:before pointcut-ref="businessService" method="monitor"/>

        ...
    </aop:aspect>
</aop:config>

注意,以這種方式定義的切入點是通過它們的XML id來引用的,不能作爲命名的切入點來使用,以形成複合切入點。因此,基於模式的定義樣式中的命名切入點支持比@AspectJ樣式提供的更有限。

= = =Declaring Advice
基於模式的AOP支持使用與@AspectJ樣式相同的五種通知,而且它們具有完全相同的語義。
= = = =Before Advice
在匹配的方法執行之前運行通知。通過在>元素之前使用<aop:aspect>聲明它,如下面的例子所示:

<aop:aspect id="beforeExample" ref="aBean">

    <aop:before
        pointcut-ref="dataAccessOperation"
        method="doAccessCheck"/>

    ...

</aop:aspect>

這裏,dataAccessOperation是在頂層(<aop:config>)定義的切入點的id。要定義內聯的切入點,用切入點屬性替換pointcut-ref屬性,如下所示:

<aop:aspect id="beforeExample" ref="aBean">

    <aop:before
        pointcut="execution(* com.xyz.myapp.dao.*.*(..))"
        method="doAccessCheck"/>

    ...

</aop:aspect>

正如我們在討論@AspectJ樣式時所注意到的,使用指定的切入點可以顯著提高代碼的可讀性。
方法屬性標識提供通知主體的方法(doAccessCheck)。必須爲包含通知的方面元素引用的bean定義此方法。在執行數據訪問操作(由切入點表達式匹配的方法執行連接點)之前,將調用方面bean上的doAccessCheck方法。

====After Returning Advice
返回後,當匹配的方法執行正常完成時,將運行通知。它在<aop:aspect>中聲明,方法與前面的通知相同。下面的例子展示瞭如何聲明它:

<aop:aspect id="afterReturningExample" ref="aBean">

    <aop:after-returning
        pointcut-ref="dataAccessOperation"
        method="doAccessCheck"/>

    ...

</aop:aspect>

與@AspectJ樣式一樣,您可以在通知體中獲得返回值。爲此,使用return屬性指定應該向其傳遞返回值的參數的名稱,如下面的示例所示:

<aop:aspect id="afterReturningExample" ref="aBean">

    <aop:after-returning
        pointcut-ref="dataAccessOperation"
        returning="retVal"
        method="doAccessCheck"/>

    ...

</aop:aspect>

doAccessCheck方法必須聲明一個名爲retVal的參數。此參數的類型以與@ afterreturn相同的方式約束匹配。例如,你可以這樣聲明方法簽名:

public void doAccessCheck(Object retVal) {...

====After Throwing Advice
當一個匹配的方法通過拋出異常退出時,拋出通知。它是通過使用後拋出元素在<aop:aspect>中聲明的,如下面的例子所示:

<aop:aspect id="afterThrowingExample" ref="aBean">

    <aop:after-throwing
        pointcut-ref="dataAccessOperation"
        method="doRecoveryActions"/>

    ...

</aop:aspect>

與@AspectJ樣式一樣,您可以在通知正文中獲得拋出的異常。爲此,使用throw屬性指定應該向其傳遞異常的參數的名稱,如下面的示例所示:

<aop:aspect id="afterThrowingExample" ref="aBean">

    <aop:after-throwing
        pointcut-ref="dataAccessOperation"
        throwing="dataAccessEx"
        method="doRecoveryActions"/>

    ...

</aop:aspect>

doRecoveryActions方法必須聲明一個名爲dataAccessEx的參數。此參數的類型以與@ afterthrow相同的方式約束匹配。例如,方法簽名可以聲明如下:

public void doRecoveryActions(DataAccessException dataAccessEx) {...

====After (Finally) Advice
無論匹配的方法執行如何退出,都會運行After (finally)通知。你可以使用after元素來聲明它,如下面的例子所示:

<aop:aspect id="afterFinallyExample" ref="aBean">

    <aop:after
        pointcut-ref="dataAccessOperation"
        method="doReleaseLock"/>

    ...

</aop:aspect>

= = = =Around Advice
最後一種建議是關於建議的。Around建議“圍繞”一個匹配的方法執行。它有機會在方法執行之前和之後執行工作,並確定何時、如何執行,甚至是否真正執行方法。Around建議通常用於以線程安全的方式共享方法執行前後的狀態(例如,啓動和停止計時器)。總是使用最不有力的建議來滿足你的需求。如果事先的建議能夠奏效,就不要使用迂迴的建議。

可以使用aop:around元素聲明around通知。通知方法的第一個參數必須是類型爲procedure edingjoinpoint的。在通知的主體中,對過程ingjoinpoint調用proceed()會導致底層方法執行。也可以使用對象[]調用proceed方法。當方法執行時,數組中的值用作方法執行的參數。請參閱[aop-ataspectj-around-advice]以獲得關於使用對象[]繼續調用的注意事項。下面的例子演示瞭如何在XML中聲明around通知:

<aop:aspect id="aroundExample" ref="aBean">

    <aop:around
        pointcut-ref="businessService"
        method="doBasicProfiling"/>

    ...

</aop:aspect>

doBasicProfiling通知的實現可以與@AspectJ示例完全相同(當然,去掉註釋),如下面的示例所示:

public Object doBasicProfiling(ProceedingJoinPoint pjp) throws Throwable {
    // start stopwatch
    Object retVal = pjp.proceed();
    // stop stopwatch
    return retVal;
}

= = = =Advice Parameters
基於模式的聲明樣式以與@AspectJ支持相同的方式支持全類型通知——通過名稱與通知方法參數匹配切入點參數。詳見 [aop-ataspectj-advice-params]。如果你想顯式地指定參數名稱建議方法(不依賴於先前描述的檢測策略),可以通過使用建議的arg-names屬性元素,在相同的方式對待argNames屬性在一個建議註釋(如 [aop-ataspectj-advice-params-names]中描述)。下面的例子演示瞭如何在XML中指定參數名:

<aop:before
    pointcut="com.xyz.lib.Pointcuts.anyPublicMethod() and @annotation(auditable)"
    method="audit"
    arg-names="auditable"/>

argname屬性接受以逗號分隔的參數名列表。
下面是基於xsd的方法的一個稍微複雜的示例,展示了一些與一些強類型參數結合使用的建議:

package x.y.service;

public interface PersonService {

    Person getPerson(String personName, int age);
}

public class DefaultFooService implements FooService {

    public Person getPerson(String name, int age) {
        return new Person(name, age);
    }
}

下一個是方面。請注意,profile(..)方法接受大量強類型參數,其中第一個恰好是用於繼續方法調用的連接點。該參數的存在表明profile(..)將被用作around advice,如下例所示:

package x.y;

import org.aspectj.lang.ProceedingJoinPoint;
import org.springframework.util.StopWatch;

public class SimpleProfiler {

    public Object profile(ProceedingJoinPoint call, String name, int age) throws Throwable {
        StopWatch clock = new StopWatch("Profiling for '" + name + "' and '" + age + "'");
        try {
            clock.start(call.toShortString());
            return call.proceed();
        } finally {
            clock.stop();
            System.out.println(clock.prettyPrint());
        }
    }
}

最後,下面的示例XML配置將對特定連接點執行上述通知產生影響:

<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:aop="http://www.springframework.org/schema/aop"
    xsi:schemaLocation="
        http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop.xsd">

    <!-- this is the object that will be proxied by Spring's AOP infrastructure -->
    <bean id="personService" class="x.y.service.DefaultPersonService"/>

    <!-- this is the actual advice itself -->
    <bean id="profiler" class="x.y.SimpleProfiler"/>

    <aop:config>
        <aop:aspect ref="profiler">

            <aop:pointcut id="theExecutionOfSomePersonServiceMethod"
                expression="execution(* x.y.service.PersonService.getPerson(String,int))
                and args(name, age)"/>

            <aop:around pointcut-ref="theExecutionOfSomePersonServiceMethod"
                method="profile"/>

        </aop:aspect>
    </aop:config>

</beans>

考慮以下驅動程序腳本:

import org.springframework.beans.factory.BeanFactory;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import x.y.service.PersonService;

public final class Boot {

    public static void main(final String[] args) throws Exception {
        BeanFactory ctx = new ClassPathXmlApplicationContext("x/y/plain.xml");
        PersonService person = (PersonService) ctx.getBean("personService");
        person.getPerson("Pengo", 12);
    }
}

有了這樣一個引導類,我們將得到類似於下面的標準輸出:

StopWatch 'Profiling for 'Pengo' and '12'': running time (millis) = 0
-----------------------------------------
ms     %     Task name
-----------------------------------------
00000  ?  execution(getFoo)

= = = =Advice Ordering
當多個通知需要在同一個連接點(執行方法)上執行時,排序規則如 [aop-ataspectj-advice-ordering]中所述。方面之間的優先級是通過向支持方面的bean添加Order註釋或讓bean實現有序接口來確定的。

= = =介紹
引入(在AspectJ中稱爲類型間聲明)讓方面聲明通知對象實現給定接口,並代表這些對象提供接口的實現。

可以在aop:方面中使用aop: declour -parent元素來進行介紹。可以使用aop:declare-parents元素聲明匹配類型有一個新的父類(因此有了這個名稱)。例如,給定一個名爲UsageTracked的接口和一個名爲DefaultUsageTracked的接口的實現,以下方面聲明所有服務接口的實現者也實現UsageTracked接口。(例如,爲了通過JMX公開統計數據。)

<aop:aspect id="usageTrackerAspect" ref="usageTracking">

    <aop:declare-parents
        types-matching="com.xzy.myapp.service.*+"
        implement-interface="com.xyz.myapp.service.tracking.UsageTracked"
        default-impl="com.xyz.myapp.service.tracking.DefaultUsageTracked"/>

    <aop:before
        pointcut="com.xyz.myapp.SystemArchitecture.businessService()
            and this(usageTracked)"
            method="recordUsage"/>

</aop:aspect>

支持usageTracking bean的類將包含以下方法:

public void recordUsage(UsageTracked usageTracked) {
    usageTracked.incrementUseCount();
}

要實現的接口由實現接口屬性決定。types-matching屬性的值是一個AspectJ類型模式。任何匹配類型的bean都實現UsageTracked接口。注意,在前面示例的before建議中,服務bean可以直接用作UsageTracked接口的實現。要以編程方式訪問bean,可以編寫以下代碼:

UsageTracked usageTracked = (UsageTracked) context.getBean("myService");

====Aspect 實例化模型
模式定義方面惟一支持的實例化模型是單例模型。在將來的版本中可能會支持其他實例化模型。

= = =Advisors
“Advisors”的概念來自於Spring中定義的AOP支持,在AspectJ中沒有一個直接的等價物。建議者就像一個獨立的小方面,只有一條建議。通知本身由一個bean表示,並且必須實現[aop-api-advice-types]中描述的一個通知接口。顧問可以利用AspectJ切入點表達式。

Spring通過<aop:advisor>元素支持advisor概念。您通常會看到它與事務性建議一起使用,在Spring中事務性建議也有自己的名稱空間支持。下面的例子顯示了一個advisor工具:

<aop:config>

    <aop:pointcut id="businessService"
        expression="execution(* com.xyz.myapp.service.*.*(..))"/>

    <aop:advisor
        pointcut-ref="businessService"
        advice-ref="tx-advice"/>

</aop:config>

<tx:advice id="tx-advice">
    <tx:attributes>
        <tx:method name="*" propagation="REQUIRED"/>
    </tx:attributes>
</tx:advice>

除了前面示例中使用的pointcut-ref屬性外,您還可以使用pointcut屬性內聯定義切入點表達式。
要定義advisor工具的優先級,以便通知可以參與排序,可以使用order屬性來定義advisor工具的有序值。
===一個AOP模式示例

本節將展示使用模式支持重寫時,來自[aop-ataspectj-example]的併發鎖定失敗重試示例的外觀。

由於併發性問題(例如死鎖失敗者),業務服務的執行有時會失敗。如果操作被重試,它很可能在下一次嘗試中成功。對於適合在這種情況下重試的業務服務(冪等操作不需要返回用戶以解決衝突),我們希望透明地重試操作,以避免客戶端看到一個悲觀的lockingfailureexception異常。這是一個明顯跨越服務層中的多個服務的需求,因此是通過方面實現的理想需求。

因爲我們想要重試操作,所以我們需要使用around通知,以便能夠多次調用proceed。下面的清單顯示了基本方面的實現(它是一個使用模式支持的常規Java類):

public class ConcurrentOperationExecutor implements Ordered {

    private static final int DEFAULT_MAX_RETRIES = 2;

    private int maxRetries = DEFAULT_MAX_RETRIES;
    private int order = 1;

    public void setMaxRetries(int maxRetries) {
        this.maxRetries = maxRetries;
    }

    public int getOrder() {
        return this.order;
    }

    public void setOrder(int order) {
        this.order = order;
    }

    public Object doConcurrentOperation(ProceedingJoinPoint pjp) throws Throwable {
        int numAttempts = 0;
        PessimisticLockingFailureException lockFailureException;
        do {
            numAttempts++;
            try {
                return pjp.proceed();
            }
            catch(PessimisticLockingFailureException ex) {
                lockFailureException = ex;
            }
        } while(numAttempts <= this.maxRetries);
        throw lockFailureException;
    }

}

請注意,方面實現了有序接口,因此我們可以將方面的優先級設置爲高於事務通知的優先級(我們希望每次重試時都有一個新的事務)。maxRetries和order屬性都是由Spring配置的。主要動作發生在doConcurrentOperation around advice方法中。我們試着繼續。如果我們以一個PessimisticLockingFailureException失敗了,我們就會再試一次,除非我們已經用盡了所有的重試嘗試。

注意:這個類與@AspectJ示例中使用的類相同,但是去掉了註釋。

對應的彈簧配置如下:

<aop:config>

    <aop:aspect id="concurrentOperationRetry" ref="concurrentOperationExecutor">

        <aop:pointcut id="idempotentOperation"
            expression="execution(* com.xyz.myapp.service.*.*(..))"/>

        <aop:around
            pointcut-ref="idempotentOperation"
            method="doConcurrentOperation"/>

    </aop:aspect>

</aop:config>

<bean id="concurrentOperationExecutor"
    class="com.xyz.myapp.service.impl.ConcurrentOperationExecutor">
        <property name="maxRetries" value="3"/>
        <property name="order" value="100"/>
</bean>

請注意,目前我們假設所有業務服務都是冪等的。如果不是這樣,我們可以改進方面,使它只重試真正的冪等操作,方法是引入冪等註釋並使用該註釋來註釋服務操作的實現,如下例所示:

@Retention(RetentionPolicy.RUNTIME)
public @interface Idempotent {
    // marker annotation
}

對方面的更改是隻重試冪等操作,這涉及到細化切入點表達式,以便只匹配@冪等操作,如下所示:

<aop:pointcut id="idempotentOperation"
        expression="execution(* com.xyz.myapp.service.*.*(..)) and
        @annotation(com.xyz.myapp.service.Idempotent)"/>

==選擇使用哪種AOP聲明樣式
一旦確定了方面是實現給定需求的最佳方法,那麼如何決定是使用Spring AOP還是AspectJ,是使用aspect語言(代碼)風格、@AspectJ註釋風格還是Spring XML風格呢?這些決策受到許多因素的影響,包括應用程序需求、開發工具和團隊對AOP的熟悉程度。

=== Spring AOP還是Full AspectJ?
用最簡單的方法。Spring AOP比使用完整的AspectJ更簡單,因爲不需要在開發和構建過程中引入AspectJ編譯器/編織器。如果您只需要建議在Spring bean上執行操作,那麼Spring AOP是正確的選擇。如果需要通知Spring容器沒有管理的對象(通常是域對象),則需要使用AspectJ。如果希望通知連接點而不是簡單的方法執行(例如,字段get或set連接點等),還需要使用AspectJ。

使用AspectJ時,可以選擇AspectJ語言語法(也稱爲“代碼樣式”)或@AspectJ註釋樣式。顯然,如果您不使用Java 5+,那麼您已經做出了選擇:使用代碼樣式。如果方面在您的設計中扮演了重要的角色,並且您能夠使用用於Eclipse的AspectJ開發工具(AJDT)插件 AspectJ Development Tools (AJDT) ,那麼AspectJ語言語法是首選的選項。它更簡潔,因爲該語言是專門爲編寫方面而設計的。如果您不使用Eclipse,或者只有幾個方面在應用程序中不發揮主要作用,那麼您可能希望考慮使用@AspectJ樣式,在IDE中堅持常規的Java編譯,並將方面編織階段添加到構建腳本中。

=== @AspectJ還是用於Spring AOP的XML ?
如果選擇使用Spring AOP,則可以選擇@AspectJ或XML樣式。有各種各樣的權衡要考慮。

現有的Spring用戶可能最熟悉XML樣式,它由真正的pojo支持。當使用AOP作爲配置企業服務的工具時,XML可能是一個不錯的選擇(一個好的測試是,您是否將切入點表達式視爲您可能希望獨立更改的配置的一部分)。使用XML樣式,從您的配置可以更清楚地看出系統中存在哪些方面。

XML樣式有兩個缺點。首先,它沒有將它所處理的需求的實現完全封裝在一個地方。DRY原則認爲,在一個系統中,任何知識都應該有一個單一的、明確的、權威的表示。在使用XML樣式時,有關需求如何實現的知識在支持bean類的聲明和配置文件中的XML之間進行劃分。當您使用@AspectJ樣式時,這些信息被封裝在一個單獨的模塊中:方面。其次,XML樣式在它所能表達的方面比@AspectJ樣式稍受限制:只支持“單例”方面實例化模型,不可能組合在XML中聲明的命名切入點。例如,在@AspectJ樣式中,您可以編寫如下內容:

@Pointcut("execution(* get*())")
public void propertyAccess() {}

@Pointcut("execution(org.xyz.Account+ *(..))")
public void operationReturningAnAccount() {}

@Pointcut("propertyAccess() && operationReturningAnAccount()")
public void accountPropertyAccess() {}

在XML風格中,你可以聲明前兩個切入點:

<aop:pointcut id="propertyAccess"
        expression="execution(* get*())"/>

<aop:pointcut id="operationReturningAnAccount"
        expression="execution(org.xyz.Account+ *(..))"/>

XML方法的缺點是不能通過組合這些定義來定義accountPropertyAccess切入點。
@AspectJ樣式支持額外的實例化模型和更豐富的切入點組合。它的優點是將方面作爲一個模塊單元。它還有一個優點,即@AspectJ方面可以被Spring AOP和AspectJ理解(從而使用)。因此,如果您以後決定需要AspectJ的功能來實現額外的需求,那麼可以輕鬆地遷移到經典的AspectJ設置。總的來說,除了簡單的企業服務配置之外,Spring團隊更喜歡使用@AspectJ風格的自定義方面。

==Mixing Aspect Types
通過使用自動代理支持、模式定義的<aop:aspect>方面、<aop:advisor>聲明的顧問、甚至在相同配置中的其他樣式的代理和攔截器,完全可以混合使用@AspectJ樣式方面。所有這些都是通過使用相同的底層支持機制實現的,並且可以毫無困難地共存。

= =代理機制
Spring AOP使用JDK動態代理或CGLIB來爲給定的目標對象創建代理。JDK動態代理構建在JDK中,而CGLIB是一個公共的開源類定義庫(重新打包到spring-core中)。

如果要代理的目標對象實現了至少一個接口,則使用JDK動態代理。目標類型實現的所有接口都是代理的。如果目標對象沒有實現任何接口,則創建一個CGLIB代理。

如果您想強制使用CGLIB代理(例如,代理爲目標對象定義的每個方法,而不僅僅是那些由其接口實現的方法),您可以這樣做。然而,你應該考慮以下問題:

  • 對於CGLIB,不能建議使用final方法,因爲它們不能在運行時生成的子類中被覆蓋。
  • 從Spring 4.0開始,代理對象的構造函數不再被調用兩次,因爲CGLIB代理實例是通過Objenesis創建的。只有當您的JVM不允許構造函數繞過時,您纔可能看到來自Spring的AOP支持的雙重調用和相應的調試日誌條目。

爲了強制使用CGLIB代理,將<aop:config>元素的proxy-target-class屬性的值設置爲true,如下所示:

<aop:config proxy-target-class="true">
    <!-- other beans defined here... -->
</aop:config>

要在使用@AspectJ自動代理支持時強制CGLIB代理,請將<aop:aspectj-autoproxy>元素的代理目標類屬性設置爲true,如下所示:

<aop:aspectj-autoproxy proxy-target-class="true"/>

多個<aop:config/>部分在運行時被分解成一個統一的自動代理創建者,它應用指定的任何<aop:config/>部分(通常來自不同的XML bean定義文件)指定的最強大的代理設置。這也適用於<tx:註釋驅動/>和<aop:aspectj-autoproxy/>元素。

需要說明的是,在<tx:annotation-driven/>上使用proxy-target-class="true", <aop:aspectj-autoproxy/>,或<aop:config/>元素強制使用這三個CGLIB代理。

===理解AOP代理
Spring AOP是基於代理的。在編寫自己的方面或使用Spring框架提供的任何基於Spring aop的方面之前,掌握最後一條語句的語義是非常重要的。
首先考慮這樣一種場景:您有一個普通的、未代理的、沒有任何特殊含義的、直接的對象引用,如下面的代碼片段所示:

public class SimplePojo implements Pojo {

    public void foo() {
        // this next method invocation is a direct call on the 'this' reference
        this.bar();
    }

    public void bar() {
        // some logic...
    }
}

如果你在一個對象引用上調用了一個方法,該方法將直接在該對象引用上調用,如下圖和清單所示:

aop proxy plain pojo call

public class Main {

    public static void main(String[] args) {
        Pojo pojo = new SimplePojo();
        // this is a direct method call on the 'pojo' reference
        pojo.foo();
    }
}

當客戶機代碼的引用是一個代理時,情況會發生輕微的變化。考慮下面的關係圖和代碼片段:

aop proxy call

public class Main {

    public static void main(String[] args) {
        ProxyFactory factory = new ProxyFactory(new SimplePojo());
        factory.addInterface(Pojo.class);
        factory.addAdvice(new RetryAdvice());

        Pojo pojo = (Pojo) factory.getProxy();
        // this is a method call on the proxy!
        pojo.foo();
    }
}

這裏需要理解的關鍵是main類的main(..)方法中的客戶機代碼有一個對代理的引用。這意味着對該對象引用的方法調用是對代理的調用。因此,代理可以委託給與特定方法調用相關的所有攔截器(通知)。然而,一旦調用最終到達目標對象(在本例中是SimplePojo引用),它可能對自身執行的任何方法調用,如this.bar()或this.foo(),都將針對this引用而不是代理被調用。這具有重要的意義。這意味着自調用不會導致與方法調用關聯的通知有機會執行。

那麼,我們該怎麼做呢?最好的方法(這裏使用的術語“最佳”比較鬆散)是重構代碼,這樣就不會發生自調用。這確實需要您做一些工作,但是這是最好的、侵入性最小的方法。下一個方法絕對是可怕的,我們不願意指出它,正是因爲它是如此可怕。你可以(對我們來說很痛苦)完全把你的類中的邏輯與Spring AOP聯繫起來,如下面的例子所示:

public class SimplePojo implements Pojo {

    public void foo() {
        // this works, but... gah!
        ((Pojo) AopContext.currentProxy()).bar();
    }

    public void bar() {
        // some logic...
    }
}

這完全將您的代碼與Spring AOP結合在一起,並使類本身意識到它是在AOP上下文中使用的,這與AOP正好相反。它還需要一些額外的配置時,正在創建的代理,如下例所示:

public class Main {

    public static void main(String[] args) {
        ProxyFactory factory = new ProxyFactory(new SimplePojo());
        factory.addInterface(Pojo.class);
        factory.addAdvice(new RetryAdvice());
        factory.setExposeProxy(true);

        Pojo pojo = (Pojo) factory.getProxy();
        // this is a method call on the proxy!
        pojo.foo();
    }
}

最後,必須注意,AspectJ沒有這種自調用問題,因爲它不是基於代理的AOP框架。
==通過編程創建@AspectJ代理

除了通過使用<aop:config>或<aop:aspectj-autoproxy>在配置中聲明方面之外,還可以通過編程方式創建通知目標對象的代理。有關Spring AOP API的完整細節,請參見 next chapter。在這裏,我們希望關注通過使用@AspectJ方面自動創建代理的能力。

您可以使用org.springframework.aop.aspectj.annotation.AspectJProxyFactory類來爲一個或多個@AspectJ方面建議的目標對象創建代理。這個類的基本用法很簡單,如下例所示:

// create a factory that can generate a proxy for the given target object
AspectJProxyFactory factory = new AspectJProxyFactory(targetObject);

// add an aspect, the class must be an @AspectJ aspect
// you can call this as many times as you need with different aspects
factory.addAspect(SecurityManager.class);

// you can also add existing aspect instances, the type of the object supplied must be an @AspectJ aspect
factory.addAspect(usageTracker);

// now get the proxy object...
MyInterfaceType proxy = factory.getProxy();

有關更多信息,請參見 javadoc
==在Spring應用程序中使用AspectJ
到目前爲止,我們在這一章中所討論的一切都是純Spring AOP。在本節中,我們將討論如何使用AspectJ編譯器或weaver來代替或補充Spring AOP,如果您的需要超出了Spring AOP所提供的功能。

Spring附帶了一個小型的AspectJ方面庫,可以在發行版中以Spring -aspect .jar的形式單獨使用。您需要將其添加到類路徑中,以便使用其中的方面。討論這個庫的內容以及如何使用它。討論如何依賴注入使用AspectJ編譯器編織的AspectJ方面。最後,[aop- j-ltw]介紹了使用AspectJ的Spring應用程序的加載時編織。

===使用AspectJ來使用Spring注入域對象
Spring容器實例化和配置在應用程序上下文中定義的bean。如果bean定義的名稱包含要應用的配置,也可以要求bean工廠配置已存在的對象。spring-aspects.jar包含一個註釋驅動的方面,它利用這個功能來支持任何對象的依賴注入。該支持用於在任何容器的控制之外創建的對象。域對象通常屬於這一類別,因爲它們通常是由新操作符以編程方式創建的,或者是由ORM工具作爲數據庫查詢的結果創建的。

@Configurable 註解將一個類標記爲適合於spring驅動的配置。在最簡單的情況下,你可以使用純它作爲一個標記註釋,如下例所示:

package com.xyz.myapp.domain;

import org.springframework.beans.factory.annotation.Configurable;

@Configurable
public class Account {
    // ...
}

當以這種方式用作標記接口時,Spring通過使用與完全限定類型名(com.xyz.myapp.domain.Account)相同的bean定義(通常是原型作用域)配置帶註釋類型的新實例(在本例中是Account)。由於bean的默認名稱是其類型的全限定名,因此聲明原型定義的一種方便方法是省略id屬性,如下面的示例所示:

<bean class="com.xyz.myapp.domain.Account" scope="prototype">
    <property name="fundsTransferService" ref="fundsTransferService"/>
</bean>

如果需要顯式指定要使用的prototype bean定義的名稱,可以在註釋中直接指定,如下面的示例所示:

package com.xyz.myapp.domain;

import org.springframework.beans.factory.annotation.Configurable;

@Configurable("account")
public class Account {
    // ...
}

Spring現在查找一個名爲account的bean定義,並將其用作配置新帳戶實例的定義。
您還可以使用自動裝配來避免必須指定專用的bean定義。要讓Spring應用自動裝配,請使用@Configurable註解自動裝配屬性。可以指定@Configurable(autowire=Autowire.BY_TYPE)或@Configurable(autowire=Autowire.BY_NAME分別按類型或按名稱自動裝配的BY_NAME。另一種選擇是,通過字段或方法級別上的@Autowired或@Inject爲您的@Configurable bean指定顯式的、註釋驅動的依賴項注入(有關詳細信息,請參閱Annotation-based Container Configuration)。

最後,您可以通過使用dependencyCheck屬性(例如,@Configurable(autowire=Autowire.BY_NAME,dependencyCheck=true))來啓用對新創建和配置的對象中的對象引用的Spring依賴項檢查)。如果將此屬性設置爲true,則在配置之後,Spring將驗證所有屬性(不是原語或集合)都已設置。

注意,單獨使用註釋沒有任何作用。spring-aspects.jar中的AnnotationBeanConfigurerAspect對註釋的存在起作用。本質上,方面是這樣說的,“在初始化了一個用@Configurable註釋來配置新對象之後,使用Spring根據註釋的屬性配置新創建的對象”。在這個上下文中,“初始化”指的是新實例化的對象(例如,用新操作符實例化的對象),以及正在反序列化的可序列化對象(例如,通過readResolve())。

注意:以上段落中的關鍵短語之一是“in essence”。對於大多數情況,“從新對象的初始化返回之後”的確切語義是正確的。在這個上下文中,“初始化之後”意味着在對象構造之後注入依賴項。這意味着依賴項在類的構造函數體中不可用。如果您希望在構造函數體執行之前注入依賴項,這樣就可以在構造函數體中使用依賴項,那麼您需要在@ configurationdeclaration上定義依賴項,如下所示:

@Configurable(preConstruction = true)

在AspectJ編程指南AspectJ Programming Guide的這個附錄中 in this appendix,您可以找到關於AspectJ中各種切入點類型的語言語義的更多信息。

要實現這一點,必須使用AspectJ編織器編織帶註釋的類型。您可以使用構建時Ant或Maven任務來完成此任務(例如,請參閱AspectJ開發環境指南 AspectJ Development Environment Guide),也可以使用加載時編織(參見 [aop-aj-ltw])。AnnotationBeanConfigurerAspect本身需要由Spring配置(以便獲得對bean工廠的引用,該引用將用於配置新對象)。如果使用基於java的配置,可以將@EnableSpringConfigured添加到任何@Configuration類中,如下所示:

@Configuration
@EnableSpringConfigured
public class AppConfig {
}

如果您喜歡基於XML的配置,Spring上下文命名空間context namespace定義了一個方便的上下文:Spring配置的元素,您可以使用如下:

<context:spring-configured/>

在配置方面之前創建的@Configurable objects的狀態會導致向調試日誌發出一條消息,並且不會對該對象進行任何配置。一個例子可能是Spring配置中的一個bean,它在被Spring初始化時創建域對象。在這種情況下,可以使用depends-on bean屬性手動指定bean依賴於配置方面。下面的例子展示瞭如何使用depends-on屬性:

<bean id="myService"
        class="com.xzy.myapp.service.MyService"
        depends-on="org.springframework.beans.factory.aspectj.AnnotationBeanConfigurerAspect">

    <!-- ... -->

</bean>

注意:不要通過bean configurer方面激活@Configurable處理,除非您真的打算在運行時依賴它的語義。特別是,確保不要在容器中註冊爲常規Spring bean的bean類上使用@Configurable。這樣做會導致兩次初始化,一次通過容器,一次通過方面。

====單元測試@Configurable對象
@Configurable支持的目標之一是支持域對象的獨立單元測試,而不需要面對與硬編碼查找相關的困難。如果@Configurable類型沒有被AspectJ編織,那麼在單元測試期間註釋就沒有影響。您可以在被測試的對象中設置模擬或存根屬性引用,然後正常進行。如果AspectJ編寫了@Configurable類型,您仍然可以像往常一樣在容器外進行單元測試,但是每次構造@Configurable對象時都會看到一條警告消息,指示Spring還沒有對它進行配置。

====處理多個應用程序上下文
用於實現@Configurable支持的AnnotationBeanConfigurerAspect是一個AspectJ單例方面。單例方面的作用域與靜態成員的作用域相同:每個類裝入器都有一個方面實例定義類型。這意味着,如果您在同一個類加載器層次結構中定義多個應用程序上下文,那麼您需要考慮在哪裏定義@EnableSpringConfigured bean,以及在哪裏將spring-aspects.jar放到類路徑中。

考慮一個典型的Spring web應用程序配置,該配置具有一個共享的父應用程序上下文,其中定義了公共業務服務、支持這些服務所需的一切以及每個servlet的一個子應用程序上下文(其中包含特定於該servlet的定義)。所有這些上下文都共存於同一個類加載器層次結構中,因此AnnotationBeanConfigurerAspect只能包含對其中一個上下文的引用。

在這種情況下,我們建議在共享(父)應用程序上下文中定義@EnableSpringConfigured bean。這定義了您可能希望注入到域對象中的服務。其結果是,您不能使用@Configurable機制(這可能不是您想要做的事情)來配置具有在子(特定於servlet)上下文中定義的bean引用的域對象。

當在同一個容器中部署多個web應用程序時,確保每個web應用程序使用自己的類加載器加載spring-aspects.jar中的類型(例如,將spring-aspects.jar放在“WEB-INF/lib”中)。如果spring-aspects.jar只添加到容器範圍的類路徑中(因此由共享的父類加載器加載),所有web應用程序共享相同的方面實例(這可能不是您想要的)。

=== AspectJ的其他Spring方面
除了@Configurable aspect之外,spring-aspects.jar還包含一個AspectJ方面,您可以使用它來驅動Spring對使用@Transactional註釋註釋的類型和方法的事務管理。這主要是針對那些希望在Spring容器之外使用Spring框架的事務支持的用戶。

解釋@Transactional註釋的方面是AnnotationTransactionAspect。當您使用這個方面時,您必須註釋實現類(或該類中的方法或兩者),而不是類實現的接口(如果有的話)。AspectJ遵循Java的規則,接口上的註釋不是繼承的。

類上的@Transactional註釋爲類中任何公共操作的執行指定默認的事務語義。
類中的方法上的@Transactional註釋覆蓋類註釋(如果存在)給出的默認事務語義。任何可見性的方法都可以進行註釋,包括私有方法。直接註釋非公共方法是獲得此類方法執行的事務界定的惟一方法。

注意:從Spring Framework 4.2開始,spring-aspects 就提供了一個類似的方面,爲標準javax.transaction.Transactional註釋提供了完全相同的特性。查看JtaAnnotationTransactionAspect以獲得更多細節。

對於希望使用Spring配置和事務管理支持但又不想(或不能)使用註釋的AspectJ程序員,spring-aspects.jar還包含一些抽象方面,您可以擴展它們來提供自己的切入點定義。有關更多信息,請參見AbstractBeanConfigurerAspect和AbstractTransactionAspect方面的源代碼。作爲一個例子,下面的摘錄展示瞭如何編寫一個方面來配置域模型中定義的所有對象實例,方法是使用原型bean定義來匹配完全限定的類名:

public aspect DomainObjectConfiguration extends AbstractBeanConfigurerAspect {

    public DomainObjectConfiguration() {
        setBeanWiringInfoResolver(new ClassNameBeanWiringInfoResolver());
    }

    // the creation of a new bean (any object in the domain model)
    protected pointcut beanCreation(Object beanInstance) :
        initialization(new(..)) &&
        SystemArchitecture.inDomainModel() &&
        this(beanInstance);
}

===使用Spring IoC配置AspectJ方面
當您在Spring應用程序中使用AspectJ方面時,很自然地希望和期望能夠用Spring配置這些方面。AspectJ運行時本身負責方面的創建,通過Spring配置AspectJ創建的方面的方法取決於方面使用的AspectJ實例化模型(per-xxx子句)。

AspectJ方面的大部分都是單例方面。配置這些方面很容易。您可以創建一個bean定義來引用方面類型,幷包含factory-method="aspectOf" bean屬性。這確保了Spring通過請求AspectJ而不是試圖創建一個實例來獲得方面實例。下面的例子展示瞭如何使用factory-method="aspectOf"屬性:

bean id="profiler" class="com.xyz.profiler.Profiler"
        factory-method="aspectOf"> //1

    <property name="profilingStrategy" ref="jamonProfilingStrategy"/>
</bean>

//1 注意factory-method="aspectOf"屬性

如果你有@ AspectJ方面你想編織與AspectJ域模型(例如,使用裝入時編織類型)和其他您想要使用Spring AOP @ AspectJ方面,而這些方面都是配置在Spring,你需要告訴Spring AOP @ AspectJ auto-proxying @ AspectJ方面的支持,它精確子集定義的配置應該用於auto-proxying。可以通過在<aop:aspectj-autoproxy/>聲明中使用一個或多個<include/>元素來實現這一點。每個<include/>元素指定一個名稱模式,只有名稱至少與其中一個模式匹配的bean才用於Spring AOP自動代理配置。下面的例子展示瞭如何使用<include/>元素:

<aop:aspectj-autoproxy>
    <aop:include name="thisBean"/>
    <aop:include name="thatBean"/>
</aop:aspectj-autoproxy>

注意:不要被<aop:aspectj-autoproxy/>元素的名稱所誤導。使用它可以創建Spring AOP代理。這裏使用了@AspectJ風格的方面聲明,但是沒有涉及AspectJ運行時。

===在Spring框架中使用AspectJ進行加載時編織
加載時編織(LTW)是指在將AspectJ方面加載到Java虛擬機(JVM)時,將它們編織到應用程序的類文件中的過程。本節的重點是在Spring框架的特定上下文中配置和使用LTW。本節不是LTW的一般介紹。有關LTW細節和僅使用AspectJ配置LTW(完全不涉及Spring)的詳細信息,請參閱 LTW section of the AspectJ Development Environment Guide

Spring框架給AspectJ LTW帶來的價值在於對編織過程實現了更細粒度的控制。“普通的”AspectJ LTW是通過使用Java(5+)代理實現的,在啓動JVM時通過指定VM參數打開代理。因此,它是一個jvm範圍的設置,在某些情況下它可能很好,但通常有點太粗糙了。啓用了spring的LTW允許您在每個類加載器的基礎上切換LTW,這是一種更細粒度的方式,在“單jvm -多應用程序”環境中更有意義(例如在典型的應用服務器環境中)。

此外,在某些環境中 in certain environments,這種支持支持加載時編織,而不需要對應用服務器的啓動腳本進行任何修改,以添加-javaagent:path/to/aspectjweaver.jar或(如我們在本節後面所述)-javaagent:path/to/spring-instrument.jar。開發人員配置應用程序上下文以啓用加載時編織,而不是依賴通常負責部署配置(如啓動腳本)的管理員。

既然推銷已經結束了,那麼讓我們先簡單介紹一個使用Spring的AspectJ LTW的快速示例,然後再詳細介紹示例中引入的元素。有關完整的示例,請參見 Petclinic sample application

====第一個例子
假設您是一名應用程序開發人員,您的任務是診斷系統中某些性能問題的原因。我們將打開一個簡單的概要分析方面,使我們能夠快速獲得一些性能指標,而不是使用一個概要分析工具。然後,我們可以立即在該特定區域應用更細粒度的分析工具。

注意:這裏提供的示例使用XML配置。您還可以使用Java configuration使用@AspectJ。具體來說,您可以使用@EnableLoadTimeWeaving註釋作爲<context:load-time-weaver/>的替代(參見下面的詳細信息)。

下面的示例顯示了分析方面,這並不複雜。它是一個基於時間的分析器,使用@AspectJ-style的aspect聲明:

package foo;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.util.StopWatch;
import org.springframework.core.annotation.Order;

@Aspect
public class ProfilingAspect {

    @Around("methodsToBeProfiled()")
    public Object profile(ProceedingJoinPoint pjp) throws Throwable {
        StopWatch sw = new StopWatch(getClass().getSimpleName());
        try {
            sw.start(pjp.getSignature().getName());
            return pjp.proceed();
        } finally {
            sw.stop();
            System.out.println(sw.prettyPrint());
        }
    }

    @Pointcut("execution(public * foo...(..))")
    public void methodsToBeProfiled(){}
}

我們還需要創建一個META-INF/aop.xml文件,以通知AspectJ編織器我們希望將ProfilingAspect編織到類中。這個文件約定,即在Java類路徑上存在一個(或多個)名爲META-INF/aop.xml的文件,這是標準的AspectJ。下面的例子展示了aop.xml文件:

<!DOCTYPE aspectj PUBLIC "-//AspectJ//DTD//EN" "https://www.eclipse.org/aspectj/dtd/aspectj.dtd">
<aspectj>

    <weaver>
        <!-- only weave classes in our application-specific packages -->
        <include within="foo.*"/>
    </weaver>

    <aspects>
        <!-- weave in just this aspect -->
        <aspect name="foo.ProfilingAspect"/>
    </aspects>

</aspectj>

現在我們可以進入配置的特定於spring的部分。我們需要配置LoadTimeWeaver(稍後解釋)。這個加載時編織器是負責將一個或多個META-INF/aop.xml文件中的方面配置編織到應用程序中的類中的基本組件。它的優點是不需要太多的配置(你可以指定更多的選項,但這些選項將在後面詳細介紹),如下面的例子所示:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:context="http://www.springframework.org/schema/context"
    xsi:schemaLocation="
        http://www.springframework.org/schema/beans
        https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context
        https://www.springframework.org/schema/context/spring-context.xsd">

    <!-- a service object; we will be profiling its methods -->
    <bean id="entitlementCalculationService"
            class="foo.StubEntitlementCalculationService"/>

    <!-- this switches on the load-time weaving -->
    <context:load-time-weaver/>
</beans>

現在,所有需要的工件(aspect、META-INF/aop.xml文件和Spring配置)都已經就緒,我們可以用main(..)方法創建下面的驅動程序類來演示LTW的作用:

package foo;

import org.springframework.context.support.ClassPathXmlApplicationContext;

public final class Main {

    public static void main(String[] args) {
        ApplicationContext ctx = new ClassPathXmlApplicationContext("beans.xml", Main.class);

        EntitlementCalculationService entitlementCalculationService =
                (EntitlementCalculationService) ctx.getBean("entitlementCalculationService");

        // the profiling aspect is 'woven' around this method execution
        entitlementCalculationService.calculateEntitlement();
    }
}

我們還有最後一件事要做。本節的介紹確實說過,可以使用Spring在每個類裝載器的基礎上有選擇地打開LTW,這是真的。但是,對於本例,我們使用Java代理(由Spring提供)打開LTW。我們使用以下命令來運行前面顯示的主類:

java -javaagent:C:/projects/foo/lib/global/spring-instrument.jar foo.Main

-javaagent是一個標誌,用於指定和啓用代理來檢測在JVM上運行的程序 agents to instrument programs that run on the JVM。Spring框架附帶了這樣一個代理,即InstrumentationSavingAgent,它打包在spring-instrument.jar中,在前面的示例中作爲-javaagent參數的值提供。

主程序執行的輸出與下面的示例類似。(我已經在calculateEntitlement()實現中引入了Thread.sleep(..)語句,所以分析器實際上捕獲的不是0毫秒(01234毫秒不是AOP引入的開銷)。下面的清單顯示了我們在運行分析器時得到的輸出:

Calculating entitlement

StopWatch 'ProfilingAspect': running time (millis) = 1234
------ ----- ----------------------------
ms     %     Task name
------ ----- ----------------------------
01234  100%  calculateEntitlement

由於這個LTW是通過使用成熟的AspectJ實現的,所以我們不僅限於通知Spring bean。主程序上的以下微小變化產生了相同的結果:

package foo;

import org.springframework.context.support.ClassPathXmlApplicationContext;

public final class Main {

    public static void main(String[] args) {
        new ClassPathXmlApplicationContext("beans.xml", Main.class);

        EntitlementCalculationService entitlementCalculationService =
                new StubEntitlementCalculationService();

        // the profiling aspect will be 'woven' around this method execution
        entitlementCalculationService.calculateEntitlement();
    }
}

請注意,在前面的程序中,我們是如何引導Spring容器的,然後完全在Spring的上下文中創建StubEntitlementCalculationService的新實例的。分析建議仍然被納入其中。
誠然,這個例子過於簡單。但是,在前面的示例中已經介紹了Spring中LTW支持的基礎,本節的其餘部分將詳細解釋每個配置和使用背後的“原因”。

注意:本例中使用的ProfilingAspect可能是基本的,但是非常有用。這是一個很好的開發時方面的例子,開發人員可以在開發期間使用它,然後很容易地從部署到UAT或生產中的應用程序的構建中排除它。

==== Aspects
在LTW中使用的方面必須是AspectJ方面。您可以用AspectJ語言本身來編寫它們,也可以用@AspectJ風格來編寫方面。這樣,方面就是有效的AspectJ和Spring AOP方面。此外,編譯後的方面類需要在類路徑上可用。

==== 'META-INF/aop.xml'
AspectJ LTW基礎結構是通過使用Java類路徑上的一個或多個META-INF/aop.xml文件來配置的(直接地或者更典型地,在jar文件中)。
這個文件的結構和內容在AspectJ參考文檔的LTW AspectJ reference documentation部分中有詳細說明。因爲aop.xml文件是100% AspectJ的,所以我們不在這裏進一步描述它。

===所需的庫(jars)
至少,您需要以下庫來使用Spring框架對AspectJ LTW的支持:

  • spring-aop.jar

  • aspectjweaver.jar

如果您使用spring提供的代理 Spring-provided agent to enable instrumentation來啓用插裝,您還需要:

  • spring-instrument.jar

= = = = Spring配置
Spring的LTW支持中的關鍵組件是LoadTimeWeaver接口(在org.springframework.instrument.classloading中)。以及Spring發行版附帶的許多實現。LoadTimeWeaver負責添加一個或多個java.lang.instrument.ClassFileTransformers。在運行時將classfiletransformer轉換爲ClassLoader,這爲各種有趣的應用程序打開了大門,其中之一就是方面的LTW。

注意:如果您不熟悉運行時類文件轉換的概念,請參閱java.lang的javadoc API文檔java.lang.instrument。雖然該文檔並不全面,但至少可以看到關鍵接口和類(在閱讀本節時作爲參考)。

爲特定的ApplicationContext配置LoadTimeWeaver就像添加一行代碼一樣簡單。(請注意,您幾乎肯定需要使用ApplicationContext作爲Spring容器—通常,BeanFactory是不夠的,因爲LTW支持使用了BeanFactoryPostProcessors。)

要啓用Spring框架的LTW支持,您需要配置LoadTimeWeaver,這通常是通過使用@EnableLoadTimeWeaving註釋來完成的,如下所示:

@Configuration
@EnableLoadTimeWeaving
public class AppConfig {
}

或者,如果喜歡基於xml的配置,可以使用<context:load-time-weaver/>元素。請注意,元素是在上下文名稱空間中定義的。下面的例子展示瞭如何使用<context:load-time-weaver/>:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:context="http://www.springframework.org/schema/context"
    xsi:schemaLocation="
        http://www.springframework.org/schema/beans
        https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context
        https://www.springframework.org/schema/context/spring-context.xsd">

    <context:load-time-weaver/>

</beans>

前面的配置會自動定義和註冊大量特定於ltw的基礎設施bean,比如LoadTimeWeaver和AspectJWeavingEnabler。默認的LoadTimeWeaver是DefaultContextLoadTimeWeaver類,它試圖修飾一個自動檢測到的LoadTimeWeaver。“自動檢測”的LoadTimeWeaver的確切類型取決於您的運行時環境。下表總結了各種LoadTimeWeaver實現:

Runtime Environment LoadTimeWeaver implementation

Running in Apache Tomcat

TomcatLoadTimeWeaver

Running in GlassFish (limited to EAR deployments)

GlassFishLoadTimeWeaver

Running in Red Hat’s JBoss AS or WildFly

JBossLoadTimeWeaver

Running in IBM’s WebSphere

WebSphereLoadTimeWeaver

Running in Oracle’s WebLogic

WebLogicLoadTimeWeaver

JVM started with Spring InstrumentationSavingAgent (java -javaagent:path/to/spring-instrument.jar)

InstrumentationLoadTimeWeaver

Fallback, expecting the underlying ClassLoader to follow common conventions (namely addTransformer and optionally a getThrowawayClassLoader method)

ReflectiveLoadTimeWeaver

注意,該表只列出了在使用DefaultContextLoadTimeWeaver時自動檢測到的LoadTimeWeavers。您可以精確地指定使用哪個LoadTimeWeaver實現。
要使用Java配置指定特定的LoadTimeWeaver,請實現LoadTimeWeavingConfigurer接口並覆蓋getLoadTimeWeaver()方法。下面的例子指定了一個ReflectiveLoadTimeWeaver:

@Configuration
@EnableLoadTimeWeaving
public class AppConfig implements LoadTimeWeavingConfigurer {

    @Override
    public LoadTimeWeaver getLoadTimeWeaver() {
        return new ReflectiveLoadTimeWeaver();
    }
}

如果使用基於xml的配置,可以將完全限定的classname指定爲<context:load-time-weaver/>元素上的weaver-class屬性的值。同樣,下面的例子指定了一個ReflectiveLoadTimeWeaver:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:context="http://www.springframework.org/schema/context"
    xsi:schemaLocation="
        http://www.springframework.org/schema/beans
        https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context
        https://www.springframework.org/schema/context/spring-context.xsd">

    <context:load-time-weaver
            weaver-class="org.springframework.instrument.classloading.ReflectiveLoadTimeWeaver"/>

</beans>

由配置定義和註冊的LoadTimeWeaver稍後可以使用衆所周知的名稱LoadTimeWeaver從Spring容器中檢索。請記住,LoadTimeWeaver僅作爲Spring的LTW基礎結構添加一個或多個classfiletransformer的機制而存在。執行LTW的實際ClassFileTransformer是ClassPreProcessorAgentAdapter(來自org.aspectj.weaver.loadtime包)類。請參閱ClassPreProcessorAgentAdapter類的類級別javadoc以獲得更多的詳細信息,因爲編織實際如何實現的細節超出了本文的範圍。

配置還有最後一個屬性需要討論:aspectjweave屬性(如果使用XML,也可以稱爲aspectj- weave)。該屬性控制是否啓用LTW。它接受三個可能的值中的一個,如果屬性不存在,默認值是自動檢測。下表總結了三種可能的值:

Annotation Value XML Value Explanation

ENABLED

on

啓動了AspectJ編織,並在適當的加載時編織方面。

DISABLED

off

LTW關閉。加載時沒有編織方面。

AUTODETECT

autodetect

如果Spring LTW基礎結構能夠找到至少一個META-INF/aop.xml文件,那麼AspectJ編織就開始了。否則,它是關閉的。這是默認值。

= = = =特定於環境的配置
最後一節包含在應用服務器和web容器等環境中使用Spring的LTW支持時需要的任何其他設置和配置。

==== Tomcat, JBoss, WebSphere, WebLogic
Tomcat、JBoss/WildFly、IBM WebSphere Application Server和Oracle WebLogic Server都提供了能夠進行本地檢測的通用應用程序類加載器。Spring的本地LTW可以利用那些類加載器實現來提供AspectJ編織。如前所述,您可以簡單地啓用 described earlier加載時編織。具體來說,您不需要修改JVM啓動腳本來添加-javaagent:path/to/spring-instrument.jar。

注意,在JBoss上,您可能需要禁用應用程序服務器掃描,以防止它在應用程序實際啓動之前加載類。一個快速的解決方案是將一個名爲WEB-INF/jboss-scanning.xml的文件添加到您的工件中,幷包含以下內容:

<scanning xmlns="urn:jboss:scanning:1.0"/>

====通用Java應用程序
當特定的LoadTimeWeaver實現不支持的環境中需要類插裝時,JVM代理是通用解決方案。對於這種情況,Spring提供了InstrumentationLoadTimeWeaver,它需要一個特定於Spring(但非常通用)的JVM代理——spring-instrument.jar。通過常見的@EnableLoadTimeWeaving和<context:load-time-weaver/>設置自動檢測。

要使用它,您必須通過提供以下JVM選項來啓動帶有Spring代理的虛擬機:

-javaagent:/path/to/spring-instrument.jar

注意,這需要修改JVM啓動腳本,這可能會阻止您在應用服務器環境中使用它(取決於您的服務器和操作策略)。也就是說,對於每個JVM一個應用程序的部署(如獨立的Spring Boot應用程序),通常可以控制整個JVM設置。

= =更多的資源
有關AspectJ的更多信息可以在AspectJ網站AspectJ website上找到。

由Adrian Colyer等人(Addison-Wesley, 2005)編寫的Eclipse AspectJ爲AspectJ語言提供了全面的介紹和參考。
AspectJ在行動,第二版由Ramnivas Laddad(曼寧,2009)高度推薦。這本書的重點是在AspectJ上,但是也討論了很多一般的AOP主題(有一定的深度)。

= Spring AOP api
前一章描述了Spring通過@AspectJ和基於模式的方面定義對AOP的支持。在本章中,我們將討論更低級別的Spring AOP api。對於常見的應用程序,我們建議使用Spring AOP和AspectJ切入點,如前一章所述。

== Spring中的切入點API
本節描述Spring如何處理關鍵的切入點概念。

= = =概念
Spring的切入點模型支持獨立於通知類型的切入點重用。您可以使用相同的切入點針對不同的通知。

org.springframework.aop.Pointcut接口是中心接口,用於將通知定向到特定的類和方法。完整界面如下:

public interface Pointcut {

    ClassFilter getClassFilter();

    MethodMatcher getMethodMatcher();

}

將切入點接口分割成兩個部分允許重用與部分匹配的類和方法以及細粒度的組合操作(例如與另一個方法匹配器執行“union”)。
ClassFilter接口用於將切入點限制到一組給定的目標類。如果matches()方法總是返回true,則匹配所有目標類。下面的清單顯示了ClassFilter接口定義:

public interface ClassFilter {

    boolean matches(Class clazz);
}

MethodMatcher接口通常更重要。完整界面如下:

public interface MethodMatcher {

    boolean matches(Method m, Class targetClass);

    boolean isRuntime();

    boolean matches(Method m, Class targetClass, Object[] args);
}

matches(方法、類)方法用於測試這個切入點是否匹配目標類上的給定方法。在創建AOP代理以避免對每個方法調用進行測試時,可以執行這個評估。如果兩個參數匹配的方法爲給定的方法返回true,而方法matcher的isRuntime()方法返回true,那麼在每次方法調用時都會調用三個參數匹配的方法。這讓切入點在執行目標通知之前查看傳遞給方法調用的參數。

大多數MethodMatcher實現都是靜態的,這意味着它們的isRuntime()方法返回false。在這種情況下,永遠不會調用三參數匹配方法。

注意:如果可能的話,嘗試讓切入點成爲靜態的,從而允許AOP框架在創建AOP代理時緩存切入點計算的結果。

===切入點上的操作
Spring支持切入點上的操作(特別是union和交集)。

Union表示兩個切入點匹配的方法。交集意味着兩個切入點匹配的方法。聯合通常更有用。您可以通過使用org.springframework.aop.support.Pointcuts類中的靜態方法來組合切入點。或者在同一個包中使用ComposablePointcut類。但是,使用AspectJ切入點表達式通常是一種更簡單的方法。

=== AspectJ表達式切入點
自2.0以來,Spring使用的最重要的切入點類型是org.springframework.aop.aspectj.AspectJExpressionPointcut。這是一個切入點,它使用AspectJ提供的庫來解析AspectJ切入點表達式字符串。

有關受支持的AspectJ切入點原語的討論,請參閱前一章 previous chapter
===方便的切入點實現

Spring提供了幾個方便的切入點實現。你可以直接使用其中的一些。其他的則打算在特定於應用程序的切入點中子類化。

= = = =靜態的切入點
靜態切入點基於方法和目標類,不能考慮方法的參數。對於大多數使用來說,靜態切入點已經足夠了,而且是最好的。Spring只能在第一次調用方法時對靜態切入點求值一次。之後,就不需要在每次方法調用時重新計算切入點了。

本節的其餘部分將介紹Spring中包含的一些靜態切入點實現。

====正則表達式切入點
指定靜態切入點的一種明顯的方法是正則表達式。除了Spring之外,還有幾個AOP框架使這成爲可能。org.springframework.aop.support。JdkRegexpMethodPointcut是一個通用的正則表達式切入點,它使用JDK中的正則表達式支持。

使用JdkRegexpMethodPointcut類,您可以提供模式字符串的列表。如果其中任何一個匹配,切入點的計算結果爲true。(因此,結果就是這些切入點的有效結合。)
下面的例子展示瞭如何使用JdkRegexpMethodPointcut:

<bean id="settersAndAbsquatulatePointcut"
        class="org.springframework.aop.support.JdkRegexpMethodPointcut">
    <property name="patterns">
        <list>
            <value>.*set.*</value>
            <value>.*absquatulate</value>
        </list>
    </property>
</bean>

Spring提供了一個名爲RegexpMethodPointcutAdvisor的便利類,它允許我們也引用一個Advice(請記住,一個通知可以是一個攔截器,在通知之前,拋出通知,以及其他)。在幕後,Spring使用JdkRegexpMethodPointcut。使用RegexpMethodPointcutAdvisor簡化了連接,因爲一個bean封裝了切入點和Advice,如下面的例子所示:

<bean id="settersAndAbsquatulateAdvisor"
        class="org.springframework.aop.support.RegexpMethodPointcutAdvisor">
    <property name="advice">
        <ref bean="beanNameOfAopAllianceInterceptor"/>
    </property>
    <property name="patterns">
        <list>
            <value>.*set.*</value>
            <value>.*absquatulate</value>
        </list>
    </property>
</bean>

您可以對任何Advice類型使用RegexpMethodPointcutAdvisor。

= = = = = Attribute-driven切入點
靜態切入點的一個重要類型是元數據驅動的切入點。它使用元數據屬性的值(通常是源級元數據)。
= = = =動態的切入點

評估動態切入點比評估靜態切入點的成本更高。它們考慮方法參數和靜態信息。這意味着必須在每次方法調用時對它們求值,並且不能緩存結果,因爲參數會有所不同。
主要的例子是控制流切入點。

====控制流切入點
Spring控制流切入點在概念上類似於AspectJ cflow切入點,儘管功能沒那麼強大。(目前沒有辦法指定一個切入點在與另一個切入點匹配的連接點下面執行。)控制流切入點與當前調用堆棧匹配。例如,如果連接點被com.mycompany中的方法調用,則可能com.mycompany.web或SomeCaller類。控制流切入點是通過使用org.springframework.aop.support.ControlFlowPointcut類。

注意:控制流切入點在運行時的計算成本比其他動態切入點要高得多。在Java 1.4中,成本大約是其他動態切入點的5倍。

= = =切入點超類
Spring提供了有用的切入點超類來幫助您實現自己的切入點。

因爲靜態切入點最有用,所以您可能應該繼承StaticMethodMatcherPointcut的子類。這只需要實現一個抽象方法(儘管您可以覆蓋其他方法來定製行爲)。下面的例子展示瞭如何子類化StaticMethodMatcherPointcut:

class TestStaticPointcut extends StaticMethodMatcherPointcut {

    public boolean matches(Method m, Class targetClass) {
        // return true if custom criteria match
    }
}

還有用於動態切入點的超類。您可以對任何通知類型使用自定義切入點。
= = =自定義切入點

因爲在Spring AOP中切入點是Java類,而不是語言特性(如在AspectJ中),所以可以聲明定製的切入點,不管是靜態的還是動態的。Spring中的定製切入點可以是任意複雜的。但是,如果可以的話,我們建議使用AspectJ切入點表達式語言。

注意:Spring的後續版本可能提供對JAC所提供的“語義切入點”的支持——例如,“更改目標對象中的實例變量的所有方法”。

==Advice API in Spring
現在我們可以研究Spring AOP如何處理通知。
= = =Advice 生命週期
每個建議都是一個Spring bean。一個通知實例可以在所有被通知對象之間共享,也可以是每個被通知對象的唯一實例。這對應於每個類或每個實例的通知。

每個類的建議是最常用的。它適用於一般的建議,例如事務顧問。它們不依賴於代理對象的狀態或添加新狀態。它們只是對方法和參數起作用。
每個實例的建議適用於介紹,以支持混合。在本例中,通知將狀態添加到代理對象。
可以在同一個AOP代理中混合使用共享和每個實例的通知。

===通知類型
Spring提供了幾種通知類型,並且可以擴展以支持任意的通知類型。本節描述基本概念和標準通知類型。
====攔截通知
Spring中最基本的通知類型是圍繞通知的攔截。

Spring與AOP Alliance接口兼容,用於提供使用方法攔截的周圍通知。實現MethodInterceptor和around advice的類也應該實現以下接口:

public interface MethodInterceptor extends Interceptor {

    Object invoke(MethodInvocation invocation) throws Throwable;
}

invoke()方法的方法調用參數將被調用的方法、目標連接點、AOP代理和參數暴露給方法。invoke()方法應該返回調用的結果:連接點的返回值。
下面的例子展示了一個簡單的MethodInterceptor實現:

public class DebugInterceptor implements MethodInterceptor {

    public Object invoke(MethodInvocation invocation) throws Throwable {
        System.out.println("Before: invocation=[" + invocation + "]");
        Object rval = invocation.proceed();
        System.out.println("Invocation returned");
        return rval;
    }
}

注意對MethodInvocation的proceed()方法的調用。這將繼續沿着攔截器鏈向連接點移動。大多數攔截器調用此方法並返回其返回值。但是,與任何around通知一樣,MethodInterceptor可以返回不同的值或拋出異常,而不是調用proceed方法。然而,如果沒有充分的理由,你是不會想這麼做的。

注意:MethodInterceptor實現提供了與其他AOP聯盟兼容的AOP實現的互操作性。本節其餘部分討論的其他通知類型實現了常見的AOP概念,但是是以特定於spring的方式實現的。雖然使用最特定的通知類型有一個優點,但是如果您可能希望在另一個AOP框架中運行方面,請堅持使用圍繞通知的MethodInterceptor。注意,切入點目前不是框架之間的互操作,AOP聯盟目前也沒有定義切入點接口。

= = = =Before Advice
更簡單的通知類型是before通知。這並不需要一個MethodInvocation對象,因爲它只在進入方法之前被調用。
before通知的主要優點是不需要調用proceed()方法,因此不可能無意中沿着攔截器鏈繼續下去。
下面的清單顯示了MethodBeforeAdvice接口:

public interface MethodBeforeAdvice extends BeforeAdvice {

    void before(Method m, Object[] args, Object target) throws Throwable;
}

(Spring的API設計允許字段先於通知,儘管通常的對象適用於字段攔截,而且Spring不太可能實現它。)

注意,返回類型爲void。Before通知可以在連接點執行之前插入自定義行爲,但不能更改返回值。如果before通知拋出異常,它將中止攔截器鏈的進一步執行。異常傳播回攔截器鏈。如果未選中它,或者調用方法的簽名未選中它,則直接將其傳遞給客戶機。否則,它會被AOP代理包裝在一個未檢查的異常中。

下面的例子顯示了Spring中的before通知,它計算了所有方法調用:

public class CountingBeforeAdvice implements MethodBeforeAdvice {

    private int count;

    public void before(Method m, Object[] args, Object target) throws Throwable {
        ++count;
    }

    public int getCount() {
        return count;
    }
}

注意:在通知可以與任何切入點一起使用之前。

= = = =Throws Advice
如果連接點拋出異常,則在連接點返回後調用拋出建議。Spring提供了類型化的拋出建議。注意,這意味着org.springframework.aop.ThrowsAdvice接口不包含任何方法。它是一個標記接口,標識給定對象實現一個或多個類型化拋出通知方法。這些文件的格式如下:

afterThrowing([Method, args, target], subclassOfThrowable)

只需要最後一個參數。方法簽名可能有一個或四個參數,這取決於advice方法是否對方法和參數感興趣。下面的兩個清單顯示了拋出建議的示例類。
如果拋出RemoteException(包括來自子類的異常),則調用以下通知:

public class RemoteThrowsAdvice implements ThrowsAdvice {

    public void afterThrowing(RemoteException ex) throws Throwable {
        // Do something with remote exception
    }
}

與前面的通知不同,下一個示例聲明瞭四個參數,因此它可以訪問被調用的方法、方法參數和目標對象。如果拋出ServletException,將調用以下建議:

public class ServletThrowsAdviceWithArguments implements ThrowsAdvice {

    public void afterThrowing(Method m, Object[] args, Object target, ServletException ex) {
        // Do something with all arguments
    }
}

最後一個示例演示瞭如何在一個同時處理RemoteException和ServletException的類中使用這兩個方法。在一個類中可以組合任意數量的拋出建議方法。下面是最後一個例子:

public static class CombinedThrowsAdvice implements ThrowsAdvice {

    public void afterThrowing(RemoteException ex) throws Throwable {
        // Do something with remote exception
    }

    public void afterThrowing(Method m, Object[] args, Object target, ServletException ex) {
        // Do something with all arguments
    }
}

注意:如果拋出建議方法本身拋出異常,它將覆蓋原來的異常(也就是說,它將更改拋出給用戶的異常)。覆蓋的異常通常是一個RuntimeException,它與任何方法簽名兼容。但是,如果一個throws-advice方法拋出一個已檢查的異常,那麼它必須與目標方法聲明的異常匹配,因此在某種程度上耦合到特定的目標方法簽名。不要拋出與目標方法的簽名不兼容的未聲明的已檢查異常!

拋出通知可以與任何切入點一起使用。

====After Returning Advice
在Spring中返回通知後必須實現org.springframe .aop。AfterReturningAdvice接口,如下所示:

public interface AfterReturningAdvice extends Advice {

    void afterReturning(Object returnValue, Method m, Object[] args, Object target)
            throws Throwable;
}

返回通知可以訪問返回值(不能修改)、調用的方法、方法的參數和目標。
下面的返回通知計數所有成功的方法調用沒有拋出異常:

public class CountingAfterReturningAdvice implements AfterReturningAdvice {

    private int count;

    public void afterReturning(Object returnValue, Method m, Object[] args, Object target)
            throws Throwable {
        ++count;
    }

    public int getCount() {
        return count;
    }
}

此建議不會更改執行路徑。如果拋出異常,則拋出攔截器鏈而不是返回值。

注意:返回後的通知可以與任何切入點一起使用。

= = = =Advice介紹
Spring將介紹建議視爲一種特殊的攔截建議。
需要一個IntroductionAdvisor 和IntroductionInterceptor 來實現以下接口:

public interface IntroductionInterceptor extends MethodInterceptor {

    boolean implementsInterface(Class intf);
}

繼承自AOP Alliance MethodInterceptor接口的invoke()方法必須實現引入。也就是說,如果被調用的方法位於已引入的接口上,則引入攔截器負責處理方法調用—它不能調用proceed()。

引入通知不能與任何切入點一起使用,因爲它只適用於類,而不是方法級別。你只能在introduction advisor中使用introduction advice,它有以下方法:

public interface IntroductionAdvisor extends Advisor, IntroductionInfo {

    ClassFilter getClassFilter();

    void validateInterfaces() throws IllegalArgumentException;
}

public interface IntroductionInfo {

    Class[] getInterfaces();
}

沒有方法匹配器,因此,沒有與引入建議相關聯的切入點。只有類過濾是合乎邏輯的。
getInterfaces()方法返回這個顧問所引入的接口。
內部使用validateInterfaces()方法來查看所引入的接口是否可以由配置的介紹性攔截器實現。
考慮一個來自Spring測試套件的例子,假設我們想要向一個或多個對象引入以下接口:

public interface Lockable {
    void lock();
    void unlock();
    boolean locked();
}

這說明了一個混合。我們希望能夠將被建議的對象強制轉換爲Lockable,無論它們的類型是什麼,並調用lock和unlock方法。如果我們調用lock()方法,我們希望所有setter方法都拋出一個LockedException。因此,我們可以添加一個方面,它提供了使對象在不瞭解對象的情況下變得不可變的能力:AOP的一個很好的例子。

首先,我們需要一個能夠挑起重任的攔截器IntroductionInterceptor。在本例中,我們擴展了org.springframework.aop.support.DelegatingIntroductionInterceptor類。我們可以直接實現IntroductionInterceptor ,但是在大多數情況下使用委派式攔截器DelegatingIntroductionInterceptor 是最好的。

委託中介攔截器DelegatingIntroductionInterceptor被設計成將中介委託給所引入接口的實際實現,隱藏了攔截的使用。可以使用構造函數參數將委託設置爲任何對象。默認委託(使用無參數構造函數時)是這樣的。因此,在下一個例子中,委託是delegate的LockMixin子類。對於給定的委託(默認情況下是其本身),一個delegating介紹性攔截器實例會查找委託實現的所有接口(介紹性攔截器除外),並支持針對其中任何一個進行介紹。像LockMixin這樣的子類可以調用suppressInterface(類intf)方法來抑制不應該公開的接口。但是,不管一個介紹性攔截器準備支持多少個接口,介紹性advisor都會控制哪些接口實際上是公開的。方法隱藏同一接口的任何實現。

因此,LockMixin擴展了委託導入攔截器,並實現了自身的Lockable。超類會自動獲得可鎖定的支持,所以我們不需要指定它。我們可以用這種方式引入任意數量的接口。

注意鎖定實例變量的使用。這有效地將額外的狀態添加到目標對象中。
下面的例子展示了LockMixin類的例子:

public class LockMixin extends DelegatingIntroductionInterceptor implements Lockable {

    private boolean locked;

    public void lock() {
        this.locked = true;
    }

    public void unlock() {
        this.locked = false;
    }

    public boolean locked() {
        return this.locked;
    }

    public Object invoke(MethodInvocation invocation) throws Throwable {
        if (locked() && invocation.getMethod().getName().indexOf("set") == 0) {
            throw new LockedException();
        }
        return super.invoke(invocation);
    }

}

通常,您不需要覆蓋invoke()方法。通常,委派中介攔截器實現(如果方法被引入,它將調用委託方法,否則將繼續向連接點前進)就足夠了。在本例中,我們需要添加一個check:如果處於鎖定模式,則不能調用任何setter方法。

所需的引入只需要持有一個不同的LockMixin實例並指定引入的接口(在本例中,僅爲Lockable)。一個更復雜的例子可能引用引入攔截器(它將被定義爲原型)。在本例中,沒有與LockMixin相關的配置,因此我們使用new來創建它。下面的例子展示了我們的LockMixinAdvisor類:

public class LockMixinAdvisor extends DefaultIntroductionAdvisor {

    public LockMixinAdvisor() {
        super(new LockMixin(), Lockable.class);
    }
}

我們可以非常簡單地應用這個advisor工具,因爲它不需要配置。(但是,如果沒有一個介紹人顧問,就不可能使用介紹人攔截器。)與介紹一樣,advisor工具必須是每個實例的,因爲它是有狀態的。對於每個被建議的對象,我們需要一個不同的LockMixinAdvisor實例,因此也需要一個LockMixin實例。advisor工具包含被建議對象的部分狀態。

我們可以通過在XML配置中使用Advised.addAdvisor()方法或(推薦的方法)以編程方式應用這個advisor,就像其他任何advisor一樣。下面討論的所有代理創建選項,包括“自動代理創建者”,正確地處理引入和有狀態混合。

== Spring中的Advisor API
在Spring中,Advisor是一個方面,它只包含一個與切入點表達式相關聯的advice對象。

除了介紹的特殊情況外,任何advisor工具都可以用於任何建議。org.springframework.aop.support。DefaultPointcutAdvisor是最常用的advisor類。它可以與MethodInterceptor、BeforeAdvice或ThrowsAdvice一起使用。

在同一個AOP代理中,可以在Spring中混合advisor和advice類型。例如,您可以在一個代理配置中使用圍繞通知、拋出通知和before通知的攔截。Spring自動創建必要的攔截器鏈。

==使用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或另一個支持ioco的類來創建AOP代理的最重要的好處之一是通知和切入點也可以由IoC管理。這是一個強大的特性,支持某些在其他AOP框架中難以實現的方法。例如,通知本身可以引用應用程序對象(除了在任何AOP框架中都應該可用的目標之外),從而受益於依賴項注入提供的所有可插拔性。

= = = JavaBean屬性

與Spring提供的大多數FactoryBean實現一樣,ProxyFactoryBean類本身就是JavaBean。其性質用於:

  • 指定要代理的目標。
  • 指定是否使用CGLIB(稍後進行描述,請參閱 [aop-pfb-proxy-types])。

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

  • proxyTargetClass:如果要代理的是目標類,而不是目標類的接口,則爲true。如果此屬性值設置爲true,則創建CGLIB代理(但也請參閱 [aop-pfb-proxy-types])。
  • optimize:控制是否對通過CGLIB創建的代理應用積極的優化。除非完全理解相關AOP代理如何處理優化,否則不應該輕率地使用這個設置。這目前僅用於CGLIB代理。它對JDK動態代理沒有影響。
  • frozen:如果凍結了代理配置,則不再允許更改配置。這對於輕微的優化和在創建代理之後不希望調用者能夠(通過建議的接口)操作代理的情況都很有用。此屬性的默認值爲false,因此允許更改(如添加其他通知)。
  • exposeProxy:確定當前代理是否應該在ThreadLocal中公開,以便目標可以訪問它。如果目標需要獲取代理,並且將曝光代理屬性設置爲true,那麼目標可以使用AopContext.currentProxy()方法。

ProxyFactoryBean的其他特性包括:

  • proxyInterfaces:一個字符串接口名數組。如果沒有提供,則使用目標類的CGLIB代理(但也請參閱[aop-pfb-proxy-types])。
  • interceptorNames:要應用的Advisor、interceptor或其他通知名稱的字符串數組。訂購是重要的,以先到先得爲基礎。也就是說,列表中的第一個攔截器是第一個能夠攔截調用的攔截器。

這些名稱是當前工廠中的bean名稱,包括來自祖先工廠的bean名稱。這裏不能提到bean引用,因爲這樣做會導致ProxyFactoryBean忽略通知的單例設置。
您可以使用星號(*)附加攔截器名稱。這樣做會導致應用所有advisor bean,它們的名稱都以要應用的星號前面的部分開始。您可以在 [aop-global-advisors]中找到使用此功能的示例。

  • singleton:不管getObject()方法被調用多少次,工廠是否應該返回單個對象。有幾個FactoryBean實現提供了這樣的方法。默認值爲true。如果您想要使用有狀態的通知—例如,對於有狀態的混合—使用prototype advice和一個單例值false。

=== JDK-和基於cglib的代理
本節是關於ProxyFactoryBean如何爲特定目標對象(要代理的對象)創建基於jdk的代理或基於cglib的代理的權威文檔。

注意:ProxyFactoryBean在創建JDK或基於cglib的代理方面的行爲在版本1.2之間發生了變化。x和2。0的彈簧。在自動檢測接口方面,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定義。
  • 用於提供建議的Advisor 和Interceptor 。
  • 一個指定目標對象(personTarget  bean)、代理接口和應用建議的AOP代理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屬性接受一個字符串列表,其中包含當前工廠中攔截器或顧問的bean名稱。您可以使用advisors, interceptors, before, after returning和throws advice objects。顧問的順序很重要。

注意:您可能想知道爲什麼這個列表不包含bean引用。這樣做的原因是,如果ProxyFactoryBean的單例屬性設置爲false,那麼它必須能夠返回獨立的代理實例。如果顧問本身就是原型,則需要返回一個獨立的實例,因此必須能夠從工廠獲得原型的實例。持有引用是不夠的。

前面顯示的person bean定義可以用來代替person實現,如下所示:

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

與普通Java對象一樣,在相同的IoC上下文中的其他bean可以表示對它的強類型依賴。下面的例子演示瞭如何做到這一點:

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

本例中的PersonUser類公開了Person類型的屬性。就AOP而言,可以透明地使用AOP代理來代替“真正的”人員實現。但是,它的類將是一個動態代理類。可以將其轉換爲建議的接口(稍後討論)。

您可以使用匿名內部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。雖然最好是對接口而不是類進行編程,但是在處理遺留代碼時,建議不實現接口的類的能力可能很有用。(一般來說,春天是沒有規定性的。雖然它使應用良好的實踐變得容易,但它避免強制採用特定的方法。

如果願意,您可以在任何情況下強制使用CGLIB,即使您有接口。
CGLIB代理通過在運行時生成目標類的子類來工作。Spring將這個生成的子類配置爲將方法調用委託給原始目標。子類用於實現裝飾器模式,在通知中編織。

CGLIB代理通常應該對用戶透明。但是,有一些問題需要考慮:

  • 不能建議使用最後的方法,因爲它們不能被覆蓋。
  • 沒有必要將CGLIB添加到類路徑中。從Spring 3.2開始,CGLIB被重新打包幷包含在Spring -core JAR中。換句話說,與JDK動態代理一樣,基於cglib的AOP工作“開箱即用”。

CGLIB代理和動態代理之間的性能差別不大。在這種情況下,業績不應是決定性的考慮因素。
===使用“全局”Advisors

通過將一個星號附加到一個攔截器名稱,所有具有與星號前部分匹配的bean名稱的顧問都被添加到顧問鏈中。如果您需要添加一組標準的“global”advisors,那麼這將非常方便。下面的例子定義了兩個全局顧問:

<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"/>

==簡潔的代理定義
特別是在定義事務代理時,您可能會得到許多類似的代理定義。使用父bean和子bean定義,以及內部bean定義,可以產生更清晰、更簡潔的代理定義。

首先,我們爲代理創建一個父類、模板、bean定義,如下:

<bean id="txProxyTemplate" abstract="true"
        class="org.springframework.transaction.interceptor.TransactionProxyFactoryBean">
    <property name="transactionManager" ref="transactionManager"/>
    <property name="transactionAttributes">
        <props>
            <prop key="*">PROPAGATION_REQUIRED</prop>
        </props>
    </property>
</bean>

它從來沒有實例化過,所以它實際上是不完整的。然後,需要創建的每個代理都是一個子bean定義,它將代理的目標包裝爲內部bean定義,因爲無論如何都不會單獨使用目標。下面的例子展示了這樣一個子bean:

<bean id="myService" parent="txProxyTemplate">
    <property name="target">
        <bean class="org.springframework.samples.MyServiceImpl">
        </bean>
    </property>
</bean>

您可以覆蓋來自父模板的屬性。在下面的例子中,我們覆蓋了事務傳播設置:

<bean id="mySpecialService" parent="txProxyTemplate">
    <property name="target">
        <bean class="org.springframework.samples.MySpecialServiceImpl">
        </bean>
    </property>
    <property name="transactionAttributes">
        <props>
            <prop key="get*">PROPAGATION_REQUIRED,readOnly</prop>
            <prop key="find*">PROPAGATION_REQUIRED,readOnly</prop>
            <prop key="load*">PROPAGATION_REQUIRED,readOnly</prop>
            <prop key="store*">PROPAGATION_REQUIRED</prop>
        </props>
    </property>
</bean>

請注意,在父bean示例中,我們通過將abstract屬性設置爲true(如前所述)顯式地將父bean定義標記爲抽象,這樣就不會實際實例化它。默認情況下,應用程序上下文(但不是簡單的bean工廠)預先實例化所有單例。因此,重要的是(至少對於單例bean來說),如果您有一個(父)bean定義,您只打算將其用作模板,並且這個定義指定了一個類,那麼您必須確保將抽象屬性設置爲true。否則,應用程序上下文實際上會嘗試預實例化它。

==使用ProxyFactory以編程方式創建AOP代理
使用Spring以編程方式創建AOP代理是很容易的。這使您可以在不依賴於Spring IoC的情況下使用Spring AOP。
由目標對象實現的接口被自動代理。下面的清單顯示了一個目標對象的代理的創建,其中有一個攔截器和一個advisor工具:

ProxyFactory factory = new ProxyFactory(myBusinessInterfaceImpl);
factory.addAdvice(myMethodInterceptor);
factory.addAdvisor(myAdvisor);
MyBusinessInterface tb = (MyBusinessInterface) factory.getProxy();

第一步是構造一個org.springframework.aop.framework.ProxyFactory類型的對象。您可以使用目標對象(如前面的示例)來創建它,或者在備用構造函數中指定要代理的接口。

您可以添加advice(將攔截器作爲一種專門的建議)、advisor,或者兩者都可以,並在ProxyFactory的生命週期中對它們進行操作。如果您添加了一個IntroductionInterceptionAroundAdvisor,您可以使代理實現額外的接口。

ProxyFactory上還有一些方便的方法(繼承自AdvisedSupport),可以添加其他通知類型,比如before和throw通知。AdvisedSupport是ProxyFactory和ProxyFactoryBean的超類。

注意:在大多數應用程序中,將AOP代理創建與IoC框架集成是最佳實踐。我們建議您使用AOP將配置從Java代碼外部化,通常您應該這樣做。

==操作被建議的對象
無論您如何創建AOP代理,您都可以通過使用org.springframework.aop.framework.Advised接口來操作它們。任何AOP代理都可以被轉換到這個接口,不管它實現了哪個其他接口。該接口包括以下方法:

Advisor[] getAdvisors();

void addAdvice(Advice advice) throws AopConfigException;

void addAdvice(int pos, Advice advice) throws AopConfigException;

void addAdvisor(Advisor advisor) throws AopConfigException;

void addAdvisor(int pos, Advisor advisor) throws AopConfigException;

int indexOf(Advisor advisor);

boolean removeAdvisor(Advisor advisor) throws AopConfigException;

void removeAdvisor(int index) throws AopConfigException;

boolean replaceAdvisor(Advisor a, Advisor b) throws AopConfigException;

boolean isFrozen();

getAdvisors()方法爲添加到工廠的每個Advisor、interceptor或其他建議類型返回一個Advisor。如果您添加了一個Advisor,那麼在這個索引處返回的Advisor就是您添加的對象。如果您添加了攔截器或其他通知類型,Spring會用一個總是返回true的切入點將其封裝到advisor中。因此,如果您添加了一個MethodInterceptor,那麼爲這個索引返回的advisor就是一個DefaultPointcutAdvisor,它返回您的MethodInterceptor和一個匹配所有類和方法的切入點。

addAdvisor()方法可用於添加任何顧問。通常,包含切入點和通知的advisor工具是通用的DefaultPointcutAdvisor,您可以將它用於任何建議或切入點(但不用於介紹)。

默認情況下,即使創建了代理,也可以添加或刪除顧問或攔截器。惟一的限制是不可能添加或刪除引入顧問,因爲工廠中的現有代理沒有顯示接口更改。(你可以從工廠獲得一個新的代理來避免這個問題。)
下面的例子顯示了將AOP代理轉換到被建議的接口,並檢查和操作它的建議:

Advised advised = (Advised) myObject;
Advisor[] advisors = advised.getAdvisors();
int oldAdvisorCount = advisors.length;
System.out.println(oldAdvisorCount + " advisors");

// Add an advice like an interceptor without a pointcut
// Will match all proxied methods
// Can use for interceptors, before, after returning or throws advice
advised.addAdvice(new DebugInterceptor());

// Add selective advice using a pointcut
advised.addAdvisor(new DefaultPointcutAdvisor(mySpecialPointcut, myAdvice));

assertEquals("Added two advisors", oldAdvisorCount + 2, advised.getAdvisors().length);

注意:在生產環境中修改關於業務對象的建議是否明智(沒有雙關語的意思)是值得懷疑的,儘管毫無疑問存在合法的使用案例。但是,它在開發(例如,在測試中)中非常有用。我們有時發現,能夠以攔截器或其他通知的形式添加測試代碼,進入我們想要測試的方法調用是非常有用的。(例如,建議可以進入爲該方法創建的事務中,在將事務標記爲回滾之前,可以運行SQL來檢查數據庫更新是否正確。)

根據創建代理的方式,通常可以設置凍結標誌。在這種情況下,被建議的isFrozen()方法返回true,任何通過添加或刪除修改通知的嘗試都會導致AopConfigException。凍結被建議對象的狀態的能力在某些情況下是有用的(例如,防止調用代碼刪除安全攔截器)。

==使用“自動代理”功能
到目前爲止,我們已經考慮了通過使用ProxyFactoryBean或類似的工廠bean顯式地創建AOP代理。

Spring還允許我們使用“自動代理”bean定義,它可以自動代理選擇的bean定義。這是在Spring的“bean後處理器”基礎結構上構建的,它支持將任何bean定義修改爲容器裝載。

在這個模型中,您在XML bean定義文件中設置了一些特殊的bean定義來配置自動代理基礎設施。這允許您聲明有資格進行自動代理的目標。你不需要使用ProxyFactoryBean。

有兩種方法:

  • 通過使用在當前上下文中引用特定bean的自動代理創建者。
  • 自動代理創建的一個特殊情況值得單獨考慮:由源級元數據屬性驅動的自動代理創建。

===自動代理Bean定義
本節討論org.springframework.aop.frame .autoproxy包提供的自動代理創建者。

= = = = BeanNameAutoProxyCreator
BeanNameAutoProxyCreator類是一個BeanPostProcessor,它自動爲名稱與文字值或通配符匹配的bean創建AOP代理。下面的例子展示瞭如何創建一個BeanNameAutoProxyCreator bean:

<bean class="org.springframework.aop.framework.autoproxy.BeanNameAutoProxyCreator">
    <property name="beanNames" value="jdk*,onlyJdk"/>
    <property name="interceptorNames">
        <list>
            <value>myInterceptor</value>
        </list>
    </property>
</bean>

與ProxyFactoryBean一樣,它有一個interceptorNames屬性,而不是一個攔截器列表,以允許原型顧問的正確行爲。命名的“攔截器”可以是顧問或任何通知類型。

與一般的自動代理一樣,使用BeanNameAutoProxyCreator的主要目的是一致地將相同的配置應用到多個對象上,而配置的體積最小。它是將聲明性事務應用於多個對象的流行選擇。

名稱匹配的Bean定義(如前面示例中的jdkMyBean和onlyJdk)是帶有目標類的普通舊Bean定義。AOP代理是由BeanNameAutoProxyCreator自動創建的。對所有匹配的bean應用相同的通知。注意,如果使用了建議器(而不是前面例子中的攔截器),切入點可能會以不同的方式應用於不同的bean。

= = = = DefaultAdvisorAutoProxyCreator
一個更一般和極其強大的自動代理的創建者是DefaultAdvisorAutoProxyCreator。這將自動地在當前上下文中應用合格的advisor工具,而不需要在auto-proxy advisor的bean定義中包含特定的bean名稱。它提供了與BeanNameAutoProxyCreator相同的配置一致性和避免複製的優點。

使用這個機制包括:

  • 指定DefaultAdvisorAutoProxyCreator bean定義。
  • 在相同或相關上下文中指定任意數量的顧問。注意,這些必須是顧問,而不是攔截器或其他建議。這是必要的,因爲必須有一個切入點來評估,來檢查每個通知到候選bean定義的資格。

DefaultAdvisorAutoProxyCreator自動計算每個advisor中包含的切入點,以查看它應該將什麼(如果有的話)通知應用到每個業務對象(例如本例中的businessObject1和businessObject2)。

這意味着可以將任意數量的advisor工具自動應用於每個業務對象。如果任何顧問中的切入點都不匹配業務對象中的任何方法,則不代理該對象。當爲新的業務對象添加bean定義時,如果需要,它們會自動代理。

一般來說,自動代理的優點是使調用者或依賴項不可能獲得未通知的對象。在這個ApplicationContext上調用getBean(“businessObject1”)將返回一個AOP代理,而不是目標業務對象。(前面展示的“內部bean”習語也提供了這個好處。)
下面的例子創建了一個DefaultAdvisorAutoProxyCreator bean和本節討論的其他元素:

<bean class="org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator"/>

<bean class="org.springframework.transaction.interceptor.TransactionAttributeSourceAdvisor">
    <property name="transactionInterceptor" ref="transactionInterceptor"/>
</bean>

<bean id="customAdvisor" class="com.mycompany.MyAdvisor"/>

<bean id="businessObject1" class="com.mycompany.BusinessObject1">
    <!-- Properties omitted -->
</bean>

<bean id="businessObject2" class="com.mycompany.BusinessObject2"/>

如果您希望將相同的建議一致地應用於許多業務對象,則DefaultAdvisorAutoProxyCreator非常有用。一旦基礎設施定義就緒,就可以添加新的業務對象,而無需包含特定的代理配置。您還可以輕鬆地刪除其他方面(例如,跟蹤或性能監視方面),只需對配置進行最小的更改。

DefaultAdvisorAutoProxyCreator提供了對過濾的支持(通過使用命名約定,只對特定的advisor進行評估,從而允許在同一工廠中使用多個不同配置的AdvisorAutoProxyCreators )和排序。顧問可以實現org.springframework.core.Ordered接口,以確保正確的順序,如果這是一個問題。前面示例中使用的TransactionAttributeSourceAdvisor有一個可配置的訂單值。默認設置是無序的。

==使用TargetSource實現
Spring提供了TargetSource的概念,在org.springframe .aop中表示。TargetSource接口。這個接口負責返回實現連接點的“目標對象”。每次AOP代理處理一個方法調用時,都會請求TargetSource實現一個目標實例。

使用Spring AOP的開發人員通常不需要直接使用TargetSource實現,但是這提供了支持池、熱可切換和其他複雜目標的強大方法。例如,通過使用池來管理實例,合用TargetSource可以爲每次調用返回不同的目標實例。

如果不指定TargetSource,則使用默認實現包裝本地對象。每次調用都返回相同的目標(正如您所期望的)。
本節的其餘部分將描述Spring提供的標準目標源以及如何使用它們。

注意:使用自定義目標源時,目標通常需要是原型,而不是單例bean定義。這允許Spring在需要時創建新的目標實例。

===可熱插拔的目標源
org.springframework.aop.target。HotSwappableTargetSource的存在是爲了讓AOP代理的目標被切換,同時讓調用者保留對它的引用。
更改目標源的目標將立即生效。HotSwappableTargetSource是線程安全的。
您可以通過使用HotSwappableTargetSource上的swap()方法來更改目標,如下面的示例所示:

HotSwappableTargetSource swapper = (HotSwappableTargetSource) beanFactory.getBean("swapper");
Object oldTarget = swapper.swap(newTarget);

下面的例子展示了所需的XML定義:

<bean id="initialTarget" class="mycompany.OldTarget"/>

<bean id="swapper" class="org.springframework.aop.target.HotSwappableTargetSource">
    <constructor-arg ref="initialTarget"/>
</bean>

<bean id="swappable" class="org.springframework.aop.framework.ProxyFactoryBean">
    <property name="targetSource" ref="swapper"/>
</bean>

前面的swap()調用將更改可切換bean的目標。持有對該bean的引用的客戶機不知道更改,但立即開始觸及新目標。
雖然本例沒有添加任何建議(使用TargetSource不需要添加建議),但是任何TargetSource都可以與任意建議結合使用。

===Pooling Target Sources
使用池目標源提供了與無狀態會話ejb類似的編程模型,其中維護了一個相同實例池,方法調用將釋放池中的對象。

Spring池和SLSB池的一個重要區別是,Spring池可以應用於任何POJO。與Spring一般情況一樣,這種服務可以以一種非侵入性的方式應用。
Spring支持Commons Pool 2.2,它提供了一個相當高效的池實現。您需要應用程序的類路徑上的公共池Jar來使用這個特性。您還可以子類化org.springframework.aop.target.AbstractPoolingTargetSource支持任何其他池API。

注意:還支持Commons Pool 1.5+,但從Spring Framework 4.2開始就不支持它了。

下面的清單顯示了一個配置示例:

<bean id="businessObjectTarget" class="com.mycompany.MyBusinessObject"
        scope="prototype">
    ... properties omitted
</bean>

<bean id="poolTargetSource" class="org.springframework.aop.target.CommonsPool2TargetSource">
    <property name="targetBeanName" value="businessObjectTarget"/>
    <property name="maxSize" value="25"/>
</bean>

<bean id="businessObject" class="org.springframework.aop.framework.ProxyFactoryBean">
    <property name="targetSource" ref="poolTargetSource"/>
    <property name="interceptorNames" value="myInterceptor"/>
</bean>

請注意,目標對象(前面示例中的businessObjectTarget)必須是原型。這允許PoolingTargetSource實現創建目標的新實例,以便根據需要擴展池。請參閱 javadoc of AbstractPoolingTargetSource和希望用於瞭解其屬性的具體子類。maxSize是最基本的,並且總是保證出現。

在本例中,myInterceptor是需要在相同的IoC上下文中定義的攔截器的名稱。但是,不需要指定攔截器來使用池。如果只想要池而不想要其他通知,則根本不要設置interceptorNames屬性。

您可以配置Spring,使其能夠將任何池中的對象強制轉換爲org.springframework.aop.target.PoolingConfig接口,通過介紹公開關於池的配置和當前大小的信息。你需要定義一個顧問類似於以下:

<bean id="poolConfigAdvisor" class="org.springframework.beans.factory.config.MethodInvokingFactoryBean">
    <property name="targetObject" ref="poolTargetSource"/>
    <property name="targetMethod" value="getPoolingConfigMixin"/>
</bean>

這個advisor工具是通過調用AbstractPoolingTargetSource類上的一個方便方法獲得的,因此使用了MethodInvokingFactoryBean。這個advisor工具的名稱(這裏是poolConfigAdvisor)必須位於暴露池對象的ProxyFactoryBean中的攔截器名稱列表中。
cast的定義如下:

PoolingConfig conf = (PoolingConfig) beanFactory.getBean("businessObject");
System.out.println("Max pool size is " + conf.getMaxSize());

注意:通常不需要使用無狀態服務對象池。我們不認爲這應該是默認的選擇,因爲大多數無狀態對象天生就是線程安全的,而且如果緩存了資源,實例池就有問題。

通過使用自動代理,可以使用更簡單的池。您可以設置任何自動代理創建者使用的TargetSource實現。
===Prototype Target Sources

設置“原型”目標源類似於設置池目標源。在這種情況下,在每次方法調用時都會創建一個新的目標實例。儘管在現代JVM中創建新對象的成本並不高,但連接新對象(滿足其IoC依賴項)的成本可能更高。因此,如果沒有很好的理由,您不應該使用這種方法。

爲此,您可以修改前面顯示的poolTargetSource定義,如下所示(爲了清晰起見,我們還更改了名稱):

<bean id="prototypeTargetSource" class="org.springframework.aop.target.PrototypeTargetSource">
    <property name="targetBeanName" ref="businessObjectTarget"/>
</bean>

惟一的屬性是目標bean的名稱。在TargetSource實現中使用繼承來確保一致的命名。與池目標源一樣,目標bean必須是原型bean定義。
=== ThreadLocal目標源

如果需要爲每個傳入請求(即每個線程)創建一個對象,那麼ThreadLocal目標源非常有用。ThreadLocal的概念提供了一個整個jdk範圍的工具來透明地在線程旁邊存儲資源。設置ThreadLocalTargetSource與爲其他類型的目標源所做的解釋基本相同,如下面的示例所示:

<bean id="threadlocalTargetSource" class="org.springframework.aop.target.ThreadLocalTargetSource">
    <property name="targetBeanName" value="businessObjectTarget"/>
</bean>

注意:ThreadLocal實例在多線程和多類加載器環境中不正確地使用時會帶來嚴重的問題(可能導致內存泄漏)。您應該始終考慮在其他類中包裝threadlocal,並且永遠不要直接使用threadlocal本身(除了在包裝器類中)。另外,您應該始終記住正確地設置和取消設置(後者僅涉及對ThreadLocal.set(null)的調用)線程的本地資源。在任何情況下都應該取消設置,因爲不取消設置可能會導致有問題的行爲。Spring的ThreadLocal支持爲您做到了這一點,並且應該始終考慮使用ThreadLocal實例而不使用其他適當的處理代碼。

==定義新的Advice類型
Spring AOP被設計成可擴展的。雖然偵聽實現策略目前在內部使用,但是除了圍繞着advice、before、throw advice和after return advice進行偵聽外,還可以支持任意的advice類型。

org.springframework.aop.framework.adapter包是一個SPI包,它允許在不更改核心框架的情況下添加新的自定義通知類型。定製通知類型的惟一約束是它必須實現org.aopalliance.aop.Advice接口。
有關更多信息,請參見org.springframework.aop.framework.adapter javadoc

= Null-safety
雖然Java不允許您用它的類型系統來表達null-safety,但是Spring框架現在在org.springframework中提供了以下注釋。讓你聲明api和字段爲空的lang包:

  • @Nullable:用於指示特定參數、返回值或字段可以爲空的註釋。
  • @NonNull:用於指示特定參數、返回值或字段不能爲空的註釋(在參數/返回值和@NonNullApi和@NonNullFields分別應用的字段上不需要)。
  • @NonNullApi:包級別的註釋,聲明非null作爲參數和返回值的默認語義。
  • @NonNullFields:包級別的註釋,聲明非null作爲字段的默認語義。

Spring框架本身利用了這些註釋,但是也可以在任何基於Spring的Java項目中使用它們來聲明空安全的api和可選的空安全字段。泛型類型參數、可變參數和數組元素可空性還不支持,但應該在即將發佈的版本中支持,有關最新信息,請參閱 SPR-15942。可空性聲明將在Spring框架版本之間進行微調,包括小版本。方法體內部使用的類型的可空性超出了該特性的範圍。

注意:反應器和Spring Data等其他公共庫提供了空安全api,它們使用類似的可空性安排,爲Spring應用程序開發人員提供了一致的整體體驗。

= =用例
除了爲Spring Framework API可空性提供顯式聲明外,IDE(如IDEA或Eclipse)還可以使用這些註釋來提供與空安全相關的有用警告,以避免在運行時出現NullPointerException。

它們還用於使Kotlin項目中的Spring API空安全,因爲Kotlin本身支持空安全。更多細節可以在Kotlin支持文檔中找到。
= = JSR-305元註釋

Spring註釋使用 JSR 305註釋(一個休眠但廣泛傳播的JSR)進行元註釋。JSR-305元註釋允許IDEA或Kotlin等工具供應商以通用的方式提供空安全支持,而無需對Spring註釋進行硬編碼支持。

爲了利用Spring空安全API,沒有必要也不建議在項目類路徑中添加JSR-305依賴項。只有基於spring的庫在其代碼庫中使用空安全註釋的項目才應該添加com.google.code.findbugs:jsr305:3.0.2,其中只提供編譯級配置或Maven,以避免編譯警告。

=數據緩衝區和編解碼器
Java NIO提供了ByteBuffer,但是許多庫在其上構建自己的字節緩衝區API,特別是對於重用緩衝區和/或使用直接緩衝區有利於提高性能的網絡操作。例如,Netty具有ByteBuf層次結構,Undertow使用XNIO, Jetty使用池化的字節緩衝區,並釋放回調,等等。spring-core模塊提供了一組抽象來處理各種字節緩衝區api,如下所示:

= = DataBufferFactory
DataBufferFactory是用來創建數據緩衝區的兩種方式之一:

  1. 分配一個新的數據緩衝區,可以預先指定容量(如果已知),這是更有效的,即使DataBuffer的實現可以根據需要增減。
  2. 封裝現有的byte[]或java.nio。ByteBuffer,它用一個DataBuffer實現裝飾給定的數據,並且不涉及分配。

請注意,WebFlux應用程序並不直接創建DataBufferFactory,而是通過客戶端上的ServerHttpResponse或ClientHttpRequest訪問它。工廠的類型取決於底層客戶端或服務器,例如NettyDataBufferFactory用於反應器Netty, DefaultDataBufferFactory用於其他。

= = DataBuffer
DataBuffer接口提供了與java.nio類似的操作。ByteBuffer也帶來了一些額外的好處,其中一些是受到Netty ByteBuf的啓發。以下是部分福利:

  • 讀寫具有獨立的位置,即不需要調用flip()來交替讀寫。
  • 通過java.lang.StringBuilder,可以根據需要擴展容量。
  • 緩衝池和引用計數通過[databuffers- bufferpooling]。
  • 將緩衝區視爲java.nio。ByteBuffer、InputStream或OutputStream。
  • 確定給定字節的索引或最後一個索引。

= = PooledDataBuffer
正如ByteBuffer的Javadoc中所解釋的,字節緩衝區 ByteBuffer可以是直接的,也可以是非直接的。直接緩衝區可能位於Java堆之外,這消除了本地I/O操作的複製需求。這使得直接緩衝區對於通過套接字接收和發送數據特別有用,但是創建和釋放它們也更昂貴,這就產生了池緩衝區的想法。

PooledDataBuffer是DataBuffer的一個擴展,它可以幫助進行引用計數,這對於字節緩衝池非常重要。它是如何工作的?當一個PooledDataBuffer被分配時,引用計數爲1。調用retain()遞增計數,而調用release()遞減計數。只要計數大於0,就保證不會釋放緩衝區。當計數減少到0時,可以釋放池中的緩衝區,這實際上可能意味着爲緩衝區保留的內存返回到內存池。

注意,與直接在PooledDataBuffer上操作不同,在大多數情況下,更好的方法是使用DataBufferUtils中的便利方法,這些方法僅在PooledDataBuffer的實例上對DataBuffer應用release或retain。
= = DataBufferUtils
DataBufferUtils提供了許多實用程序方法來操作數據緩衝區:

  • 如果底層字節緩衝區API支持的話,可以通過複合緩衝區將數據緩衝區流連接到單個緩衝區(可能是零拷貝)。
  • 將InputStream或NIO通道轉換爲Flux<DataBuffer>,反之,將Publisher<DataBuffer>轉換爲OutputStream或NIO通道。
  • 方法來釋放或保留一個DataBuffer(如果緩衝區是PooledDataBuffer的實例)。
  • 跳過或從一個字節流中取出一個特定的字節數。

= =編解碼器

  • org.springframework.core.codes包提供以下策略接口:
  • 編碼器將發佈服務器<T>編碼到數據緩衝區流中。
  • 解碼器將出版商<DataBuffer>解碼成更高級別對象流。

spring-core模塊提供了byte[]、ByteBuffer、DataBuffer、Resource以及字符串編碼器和解碼器實現。spring-web模塊添加Jackson JSON、Jackson Smile、JAXB2、協議緩衝區和其他編碼器和解碼器。參見WebFlux部分的Codecs

==使用DataBuffer
在使用數據緩衝區時,必須特別注意確保釋放緩衝區,因爲它們可能被池化 pooled。我們將使用編解碼器來說明它是如何工作的,但是這些概念適用於更廣泛的情況。讓我們看看編解碼器必須在內部做些什麼來管理數據緩衝區。

解碼器是在創建更高級別對象之前最後讀取輸入數據緩衝區的,因此它必須按如下方式釋放這些緩衝區:

  1. 如果解碼器只是讀取每個輸入緩衝區並準備立即釋放它,那麼它可以通過databufferutil .release(dataBuffer)來做。
  2. 如果解碼器使用Flux或Mono操作符(如flatMap、reduce)和其他在內部預取和緩存數據項的操作符,或者使用filter、skip和其他遺漏數據項的操作符,那麼doonreject (PooledDataBuffer類,DataBufferUtils::release)必須添加到組合鏈中,以確保在丟棄緩衝區之前釋放這些緩衝區,也可能作爲錯誤或取消信號的結果。
  3. 如果解碼器以任何其他方式保留一個或多個數據緩衝區,則必須確保在完全讀取時釋放它們,或者在讀取和釋放緩存的數據緩衝區之前發生錯誤或取消信號時釋放它們。

注意,DataBufferUtils#join提供了一種安全而有效的方法,可以將數據緩衝區流聚合到單個數據緩衝區中。同樣,skipUntilByteCount和takeUntilByteCount也是解碼器使用的附加安全方法。

編碼器分配其他人必須讀取(並釋放)的數據緩衝區。所以一個編碼器沒有太多事情要做。然而,如果在用數據填充緩衝區時發生序列化錯誤,編碼器必須注意釋放數據緩衝區。例如:

DataBuffer buffer = factory.allocateBuffer();
boolean release = true;
try {
    // serialize and populate buffer..
    release = false;
}
finally {
    if (release) {
        DataBufferUtils.release(buffer);
    }
}
return buffer;

編碼器的使用者負責釋放它接收到的數據緩衝區。在WebFlux應用程序中,編碼器的輸出用於向HTTP服務器響應或客戶端HTTP請求寫入,在這種情況下,釋放數據緩衝區是向服務器響應或客戶端請求寫入代碼的責任。

注意,在Netty上運行時,有用於排除緩衝區泄漏故障的調試選項。
=附錄
= = XML模式
附錄的這一部分列出了與核心容器相關的XML模式。
=== util模式

顧名思義,util標記處理常見的實用程序配置問題,如配置集合、引用常量等。要在util模式中使用標記,您需要在Spring XML配置文件的頂部有以下序言(代碼片段中的文本引用了正確的模式,因此可以使用util名稱空間中的標記):

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:util="http://www.springframework.org/schema/util"
    xsi:schemaLocation="
        http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/util https://www.springframework.org/schema/util/spring-util.xsd">

        <!-- bean definitions here -->

</beans>

==== 使用 <util:constant/>

考慮以下bean定義:

<bean id="..." class="...">
    <property name="isolation">
        <bean id="java.sql.Connection.TRANSACTION_SERIALIZABLE"
                class="org.springframework.beans.factory.config.FieldRetrievingFactoryBean" />
    </property>
</bean>

前面的配置使用Spring FactoryBean實現(FieldRetrievingFactoryBean)將bean上的隔離屬性的值設置爲java.sql.Connection.TRANSACTION_SERIALIZABLE常數的值。這一切都很好,但是它很冗長,並且(不必要地)向最終用戶暴露了Spring的內部管道。
下面這個基於XML模式的版本更簡潔,清楚地表達了開發人員的意圖(“注入這個常量值”),並且讀起來更好:

<bean id="..." class="...">
    <property name="isolation">
        <util:constant static-field="java.sql.Connection.TRANSACTION_SERIALIZABLE"/>
    </property>
</bean>

====從字段值設置Bean屬性或構造函數參數
FieldRetrievingFactoryBean是一個FactoryBean,它檢索靜態或非靜態字段值。它通常用於檢索公共靜態final常量,然後可以用來設置另一個bean的屬性值或構造函數參數。
下面的示例展示瞭如何使用staticField屬性公開靜態字段:

<bean id="myField"
        class="org.springframework.beans.factory.config.FieldRetrievingFactoryBean">
    <property name="staticField" value="java.sql.Connection.TRANSACTION_SERIALIZABLE"/>
</bean>

還有一個方便的用法表單,其中靜態字段被指定爲bean名稱,如下面的示例所示:

<bean id="java.sql.Connection.TRANSACTION_SERIALIZABLE"
        class="org.springframework.beans.factory.config.FieldRetrievingFactoryBean"/>

這並意味着不再有任何選擇bean id是什麼(任何其他bean,指的是它也有使用這個名稱),但這種形式是非常簡潔的定義和方便使用以來作爲內在bean id不需要指定的bean引用,如以下示例所示:

<bean id="..." class="...">
    <property name="isolation">
        <bean id="java.sql.Connection.TRANSACTION_SERIALIZABLE"
                class="org.springframework.beans.factory.config.FieldRetrievingFactoryBean" />
    </property>
</bean>

您還可以訪問另一個bean的非靜態(實例)字段,如FieldRetrievingFactoryBean類的API文檔中所述。
在Spring中很容易將枚舉值作爲屬性或構造函數參數注入bean。實際上,您不需要做任何事情或者瞭解任何關於Spring內部的內容(甚至不需要了解諸如FieldRetrievingFactoryBean之類的類)。下面的枚舉示例演示了注入enum值是多麼容易:

package javax.persistence;

public enum PersistenceContextType {

    TRANSACTION,
    EXTENDED
}

現在考慮以下PersistenceContextType類型的setter和相應的bean定義:

package example;

public class Client {

    private PersistenceContextType persistenceContextType;

    public void setPersistenceContextType(PersistenceContextType type) {
        this.persistenceContextType = type;
    }
}
<bean class="example.Client">
    <property name="persistenceContextType" value="TRANSACTION"/>
</bean>

==== Using <util:property-path/>

考慮下面的例子:

<!-- target bean to be referenced by name -->
<bean id="testBean" class="org.springframework.beans.TestBean" scope="prototype">
    <property name="age" value="10"/>
    <property name="spouse">
        <bean class="org.springframework.beans.TestBean">
            <property name="age" value="11"/>
        </bean>
    </property>
</bean>

<!-- results in 10, which is the value of property 'age' of bean 'testBean' -->
<bean id="testBean.age" class="org.springframework.beans.factory.config.PropertyPathFactoryBean"/>

前面的配置使用了一個Spring FactoryBean實現(PropertyPathFactoryBean)來創建一個名爲testBean(類型爲int)的bean。其值等於testBean bean的年齡屬性。
現在考慮下面的例子,它添加了一個<util:property-path/>元素:

<!-- target bean to be referenced by name -->
<bean id="testBean" class="org.springframework.beans.TestBean" scope="prototype">
    <property name="age" value="10"/>
    <property name="spouse">
        <bean class="org.springframework.beans.TestBean">
            <property name="age" value="11"/>
        </bean>
    </property>
</bean>

<!-- results in 10, which is the value of property 'age' of bean 'testBean' -->
<util:property-path id="name" path="testBean.age"/>

<property-path/>元素的path屬性的值遵循bean . beanproperty的形式。在本例中,它獲取名爲testBean的bean的年齡屬性。age屬性的值是10。

====使用<util: Property -path/>設置Bean屬性或構造函數參數
PropertyPathFactoryBean是一個FactoryBean,它計算給定目標對象上的屬性路徑。目標對象可以直接指定,也可以通過bean名稱指定。然後可以在另一個bean定義中使用這個值作爲屬性值或構造函數參數。
下面的例子顯示了對另一個bean按名稱使用的路徑:

// target bean to be referenced by name
<bean id="person" class="org.springframework.beans.TestBean" scope="prototype">
    <property name="age" value="10"/>
    <property name="spouse">
        <bean class="org.springframework.beans.TestBean">
            <property name="age" value="11"/>
        </bean>
    </property>
</bean>

// results in 11, which is the value of property 'spouse.age' of bean 'person'
<bean id="theAge"
        class="org.springframework.beans.factory.config.PropertyPathFactoryBean">
    <property name="targetBeanName" value="person"/>
    <property name="propertyPath" value="spouse.age"/>
</bean>

在下面的示例中,將根據內部bean計算路徑

<!-- results in 12, which is the value of property 'age' of the inner bean -->
<bean id="theAge"
        class="org.springframework.beans.factory.config.PropertyPathFactoryBean">
    <property name="targetObject">
        <bean class="org.springframework.beans.TestBean">
            <property name="age" value="12"/>
        </bean>
    </property>
    <property name="propertyPath" value="age"/>
</bean>

還有一個快捷表單,其中bean名稱是屬性路徑。下面的例子顯示了快捷表單:

<!-- results in 10, which is the value of property 'age' of bean 'person' -->
<bean id="person.age"
        class="org.springframework.beans.factory.config.PropertyPathFactoryBean"/>

這種形式意味着在bean的名稱上沒有選擇。對它的任何引用也必須使用相同的id,即路徑。如果作爲內部bean使用,則根本不需要引用它,如下面的示例所示:

<bean id="..." class="...">
    <property name="age">
        <bean id="person.age"
                class="org.springframework.beans.factory.config.PropertyPathFactoryBean"/>
    </property>
</bean>

您可以在實際定義中專門設置結果類型。對於大多數用例來說,這並不是必需的,但是有時候它是有用的。有關此特性的更多信息,請參見javadoc。

==== Using <util:properties/>

考慮下面例子:

<!-- creates a java.util.Properties instance with values loaded from the supplied location -->
<bean id="jdbcConfiguration" class="org.springframework.beans.factory.config.PropertiesFactoryBean">
    <property name="location" value="classpath:com/foo/jdbc-production.properties"/>
</bean>

前面的配置使用了一個Spring FactoryBean實現(PropertiesFactoryBean)來實例化java.util。屬性實例,其值從提供的資源位置加載)。
下面的例子使用了一個util:properties元素來進行更簡潔的表示:

<!-- creates a java.util.Properties instance with values loaded from the supplied location -->
<util:properties id="jdbcConfiguration" location="classpath:com/foo/jdbc-production.properties"/>

==== Using <util:list/>

考慮下面例子:

<!-- creates a java.util.List instance with values loaded from the supplied 'sourceList' -->
<bean id="emails" class="org.springframework.beans.factory.config.ListFactoryBean">
    <property name="sourceList">
        <list>
            <value>[email protected]</value>
            <value>[email protected]</value>
            <value>[email protected]</value>
            <value>[email protected]</value>
        </list>
    </property>
</bean>

前面的配置使用了一個Spring FactoryBean實現(ListFactoryBean)來創建一個java.util.List實例。列出實例並使用從提供的sourceList獲取的值初始化它。
下面的例子使用了<util:list/>元素來進行更簡潔的表示:

<!-- creates a java.util.List instance with the supplied values -->
<util:list id="emails">
    <value>[email protected]</value>
    <value>[email protected]</value>
    <value>[email protected]</value>
    <value>[email protected]</value>
</util:list>

您還可以顯式地控制由<util: List />元素上的List -class屬性實例化和填充的列表的確切類型。例如,如果我們真的需要java.util。要實例化LinkedList,我們可以使用以下配置:

<util:list id="emails" list-class="java.util.LinkedList">
    <value>[email protected]</value>
    <value>[email protected]</value>
    <value>[email protected]</value>
    <value>d'[email protected]</value>
</util:list>

如果沒有提供List -class屬性,則容器選擇List實現。

==== Using <util:map/>

考慮下面例子:

<!-- creates a java.util.Map instance with values loaded from the supplied 'sourceMap' -->
<bean id="emails" class="org.springframework.beans.factory.config.MapFactoryBean">
    <property name="sourceMap">
        <map>
            <entry key="pechorin" value="[email protected]"/>
            <entry key="raskolnikov" value="[email protected]"/>
            <entry key="stavrogin" value="[email protected]"/>
            <entry key="porfiry" value="[email protected]"/>
        </map>
    </property>
</bean>

前面的配置使用了一個Spring FactoryBean實現(MapFactoryBean)來創建一個java.util.Map實例。映射實例初始化的鍵值對取自提供的“sourceMap”。

下面的例子使用了一個<util:map/>元素來進行更簡潔的表示:

<!-- creates a java.util.Map instance with the supplied key-value pairs -->
<util:map id="emails">
    <entry key="pechorin" value="[email protected]"/>
    <entry key="raskolnikov" value="[email protected]"/>
    <entry key="stavrogin" value="[email protected]"/>
    <entry key="porfiry" value="[email protected]"/>
</util:map>

您還可以顯式地控制由<util: Map />元素上的' Map -class'屬性實例化和填充的映射的確切類型。例如,如果我們真的需要java.util.TreeMap實例化,我們可以使用以下配置:

<util:map id="emails" map-class="java.util.TreeMap">
    <entry key="pechorin" value="[email protected]"/>
    <entry key="raskolnikov" value="[email protected]"/>
    <entry key="stavrogin" value="[email protected]"/>
    <entry key="porfiry" value="[email protected]"/>
</util:map>

如果沒有提供“map -class”屬性,則容器選擇Map實現。

==== Using <util:set/>

例子:

<!-- creates a java.util.Set instance with values loaded from the supplied 'sourceSet' -->
<bean id="emails" class="org.springframework.beans.factory.config.SetFactoryBean">
    <property name="sourceSet">
        <set>
            <value>[email protected]</value>
            <value>[email protected]</value>
            <value>[email protected]</value>
            <value>[email protected]</value>
        </set>
    </property>
</bean>

前面的配置使用了一個Spring FactoryBean實現(SetFactoryBean)來創建一個java.util.Set。設置實例初始化的值取自提供的sourceSet。

下面的例子使用了一個<util:set/>元素來進行更簡潔的表示:

<!-- creates a java.util.Set instance with the supplied values -->
<util:set id="emails">
    <value>[email protected]</value>
    <value>[email protected]</value>
    <value>[email protected]</value>
    <value>[email protected]</value>
</util:set>

您還可以顯式地控制通過使用<util:set/>元素上的set-class屬性來實例化和填充的Set的確切類型。例如,如果我們真的需要java.util.TreeSet實例化,我們可以使用以下配置:

<util:set id="emails" set-class="java.util.TreeSet">
    <value>[email protected]</value>
    <value>[email protected]</value>
    <value>[email protected]</value>
    <value>[email protected]</value>
</util:set>

如果沒有提供set-class屬性,則容器選擇Set實現。

=== aop模式
aop標記處理在Spring中配置aop的所有事情,包括Spring自己的基於代理的aop框架和Spring與AspectJ aop框架的集成。這些標籤在Spring面向方面編程一章中全面介紹Aspect Oriented Programming with Spring

出於完整性的考慮,要在aop模式中使用標記,需要在Spring XML配置文件的頂部有以下序言(代碼片段中的文本引用了正確的模式,因此aop名稱空間中的標記對您是可用的):

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:aop="http://www.springframework.org/schema/aop"
    xsi:schemaLocation="
        http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop.xsd">

    <!-- bean definitions here -->

</beans>

===context 模式
context 標記處理與管道相關的ApplicationContext配置——也就是說,通常不是對最終用戶很重要的bean,而是在Spring中執行大量“繁重”工作的bean,比如BeanfactoryPostProcessors。下面的代碼片段引用了正確的模式,因此上下文名稱空間中的元素對您是可用的:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:context="http://www.springframework.org/schema/context"
    xsi:schemaLocation="
        http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd">

    <!-- bean definitions here -->

</beans>

= = = = 使用< property-placeholder / >
這個元素激活${…}佔位符的替換,這些佔位符根據指定的屬性文件解析(作爲Spring資源位置Spring resource location)。此元素是一種方便的機制,可爲您設置PropertySourcesPlaceholderConfigurer。如果您需要對特定的PropertySourcesPlaceholderConfigurer設置有更多的控制,您可以自己顯式地將它定義爲一個bean。

==== Using <annotation-config/>

這個元素激活Spring基礎結構來檢測bean類中的註釋:

  • Spring’s @Configuration model

  • @Autowired/@Inject and @Value

  • JSR-250’s @Resource@PostConstruct and @PreDestroy (if available)

  • JPA’s @PersistenceContext and @PersistenceUnit (if available)

  • Spring’s @EventListener

或者,您可以選擇爲這些註釋顯式地激活各個BeanPostProcessors 。

注意:此元素不激活對Spring的@Transactional註釋的處理;爲此,可以使用<tx:annotation-driven/>元素。類似地,Spring的 caching annotations也需要顯式地 enabled

==== Using <component-scan/>

This element is detailed in the section on annotation-based container configuration.

==== Using <load-time-weaver/>

This element is detailed in the section on load-time weaving with AspectJ in the Spring Framework.

==== Using <spring-configured/>

This element is detailed in the section on using AspectJ to dependency inject domain objects with Spring.

==== Using <mbean-export/>

This element is detailed in the section on configuring annotation-based MBean export.

=== The Beans Schema

最後,我們在beans模式中有元素。這些元素從框架的一開始就在Spring中。這裏沒有顯示bean模式中各種元素的示例,因爲它們在依賴項和配置中得到了全面的詳細介紹 dependencies and configuration in detail(實際上,在那一章中)。

注意,您可以向<bean/> XML定義添加零個或多個鍵值對。如何處理這些額外的元數據完全取決於您自己的定製邏輯(因此,通常只有在您按照附錄[xml-custom]中描述的那樣編寫自己的定製元素時纔有用)。

下面的例子顯示了<bean/>環境中的<meta/>元素(注意,如果沒有任何邏輯來解釋它,元數據實際上是無用的)。

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="
        http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd">

    <bean id="foo" class="x.y.Foo">
        <meta key="cacheName" value="foo"/> 
        <property name="name" value="Rick"/>
    </bean>

</beans>

在前面的例子中,您可以假設有一些邏輯使用bean定義並設置一些使用提供的元數據的緩存基礎結構。

== XML Schema Authoring
自2.0版以來,Spring提供了一種機制,用於將基於模式的擴展添加到基本的Spring XML格式中,以定義和配置bean。本節介紹如何編寫自己的自定義XML bean定義解析器,並將這些解析器集成到Spring IoC容器中。

爲了方便編寫使用模式感知XML編輯器的配置文件,Spring的可擴展XML配置機制基於XML模式。如果您不熟悉Spring標準發行版附帶的當前XML配置擴展,那麼您應該首先閱讀名爲 [xsd-config]的附錄。

要創建新的XML配置擴展:

  1. Author一個XML模式來描述您的自定義元素。
  2. Code編寫自定義NamespaceHandler實現的代碼。
  3. Code編寫一個或多個BeanDefinitionParser實現(這是完成真正工作的地方)。
  4. Register用Spring註冊您的新工件。

對於一個統一的示例,我們創建一個XML擴展(一個自定義XML元素),它允許我們配置SimpleDateFormat類型的對象(來自java.text package)。完成後,我們將能夠定義SimpleDateFormat類型的bean定義,如下所示:

<myns:dateformat id="dateFormat"
    pattern="yyyy-MM-dd HH:mm"
    lenient="true"/>

(我們將在本附錄後面提供更詳細的示例。第一個簡單示例的目的是指導您完成定製擴展的基本步驟。

===編寫模式
爲Spring的IoC容器創建一個XML配置擴展,首先要編寫一個XML模式來描述擴展。在我們的例子中,我們使用以下模式來配置SimpleDateFormat對象:

<!-- myns.xsd (inside package org/springframework/samples/xml) -->

<?xml version="1.0" encoding="UTF-8"?>
<xsd:schema xmlns="http://www.mycompany.example/schema/myns"
        xmlns:xsd="http://www.w3.org/2001/XMLSchema"
        xmlns:beans="http://www.springframework.org/schema/beans"
        targetNamespace="http://www.mycompany.example/schema/myns"
        elementFormDefault="qualified"
        attributeFormDefault="unqualified">

    <xsd:import namespace="http://www.springframework.org/schema/beans"/>

    <xsd:element name="dateformat">
        <xsd:complexType>
            <xsd:complexContent>
                <xsd:extension base="beans:identifiedType"> //1
                    <xsd:attribute name="lenient" type="xsd:boolean"/>
                    <xsd:attribute name="pattern" type="xsd:string" use="required"/>
                </xsd:extension>
            </xsd:complexContent>
        </xsd:complexType>
    </xsd:element>
</xsd:schema>

//1 指示的行包含所有可識別標記的擴展基(意味着它們有一個id屬性,我們可以將其用作容器中的bean標識符)。我們可以使用這個屬性,因爲我們導入了spring提供的bean名稱空間。

前面的模式允許我們使用<myns:dateformat/>元素直接在XML應用程序上下文文件中配置SimpleDateFormat對象,如下面的示例所示:

<myns:dateformat id="dateFormat"
    pattern="yyyy-MM-dd HH:mm"
    lenient="true"/>

注意,在創建基礎設施類之後,前面的XML片段與下面的XML片段基本相同:

<bean id="dateFormat" class="java.text.SimpleDateFormat">
    <constructor-arg value="yyyy-HH-dd HH:mm"/>
    <property name="lenient" value="true"/>
</bean>

前面兩個代碼片段中的第二個代碼片段在容器中創建了一個bean(通過SimpleDateFormat類型的名稱dateFormat標識),並設置了兩個屬性。

注意:基於模式的配置格式創建方法允許與具有模式感知XML編輯器的IDE進行緊密集成。通過使用正確編寫的模式,可以使用自動補全讓用戶在枚舉中定義的幾個配置選項之間進行選擇。

===Coding a NamespaceHandler
除了模式之外,我們還需要一個NamespaceHandler來解析Spring在解析配置文件時遇到的這個特定名稱空間的所有元素。對於本例,NamespaceHandler應該負責解析myns:dateformat元素。

NamespaceHandler接口有三個特性:

  • init():允許初始化NamespaceHandler,並在使用該處理程序之前由Spring調用。
  • BeanDefinition parse(Element, ParserContext):當Spring遇到頂級元素(不是嵌套在bean定義或不同名稱空間中)時調用。這個方法本身可以註冊bean定義,返回一個bean定義,或者兩者都可以。
  • BeanDefinitionHolder裝飾(Node, BeanDefinitionHolder, ParserContext):當Spring遇到不同名稱空間的屬性或嵌套元素時調用。一個或多個bean定義的修飾(例如)與Spring支持的範圍一起使用scopes that Spring supports。我們首先突出顯示一個簡單的示例,不使用裝飾,然後在一個更高級的示例中顯示裝飾。

雖然您可以編寫自己的NamespaceHandler整個名稱空間(因此提供代碼解析每一個元素的名稱空間),通常情況下,每個頂級XML元素在Spring XML配置文件的結果在一個bean定義(在我們的例子中,一個< myns: dateformat / >元素的結果在一個SimpleDateFormat bean定義)。Spring提供了許多支持此場景的方便類。在下面的例子中,我們使用了NamespaceHandlerSupport類:

package org.springframework.samples.xml;

import org.springframework.beans.factory.xml.NamespaceHandlerSupport;

public class MyNamespaceHandler extends NamespaceHandlerSupport {

    public void init() {
        registerBeanDefinitionParser("dateformat", new SimpleDateFormatBeanDefinitionParser());
    }
}

您可能會注意到,這個類中實際上沒有很多解析邏輯。實際上,NamespaceHandlerSupport類有一個內置的委託概念。它支持註冊任意數量的BeanDefinitionParser實例,當需要解析名稱空間中的元素時,它將委託給這些實例。這種清晰的關注點分離讓NamespaceHandler處理解析其名稱空間中所有自定義元素的編排,同時委託BeanDefinitionParsers來完成XML解析的繁重工作。這意味着每個BeanDefinitionParser只包含解析單個自定義元素的邏輯,我們將在下一步中看到這一點。

= = =使用BeanDefinitionParser
如果NamespaceHandler遇到已映射到特定bean定義解析器的類型的XML元素(本例中爲dateformat),則使用BeanDefinitionParser。換句話說,BeanDefinitionParser負責解析模式中定義的一個不同的頂級XML元素。在解析器中,我們可以訪問XML元素(也可以訪問它的子元素),這樣我們就可以解析定製的XML內容,如下例所示:

package org.springframework.samples.xml;

import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.xml.AbstractSingleBeanDefinitionParser;
import org.springframework.util.StringUtils;
import org.w3c.dom.Element;

import java.text.SimpleDateFormat;

public class SimpleDateFormatBeanDefinitionParser extends AbstractSingleBeanDefinitionParser { //1

    protected Class getBeanClass(Element element) {
        return SimpleDateFormat.class; //2
    }

    protected void doParse(Element element, BeanDefinitionBuilder bean) {
        // this will never be null since the schema explicitly requires that a value be supplied
        String pattern = element.getAttribute("pattern");
        bean.addConstructorArgValue(pattern);

        // this however is an optional property
        String lenient = element.getAttribute("lenient");
        if (StringUtils.hasText(lenient)) {
            bean.addPropertyValue("lenient", Boolean.valueOf(lenient));
        }
    }

}

//1 我們使用spring提供的AbstractSingleBeanDefinitionParser來處理創建單個bean定義的大量基本工作。
//2 我們爲AbstractSingleBeanDefinitionParser超類提供單個BeanDefinition表示的類型。

在這個簡單的例子中,這就是我們需要做的。我們單個bean定義的創建由AbstractSingleBeanDefinitionParser超類處理,提取和設置bean定義的唯一標識符也是如此。

===註冊處理程序和模式
編碼完成了。剩下要做的就是讓Spring XML解析基礎設施知道我們的自定義元素。爲此,我們將自定義namespaceHandler和自定義XSD文件註冊到兩個特殊用途的屬性文件中。這些屬性文件都放在應用程序的META-INF目錄中,例如,可以與JAR文件中的二進制類一起發佈。Spring XML解析基礎結構通過使用這些特殊的屬性文件來自動獲取新的擴展,這些文件的格式將在下兩個小節中詳細介紹。

==== Writing META-INF/spring.handlers
名爲spring.handlers的屬性文件包含XML模式uri到名稱空間處理程序類的映射。對於我們的例子,我們需要寫以下內容:

http\://www.mycompany.example/schema/myns=org.springframework.samples.xml.MyNamespaceHandler

(:字符是Java屬性格式中的有效分隔符,因此:URI中的字符需要用反斜槓進行轉義。)

鍵-值對的第一部分(鍵)是與自定義名稱空間擴展相關聯的URI,需要與targetNamespace屬性的值完全匹配,正如在自定義XSD模式中指定的那樣。

==== Writing 'META-INF/spring.schemas'

名爲spring.schemas的屬性文件包含XML模式位置(與模式聲明一起在XML文件中引用,這些文件使用模式作爲xsi:schemaLocation屬性的一部分)到類路徑資源的映射。需要這個文件是爲了防止Spring必須使用默認的EntityResolver(需要Internet訪問來檢索模式文件)。如果在這個屬性文件中指定映射,Spring將搜索模式(在本例中是myns)。類路徑上的org.springframework.samples.xml包中的xsd)。下面的代碼片段顯示了我們需要爲自定義模式添加的行:

http\://www.mycompany.example/schema/myns/myns.xsd=org/springframework/samples/xml/myns.xsd

(記住:字符必須轉義)
建議您將XSD文件(或多個文件)與NamespaceHandler和BeanDefinitionParser類一起部署在類路徑上。

===在Spring XML配置中使用自定義擴展
使用您自己實現的自定義擴展與使用Spring提供的“自定義”擴展沒有什麼不同。下面的示例使用了在Spring XML配置文件中前面步驟中開發的自定義<dateformat/>元素:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:myns="http://www.mycompany.example/schema/myns"
    xsi:schemaLocation="
        http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.mycompany.example/schema/myns http://www.mycompany.com/schema/myns/myns.xsd">

    <!-- as a top-level bean -->
    <myns:dateformat id="defaultDateFormat" pattern="yyyy-MM-dd HH:mm" lenient="true"/> 

    <bean id="jobDetailTemplate" abstract="true">
        <property name="dateFormat">
            <!-- as an inner bean -->
            <myns:dateformat pattern="HH:mm MM-dd-yyyy"/>
        </property>
    </bean>

</beans>

===更詳細的例子
本節將提供一些更詳細的自定義XML擴展示例。
===在自定義元素中嵌套自定義元素
本節給出的示例展示瞭如何編寫滿足以下配置目標所需的各種工件:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:foo="http://www.foo.example/schema/component"
    xsi:schemaLocation="
        http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.foo.example/schema/component http://www.foo.example/schema/component/component.xsd">

    <foo:component id="bionic-family" name="Bionic-1">
        <foo:component name="Mother-1">
            <foo:component name="Karate-1"/>
            <foo:component name="Sport-1"/>
        </foo:component>
        <foo:component name="Rock-1"/>
    </foo:component>

</beans>

前面的配置將自定義擴展彼此嵌套在一起。實際上由<foo:component/>元素配置的類是component類(在下一個示例中顯示)。請注意,組件類如何不公開組件屬性的setter方法。這使得通過使用setter注入來爲組件類配置bean定義變得困難(甚至不可能)。下面的清單顯示了組件類:

package com.foo;

import java.util.ArrayList;
import java.util.List;

public class Component {

    private String name;
    private List<Component> components = new ArrayList<Component> ();

    // mmm, there is no setter method for the 'components'
    public void addComponent(Component component) {
        this.components.add(component);
    }

    public List<Component> getComponents() {
        return components;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

這個問題的典型解決方案是創建一個自定義FactoryBean,它爲組件屬性公開一個setter屬性。下面的清單顯示了這樣一個自定義FactoryBean:

package com.foo;

import org.springframework.beans.factory.FactoryBean;

import java.util.List;

public class ComponentFactoryBean implements FactoryBean<Component> {

    private Component parent;
    private List<Component> children;

    public void setParent(Component parent) {
        this.parent = parent;
    }

    public void setChildren(List<Component> children) {
        this.children = children;
    }

    public Component getObject() throws Exception {
        if (this.children != null && this.children.size() > 0) {
            for (Component child : children) {
                this.parent.addComponent(child);
            }
        }
        return this.parent;
    }

    public Class<Component> getObjectType() {
        return Component.class;
    }

    public boolean isSingleton() {
        return true;
    }
}

這工作得很好,但是它向最終用戶暴露了許多Spring管道。我們要做的是編寫一個自定義擴展來隱藏所有的Spring管道。如果我們堅持前面描述的步驟,我們首先創建XSD模式來定義自定義標記的結構,如下面的清單所示:

<?xml version="1.0" encoding="UTF-8" standalone="no"?>

<xsd:schema xmlns="http://www.foo.example/schema/component"
        xmlns:xsd="http://www.w3.org/2001/XMLSchema"
        targetNamespace="http://www.foo.example/schema/component"
        elementFormDefault="qualified"
        attributeFormDefault="unqualified">

    <xsd:element name="component">
        <xsd:complexType>
            <xsd:choice minOccurs="0" maxOccurs="unbounded">
                <xsd:element ref="component"/>
            </xsd:choice>
            <xsd:attribute name="id" type="xsd:ID"/>
            <xsd:attribute name="name" use="required" type="xsd:string"/>
        </xsd:complexType>
    </xsd:element>

</xsd:schema>

同樣遵循前面描述的過程the process described earlier,然後我們創建一個自定義NamespaceHandler:

package com.foo;

import org.springframework.beans.factory.xml.NamespaceHandlerSupport;

public class ComponentNamespaceHandler extends NamespaceHandlerSupport {

    public void init() {
        registerBeanDefinitionParser("component", new ComponentBeanDefinitionParser());
    }
}

接下來是定製的BeanDefinitionParser。請記住,我們正在創建一個描述ComponentFactoryBean的BeanDefinition。下面的清單顯示了我們自定義的BeanDefinitionParser實現:

package com.foo;

import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.support.AbstractBeanDefinition;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.support.ManagedList;
import org.springframework.beans.factory.xml.AbstractBeanDefinitionParser;
import org.springframework.beans.factory.xml.ParserContext;
import org.springframework.util.xml.DomUtils;
import org.w3c.dom.Element;

import java.util.List;

public class ComponentBeanDefinitionParser extends AbstractBeanDefinitionParser {

    protected AbstractBeanDefinition parseInternal(Element element, ParserContext parserContext) {
        return parseComponentElement(element);
    }

    private static AbstractBeanDefinition parseComponentElement(Element element) {
        BeanDefinitionBuilder factory = BeanDefinitionBuilder.rootBeanDefinition(ComponentFactoryBean.class);
        factory.addPropertyValue("parent", parseComponent(element));

        List<Element> childElements = DomUtils.getChildElementsByTagName(element, "component");
        if (childElements != null && childElements.size() > 0) {
            parseChildComponents(childElements, factory);
        }

        return factory.getBeanDefinition();
    }

    private static BeanDefinition parseComponent(Element element) {
        BeanDefinitionBuilder component = BeanDefinitionBuilder.rootBeanDefinition(Component.class);
        component.addPropertyValue("name", element.getAttribute("name"));
        return component.getBeanDefinition();
    }

    private static void parseChildComponents(List<Element> childElements, BeanDefinitionBuilder factory) {
        ManagedList<BeanDefinition> children = new ManagedList<BeanDefinition>(childElements.size());
        for (Element element : childElements) {
            children.add(parseComponentElement(element));
        }
        factory.addPropertyValue("children", children);
    }
}

最後,通過修改META-INF/spring.handlers 和META-INF/spring.schemas文件,需要將各種構件註冊到Spring XML基礎結構中。如下:

# in 'META-INF/spring.handlers'
http\://www.foo.example/schema/component=com.foo.ComponentNamespaceHandler
# in 'META-INF/spring.schemas'
http\://www.foo.example/schema/component/component.xsd=com/foo/component.xsd

===“Normal”元素的自定義屬性
編寫您自己的自定義解析器和相關的構件並不困難。然而,有時這樣做是不對的。考慮這樣一個場景,您需要向已經存在的bean定義添加元數據。在這種情況下,您當然不希望必須編寫自己的整個自定義擴展。相反,您只想向現有的bean定義元素添加一個附加屬性。

通過另一個示例,假設您爲訪問集羣JCache的服務對象(它不知道)定義了一個bean定義,並且您希望確保命名的JCache實例在周圍的集羣中被急切地啓動。下面的清單顯示了這樣一個定義:

<bean id="checkingAccountService" class="com.foo.DefaultCheckingAccountService"
        jcache:cache-name="checking.account">
    <!-- other dependencies here... -->
</bean>

然後,我們可以在解析“jcache:cache-name”屬性時創建另一個bean定義。然後,這個BeanDefinition爲我們初始化命名的JCache。我們還可以修改'checkingAccountService'的現有BeanDefinition,使其依賴於這個新的jcache初始化的BeanDefinition。下面的清單顯示了我們的JCacheInitializer:

package com.foo;

public class JCacheInitializer {

    private String name;

    public JCacheInitializer(String name) {
        this.name = name;
    }

    public void initialize() {
        // lots of JCache API calls to initialize the named cache...
    }
}

現在我們可以進入自定義擴展了。首先,我們需要創建描述自定義屬性的XSD模式,如下所示:

<?xml version="1.0" encoding="UTF-8" standalone="no"?>

<xsd:schema xmlns="http://www.foo.example/schema/jcache"
        xmlns:xsd="http://www.w3.org/2001/XMLSchema"
        targetNamespace="http://www.foo.example/schema/jcache"
        elementFormDefault="qualified">

    <xsd:attribute name="cache-name" type="xsd:string"/>

</xsd:schema>

接下來,我們需要創建相關聯的NamespaceHandler,如下所示:

package com.foo;

import org.springframework.beans.factory.xml.NamespaceHandlerSupport;

public class JCacheNamespaceHandler extends NamespaceHandlerSupport {

    public void init() {
        super.registerBeanDefinitionDecoratorForAttribute("cache-name",
            new JCacheInitializingBeanDefinitionDecorator());
    }

}

接下來,我們需要創建解析器。注意,在本例中,因爲我們要解析一個XML屬性,所以我們編寫了一個BeanDefinitionDecorator而不是BeanDefinitionParser。下面的清單顯示了我們的BeanDefinitionDecorator實現:

package com.foo;

import org.springframework.beans.factory.config.BeanDefinitionHolder;
import org.springframework.beans.factory.support.AbstractBeanDefinition;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.xml.BeanDefinitionDecorator;
import org.springframework.beans.factory.xml.ParserContext;
import org.w3c.dom.Attr;
import org.w3c.dom.Node;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

public class JCacheInitializingBeanDefinitionDecorator implements BeanDefinitionDecorator {

    private static final String[] EMPTY_STRING_ARRAY = new String[0];

    public BeanDefinitionHolder decorate(Node source, BeanDefinitionHolder holder,
            ParserContext ctx) {
        String initializerBeanName = registerJCacheInitializer(source, ctx);
        createDependencyOnJCacheInitializer(holder, initializerBeanName);
        return holder;
    }

    private void createDependencyOnJCacheInitializer(BeanDefinitionHolder holder,
            String initializerBeanName) {
        AbstractBeanDefinition definition = ((AbstractBeanDefinition) holder.getBeanDefinition());
        String[] dependsOn = definition.getDependsOn();
        if (dependsOn == null) {
            dependsOn = new String[]{initializerBeanName};
        } else {
            List dependencies = new ArrayList(Arrays.asList(dependsOn));
            dependencies.add(initializerBeanName);
            dependsOn = (String[]) dependencies.toArray(EMPTY_STRING_ARRAY);
        }
        definition.setDependsOn(dependsOn);
    }

    private String registerJCacheInitializer(Node source, ParserContext ctx) {
        String cacheName = ((Attr) source).getValue();
        String beanName = cacheName + "-initializer";
        if (!ctx.getRegistry().containsBeanDefinition(beanName)) {
            BeanDefinitionBuilder initializer = BeanDefinitionBuilder.rootBeanDefinition(JCacheInitializer.class);
            initializer.addConstructorArg(cacheName);
            ctx.getRegistry().registerBeanDefinition(beanName, initializer.getBeanDefinition());
        }
        return beanName;
    }
}

最後,我們需要向Spring XML基礎設施註冊各種構件,通過修改META-INF/spring.handlers和META-INF/spring.schemas文件, as follows:

# in 'META-INF/spring.handlers'
http\://www.foo.example/schema/jcache=com.foo.JCacheNamespaceHandler
# in 'META-INF/spring.schemas'
http\://www.foo.example/schema/jcache/jcache.xsd=com/foo/jcache.xsd

 

發佈了23 篇原創文章 · 獲贊 0 · 訪問量 485
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章