Spring 5 中文解析核心篇-IoC容器之AOP編程(上)

面向切面的編程(AOP)經過提供另外一種思考程序結構的方式來補充面向對像的編程(OOP)。OOP中模塊化的關鍵單元是<u>類</u>,而在AOP中模塊化是<u>切面</u>。切面使關注點(例如事務管理)的模塊化能夠跨越多種類型和對象。(這種關注在AOP文獻中一般被稱爲「跨領域」關注。)html

Spring的關鍵組件之一是AOP框架。雖然Spring IoC容器不依賴於AOP(這意味着若是你不想使用AOP,就不須要使用AOP),但AOP對Spring IoC進行了補充,提供了一個很是強大的中間件解決方案。java

具備AspectJ切入點的Spring AOPgit

Spring提供了使用基於schema的方法或@AspectJ註解樣式來編寫自定義切面的簡單而強大的方法。這兩種樣式都提供了徹底類型化的建議,並使用了AspectJ切入點語言,同時仍然使用Spring AOP進行編織。web

本章討論基於schema和基於@AspectJ的AOP支持。下一章將討論較低級別的AOP支持。spring

AOP在Spring框架中用於:express

  • 提供聲明式企業服務。此類服務中最重要的是聲明式事務管理
  • 讓用戶實現自定義切面,並用AOP補充其對OOP的使用。

若是你只對通用聲明性服務或其餘預包裝的聲明性中間件服務(例如池)感興趣,則無需直接使用Spring AOP,而且能夠跳過本章的大部份內容。編程

5.1 AOP概念

讓咱們首先定義一些主要的AOP概念和術語。這些術語不是特定於Spring的。不幸的是,AOP術語並非特別直觀。可是,若是使用Spring本身的術語,將會更加使人困惑。api

  • 切面:涉及多個類別的關注點的模塊化。事務管理是企業Java應用程序中橫切關注的一個很好的例子。在Spring AOP中,切面是經過使用常規類(基於schema的方法)或使用@Aspect註解(@AspectJ樣式)註釋的常規類來實現的。
  • 鏈接點:程序執行過程當中的一點,例如方法的執行或異常的處理。在Spring AOP中,鏈接點始終表明方法的執行。
  • 通知:切面在特定的鏈接點處採起的操做。不一樣類型的通知包括:「around」,「before」和「after」通知。(通知類型將在後面討論。)包括Spring在內的許多AOP框架都將通知建模爲攔截器,並在鏈接點周圍維護一系列攔截器。
  • 切入點:表示匹配鏈接點。通知與切入點表達式關聯,並在與該切入點匹配的任何鏈接點處運行(例如,執行具備特定名稱的方法)。切入點表達式匹配的鏈接點的概念是AOP的核心,默認狀況下,Spring使用AspectJ切入點表達語言。
  • 引入:在類型上聲明其餘方法或字段。Spring AOP容許你向任何通知對象引入新的接口(和相應的實現)。例如,你可使用引入使Bean實現IsModified接口,以簡化緩存。(引入在AspectJ社區中稱爲類型間聲明。)
  • 目標對象:一個或多個切面通知的對象。也稱爲「通知對象」。因爲Spring AOP是使用運行時代理實現的,所以該對象始終是代理對象。
  • AOP代理:由AOP框架建立的對象,用於實現切面約定(通知方法執行等)。在Spring Framework中,AOP代理是JDK動態代理或CGLIB代理。
  • 編織:將切面與其餘應用程序類型或對象連接以建立通知的對象。這能夠在編譯時(例如,使用AspectJ編譯器),加載時或在運行時完成。像其餘純Java AOP框架同樣,Spring AOP在運行時執行編織。

Spring AOP包括如下類型的通知:數組

  • 前置通知:在鏈接點以前運行但沒法阻止執行流前進到鏈接點的通知(除非它引起異常)。
  • 後置通知:鏈接點正常完成後要運行的通知(例如,若是某個方法返回而沒有引起異常)。
  • 後置異常通知:若是方法因拋出異常而退出,將執行的通知。
  • 最終通知:不管鏈接點退出的方式如何(正常或異常返回),都將執行通知。
  • 環繞通知:圍繞鏈接點的通知,例如方法調用。這是最強大的通知。環繞通知能夠在方法調用以前和以後執行自定義行爲。它還負責選擇是繼續到鏈接點,仍是經過返回本身的返回值或拋出異常來簡化通知的方法執行。

環繞通知是最通用的通知。因爲Spring AOP與AspectJ同樣,提供了各類通知類型,所以咱們建議你使用功能最弱的建議類型,以實現所需的行爲。例如,若是你只須要使用方法的返回值更新緩存,則最好使用後置通知而不是環繞通知,儘管環繞通知能夠完成相同的事情。使用最具體的通知類型可提供更簡單的編程模型,並減小出錯的可能性。例如,你不須要在用於環繞通知的JoinPoint上調用proceed()方法,所以,你不會失敗。緩存

全部通知參數都是靜態類型的,所以你可使用適當類型(例如,從方法執行返回的值的類型)而不是對象數組的 通知參數。

切入點匹配的鏈接點的概念是AOP的關鍵,它與僅提供攔截功能的舊技術不一樣。切入點使通知的目標獨立於面向對象的層次結構。例如,你能夠將提供聲明性事務管理的環繞通知應用於跨越多個對象(例在服務層中的全部業務操做)的一組方法。

5.2 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 AOP仍然向後兼容。請參閱下一章,以討論Spring AOP API。

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

與本章相關的一種選擇是選擇哪一種AOP框架(以及哪一種AOP樣式)。你能夠選擇AspectJ和或Spring AOP。你也能夠選擇@AspectJ註解樣式方法或Spring XML配置樣式方法。本章選擇首先介紹@AspectJ風格的方法,這不能代表Spring比Spring XML配置風格更喜歡@AspectJ註釋風格的方法(備註:使用AspectJ編寫例子不能說明Spring更喜歡AspectJ註解編程)。

有關每種樣式的「前因後果」的更完整討論,請參見選擇要使用的AOP聲明樣式

5.3 AOP代理

Spring AOP默認將標準JDK動態代理用於AOP代理。這使得能夠代理任何接口(或一組接口)。

Spring AOP也可使用CGLIB代理。這對於代理類而不是接口是必需的。默認狀況下,若是業務對象未實現接口,則使用CGLIB。因爲對接口而不是對類進行編程是一種好習慣,所以業務類一般實現一個或多個業務接口。在某些狀況下(可能極少發生),你須要通知在接口上未聲明的方法,或須要將代理對象做爲具體類型傳遞給方法,則能夠強制使用CGLIB

掌握Spring AOP是基於代理的這一事實很重要。請參閱瞭解AOP代理以全面瞭解此實現細節的實際含義。

5.4 @AspectJ支持

@AspectJ是一種將切面聲明爲帶有註解的常規Java類的樣式。@AspectJ樣式是AspectJ項目在AspectJ 5版本中引入的。Spring使用AspectJ提供的用於切入點解析和匹配的庫來解釋與AspectJ 5相同的註解。可是,AOP運行時仍然是純Spring AOP,而且不依賴於AspectJ編譯器或編織器。

使用AspectJ編譯器和編織器可使用完整的AspectJ語言,有關在Spring Applications中使用AspectJ進行了討論。

5.4.1 激活@AspectJ支持

要在Spring配置中使用@AspectJ切面,你須要啓用Spring支持以基於@AspectJ切面配置Spring AOP,並根據這些切面是否通知對Bean進行自動代理。經過自動代理,咱們的意思是,若是Spring肯定一個或多個切面通知一個bean,它會自動爲該bean生成一個代理來攔截方法調用並確保按需執行通知。

可使用XML或Java樣式的配置來啓用@AspectJ支持。不管哪一種狀況,你都須要確保AspectJAspectjweaver.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/>

假定你使用基於XML Schema的配置中所述的架構支持。有關如何在aop名稱空間中導入標籤的信息,請參見AOP schema

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(..))") // 切入點表達式
private void anyOldTransfer() {} // 切入點方法簽名

造成@Pointcut註解的值的切入點表達式是一個常規的AspectJ 5切入點表達式。有關AspectJ的切入點語言的完整討論,請參見AspectJ編程指南(以及擴展,包括AspectJ 5開發人員手冊)或有關AspectJ的書籍之一(如《Eclipse AspectJ》或《 AspectJ in Action》 )。

支持的切入點指示符

Spring AOP支持如下在切入點表達式中使用的AspectJ<u>切入點指示符(PCD)</u>:

  • 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@withincode(備註:意思是Spring不支持這些指示符)。在Spring AOP解釋的切入點表達式中使用這些切入點指示符會致使拋出IllegalArgumentException

Spring AOP支持的切入點指示符集合可能會在未來的版本中擴展,以支持更多的 AspectJ切入點指示符。

因爲Spring AOP僅將匹配限制爲僅方法執行鏈接點,所以前面對切入點指示符的討論所給出的定義比在AspectJ編程指南中所能找到的要窄。此外,AspectJ自己具備基於類型的語義,而且在執行鏈接點處,thistarget都引用同一個對象:執行該方法的對象。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() {} //1

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

@Pointcut("anyPublicOperation() && inTrading()")
private void tradingOperation() {} //3
  1. 若是方法執行鏈接點表示任何公共方法的執行,則anyPublicOperation匹配。
  2. 若是交易模塊中有方法執行,則inTrading匹配。
  3. 若是方法執行表明交易模塊中的任何公共方法,則tradingOperation匹配。

最佳實踐是從較小的命名組件中構建更復雜的切入點表達式,如先前所示。按名稱引用切入點時,將應用常規的Java可見性規則(你能夠看到相同類型的private切入點,層次結構中protected的切入點,任何位置的public切入點,等等)。可見性不影響切入點匹配。

共享通用切入點定義

在企業級應用中,開發人員一般但願從多個方面引用應用程序的模塊和特定的操做集。咱們建議爲此定義一個 SystemArchitecture切面,以捕獲常見的切入點表達式意圖。這樣的切面一般相似於如下示例:

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>

基於schema的AOP支持中討論了<aop:config><aop:advisor>元素。事務管理中討論了事務元素。

實例

Spring AOP用戶可能最常使用execution切入點指示符。執行表達式的格式以下:

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

除了返回類型模式(前面的代碼片斷中的ret-type-pattern),名稱模式(name-pattern)和參數模式(param-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)

    this一般以綁定形式使用。有關如何在通知正文中使代理對象可用的信息,請參閱「聲明通知」部分

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

    target(com.xyz.service.AccountService)

    target一般以綁定形式使用。有關如何使目標對象在建議正文中可用的信息,請參見「聲明通知」部分。

  • 任何採用單個參數而且在運行時傳遞的參數爲Serializable的鏈接點(僅在Spring AOP中執行方法):

    args(java.io.Serializable)

    args一般以綁定形式使用。有關如何使方法參數在通知正文中可用的信息,請參見「聲明通知」部分。

    請注意,此示例中給出的切入點與execution(* *(java.io.Serializable))不一樣。若是在運行時傳遞的參數爲Serializable,則args版本匹配;若是方法簽名聲明一個類型爲Serializable的參數,則執行版本匹配。

  • 目標對象具備@Transactional註解的任何鏈接點(僅在Spring AOP中方法執行):

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

你也能夠在綁定形式中使用@target。有關如何使註解對象在建議正文中可用的信息,請參見「聲明通知」部分。

  • 目標對象的聲明類型具備@Transactional註解的任何鏈接點(僅在Spring AOP中方法執行):

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

你也能夠在綁定形式中使用@within。有關如何使註解對象在通知正文中可用的信息,請參見「聲明通知」部分。

  • 任何執行方法帶有@Transactional註解的鏈接點(僅在Spring AOP中是方法執行):

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

你也能夠在綁定形式中使用@annotation。有關如何使註解對象在通知正文中可用的信息,請參見「聲明通知」部分。

  • 任何採用單個參數的鏈接點(僅在Spring AOP中是方法執行),而且傳遞的參數的運行時類型具備@Classified註解:

    @args(com.xyz.security.Classified)

你也能夠在綁定形式中使用@args。請參閱「聲明通知」部分,如何使通知對象中的註解對象可用。

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

    bean(tradeService)

  • Spring Bean上具備與通配符表達式* Service匹配的名稱的任何鏈接點(僅在Spring AOP中才執行方法):

    bean(*Service)

寫一個好的鏈接點

在編譯期間,AspectJ處理切入點以優化匹配性能。檢查代碼並肯定每一個鏈接點是否(靜態或動態)匹配給定的切入點是一個耗時的過程。(動態匹配意味着沒法從靜態分析中徹底肯定匹配,而且在代碼中進行了測試以肯定在運行代碼時是否存在實際匹配)。首次遇到切入點聲明時,AspectJ將其重寫爲匹配過程的最佳形式。這是什麼意思?基本上,切入點以DNF(析取範式)重寫,而且對切入點的組件進行排序,以便首先檢查那些較便宜(消耗最小)的組件。這意味着你沒必要擔憂理解各類切入點指示符的性能,而且能夠在切入點聲明中以任何順序提供它們。

可是,AspectJ只能使用所告訴的內容。爲了得到最佳的匹配性能,你應該考慮他們試圖達到的目標,並在定義中儘量縮小匹配的搜索空間。現有的指示符天然分爲三類之一:同類、做用域和上下文:

  • Kinded指示器選擇特定類型的鏈接點:executiongetsetcallhandler

  • Scoping指示器選擇一組感興趣的鏈接點(多是多種類型的):withinwithincode

  • Contextual指示符根據上下文匹配(和可選綁定):thistarget@annotation

編寫正確的切入點至少應包括前兩種類型(KindedScoping)。你能夠包括上下文指示符以根據鏈接點上下文進行匹配,也能夠綁定該上下文以在通知中使用。僅提供Kinded的標識符或僅提供Contextual的標識符是可行的,可是因爲額外的處理和分析,可能會影響編織性能(使用的時間和內存)。Scoping指定符的匹配很是快,使用它們意味着AspectJ能夠很是迅速地消除不該進一步處理的鏈接點組。一個好的切入點應儘量包括一個切入點。

參考代碼:com.liyong.ioccontainer.starter.AopIocContiner

5.4.4 聲明通知

通知與切入點表達式關聯,而且在切入點匹配的方法執行以前、以後或周圍運行。切入點表達式能夠是對命名切入點的簡單引用,也能夠是在適當位置聲明的切入點表達式。

前置通知

你可使用@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() {
        // ...
    }

}

返回通知

在當匹配方法正常的執行返回時,返回通知運行。你可使用@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() {
        // ...
    }

}

你能夠在同一切面內擁有多個通知聲明(以及其餘成員)。在這些示例中,咱們僅顯示單個通知聲明,以及其中每一個通知的效果。

有時,你須要在通知正文中訪問返回的實際值。你可使用@AfterReturning的形式綁定返回值以獲取該訪問,如如下示例所示:

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方法中的參數名稱相對應。當方法執行返回時,返回值將做爲相應的參數值傳遞到通知方法。returning也將匹配限制爲僅返回指定類型值的方法執行(在這種狀況下爲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() {
        // ...
    }

}

一般,你但願通知僅在引起給定類型的異常時才運行,而且你一般還須要訪問通知正文中的引起異常。你可使用throwing屬性來限制匹配(若是須要)(不然,請使用Throwable做爲異常類型),並將拋出的異常綁定到通知的參數。如下示例顯示瞭如何執行此操做:

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) {
        // ...
    }

}

throwing屬性中使用的名稱必須與通知方法中的參數名稱相對應。當經過拋出異常退出方法執行時,該異常將做爲相應的參數值傳遞給通知的方法。throwing還將匹配僅限制爲拋出指定類型的異常(在這種狀況下爲DataAccessException)的方法執行。

最終通知

當匹配的方法執行退出時,通知(最終)運行。經過使用@After註解聲明它。以後必須準備處理正常和異常返回條件的通知。它一般用於釋放資源和相似目的。如下示例顯示了最終通知的用法:

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註解來聲明環繞通知。通知方法的第一個參數必須是ProceedingJoinPoint類型。在通知的正文中,在ProceedingJoinPoint上調用proceed()會使底層(真正的執行方法)方法執行。proceed方法也能夠傳入Object []。數組中的值用做方法執行時的參數。

當用Object []進行調用時,proceed的行爲與AspectJ編譯器所編譯的around 通知的proceed爲略有不一樣。對於使用傳統AspectJ語言編寫的環繞通知,傳遞給proceed的參數數量必須與傳遞給環繞通知的參數數量(而不是基礎鏈接點採用的參數數量)相匹配,而且傳遞給給定的參數位置會取代該值綁定到的實體的鏈接點處的原始值(不要擔憂,若是這如今沒有意義)。Spring採起的方法更簡單,而且更適合其基於代理的,僅執行的語義。若是你編譯爲Spring編寫的@AspectJ切面,並在AspectJ編譯器和weaver中使用參數進行處理,則只須要意識到這種區別。有一種方法能夠在Spring AOP和AspectJ之間100%兼容,而且在下面有關通知參數的部分中對此進行了討論。

如下示例顯示瞭如何使用環繞通知:

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

}

環繞通知返回的值是該方法的調用者看到的返回值。例如,若是一個簡單的緩存切面有一個值,則它能夠從緩存中返回一個值,若是沒有,則調用proceed()。請注意,在環繞通知的正文中,proceed可能被調用一次,屢次或徹底不被調用。全部這些都是合法的。

參考代碼:com.liyong.ioccontainer.starter.AopIocContiner

通知參數

Spring提供了徹底類型化的通知,這意味着你能夠在通知簽名中聲明所需的參數(如咱們先前在返回和拋出示例中所看到的),而不是一直使用Object []數組。咱們將在本節的後面部分介紹如何使參數和其餘上下文值可用於通知主體。首先,咱們看一下如何編寫通用通知,以瞭解該通知當前通知的方法。

獲取當前JoinPoint

任何通知方法均可以將org.aspectj.lang.JoinPoint類型的參數聲明爲其第一個參數。請注意,環繞通知聲明ProceedingJoinPoint類型爲第一個參數,該參數是JoinPoint的子類。JoinPoint接口提供了許多有用的方法:

  • getArgs(): 返回方法參數。
  • getThis(): 返回代理對象。
  • getTarget(): 返回目標對象。
  • getSignature(): 返回通知使用的方法的描述。
  • toString(): 打印有關全部通知方法的有用描述。

有關更多詳細信息,請參見javadoc

傳遞參數給通知

咱們已經看到了如何綁定返回的值或異常值(在返回以後和引起通知以後)。要使參數值可用於通知正文,可使用args的綁定形式。若是在args表達式中使用參數名稱代替類型名稱,則在調用通知時會將相應參數的值做爲參數值傳遞。一個例子應該使這一點更清楚。假設你要通知以Account對象做爲第一個參數的DAO操做的執行,而且你須要在通知正文中訪問該賬戶。你能夠編寫如下內容:

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

爲了使這項工做有效,咱們將不得不檢查集合的每一個元素,這是不合理的,由於咱們也沒法決定一般如何處理null。要實現相似的目的,你必須將參數鍵入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
}

若是第一個參數是JoinPointProceedingJoinPointJoinPoint.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
}

JoinPointProceedingJoinPointJoinPoint.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

proceed參數

前面咱們提到過,咱們將描述如何編寫一個在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});
}

在許多狀況下,不管如何都要進行此綁定(如上例所示)。

通知順序

當多條通知都但願在同一鏈接點上運行時會發生什麼? Spring AOP遵循與AspectJ相同的優先級規則來肯定通知執行的順序。優先級最高的通知在進入時首先運行(所以,給定兩個before通知,優先級最高的通知首先運行)。在從鏈接點出來的過程當中,優先級最高的通知最後運行(所以,給定兩個after通知,優先級最高的通知將排在第二)。

在不一樣切面定義的兩個通知都須要在同一個鏈接點上運行時,除非另行指定,不然執行順序是未定義的。你能夠經過指定優先級來控制執行順序。經過在切面類中實現org.springframework.core.Ordered接口或使用Order註解對其進行註解,能夠經過常規的Spring方法來完成。給定兩個切面,從Ordered.getValue()(或註解值)返回較低值的切面具備較高的優先級

當在同一個切面中定義的兩個通知都須要在同一個鏈接點上運行時,順序是未定義的(由於沒法經過java編譯類的反射檢索聲明順序)。考慮將此類通知方法分解爲每一個切面類中的每一個鏈接點的一個通知方法,或者將通知片斷重構爲能夠在切面級別排序的單獨切面類。

5.4.5 引入

引入(在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註解的value屬性是AspectJ類型的模式。匹配類型的任何bean都實現UsageTracked接口。注意,在前面示例的before通知中,服務bean能夠直接用做UsageTracked接口的實現。若是以編程方式訪問bean,則應編寫如下內容:

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

參考代碼:com.liyong.ioccontainer.starter.AopDeclareParentsIocContiner

5.4.6 切面實例化模型

這是一個高級主題。若是你剛開始使用AOP,則能夠放心地跳過它,直到之後。

默認狀況下,應用程序上下文中每一個切面都有一個實例。 AspectJ將此稱爲單例實例化模型。可使用bean生命週期來定義切面。Spring支持AspectJ的perthispertarget實例化模型(當前不支持percflowpercflowbelowpertypewithin)。

你能夠經過在@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」的惟一對象)建立一個切面實例。切面實例是在服務對象上首次調用方法時建立的。當服務對象超出範圍時,切面將超出範圍。在建立切面實例以前,其中的任何通知都不會執行。一旦建立了切面實例,在其中聲明的通知就會在匹配的鏈接點上執行,但僅當服務對象與此切面相關聯時才執行。有關每一個子句的更多信息,請參見AspectJ編程指南。

pertarget實例化模型的工做方式與perthis徹底相同,可是它在匹配的鏈接點爲每一個惟一目標對象建立一個切面實例。

5.4.7 AOP例子

如今你已經瞭解了全部組成部分是如何工做的,咱們能夠將它們組合在一塊兒作一些有用的事情。

有時因爲併發問題(例如,死鎖失敗),業務服務的執行可能會失敗。若是重試該操做,則極可能在下一次嘗試中成功。對於適合在這種狀況下重試的業務服務(不須要爲解決衝突而須要返回給用戶的冪等操做),咱們但願透明地重試該操做,以免客戶端看到PessimisticLockingFailureException。這是一個明顯跨越服務層中的多個服務的需求,所以很是適合經過切面實現。

由於咱們想重試該操做,因此咱們須要使用環繞通知,以即可以屢次調用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;
    }

}

請注意,切面實現了Ordered接口,以便咱們能夠將切面的優先級設置爲高於事務通知的優先級(每次重試時都須要一個新的事務)。maxRetriesorder屬性均由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.SystemArchitecture.businessService() && " +
        "@annotation(com.xyz.myapp.service.Idempotent)")
public Object doConcurrentOperation(ProceedingJoinPoint pjp) throws Throwable {
    // ...
}

做者

我的從事金融行業,就任過易極付、思建科技、某網約車平臺等重慶一流技術團隊,目前就任於某銀行負責統一支付系統建設。自身對金融行業有強烈的愛好。同時也實踐大數據、數據存儲、自動化集成和部署、分佈式微服務、響應式編程、人工智能等領域。同時也熱衷於技術分享創立公衆號和博客站點對知識體系進行分享。關注公衆號:青年IT男 獲取最新技術文章推送!

博客地址: http://youngitman.tech

CSDN: https://blog.csdn.net/liyong1028826685

微信公衆號:

技術交流羣:

相關文章
相關標籤/搜索