1. 程式人生 > 其它 >Spring Aspect Oriented Programming with Spring

Spring Aspect Oriented Programming with Spring

技術標籤:springspring

文章目錄

面向方面的程式設計(AOP)通過提供另一種思考程式結構的方式來補充面向物件的程式設計(OOP)。 OOP中模組化的關鍵單元是類,而在AOP中模組化是方面。方面支援跨多種型別和物件的關注點(例如事務管理)的模組化。 (這種關注在AOP文獻中通常被稱為“跨領域”關注。)

Spring的關鍵元件之一是AOP框架。儘管Spring IoC容器不依賴於AOP(這意味著您不需要的話就不需要使用AOP),但AOP是對Spring IoC的補充,以提供功能強大的中介軟體解決方案。

AOP在Spring框架中用於:

  1. 提供宣告式企業服務。最重要的服務是宣告式事務管理。

  2. 讓使用者實現自定義方面,以AOP補充其對OOP的使用。

5.1 AOP 概念

讓我們首先定義一些主要的AOP概念和術語。這些術語不是特定於Spring的。不幸的是,AOP術語並不是特別直觀。但是,如果使用Spring自己的術語,將會更加令人困惑。

  • Aspect: 涉及多個類別的關注點的模組化。事務管理是企業Java應用程式中橫切關注的一個很好的例子。在Spring AOP中,切面是通過使用常規類(基於模式的方法)或使用@Aspect註釋(@AspectJ樣式)註釋的常規類來實現的。
  • Join point: 程式執行過程中的一點,例如方法的執行或異常的處理。在Spring AOP中,連線點始終代表方法的執行。
  • Advice: 切面在特定的連線點處採取的操作。不同型別的建議包括“around”, “before” and “after” 通知。 包括Spring在內的許多AOP框架都將 通知建模為攔截器,並在連線點周圍維護一系列攔截器。
  • Pointcut: 匹配連線點的謂詞。建議與切入點表示式關聯,並在與該切入點匹配的任何連線點處執行(例如,執行具有特定名稱的方法)。切入點表示式匹配的連線點的概念是AOP的核心,預設情況下,Spring使用AspectJ切入點表達語言。
  • Introduction: 代表型別宣告其他方法或欄位。 Spring AOP允許您向任何建議物件引入新介面(和相應的實現)。例如,您可以使用簡介使Bean實現IsModified介面,以簡化快取。 (在AspectJ社群中,介紹被稱為型別間宣告。)
  • Target object: 一個或多個方面建議的物件。也稱為“建議物件”。由於Spring AOP是使用執行時代理實現的,因此該物件始終是代理物件。
  • AOP proxy: 由AOP框架建立的物件,用於實施方面協定(建議方法執行等)。在Spring Framework中,AOP代理是JDK動態代理或CGLIB代理
  • Weaving: 將方面與其他應用程式型別或物件連結以建立建議的物件。這可以在編譯時(例如,使用AspectJ編譯器),載入時或在執行時完成。像其他純Java AOP框架一樣,Spring AOP在執行時執行編織。

Spring AOP包括以下型別的advice通知:

  • Before advice: 在連線點之前執行但無法阻止執行流前進到連線點的通知(除非它引發異常)。
  • After returning advice: 聯接點正常完成後要執行的通知(例如,如果方法返回而沒有引發異常)
  • After throwing advice: 如果方法因丟擲異常而退出,則要執行的通知。
  • After (finally) advice: 無論連線點退出的方式如何(正常或異常返回),都應執行的通知。
  • Around advice: 圍繞連線點的通知,例如方法呼叫。這是最有力的通知。周圍通知可以在方法呼叫之前和之後執行自定義行為。它還負責選擇是返回連線點還是通過返回其自身的返回值或引發異常來捷徑通知的方法執行。

圍繞通知是最通用的通知。由於Spring AOP與AspectJ一樣,提供了各種通知型別,因此我們建議您使用功能最弱的通知型別,以實現所需的行為。例如,如果您只需要使用方法的返回值更新快取,則最好使用返回後的通知而不是周圍的通知,儘管周圍的通知可以完成相同的事情。使用最具體的通知型別可以提供更簡單的程式設計模型,並減少出錯的可能性。例如,您不需要在用於周圍通知的JoinPoint上呼叫proce()方法,因此,您不會失敗。

所有通知引數都是靜態型別的,因此您可以使用適當型別的通知引數(例如,方法執行的返回值的型別),而不是物件陣列。

切入點匹配的連線點的概念是AOP的關鍵,它與僅提供攔截功能的舊技術有所不同。切入點使通知的目標獨立於面向物件的層次結構。例如,您可以將提供宣告性事務管理的環繞建議應用於跨越多個物件(例如服務層中的所有業務操作)的一組方法。

5.2 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 Framework的AOP功能與Spring IoC容器結合使用。通過使用常規bean定義語法來配置方面(儘管這允許強大的“自動代理”功能)。這是與其他AOP實現的關鍵區別。使用Spring AOP無法輕鬆或高效地完成某些事情,例如建議非常細粒度的物件(通常是域物件)。在這種情況下,AspectJ是最佳選擇。但是,我們的經驗是,Spring AOP為AOP可以解決的企業Java應用程式中的大多數問題提供了出色的解決方案。

Spring AOP從未努力與AspectJ競爭以提供全面的AOP解決方案。我們認為,基於代理的框架(例如Spring AOP)和成熟的框架(例如AspectJ)都是有價值的,並且它們是互補的,而不是競爭。 Spring無縫地將Spring AOP和IoC與AspectJ整合在一起,以在基於Spring的一致應用程式架構中支援AOP的所有使用。這種整合不會影響Spring AOP API或AOP Alliance API。

Spring框架的中心宗旨之一是非侵入性。這是一個想法,不應強迫您將特定於框架的類和介面引入業務或域模型。但是,在某些地方,Spring Framework確實為您提供了將特定於Spring Framework的依賴項引入程式碼庫的選項。提供此類選項的理由是,在某些情況下,以這種方式閱讀或編碼某些特定功能可能會變得更加容易。但是,Spring框架(幾乎)總是為您提供選擇:您可以自由地就哪個選項最適合您的特定用例或場景做出明智的決定。

與本章相關的一種選擇是選擇哪種AOP框架(以及哪種AOP樣式)。您可以選擇AspectJ和/或Spring AOP。您還可以選擇@AspectJ批註樣式方法或Spring XML配置樣式方法。本章選擇首先介紹@AspectJ風格的方法這一事實不應被視為表明Spring團隊更喜歡@AspectJ註釋風格的方法,而不是Spring XML配置風格。

5.3 AOP Proxies

Spring AOP預設將標準JDK動態代理用於AOP代理。這使得可以代理任何介面(或一組介面)。

Spring AOP也可以使用CGLIB代理。這對於代理類而不是介面是必需的。預設情況下,如果業務物件未實現介面,則使用CGLIB。由於對介面而不是對類進行程式設計是一種好習慣,因此業務類通常實現一個或多個業務介面。在某些情況下(可能極少發生),當您需要建議未在介面上宣告的方法或需要將代理物件作為具體型別傳遞給方法時,可以強制使用CGLIB。

掌握Spring AOP是基於代理的這一事實非常重要。

5.4 @AspectJ支援

@AspectJ是一種將方面宣告為帶有註釋的常規Java類的樣式。 @AspectJ樣式是AspectJ專案在AspectJ 5版本中引入的。 Spring使用AspectJ提供的用於切入點解析和匹配的庫來解釋與AspectJ 5相同的註釋。但是,AOP執行時仍然是純Spring AOP,並且不依賴於AspectJ編譯器或編織器。

5.4.1 啟用@AspectJ支援

要在Spring配置中使用@AspectJ方面,您需要啟用Spring支援以基於@AspectJ方面配置Spring AOP,並基於這些方面是否建議對Bean進行自動代理。通過自動代理,我們的意思是,如果Spring確定一個或多個方面建議使用bean,它將自動為該bean生成一個代理以攔截方法呼叫並確保按需執行建議。

可以使用XML或Java樣式的配置啟用@AspectJ支援。無論哪種情況,您都需要確保AspectJ的AspectJweaver.jar庫位於應用程式的類路徑(版本1.8或更高版本)上。該庫在AspectJ發行版的lib目錄中或從Maven Central儲存庫中可用。

  • 通過Java配置啟用@AspectJ支援

    要通過Java @Configuration啟用@AspectJ支援,請新增@EnableAspectJAutoProxy批註,如以下示例所示:

    @Configuration
    @EnableAspectJAutoProxy
    public class AppConfig {
    
    }
    
  • 通過XML配置啟用@AspectJ支援

    要通過基於XML的配置啟用@AspectJ支援,請使用aop:aspectj-autoproxy元素,如以下示例所示:

    <aop:aspectj-autoproxy/>
    

5.4.2 宣告一個切面

啟用@AspectJ支援後,Spring會自動檢測在應用程式上下文中使用@AspectJ方面(具有@Aspect批註)的類定義的任何bean,並用於配置Spring AOP。接下來的兩個示例顯示了一個不太有用的方面所需的最小定義。

這兩個示例中的第一個示例顯示了應用程式上下文中的常規bean定義,該定義指向具有@Aspect批註的bean類:

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

這兩個示例中的第二個示例顯示了NotVeryUsefulAspect類定義,該類定義使用org.aspectj.lang.annotation.Aspect註釋進行了註釋;

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註釋將其標記為一個方面,因此將其從自動代理中排除。

5.4.3 宣告切入點

切入點確定了感興趣的連線點,從而使我們能夠控制執行建議的時間。 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 5開發者手冊》)或有關AspectJ的書籍之一(如Colyer等人的Eclipse AspectJ,或《 AspectJ in Action》 ,由Ramnivas Laddad撰寫)。

  • 支援的切入點指示符

    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不支援的其他切入點指示符:call,get,set,preinitialization,staticinitialization,initialization,handler,adviceexecution,withincode,cflow,cflowbelow,if,@this, and@withincode。在Spring AOP解釋的切入點表示式中使用這些切入點指示符會導致丟擲IllegalArgumentException。 Spring AOP支援的切入點指示符集合可能會在將來的版本中擴充套件,以支援更多的AspectJ切入點指示符。

    由於Spring AOP僅將匹配限制為僅方法執行連線點,因此,前面對切入點指示符的討論所給出的定義比在AspectJ程式設計指南中所能找到的要窄。此外,AspectJ本身具有基於型別的語義,並且在執行連線點處,此物件和目標都引用同一個物件:執行該方法的物件。 Spring AOP是一個基於代理的系統,區分代理物件本身(繫結到此物件)和代理後面的目標物件(繫結到目標)。

    由於Spring的AOP框架基於代理的性質,因此根據定義,不會攔截目標物件內的呼叫。對於JDK代理,只能攔截代理上的公共介面方法呼叫。使用CGLIB,將攔截代理上的公共方法和受保護的方法呼叫(必要時甚至包括程式包可見的方法)。但是,應始終通過公共簽名設計通過代理進行的常見互動。

    請注意,切入點定義通常與任何攔截方法匹配。如果嚴格地將切入點設定為僅公開使用,即使在CGLIB代理方案中可能存在通過代理進行的非公開互動,也需要相應地進行定義。

    如果您的攔截需求包括目標類中的方法呼叫甚至建構函式,請考慮使用Spring驅動的本機AspectJ編織而不是Spring的基於代理的AOP框架。這構成了具有不同特性的AOP使用模式,因此請確保在做出決定之前先熟悉編織。

    Spring AOP還支援其他名為bean的PCD。使用PCD,可以將連線點的匹配限制為特定的命名Spring Bean或一組命名Spring Bean(使用萬用字元時)。 Bean PCD具有以下形式:

    bean(idOrNameOfBean)
    

    idOrNameOfBean令牌可以是任何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() {} 
    
    @Pointcut("within(com.xyz.myapp.trading..*)")
    private void inTrading() {} 
    
    @Pointcut("anyPublicOperation() && inTrading()")
    private void tradingOperation() {} 
    

    最佳實踐是從較小的命名元件中構建更復雜的切入點表示式,如先前所示。當按名稱引用切入點時,將應用常規的Java可見性規則(您可以看到相同型別的私有切入點,層次結構中受保護的切入點,任何位置的公共切入點,等等)。可見性不影響切入點匹配。

  • 共享通用切入點定義

    在使用企業應用程式時,開發人員通常希望從多個方面引用應用程式的模組和特定的操作集。我們建議為此定義一個CommonPointcuts方面,以捕獲公共切入點表示式。這樣的方面通常類似於以下示例:

    package com.xyz.myapp;
    
    import org.aspectj.lang.annotation.Aspect;
    import org.aspectj.lang.annotation.Pointcut;
    
    @Aspect
    public class CommonPointcuts {
    
        /**
         * A join point is in the web layer if the method is defined
         * in a type in the com.xyz.myapp.web package or any sub-package
         * under that.
         */
        @Pointcut("within(com.xyz.myapp.web..*)")
        public void inWebLayer() {}
    
        /**
         * A join point is in the service layer if the method is defined
         * in a type in the com.xyz.myapp.service package or any sub-package
         * under that.
         */
        @Pointcut("within(com.xyz.myapp.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.myapp.dao package or any sub-package
         * under that.
         */
        @Pointcut("within(com.xyz.myapp.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.myapp.abc.service and com.xyz.myapp.def.service) then
         * the pointcut expression "execution(* com.xyz.myapp..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.myapp..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.myapp.dao.*.*(..))")
        public void dataAccessOperation() {}
    
    }
    

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

    <aop:config>
        <aop:advisor
            pointcut="com.xyz.myapp.CommonPointcuts.businessService()"
            advice-ref="tx-advice"/>
    </aop:config>
    
    <tx:advice id="tx-advice">
        <tx:attributes>
            <tx:method name="*" propagation="REQUIRED"/>
        </tx:attributes>
    </tx:advice>
    

    在基於模式的AOP支援中討論了<aop:config>和<aop:advisor>元素。事務管理中討論了事務元素。

  • 各個切入點表示式的例子

    Spring AOP使用者可能最常使用執行切入點指示符。執行表示式的格式如下:

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

    除了返回型別模式(前面的程式碼片段中的ret-type-pattern),名稱模式和引數模式以外的所有部分都是可選的。返回型別模式確定該方法的返回型別必須是什麼才能使連線點匹配。 最常用作返回型別模式。它匹配任何返回型別。僅當方法返回給定型別時,標準型別名稱才匹配。名稱模式與方法名稱匹配。您可以將萬用字元用作名稱模式的全部或一部分。如果您指定一個宣告型別模式,請在其後加上尾隨。將其加入名稱模式元件。引數模式稍微複雜一些:()匹配不帶引數的方法,而(…)匹配任意數量(零個或多個)的引數。 ()模式與採用任何型別的一個引數的方法匹配。 (,String)與採用兩個引數的方法匹配。第一個可以是任何型別,而第二個必須是字串。有關更多資訊,請查閱AspectJ程式設計指南的“語言語義”部分。

    以下示例顯示了一些常見的切入點表示式:

    • 任何公共方法的執行:

      execution(public * *(..))
      
    • 名稱以set開頭的任何方法的執行:

        execution(* set*(..))
      
    • AccountService介面定義的任何方法的執行:

      execution(* com.xyz.service.AccountService.*(..))
      
    • service 包中定義的任何方法的執行:

      execution(* com.xyz.service.*.*(..))
      
    • service 包或其子包之一中定義的任何方法的執行:

      execution(* com.xyz.service..*.*(..))
      
    • service 包中的任何連線點(僅在Spring AOP中執行方法):

       within(com.xyz.service.*)
      
    • service 包或其子包之一中的任何連線點(僅在Spring AOP中執行方法):

       within(com.xyz.service..*)
      
    • 代理實現AccountService介面的任何連線點(僅在Spring AOP中是方法執行):

      this(com.xyz.service.AccountService)
      
    • 目標物件實現AccountService介面的任何連線點(僅在Spring AOP中執行方法):

      target(com.xyz.service.AccountService)
      
    • 任何採用單個引數並且在執行時傳遞的引數是可序列化的連線點(僅在Spring AOP中是方法執行):

       args(java.io.Serializable)
      

      請注意,此示例中給出的切入點與execution(* *(java.io.Serializable))不同。如果在執行時傳遞的引數為Serializable,則args版本匹配;如果方法簽名宣告單個型別為Serializable的引數,則執行版本匹配。

    • 目標物件具有@Transactional批註的任何連線點(僅在Spring AOP中執行方法):

       @target(org.springframework.transaction.annotation.Transactional)
      
    • 目標物件的宣告型別具有@Transactional註釋的任何連線點(僅在Spring AOP中是方法執行):

      @within(org.springframework.transaction.annotation.Transactional)
      
    • 任何執行方法帶有@Transactional批註的連線點(僅在Spring AOP中是方法執行):

      @annotation(org.springframework.transaction.annotation.Transactional)
      
    • 任何採用單個引數的聯接點(僅在Spring AOP中是方法執行),並且傳遞的引數的執行時型別具有@Classified批註:

      @args(com.xyz.security.Classified)
      
    • 名為tradeService的Spring bean上的任何連線點(僅在Spring AOP中執行方法):

       bean(tradeService)
      
    • Spring Bean上具有與萬用字元表示式* Service匹配的名稱的任何連線點(僅在Spring AOP中執行方法):

       bean(*Service)
      
  • 編寫好的切入點

    在編譯期間,AspectJ處理切入點以優化匹配效能。檢查程式碼並確定每個連線點是否(靜態或動態)匹配給定的切入點是一個昂貴的過程。 (動態匹配意味著無法從靜態分析中完全確定匹配,並且在程式碼中進行測試以確定在執行程式碼時是否存在實際匹配)。首次遇到切入點宣告時,AspectJ將其重寫為匹配過程的最佳形式。這是什麼意思?基本上,切入點以DNF(析取正規化)重寫,並且對切入點的元件進行排序,以便首先檢查那些較便宜的元件。這意味著您不必擔心理解各種切入點指示符的效能,並且可以在切入點宣告中以任何順序提供它們。

    但是,AspectJ只能使用所告訴的內容。為了獲得最佳的匹配效能,您應該考慮他們試圖實現的目標,並在定義中儘可能縮小匹配的搜尋空間。現有的指示符自然會屬於以下三類之一:同類,作用域和上下文:

    1. 種類的指示者選擇一種特殊的連線點:execution, get, set, call, and handler.
    2. 作用域指定者選擇一組感興趣的連線點(可能是多種):withinandwithincode
    3. 上下文指示符根據上下文匹配(並可選地繫結):this,target, and@annotation

    編寫正確的切入點至少應包括前兩種型別(種類和作用域)。您可以包括上下文指示符以根據連線點上下文進行匹配,也可以繫結該上下文以在建議中使用。僅提供同類的識別符號或僅提供上下文的識別符號是可行的,但是由於額外的處理和分析,可能會影響編織效能(使用的時間和記憶體)。作用域指示符的匹配非常快,使用它們的使用意味著AspectJ可以非常迅速地消除不應進一步處理的連線點組。一個好的切入點應該始終包括一個切入點。

5.4.4 宣告通知advice

通知與切入點表示式關聯,並且在切入點匹配的方法執行之前,之後或周圍執行。切入點表示式可以是對命名切入點的簡單引用,也可以是就地宣告的切入點表示式。

  • Before Advice

    您可以使用@Before註釋在方面中宣告通知。

    import org.aspectj.lang.annotation.Aspect;
    import org.aspectj.lang.annotation.Before;
    
    @Aspect
    public class BeforeExample {
    
        @Before("com.xyz.myapp.CommonPointcuts.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.CommonPointcuts.dataAccessOperation()")
        public void doAccessCheck() {
            // ...
        }
    }
    

    有時,您需要在建議正文中訪問返回的實際值。您可以使用@AfterReturning的形式繫結返回值以獲取該訪問許可權,如以下示例所示:

    import org.aspectj.lang.annotation.Aspect;
    import org.aspectj.lang.annotation.AfterReturning;
    
    @Aspect
    public class AfterReturningExample {
    
        @AfterReturning(
            pointcut="com.xyz.myapp.CommonPointcuts.dataAccessOperation()",
            returning="retVal")
        public void doAccessCheck(Object retVal) {
            // ...
        }
    }
    

    返回屬性中使用的名稱必須與advice方法中的引數名稱相對應。當方法執行返回時,返回值將作為相應的引數值傳遞到通知方法。返回子句也將匹配限制為僅返回指定型別值的方法執行(在這種情況下為Object,它匹配任何返回值)。

  • After Throwing Advice

    丟擲建議後,當匹配的方法執行通過丟擲異常退出時,執行建議。您可以使用@AfterThrowing批註進行宣告,如以下示例所示:

    import org.aspectj.lang.annotation.Aspect;
    import org.aspectj.lang.annotation.AfterThrowing;
    
    @Aspect
    public class AfterThrowingExample {
    
        @AfterThrowing("com.xyz.myapp.CommonPointcuts.dataAccessOperation()")
        public void doRecoveryActions() {
            // ...
        }
    }
    

    通常,您希望通知僅在引發給定型別的異常時才執行,並且您通常還需要訪問通知正文中的引發異常。您可以使用throwing屬性來限制匹配(如果需要的話)(否則將Throwable用作異常型別),並將丟擲的異常繫結到advice引數。以下示例顯示瞭如何執行此操作:

    import org.aspectj.lang.annotation.Aspect;
    import org.aspectj.lang.annotation.AfterThrowing;
    
    @Aspect
    public class AfterThrowingExample {
    
        @AfterThrowing(
            pointcut="com.xyz.myapp.CommonPointcuts.dataAccessOperation()",
            throwing="ex")
        public void doRecoveryActions(DataAccessException ex) {
            // ...
        }
    }
    

    throwing屬性中使用的名稱必須與advice方法中的引數名稱相對應。當通過丟擲異常退出方法執行時,該異常將作為相應的引數值傳遞給通知方法。 throwing子句還將匹配僅限制為丟擲指定型別的異常(在這種情況下為DataAccessException)的那些方法執行。

    請注意,@ AfterThrowing並不表示常規的異常處理回撥。具體來說,@ AfterThrowing建議方法僅應從連線點(使用者宣告的目標方法)本身接收異常,而不能從隨附的@ After / @ AfterReturning方法接收異常。

  • After (Finally) Advice

    當匹配的方法執行退出時,建議(最終)執行。通過使用@After註釋宣告它。之後必須準備處理正常和異常返回條件的建議。它通常用於釋放資源和類似目的。以下示例顯示了最終建議後的用法:

    import org.aspectj.lang.annotation.Aspect;
    import org.aspectj.lang.annotation.After;
    
    @Aspect
    public class AfterFinallyExample {
    
        @After("com.xyz.myapp.CommonPointcuts.dataAccessOperation()")
        public void doReleaseLock() {
            // ...
        }
    }
    

    請注意,AspectJ中的@After建議被定義為“ after finally建議”,類似於try-catch語句中的finally塊。與@AfterReturning只適用於成功的正常返回相反,它將為從連線點(使用者宣告的目標方法)引發的任何結果,正常返回或異常呼叫。

  • Around Advice

    最後一種建議是圍繞建議。圍繞建議在匹配方法的執行過程中“圍繞”執行。它有機會在方法執行之前和之後進行工作,並確定何時,如何以及什至實際上該方法可以執行。如果需要以執行緒安全的方式(例如,啟動和停止計時器)在方法執行之前和之後共享狀態,則通常使用繞行建議。始終使用最不符合要求的建議形式(即,在建議可以使用之前,不要在建議周圍使用)。

    通過使用@Around批註來宣告周圍建議。諮詢方法的第一個引數必須是ProceedingJoinPoint型別。在建議的正文中,在ProceedingJoinPoint上呼叫proce()會使底層方法執行。前進方法也可以傳入Object []。陣列中的值用作方法執行時的引數。

    當用Object []進行呼叫時,procedes的行為與AspectJ編譯器所編譯的aroundadvice的行為稍有不同。對於使用傳統AspectJ語言編寫的環繞通知,傳遞給proc的引數數量必須與傳遞給環繞通知的引數數量(而不是基礎連線點採用的引數數量)相匹配,並且傳遞給給定的引數位置會取代該值繫結到的實體的連線點處的原始值(不要擔心,如果這現在沒有意義)。 Spring採取的方法更簡單,並且更適合其基於代理的,僅執行的語義。僅當編譯為Spring編寫的@AspectJ方面並使用AspectJ編譯器和weaver的引數進行處理時,才需要意識到這種區別。

    以下示例顯示瞭如何使用周圍建議:

    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.CommonPointcuts.businessService()")
        public Object doBasicProfiling(ProceedingJoinPoint pjp) throws Throwable {
            // start stopwatch
            Object retVal = pjp.proceed();
            // stop stopwatch
            return retVal;
        }
    }
    

    周圍建議返回的值是該方法的呼叫者看到的返回值。例如,如果一個簡單的快取方面有一個值,則它可以從快取中返回一個值,如果沒有,則呼叫proce()。請注意,在周圍建議的正文中,proc可能被呼叫一次,多次或完全不被呼叫。所有這些都是合法的。

  • Advice Parameters

    Spring提供了完全型別化的建議,這意味著您可以在建議簽名中宣告所需的引數(如我們先前在返回和丟擲示例中所看到的),而不是一直使用Object []陣列。我們將在本節的後面部分介紹如何使引數和其他上下文值可用於建議主體。首先,我們看一下如何編寫通用建議,以瞭解該建議當前建議的方法。

    • Access to the Current JoinPoint

      任何建議方法都可以將org.aspectj.lang.JoinPoint型別的引數宣告為它的第一個引數(請注意,需要周圍建議以宣告ProceedingJoinPoint型別的第一個引數,該型別是JoinPoint的子類。JoinPoint介面提供了一個多種有用的方法:

      • getArgs():返回方法引數
      • getThis():返回代理物件。
      • getTarget():返回目標物件。
      • getSignature():返回所建議方法的描述。
      • toString():列印有關所建議方法的有用描述。
    • Passing Parameters to Advice

      我們已經看到了如何繫結返回的值或異常值(在返回和丟擲建議後使用)。要使引數值可用於建議正文,可以使用args的繫結形式。如果在args表示式中使用引數名稱代替型別名稱,則在呼叫建議時會將相應引數的值作為引數值傳遞。一個例子應該使這一點更清楚。假設您想建議以Account物件作為第一個引數的DAO操作的執行,並且您需要在通知正文中訪問該帳戶。您可以編寫以下內容:

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

      切入點表示式的args(account,…)部分有兩個用途。首先,它將匹配限制為僅方法採用至少一個引數且傳遞給該引數的引數為Account例項的方法執行。其次,它通過account引數使實際的Account物件可用於建議。

      編寫此程式碼的另一種方法是宣告一個切入點,當切入點與匹配點匹配時“提供” Account物件值,然後從通知中引用命名切入點。如下所示:

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

      代理物件(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();
          // ...
      }
      
    • Advice Parameters and Generics

      pring 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 <?>並手動檢查元素的型別。

    • Determining Argument Names

      通知呼叫中的引數繫結依賴於切入點表示式中使用的名稱與通知和切入點方法簽名中宣告的引數名稱的匹配。通過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
      }
      

      如果第一個引數是JoinPoint,ProceedingJoinPoint或JoinPoint.StaticPart型別,則可以從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
      }
      

      對JoinPoint,ProceedingJoinPoint和JoinPoint.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。

    • Proceeding with Arguments

      前面我們提到過,我們將描述如何編寫一個在Spring AOP和AspectJ中始終有效的引數的proceed呼叫。解決方案是確保建議簽名按順序繫結每個方法引數。以下示例顯示瞭如何執行此操作:

      @Around("execution(List<Account> find*(..)) && " +
              "com.xyz.myapp.CommonPointcuts.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相同的優先順序規則來確定建議執行的順序。優先順序最高的建議首先“在途中”執行(因此,給定兩條優先建議,則優先順序最高的建議首先執行)。從連線點“出路”時,優先順序最高的建議將最後執行(因此,給定兩條後置通知,優先順序最高的建議將第二次執行)。

    當在不同方面定義的兩條建議都需要在同一連線點上執行時,除非另有說明,否則執行順序是不確定的。您可以通過指定優先順序來控制執行順序。通過在Aspect類中實現org.springframework.core.Ordered介面或使用@Order批註對其進行註釋,可以通過普通的Spring方法來完成。給定兩個方面,從Ordered.getOrder()(或註釋值)返回較低值的方面具有較高的優先順序。

    特定方面的每種不同建議型別在概念上均應直接應用於連線點。因此,@ AfterThrowing建議方法不應從隨附的@ After / @ AfterReturning方法接收異常。 從Spring Framework 5.2.7開始,在相同@Aspect類中定義的,需要在相同連線點執行的通知方法將根據其建議型別從高到低的優先順序分配優先順序:@Around,@Before ,@After,@AfterReturning,@AfterThrowing。但是請注意,在遵循方面的AspectJ的@After建議語義之後,在同一方面中的@AfterReturning或@AfterThrowing建議方法之後,將有效地呼叫@After建議方法。 當在同一個@Aspect類中定義的兩個相同型別的建議(例如,兩個@After建議方法)都需要在同一連線點上執行時,其順序是不確定的(因為無法檢索源)程式碼反射通過javac編譯類的程式碼宣告順序)。考慮將此類建議方法摺疊為每個@Aspect類中每個連線點的一個建議方法,或將建議重構為單獨的@Aspect類,您可以在這些方面通過Ordered或@Order進行訂購。

5.4.5 Introductions

簡介(在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.CommonPointcuts.businessService() && this(usageTracked)")
    public void recordUsage(UsageTracked usageTracked) {
        usageTracked.incrementUseCount();
    }

}

要實現的介面由帶註釋的欄位的型別確定。 @DeclareParents批註的value屬性是AspectJ型別的模式。匹配型別的任何bean均實現UsageTracked介面。請注意,在前面示例的建議中,服務Bean可以直接用作UsageTracked介面的實現。如果以程式設計方式訪問bean,則應編寫以下內容:

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

5.4.6 Aspect 例項化模型

這是一個高階主題。如果您剛開始使用AOP,則可以放心地跳過它,直到以後。

預設情況下,應用程式上下文中每個方面都有一個例項。 AspectJ將此稱為單例例項化模型。可以使用備用生命週期來定義方面。 Spring支援AspectJ的perthis和pertarget例項化模型;目前不支援percflow,percflowbelow和pertypewithin。 您可以通過在@Aspect批註中指定perthis子句來宣告perthis方面。考慮以下示例:

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

    private int someState;

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

在前面的示例中,perthis子句的作用是為執行業務服務的每個唯一服務物件(每個與切入點表示式匹配的連線點繫結到該唯一物件)建立一個方面例項。方面例項是在服務物件上首次呼叫方法時建立的。當服務物件超出範圍時,方面將超出範圍。在建立方面例項之前,其中的任何建議都不會執行。建立方面例項後,在其中宣告的建議將在匹配的連線點處執行,但是僅當服務物件是與此方面相關聯的物件時才執行。有關每個子句的更多資訊,請參見AspectJ程式設計指南。 pertarget例項化模型的工作方式與perthis完全相同,但是它在匹配的連線點為每個唯一目標物件建立一個方面例項。

5.4.7 An AOP Example

既然您已經瞭解了所有組成部分是如何工作的,那麼我們可以將它們放在一起做一些有用的事情。

有時由於併發問題(例如,死鎖失敗者),業務服務的執行可能會失敗。如果重試該操作,則很可能在下一次嘗試中成功。對於適合在這種情況下重試的業務(不需要為解決衝突而需要返回給使用者的冪等操作),我們希望透明地重試該操作,以避免客戶端看到PessimisticLockingFailureException。這項要求清楚地跨越了服務層中的多個服務,因此非常適合通過一個方面實施。

因為我們想重試該操作,所以我們需要使用圍繞建議,以便可以多次呼叫proced。以下清單顯示了基本方面的實現:

@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.CommonPointcuts.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;
    }
}

請注意,方面實現了Ordered介面,因此我們可以將方面的優先順序設定為高於事務建議(每次重試時都希望有新的事務)。 maxRetries和order屬性均由Spring配置。建議的主要動作發生在doConcurrentOperation中。注意,目前,我們將重試邏輯應用於每個businessService()。我們嘗試繼續,如果失敗並出現PessimisticLockingFailureException,則我們將再次嘗試,除非我們用盡了所有重試嘗試。

相應的Spring配置如下:

<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.CommonPointcuts.businessService() && " +
        "@annotation(com.xyz.myapp.service.Idempotent)")
public Object doConcurrentOperation(ProceedingJoinPoint pjp) throws Throwable {
    // ...
}

5.5 基於XML格式的AOP支援

如果您喜歡基於XML的格式,Spring還提供了對使用aop名稱空間標籤定義方面的支援。支援與使用@AspectJ樣式時完全相同的切入點表示式和建議型別。因此,在本節中,我們將重點放在該語法上,並使讀者參考上一節的討論(@AspectJ支援),以瞭解編寫切入點表示式和建議引數的繫結。

在您的Spring配置中,所有pointcut, advisor 程式元素都必須放在<aop:config>元素內(在應用程式上下文配置中可以有多個<aop:config>元素)。 <aop:config>元素可以包含pointcut, advisor, and aspect 元素(請注意,必須按此順序宣告它們)。

<aop:config>的配置風格大量使用了Spring的自動代理機制。如果您已經通過使用BeanNameAutoProxyCreator或類似方法使用顯式自動代理,則可能會導致問題(例如,未編制建議)。推薦的使用模式是僅使用<aop:config>樣式或僅使用AutoProxyCreator樣式,並且不要混合使用。

5.5.1 Declaring an 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一樣進行配置並注入依賴項。

5.5.2 Declaring a Pointcut

您可以在<aop:config>元素內宣告一個命名的切入點,讓切入點定義在多個方面和顧問程式之間共享。

可以定義代表服務層中任何業務服務的執行的切入點:

<aop:config>

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

</aop:config>

請注意,切入點表示式本身使用的是@AspectJ支援中所述的AspectJ切入點表示式語言。如果使用基於架構的宣告樣式,則可以引用在切入點表示式中的型別(@Aspects)中定義的命名切入點。定義上述切入點的另一種方法如下:

<aop:config>

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

</aop:config>

假定您具有共享公共切入點定義中所述的CommonPointcuts方面。

然後,在方面內宣告切入點與宣告頂級切入點非常相似,如以下示例所示:

<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樣式所提供的更受限制。

5.5.3 Declaring Advice

基於模式的AOP支援使用與@AspectJ樣式相同的五種建議,並且它們具有完全相同的語義。

  • Before Advice

    在執行匹配的方法之前,先執行建議。使用<aop:before>元素在<aop:aspect>中宣告它,如以下示例所示:

    <aop:aspect id="beforeExample" ref="aBean">
    
        <aop:before
            pointcut-ref="dataAccessOperation"
            method="doAccessCheck"/>
    
        ...
    
    </aop:aspect>
    

    在這裡,dataAccessOperation是在最高(<aop:config>)級別定義的切入點的ID。要定義內聯切入點,請使用以下方法將pointcut-ref屬性替換為pointcut屬性:

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

    正如我們在@AspectJ樣式的討論中所指出的那樣,使用命名的切入點可以顯著提高程式碼的可讀性。 method屬性標識提供建議正文的方法(doAccessCheck)。必須為包含建議的Aspect元素所引用的bean定義此方法。在執行資料訪問操作(與切入點表示式匹配的方法執行連線點)之前,將呼叫Aspect Bean上的doAccessCheck方法。

  • After Returning Advice

    返回的建議在匹配的方法執行正常完成時執行。它在<aop:aspect>內以與之前建議相同的方式宣告。以下示例顯示瞭如何宣告它:

    <aop:aspect id="afterReturningExample" ref="aBean">
    
        <aop:after-returning
            pointcut-ref="dataAccessOperation"
            method="doAccessCheck"/>
    
        ...
    </aop:aspect>
    

    與@AspectJ樣式一樣,您可以在建議正文中獲取返回值。為此,使用returning屬性指定返回值應傳遞到的引數的名稱,如以下示例所示:

    <aop:aspect id="afterReturningExample" ref="aBean">
    
        <aop:after-returning
            pointcut-ref="dataAccessOperation"
            returning="retVal"
            method="doAccessCheck"/>
    
        ...
    </aop:aspect>
    

    doAccessCheck方法必須宣告一個名為retVal的引數。該引數的型別以與@AfterReturning中所述相同的方式約束匹配。例如,您可以按以下方式宣告方法簽名:

    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樣式一樣,您可以在通知正文中獲取引發的異常。為此,請使用throwing屬性指定異常應傳遞到的引數的名稱,如以下示例所示:

    <aop:aspect id="afterThrowingExample" ref="aBean">
    
        <aop:after-throwing
            pointcut-ref="dataAccessOperation"
            throwing="dataAccessEx"
            method="doRecoveryActions"/>
    
        ...
    </aop:aspect>
    

    doRecoveryActions方法必須宣告一個名為dataAccessEx的引數。此引數的型別以與@AfterThrowing中所述相同的方式約束匹配。例如,方法簽名可以宣告如下:

    public void doRecoveryActions(DataAccessException dataAccessEx) {...
    
  • After (Finally) Advice

    無論最終如何執行匹配的方法,建議(最終)都會執行。您可以使用after元素宣告它,如以下示例所示:

    <aop:aspect id="afterFinallyExample" ref="aBean">
    
        <aop:after
            pointcut-ref="dataAccessOperation"
            method="doReleaseLock"/>
    
        ...
    </aop:aspect>
    
  • Around Advice

    最後一種建議是圍繞建議。圍繞建議在匹配的方法執行過程中“圍繞”執行。它有機會在方法執行之前和之後進行工作,並確定何時,如何以及什至實際上該方法可以執行。周圍建議通常用於以執行緒安全的方式(例如,啟動和停止計時器)在方法執行之前和之後共享狀態。始終使用最不強大的建議形式,以滿足您的要求。如果建議可以完成這項工作,請不要在建議周圍使用。

    您可以使用aop:around元素宣告周圍建議。諮詢方法的第一個引數必須是ProceedingJoinPoint型別。在建議的正文中,在ProceedingJoinPoint上呼叫proce()會使底層方法執行。還可以使用Object []呼叫proce方法。陣列中的值用作方法執行時的引數。有關呼叫Object []的注意事項,請參見“周圍建議”。以下示例顯示瞭如何在XML中圍繞建議進行宣告:

    <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支援相同的方式支援完全型別的建議,即通過名稱與建議方法引數匹配切入點引數。有關詳細資訊,請參見建議引數。如果您希望顯式指定建議方法的引數名稱(不依賴於先前描述的檢測策略),則可以通過使用advice元素的arg-names屬性來實現,該屬性的處理方式與argNames屬性相同在建議註釋中(如確定引數名稱中所述)。以下示例顯示如何在XML中指定引數名稱:

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

    arg-names屬性接受以逗號分隔的引數名稱列表。

    以下基於XSD的方法中涉及程度稍高的示例顯示了一些與一些強型別引數結合使用的建議:

    package x.y.service;
    
    public interface PersonService {
    
        Person getPerson(String personName, int age);
    }
    
    public class DefaultPersonService implements PersonService {
    
        public Person getPerson(String name, int age) {
            return new Person(name, age);
        }
    }
    

    接下來是方面。請注意profile(…)方法接受許多強型別引數的事實,其中第一個恰好是用於進行方法呼叫的連線點。此引數的存在表明profile(…)將用作建議,如以下示例所示:

    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);
        }
    }
    

    有了這樣的Boot類,我們將在標準輸出上獲得類似於以下內容的輸出:

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

    當需要在同一連線點(執行方法)上執行多個建議時,排序規則如“建議排序”中所述。方面之間的優先順序是通過<aop:aspect>元素中的order屬性或通過將@Order批註新增到支援方面的bean或通過使bean實現Ordered介面來確定的。

    與在同一@Aspect類中定義的通知方法的優先規則相反,當在同一<aop:aspect>元素中定義的兩條建議都需要在同一連線點上執行時,優先順序由中的順序確定在封閉的<aop:aspect>元素中宣告的通知元素,從最高優先順序到最低優先順序。 例如,給定一個繞行建議和一個在同一<aop:aspect>元素中定義的,適用於同一聯接點的事前通知,以確保繞行建議的優先順序高於事前通知的<aop:around>元素必須在<aop:before>元素之前宣告。 根據一般經驗,如果發現在同一<aop:aspect>元素中定義了多個建議,這些建議適用於同一連線點,請考慮將這些建議方法摺疊為每個< aop:aspect>元素,或將建議重構為單獨的<aop:aspect>元素,您可以在方面級別進行訂購。

5.5.4 Introductions

簡介(在AspectJ中稱為型別間宣告)使方面可以宣告建議的物件實現給定的介面,並代表那些物件提供該介面的實現。

您可以通過在aop:aspect中使用aop:declare-parents元素進行介紹。您可以使用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.CommonPointcuts.businessService()
            and this(usageTracked)"
            method="recordUsage"/>

</aop:aspect>

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

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

要實現的介面由Implement-interface屬性確定。型別匹配屬性的值是AspectJ型別模式。匹配型別的任何bean均實現UsageTracked介面。請注意,在前面示例的建議中,服務Bean可以直接用作UsageTracked介面的實現。要以程式設計方式訪問bean,可以編寫以下程式碼:

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

5.5.5. Aspect Instantiation Models

模式定義方面唯一受支援的例項化模型是單例模型。將來的版本中可能會支援其他例項化模型。

5.5.6. Advisors

“顧問”的概念來自Spring中定義的AOP支援,並且在AspectJ中沒有直接等效的概念。顧問就像一個獨立的小方面,只有一條建議。通知本身由Bean表示,並且必須實現Spring的“建議型別”中描述的建議介面之一。顧問可以利用AspectJ切入點表示式。

Spring通過<aop:advisor>元素支援顧問程式概念。您通常會看到它與事務建議結合使用,事務建議在Spring中也有自己的名稱空間支援。以下示例顯示顧問程式:

<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屬性內聯定義一個pointcut表示式。 要定義顧問程式的優先順序以使建議書可以參與訂購,請使用order屬性定義顧問程式的Ordered值。

5.5.7. An AOP Schema Example

本節說明使用模式支援重寫時,AOP示例中的併發鎖定失敗重試示例的外觀。

有時由於併發問題(例如,死鎖失敗者),業務服務的執行可能會失敗。如果重試該操作,則很可能在下一次嘗試中成功。對於適合在這種情況下重試的業務(不需要為解決衝突而需要返回給使用者的冪等操作),我們希望透明地重試該操作,以避免客戶端看到PessimisticLockingFailureException。這項要求清楚地跨越了服務層中的多個服務,因此非常適合通過一個方面實施。 因為我們想重試該操作,所以我們需要使用圍繞建議,以便可以多次呼叫proced。以下清單顯示了基本方面的實現(這是使用模式支援的常規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;
    }
}

請注意,方面實現了Ordered介面,因此我們可以將方面的優先順序設定為高於事務建議(每次重試時都希望有新的事務)。 maxRetries和order屬性均由Spring配置。主要操作發生在建議方法周圍的doConcurrentOperation中。我們嘗試繼續。如果由於PessimisticLockingFailureException失敗,則將重試,除非我們用盡了所有重試嘗試。

相應的Spring配置如下:

<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
}

方面的更改僅重試冪等操作涉及到修改切入點表示式,以便僅@Idempotent操作匹配,如下所示:

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

5.6 選擇要使用的AOP宣告樣式

一旦確定方面是實現給定需求的最佳方法,您如何在使用Spring AOP或AspectJ以及在Aspect語言(程式碼)樣式,@ AspectJ批註樣式或Spring XML樣式之間做出選擇?這些決定受許多因素影響,包括應用程式需求,開發工具和團隊對AOP的熟悉程度。

5.6.1 Spring AOP or Full AspectJ?

使用最簡單的方法即可。 Spring AOP比使用完整的AspectJ更簡單,因為不需要在開發和構建過程中引入AspectJ編譯器/編織器。如果您只需要建議在Spring bean上執行操作,則Spring AOP是正確的選擇。如果您需要建議不受Spring容器管理的物件(通常是域物件),則需要使用AspectJ。如果您希望建議除簡單方法執行以外的連線點(例如,欄位get或設定連線點等),則還需要使用AspectJ。

使用AspectJ時,可以選擇AspectJ語言語法(也稱為“程式碼樣式”)或@AspectJ註釋樣式。顯然,如果您不使用Java 5+,則可以為您做出選擇:使用程式碼樣式。如果方面在您的設計中起著重要的作用,並且您能夠將AspectJ開發工具(AJDT)外掛用於Eclipse,則AspectJ語言語法是首選。它更乾淨,更簡單,因為該語言是專為編寫方面而設計的。如果您不使用Eclipse或只有少數幾個方面在您的應用程式中不起作用,那麼您可能要考慮使用@AspectJ樣式,在IDE中堅持常規Java編譯,並向其中新增方面編織階段您的構建指令碼。

5.6.2 @AspectJ or XML for Spring AOP?

如果選擇使用Spring AOP,則可以選擇@AspectJ或XML樣式。有各種折衷考慮。

XML樣式可能是現有Spring使用者最熟悉的,並且得到了真正的POJO的支援。將AOP用作配置企業服務的工具時,XML可能是一個不錯的選擇(一個很好的測試是您是否將切入點表示式視為配置的一部分,而您可能希望獨立更改)。使用XML樣式,可以說從您的配置中可以更清楚地瞭解系統中存在哪些方面。

XML樣式有兩個缺點。首先,它沒有完全將要解決的需求的實現封裝在一個地方。 DRY原則說,系統中的任何知識都應該有一個單一,明確,權威的表示形式。使用XML樣式時,關於如何實現需求的知識會在配置檔案中的後備bean類的宣告和XML中分散。當您使用@AspectJ樣式時,此資訊將封裝在一個模組中:方面。其次,與@AspectJ樣式相比,XML樣式在表達能力上有更多限制:僅支援“單例”方面例項化模型,並且無法組合以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樣式支援其他例項化模型和更豐富的切入點組合。它具有將方面保持為模組化單元的優勢。它還具有的優點是,Spring AOP和AspectJ都可以理解@AspectJ方面。因此,如果您以後決定需要AspectJ的功能來實現其他要求,則可以輕鬆地遷移到經典的AspectJ設定。總而言之,Spring團隊在自定義方面更喜歡@AspectJ樣式,而不是簡單地配置企業服務。

5.7 混合切面型別

通過使用自動代理支援,模式定義的<aop:aspect>方面,<aop:advisor>宣告的顧問程式,甚至是同一配置中其他樣式的代理和攔截器,完全可以混合@AspectJ樣式的方面。所有這些都是通過使用相同的基礎支援機制實現的,並且可以毫無困難地共存。

5.8 代理機制

Spring AOP使用JDK動態代理或CGLIB建立給定目標物件的代理。 JDK內建了JDK動態代理,而CGLIB是常見的開源類定義庫(重新包裝到spring-core中)。

如果要代理的目標物件實現至少一個介面,則使用JDK動態代理。代理了由目標型別實現的所有介面。如果目標物件未實現任何介面,則將建立CGLIB代理。

如果要強制使用CGLIB代理(例如,代理為目標物件定義的每個方法,而不僅是由其介面實現的方法),都可以這樣做。但是,您應該考慮以下問題:

  • 使用CGLIB,不能advised 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>元素的proxy-target-class屬性設定為true,如下所示:

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

多個<aop:config />部分在執行時摺疊到一個統一的自動代理建立器中,該建立器將應用任何<aop:config />部分(通常來自不同XML Bean定義檔案)指定的最強代理設定。這也適用於<tx:annotation-driven />和<aop:aspectj-autoproxy />元素。 明確地說,在<tx:annotation-driven />,<aop:aspectj-autoproxy />或<aop:config />元素上使用proxy-target-class =“ true”會強制對所有三個元素使用CGLIB代理其中。

5.8.1 瞭解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...
    }
}

如果在物件引用上呼叫方法,則直接在該物件引用上呼叫該方法,如下圖和清單所示:

在這裡插入圖片描述

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();
    }
}

當客戶端程式碼具有的引用是代理時,情況會稍有變化。考慮以下圖表和程式碼片段:

在這裡插入圖片描述

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()。此參考,而不是代理。這具有重要意義。這意味著自呼叫不會導致與方法呼叫相關的建議得到執行的機會。

好吧,那該怎麼辦?最佳方法(此處寬鬆地使用術語“最佳”)是重構程式碼,以免發生自呼叫。這確實需要您做一些工作,但這是最好的,侵入性最小的方法。下一種方法絕對可怕,我們正要指出這一點,恰恰是因為它是如此可怕。您可以(對我們來說是痛苦的)完全將類中的邏輯與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框架。

5.9 以程式設計方式建立@AspectJ代理

除了使用<aop:config>或<aop:aspectj-autoproxy>宣告配置中的各個方面外,還可以通過程式設計方式建立建議目標物件的代理。在這裡,我們要重點介紹通過使用@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();

5.10 在Spring應用程式中使用AspectJ

​ 參考附錄參考文獻中的使用介紹

5.11 更多資源

可以在AspectJ網站上找到有關AspectJ的更多資訊。 Eclipse AspectJ,作者:Adrian Colyer等。等(Addison-Wesley,2005年)為AspectJ語言提供了全面的介紹和參考。 強烈推薦Ramnivas Laddad撰寫的《 AspectJ in Action》第二版(Manning,2009年)。本書的重點是AspectJ,但在一定程度上探討了許多通用的AOP主題。

參考文獻

【https://docs.spring.io/spring-framework/docs/current/reference/html/core.html】【5. Aspect Oriented Programming with Spring】