面向切面編程(AOP)經過提供另一種思考程序結構的途經來彌補面向對象編程(OOP)的不足。在OOP中模塊化的關鍵單元是類(classes),而在AOP中模塊化的單元則是切面。切面能對關注點進行模塊化,例如橫切多個類型和對象的事務管理。(在AOP術語中一般稱做橫切(crosscutting)關注點。)html
AOP框架是Spring的一個重要組成部分。可是Spring IoC容器並不依賴於AOP,這意味着你有權利選擇是否使用AOP,AOP作爲Spring IoC容器的一個補充,使它成爲一個強大的中間件解決方案。java
AOP在Spring Framework中的做用數據庫
提供聲明式企業服務,特別是爲了替代EJB聲明式服務。最重要的服務是聲明性事務管理。express
容許用戶實現自定義切面,用AOP來完善OOP的使用。編程
若是你只打算使用通用的聲明式服務或者封裝好的聲明式中間件服務,例如緩衝池(pooling),那麼你沒必要與Spring AOP直接打交道,而且本章的大部份內容能夠跳過了。api
首先讓咱們從一些重要的AOP概念和術語開始。這些術語不是Spring特有的。不過AOP術語並非特別的直觀,若是Spring使用本身的術語,將會變得更加使人困惑。數組
切面(Aspect):一個關注點的模塊化,這個關注點可能會橫切多個對象。事務管理是J2EE應用中一個關於橫切關注點的很好的例子。在Spring AOP中,切面可使用基於模式)或者基於@Aspect註解的方式來實現。
鏈接點(Joinpoint):在程序執行過程當中某個特定的點,好比某方法調用的時候或者處理異常的時候。在Spring AOP中,一個鏈接點老是表示一個方法的執行。
通知(Advice):在切面的某個特定的鏈接點上執行的動做。其中包括了「around」、「before」和「after」等不一樣類型的通知(通知的類型將在後面部分進行討論)。許多AOP框架(包括Spring)都是以攔截器作通知模型,並維護一個以鏈接點爲中心的攔截器鏈。
切入點(Pointcut):匹配鏈接點的斷言。通知和一個切入點表達式關聯,並在知足這個切入點的鏈接點上運行(例如,當執行某個特定名稱的方法時)。切入點表達式如何和鏈接點匹配是AOP的核心:Spring缺省使用AspectJ切入點語法。
引入(Introduction):用來給一個類型聲明額外的方法或屬性(也被稱爲鏈接類型聲明(inter-type declaration))。Spring容許引入新的接口(以及一個對應的實現)到任何被代理的對象。例如,你可使用引入來使一個bean實現IsModified接口,以便簡化緩存機制。
目標對象(Target Object): 被一個或者多個切面所通知的對象。也被稱作被通知(advised)對象。 既然Spring AOP是經過運行時代理實現的,這個對象永遠是一個被代理(proxied)對象。
AOP代理(AOP Proxy):AOP框架建立的對象,用來實現切面契約(例如通知方法執行等等)。在Spring中,AOP代理能夠是JDK動態代理或者CGLIB代理。
織入(Weaving):把切面鏈接到其它的應用程序類型或者對象上,並建立一個被通知的對象。這些能夠在編譯時(例如使用AspectJ編譯器),類加載時和運行時完成。Spring和其餘純Java AOP框架同樣,在運行時完成織入。
通知類型:
前置通知(Before advice):在某鏈接點以前執行的通知,但這個通知不能阻止鏈接點以前的執行流程(除非它拋出一個異常)。
後置通知(After returning advice):在某鏈接點正常完成後執行的通知:例如,一個方法沒有拋出任何異常,正常返回。
異常通知(After throwing advice):在方法拋出異常退出時執行的通知。
最終通知(After (finally) advice):當某鏈接點退出的時候執行的通知(不管是正常返回仍是異常退出)。
環繞通知(Around Advice):包圍一個鏈接點的通知,如方法調用。這是最強大的一種通知類型。環繞通知能夠在方法調用先後完成自定義的行爲。它也會選擇是否繼續執行鏈接點或直接返回它本身的返回值或拋出異常來結束執行。
環繞通知是最經常使用的通知類型。和AspectJ同樣,Spring提供全部類型的通知,咱們推薦你使用盡量簡單的通知類型來實現須要的功能。例如,若是你只是須要一個方法的返回值來更新緩存,最好使用後置通知而不是環繞通知,儘管環繞通知也能完成一樣的事情。用最合適的通知類型可使得編程模型變得簡單,而且可以避免不少潛在的錯誤。好比,你不須要在JoinPoint上調用用於環繞通知的proceed()方法,就不會有調用的問題。
在Spring 2.0中,全部的通知參數都是靜態類型,所以你可使用合適的類型(例如一個方法執行後的返回值類型)做爲通知的參數而不是使用Object數組。
經過切入點匹配鏈接點的概念是AOP的關鍵,這使得AOP不一樣於其它僅僅提供攔截功能的舊技術。 切入點使得通知能夠獨立對應到面向對象的層次結構中。例如,一個提供聲明式事務管理 的環繞通知能夠被應用到一組橫跨多個對象的方法上(例如服務層的全部業務操做)。
Spring AOP使用純Java實現。它不須要專門的編譯過程。Spring AOP不須要控制類裝載器層次,所以它適用於J2EE web容器或應用服務器。
Spring目前僅支持使用方法調用做爲鏈接點(join point)(在Spring bean上通知方法的執行)。雖然能夠在不影響到Spring AOP核心API的狀況下加入對成員變量攔截器支持,但Spring並無實現成員變量攔截器。若是你須要把對成員變量的訪問和更新也做爲通知的鏈接點,能夠考慮其它的語言,如AspectJ。
Spring實現AOP的方法跟其餘的框架不一樣。Spring並非要提供最完整的AOP實現(儘管Spring AOP有這個能力),相反的,它其實側重於提供一種AOP實現和Spring IoC容器之間的整合,用於幫助解決在企業級開發中的常見問題。
所以,Spring的AOP功能一般都和Spring IoC容器一塊兒使用。切面使用普通的bean定義語法來配置(儘管Spring提供了強大的"自動代理(autoproxying)"功能):與其餘AOP實現相比這是一個顯著的區別。有些事使用Spring AOP是沒法輕鬆或者高效完成的,好比說通知一個細粒度的對象(例如典型的域對象):這種時候,使用AspectJ是最好的選擇。不過經驗告訴咱們,對於大多數在J2EE應用中適合用AOP來解決的問題,Spring AOP都提供了一個很是好的解決方案。
Spring AOP歷來沒有打算經過提供一種全面的AOP解決方案來與AspectJ競爭。咱們相信不管是基於代理(proxy-based)的框架如Spring AOP或者是成熟的框架如AspectJ都是頗有價值的,他們之間應該是互補而不是競爭的關係。Spring 2.0能夠無縫的整合Spring AOP,IoC和AspectJ,使得全部的AOP應用徹底融入基於Spring的應用體系。這樣的集成不會影響Spring AOP API或者AOP Alliance API;Spring AOP保持了向下兼容性。下一章會詳細討論Spring AOP的API。
![]() |
Note |
---|---|
Spring Framework一個重要的原則就是無侵入性(non-invasiveness); 這個思想指你不該當被迫引入框架特定的類和接口到你的業務/領域模型中。然而,Spring Framework在某些地方給你一個是否引入Spring框架特定依賴到你的代碼的選項: 給你這個選項的理由是由於在特定的場景中它可能僅僅是容易閱讀或用這種方法編寫特定的功能塊。Spring Framework(幾乎)一直會爲你提供這種選擇:從而使你能作出一個明智的決定,使它最適應你的特定用例或場景。 你能夠選擇AspectJ或者Spring AOP,以及選擇是使用@AspectJ註解風格仍是Spring XML配置風格。事實上本章選擇先介紹@AspectJ風格的方法不該當被看做是這樣一個暗示:Spring小組喜歡@AspectJ註解風格更勝於Spring XML配置。 在Section 6.4, 「AOP聲明風格的選擇」一章有對使用各個風格理由的一個更全面的討論。 |
Spring缺省使用J2SE 動態代理(dynamic proxies)來做爲AOP的代理。 這樣任何接口(或者接口集)均可以被代理。
Spring也可使用CGLIB代理. 對於須要代理類而不是代理接口的時候CGLIB代理是頗有必要的。若是一個業務對象並無實現一個接口,默認就會使用CGLIB。做爲面向接口編程的最佳實踐,業務對象一般都會實現一個或多個接口。但也有可能會強制使用CGLIB,在這種狀況(但願不常有)下,你可能須要通知一個沒有在接口中聲明的方法,或者須要傳入一個代理對象給方法做爲具體類型
爲了明白Spring AOP是基於代理(proxy-based)的事實,請參閱Section 6.6.1, 「理解AOP代理」。
@AspectJ使用了Java 5的註解,能夠將切面聲明爲普通的Java類。@AspectJ樣式在AspectJ 5發佈的AspectJ project部分中被引入。Spring 2.0使用了和AspectJ 5同樣的註解,並使用AspectJ來作切入點解析和匹配。可是,AOP在運行時仍舊是純的Spring AOP,並不依賴於AspectJ的編譯器或者織入器(weaver)。
使用AspectJ的編譯器或者織入器的話就可使用完整的AspectJ語言,咱們將在Section 6.8, 「在Spring應用中使用AspectJ」中討論這個問題。爲了在Spring配置中使用@AspectJ切面,你首先必須啓用Spring對@AspectJ切面配置的支持,並確保自動代理(autoproxying)的bean是否能被這些切面通知。自動代理是指Spring會判斷一個bean是否使用了一個或多個切面通知,並據此自動生成相應的代理以攔截其方法調用,而且確保通知在須要時執行。
經過在你的Spring的配置中引入下列元素來啓用Spring對@AspectJ的支持:
<aop:aspectj-autoproxy/>
咱們假定你正在使用Appendix A, XML Schema-based configuration所描述的schema支持。關於如何在aop的命名空間中引入這些標籤,請參見Section A.2.7, 「The aop schema」
若是你正在使用DTD,你仍然能夠經過在你的application context中添加以下定義來啓用@AspectJ支持:
<bean class="org.springframework.aop.aspectj.annotation.AnnotationAwareAspectJAutoProxyCreator" />
你須要在你的應用程序的classpath中引入兩個AspectJ庫:aspectjweaver.jar和aspectjrt.jar。這些庫能夠在AspectJ的安裝包(1.5.1或者以後的版本)的'lib'目錄裏找到,或者也能夠在Spring-with-dependencies發佈包的'lib/aspectj'目錄下找到。
啓用@AspectJ支持後,在application context中定義的任意帶有一個@Aspect切面(擁有@Aspect註解)的bean都將被Spring自動識別並用於配置Spring AOP。如下例子展現了爲完成一個不是很是有用的切面所須要的最小定義:
application context中一個常見的bean定義,它指向一個使用了@Aspect註解的bean類:
<bean id="myAspect" class="org.xyz.NotVeryUsefulAspect">
<!-- configure properties of aspect here as normal -->
</bean>
以及NotVeryUsefulAspect類的定義,使用了 org.aspectj.lang.annotation.Aspect註解。
package org.xyz; import org.aspectj.lang.annotation.Aspect; @Aspect public class NotVeryUsefulAspect { }
切面(用@Aspect註解的類)和其餘類同樣有方法和字段定義。他們也可能包括切入點,通知和引入(inter-type)聲明。
![]() |
通知切面 |
---|---|
在Spring AOP中,擁有切面的類自己不多是其它切面中通知的目標。一個類上面的@Aspect註解標識它爲一個切面,而且從自動代理中排除它。 |
在前面咱們提到,切入點決定了鏈接點關注的內容,使得咱們能夠控制通知何時執行。Spring AOP只支持Spring bean的方法執行鏈接點。因此你能夠把切入點看作是Spring bean上方法執行的匹配。一個切入點聲明有兩個部分:一個包含名字和任意參數的簽名,還有一個切入點表達式,該表達式決定了咱們關注那個方法的執行。在@AspectJ註解風格的AOP中,一個切入點簽名經過一個普通的方法定義來提供,而且切入點表達式使用@Pointcut註解來表示(做爲切入點簽名的方法必須返回void 類型)。
用一個例子能幫咱們清楚的區分切入點簽名和切入點表達式之間的差異,下面的例子定義了一個切入點'anyOldTransfer',這個切入點將匹配任何名爲 "transfer" 的方法的執行:
@Pointcut("execution(* transfer(..))")// the pointcut expression private void anyOldTransfer() {}// the pointcut signature
切入點表達式,也就是組成@Pointcut註解的值,是正規的AspectJ 5切入點表達式。若是你想要更多瞭解AspectJ的切入點語言,請參見AspectJ編程指南(若是要了解基於Java 5的擴展請參閱AspectJ 5 開發手冊)或者其餘人寫的關於AspectJ的書,例如Colyer et. al.著的「Eclipse AspectJ」或者Ramnivas Laddad著的「AspectJ in Action」。
Spring AOP支持在切入點表達式中使用以下的AspectJ切入點指示符:
execution - 匹配方法執行的鏈接點,這是你將會用到的Spring的最主要的切入點指示符。
within - 限定匹配特定類型的鏈接點(在使用Spring AOP的時候,在匹配的類型中定義的方法的執行)。
this - 限定匹配特定的鏈接點(使用Spring AOP的時候方法的執行),其中bean reference(Spring AOP 代理)是指定類型的實例。
target - 限定匹配特定的鏈接點(使用Spring AOP的時候方法的執行),其中目標對象(被代理的應用對象)是指定類型的實例。
args - 限定匹配特定的鏈接點(使用Spring AOP的時候方法的執行),其中參數是指定類型的實例。
@target - 限定匹配特定的鏈接點(使用Spring AOP的時候方法的執行),其中正執行對象的類持有指定類型的註解。
@args - 限定匹配特定的鏈接點(使用Spring AOP的時候方法的執行),其中實際傳入參數的運行時類型持有指定類型的註解。
@within - 限定匹配特定的鏈接點,其中鏈接點所在類型已指定註解(在使用Spring AOP的時候,所執行的方法所在類型已指定註解)。
@annotation - 限定匹配特定的鏈接點(使用Spring AOP的時候方法的執行),其中鏈接點的主題持有指定的註解。
另外,Spring AOP還提供了一個名爲'bean'的PCD。這個PCD容許你限定匹配鏈接點到一個特定名稱的Spring bean,或者到一個特定名稱Spring bean的集合(當使用通配符時)。'bean' PCD具備下列的格式:
bean(idOrNameOfBean)
'idOrNameOfBean'標記能夠是任何Spring bean的名字:限定通配符使用'*'來提供,若是你爲Spring bean制定一些命名約定,你能夠很是容易地編寫一個'bean' PCD表達式將它們選出來。和其它鏈接點指示符同樣,'bean' PCD也支持&&, ||和 !邏輯操做符。
![]() |
Note |
---|---|
請注意'bean' PCD僅僅 被Spring AOP支持而不是AspectJ. 這是Spring對AspectJ中定義的標準PCD的一個特定擴展。 'bean' PCD不只僅能夠在類型級別(被限制在基於織入AOP上)上操做而還能夠在實例級別(基於Spring bean的概念)上操做。 |
切入點表達式可使用'&', '||' 和 '!'來組合。還能夠經過名字來指向切入點表達式。如下的例子展現了三種切入點表達式: anyPublicOperation(在一個方法執行鏈接點表明了任意public方法的執行時匹配);inTrading(在一個表明了在交易模塊中的任意的方法執行時匹配)和 tradingOperation(在一個表明了在交易模塊中的任意的公共方法執行時匹配)。
@Pointcut("execution(public * *(..))") private void anyPublicOperation() {} @Pointcut("within(com.xyz.someapp.trading..*") private void inTrading() {} @Pointcut("anyPublicOperation() && inTrading()") private void tradingOperation() {}
如上所示,用更少的命名組件來構建更加複雜的切入點表達式是一種最佳實踐。當用名字來指定切入點時使用的是常見的Java成員可視性訪問規則。(好比說,你能夠在同一類型中訪問私有的切入點,在繼承關係中訪問受保護的切入點,能夠在任意地方訪問公共切入點)。成員可視性訪問規則不影響到切入點的匹配。
當開發企業級應用的時候,你一般會想要從幾個切面來引用模塊化的應用和特定操做的集合。咱們推薦定義一個「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.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() {} }
示例中的切入點定義了一個你能夠在任何須要切入點表達式的地方可引用的切面。好比,爲了使service層事務化,你能夠寫成:
<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>
咱們將在Section 6.3, 「基於Schema的AOP支持」中討論 <aop:config>和<aop:advisor>標籤。在Chapter 9, 事務管理中討論事務標籤。
Spring AOP 用戶可能會常用 execution切入點指示符。執行表達式的格式以下:
execution(modifiers-pattern? ret-type-pattern declaring-type-pattern? name-pattern(param-pattern) throws-pattern?)
除了返回類型模式(上面代碼片段中的ret-type-pattern),名字模式和參數模式之外, 全部的部分都是可選的。返回類型模式決定了方法的返回類型必須依次匹配一個鏈接點。 你會使用的最頻繁的返回類型模式是*,它表明了匹配任意的返回類型。 一個全限定的類型名將只會匹配返回給定類型的方法。名字模式匹配的是方法名。 你可使用*通配符做爲全部或者部分命名模式。 參數模式稍微有點複雜:()匹配了一個不接受任何參數的方法, 而(..)匹配了一個接受任意數量參數的方法(零或者更多)。 模式(*)匹配了一個接受一個任何類型的參數的方法。 模式(*,String)匹配了一個接受兩個參數的方法,第一個能夠是任意類型, 第二個則必須是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)): args版本只有在動態運行時候傳入參數是Serializable時才匹配,而execution版本在方法簽名中聲明只有一個 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'在綁定表單中更加經常使用:- 請參見後面的通知一節中瞭解如何使得註解對象在通知體內可用。
任何一個只接受一個參數,而且運行時所傳入的參數類型具備@Classified 註解的鏈接點(在Spring AOP中只是方法執行)
@args(com.xyz.security.Classified)
'@args'在綁定表單中更加經常使用:- 請參見後面的通知一節中瞭解如何使得註解對象在通知體內可用。
任何一個在名爲'tradeService'的Spring bean之上的鏈接點 (在Spring AOP中只是方法執行):
bean(tradeService)
任何一個在名字匹配通配符表達式'*Service'的Spring bean之上的鏈接點 (在Spring AOP中只是方法執行):
bean(*Service)
通知是跟一個切入點表達式關聯起來的,而且在切入點匹配的方法執行以前或者以後或者先後運行。 切入點表達式多是指向已命名的切入點的簡單引用或者是一個已經聲明過的切入點表達式。
一個切面裏使用 @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() {
// ...
}
}
若是使用一個in-place 的切入點表達式,咱們能夠把上面的例子換個寫法:
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) {
// ...
}
}
在 returning屬性中使用的名字必須對應於通知方法內的一個參數名。 當一個方法執行返回後,返回值做爲相應的參數值傳入通知方法。 一個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[]對象的時候,處理的方法與經過AspectJ編譯器處理環繞通知略有不一樣。 對於使用傳統AspectJ語言寫的環繞通知來講,傳入參數的數量必須和傳遞給環繞通知的參數數量匹配 (不是後臺的鏈接點接受的參數數量),而且特定順序的傳入參數代替了將要綁定給鏈接點的原始值 (若是你看不懂不用擔憂)。Spring採用的方法更加簡單而且能更好匹配它基於代理(proxy-based)的執行語法, 若是你使用AspectJ的編譯器和編織器來編譯爲Spring而寫的@AspectJ切面和處理參數,你只須要知道這一區別便可。 有一種方法可讓你寫出100%兼容Spring AOP和AspectJ的表達式,咱們將會在後續的通知參數的章節中討論它。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可能在通知體內部被調用一次,許屢次,或者根本不被調用,全部這些都是合法的。
Spring 2.0 提供了完整的通知類型 - 這意味着你能夠在通知簽名中聲明所需的參數, (就像咱們在前面看到的後置和異常通知同樣)而不老是使用Object[]。 咱們將會看到如何使得參數和其餘上下文值對通知體可用。 首先讓咱們看如下如何編寫普通的通知以找出正在被通知的方法。
任何通知方法能夠將第一個參數定義爲org.aspectj.lang.JoinPoint類型 (環繞通知須要定義第一個參數爲ProceedingJoinPoint類型, 它是 JoinPoint 的一個子類)。JoinPoint 接口提供了一系列有用的方法,好比 getArgs()(返回方法參數)、 getThis()(返回代理對象)、getTarget()(返回目標)、getSignature()(返回正在被通知的方法相關信息)和 toString() (打印出正在被通知的方法的有用信息)。詳細的內容請參考JavaDoc。
咱們已經看到了如何綁定返回值或者異常(使用後置通知和異常通知)。爲了能夠在通知體內訪問參數, 你可使用args來綁定。若是在一個args表達式中應該使用類型名字的地方 使用一個參數名字,那麼當通知執行的時候對應的參數值將會被傳遞進來。用一個例子應該會使它變得清晰。 假使你想要通知以一個Account對象做爲第一個參數的DAO操做的執行, 你想要在通知體內也能訪問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註解來匹配方法執行,並提取Audit代碼。
首先是@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();
// ...
}
綁定在通知上的參數依賴切入點表達式的匹配名,並藉此在(通知和切入點)的方法簽名中聲明參數名。 參數名沒法 經過Java反射來獲取,因此Spring AOP使用以下的策略來肯定參數名字:
若是參數名字已經被用戶明確指定,則使用指定的參數名: 通知和切入點註解有一個額外的"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將查看類的debug信息並嘗試從本地的變量表肯定參數名。只要類編譯時有debug信息, (最少要有'-g:vars')這個信息將會出現。打開這個標誌編譯的結果是: (1)你的代碼稍微容易理解(反向工程), (2)class文件的大小稍微有些大(一般不重要), (3)你的編譯器將不會應用優化去移除未使用的本地變量。換句話說,打開這個標誌建立時你應當不會遇到困難。
若是一個@AspectJ切面已經被AspectJ編譯器(ajc)編譯過,即便沒有debug信息, 也不須要添加argNames參數,由於編譯器會保留必需的信息。若是不加上必要的debug信息來編譯的話,Spring AOP將會嘗試推斷綁定變量到參數的配對。 (例如,要是隻有一個變量被綁定到切入點表達式,通知方法只接受一個參數, 配對是顯而易見的)。 若是變量的綁定不明確,將會拋出一個AmbiguousBindingException異常。
若是以上全部策略都失敗了,將會拋出一個IllegalArgumentException異常。
咱們以前提過咱們將會討論如何編寫一個帶參數的的proceed()調用, 使得在Spring AOP和AspectJ中都能正常工做。解決方法是僅僅確保通知簽名按順序綁定方法參數。例如:
@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同樣的優先規則來肯定通知執行的順序。 在「進入」鏈接點的狀況下,最高優先級的通知會先執行(因此給定的兩個前置通知中,優先級高的那個會先執行)。 在「退出」鏈接點的狀況下,最高優先級的通知會最後執行。(因此給定的兩個後置通知中, 優先級高的那個會第二個執行)。
當定義在不一樣的切面裏的兩個通知都須要在一個相同的鏈接點中運行, 那麼除非你指定,不然執行的順序是未知的。你能夠經過指定優先級來控制執行順序。 在標準的Spring方法中能夠在切面類中實現org.springframework.core.Ordered 接口或者用Order註解作到這一點。在兩個切面中, Ordered.getValue()方法返回值(或者註解值)較低的那個有更高的優先級。
當定義在相同的切面裏的兩個通知都須要在一個相同的鏈接點中運行, 執行的順序是未知的(由於這裏沒有方法經過反射javac編譯的類來獲取聲明順序)。 考慮在每一個切面類中按鏈接點壓縮這些通知方法到一個通知方法,或者重構通知的片斷到各自的切面類中 - 它能在切面級別進行排序。
引入(在AspectJ中被稱爲inter-type聲明)使得一個切面能夠定義被通知對象實現給定的接口, 而且能夠爲那些對象提供具體的實現。
使用@DeclareParents註解來定義引入。這個註解用來定義匹配的類型 擁有一個新的父類(因此有了這個名字)。好比,給定一個接口UsageTracked, 和接口的具體實現DefaultUsageTracked類, 接下來的切面聲明瞭全部的service接口的實現都實現了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接口。請注意,在上面的前置通知的例子中,service beans 能夠直接用做UsageTracked接口的實現。若是須要編程式的來訪問一個bean, 你能夠這樣寫:
UsageTracked usageTracked = (UsageTracked) context.getBean("myService");
默認狀況下,在application context中每個切面都會有一個實例。AspectJ把這個叫作單例化模型。 也能夠用其餘的生命週期來定義切面:Spring支持AspectJ的 perthis 和pertarget實例化模型(如今還不支持percflow、percflowbelow 和pertypewithin)。
一個"perthis" 切面經過在@Aspect註解中指定perthis 子句來聲明。讓咱們先來看一個例子,而後解釋它是如何運做的:
@Aspect("perthis(com.xyz.myapp.SystemArchitecture.businessService())")
public class MyAspect {
private int someState;
@Before(com.xyz.myapp.SystemArchitecture.businessService())
public void recordServiceUsage() {
// ...
}
}
這個'perthis'子句的效果是每一個獨立的service對象執行一個業務時都會 建立一個切面實例(切入點表達式所匹配的鏈接點上的每個獨立的對象都會綁定到'this'上)。 在service對象上第一次調用方法的時候,切面實例將被建立。切面在service對象失效的同時失效。 在切面實例被建立前,全部的通知都不會被執行,一旦切面對象建立完成, 定義的通知將會在匹配的鏈接點上執行,可是隻有當service對象是和切面關聯的才能夠。 請參閱 AspectJ 編程指南瞭解更多關於per-clauses的信息。
'pertarget'實例模型的跟「perthis」徹底同樣,只不過是爲每一個匹配於鏈接點 的獨立目標對象建立一個切面實例。
如今你已經看到了每一個獨立的部分是如何運做的了,是時候把他們放到一塊兒作一些有用的事情了!
由於併發的問題,有時候業務服務(business services)可能會失敗(例如,死鎖失敗)。若是從新嘗試一下, 頗有可能就會成功。對於業務服務來講,重試幾回是很正常的(Idempotent操做不須要用戶參與,不然會得出矛盾的結論) 咱們可能須要透明的重試操做以免客戶看到一個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 接口,這樣咱們就能夠把切面的優先級設定爲高於事務通知 (咱們每次重試的時候都想要在一個全新的事務中進行)。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>
爲改進切面,使之僅僅重試idempotent操做,咱們能夠定義一個 Idempotent註解:
@Retention(RetentionPolicy.RUNTIME)
public @interface Idempotent {
// marker annotation
}
而且對service操做的實現進行註解。爲了只重試idempotent操做,切面的修改只須要改寫切入點表達式, 使得只匹配@Idempotent操做:
@Around("com.xyz.myapp.SystemArchitecture.businessService() && " + "@annotation(com.xyz.myapp.service.Idempotent)") public Object doConcurrentOperation(ProceedingJoinPoint pjp) throws Throwable { ... }
若是你沒法使用Java 5,或者你比較喜歡使用XML格式,Spring2.0也提供了使用新的"aop"命名空間來定義一個切面。 和使用@AspectJ風格徹底同樣,切入點表達式和通知類型一樣獲得了支持,所以在這一節中咱們將着重介紹新的 語法並回顧前一節(Section 6.2, 「@AspectJ支持」)對編寫一個切入點表達式和綁定通知參數的討論。
使用本章所介紹的aop命名空間標籤,你須要引入Appendix A, XML Schema-based configuration中說起的spring-aop schema。 參見Section A.2.7, 「The aop schema」瞭解如何在aop命名空間中引入標籤。
在Spring的配置文件中,全部的切面和通知都必須定義在<aop:config>元素內部。 (一個application context能夠包含多個 <aop:config>)。 一個<aop:config>能夠包含pointcut,advisor和aspect元素 (注意這三個元素必須按照這個順序進行聲明)。
![]() |
Warning |
---|---|
<aop:config>風格的配置使得Spring auto-proxying機制的使用變得很笨重。若是你已經經過 BeanNameAutoProxyCreator或相似的東西顯式使用auto-proxying,它可能會致使問題 (例如通知沒有被織入)。 推薦的使用模式是僅僅使用<aop:config>風格, 或者僅僅使用AutoProxyCreator風格。 |
有了schema的支持,切面就和常規的Java對象同樣被定義成application context中的一個bean。 對象的字段和方法提供了狀態和行爲信息,XML文件則提供了切入點和通知信息。
切面使用<aop:aspect>來聲明,backing bean(支持bean)經過 ref 屬性來引用:
<aop:config> <aop:aspect id="myAspect" ref="aBean"> ... </aop:aspect> </aop:config> <bean id="aBean" class="..."> ... </bean>
切面的支持bean(上例中的"aBean")能夠象其餘Spring bean同樣被容器管理配置以及依賴注入。
一個命名切入點能夠在<aop:config>元素中定義,這樣多個切面和通知就能夠共享該切入點。
一個描述service層中全部service執行的切入點能夠定義以下:
<aop:config> <aop:pointcut id="businessService" expression="execution(* com.xyz.myapp.service.*.*(..))"/> </aop:config>
注意切入點表達式自己使用了與Section 6.2, 「@AspectJ支持」中描述的相同的AspectJ切入點表達式語言。 若是你在Java 5環境下使用基於schema的聲明風格,可參考切入點表達式類型(@Aspects)中定義的命名切入點, 不過這個特性在JDK1.4及如下版本中是不可用的(由於依賴於Java 5中的AspectJ反射API)。因此在JDK 1.5中, 上面的切入點的另一種定義形式以下:
<aop:config> <aop:pointcut id="businessService" expression="com.xyz.myapp.SystemArchitecture.businessService()"/> </aop:config>
假定你有一個在Section 6.2.3.3, 「共享通用切入點定義」中 描述的SystemArchitecture切面。
在切面裏面聲明一個切入點和聲明一個頂級的切入點很是相似:
<aop:config> <aop:aspect id="myAspect" ref="aBean"> <aop:pointcut id="businessService" expression="execution(* com.xyz.myapp.service.*.*(..))"/> ... </aop:aspect> </aop:config>
幾乎和@AspectJ切面中的同樣,使用基於schema定義風格聲明的切入點能夠收集(collect) 鏈接點上下文。例如,下面的切入點收集'this'對象做爲鏈接點上下文並傳遞它給通知:
<aop:config> <aop:aspect id="myAspect" ref="aBean"> <aop:pointcut id="businessService" expression="execution(* com.xyz.myapp.service.*.*(..)) && 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來查找,而且不能定義切入點參數。在基於schema的定義風格中 命名切入點支持較之@AspectJ風格受到了不少的限制。
和@AspectJ風格同樣,基於schema的風格也支持5種通知類型而且二者具備一樣的語義。
前置通知在匹配方法執行前運行。在<aop:aspect>中使用<aop:before> 元素來聲明它。
<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)。 這個方法必須定義在包含通知的切面元素所引用的bean中。在一個數據訪問操做執行以前 (一個方法執行由切入點表達式所匹配的鏈接點),切面中的"doAccessCheck"會被調用。
後置通知在匹配的方法徹底執行後運行。和前置通知同樣,能夠在<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) {...
異常通知在匹配方法拋出異常退出時執行。在<aop:aspect>中使用 after-throwing元素來聲明:
<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元素來聲明它:
<aop:aspect id="afterFinallyExample" ref="aBean"> <aop:after pointcut-ref="dataAccessOperation" method="doReleaseLock"/> ... </aop:aspect>
環繞通知是最後一種通知類型。環繞通知在匹配方法運行期的「周圍」執行。 它有機會在目標方法的前面和後面執行,並決定何時運行,怎麼運行,甚至是否運行。 環繞通知常常在須要在一個方法執行先後共享狀態信息,而且是在線程安全的狀況下使用 (啓動和中止一個計時器就是一個例子)。注意選擇能知足你需求的最簡單的通知類型; 若是簡單的前置通知能作的事情就絕對不要使用環繞通知。
Around通知使用aop:around元素來聲明。通知方法的第一個參數的類型必須是 ProceedingJoinPoint類型。在通知的主體中,調用 ProceedingJoinPoint的proceed()方法來執行真正的方法。 proceed方法也可能會被調用而且傳入一個Object[]對象 - 該數組將做爲方法執行時候的參數。參見Section 6.2.4.5, 「環繞通知」中調用具備 Object[]的proceed方法。
<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; }
Schema-based聲明風格和@AspectJ同樣,支持多種類型的通知:經過通知方法參數名字來匹配切入點參數。 參見Section 6.2.4.6, 「通知參數(Advice parameters)」獲取詳細信息。若是你但願顯式指定通知方法的參數名 (而不是依靠先前說起的偵測策略),能夠經過通知元素的arg-names屬性來實現,它的處理和 在Section 6.2.4.6.3, 「肯定參數名」中所描述的對通知註解中"argNames"屬性的處理方式同樣。 示例以下:
<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 FooService { Foo getFoo(String fooName, int age); } public class DefaultFooService implements FooService { public Foo getFoo(String name, int age) { return new Foo(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 http://www.springframework.org/schema/beans/spring-beans-2.5.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-2.5.xsd"> <!-- this is the object that will be proxied by Spring's AOP infrastructure --> <bean id="fooService" class="x.y.service.DefaultFooService"/> <!-- this is the actual advice itself --> <bean id="profiler" class="x.y.SimpleProfiler"/> <aop:config> <aop:aspect ref="profiler"> <aop:pointcut id="theExecutionOfSomeFooServiceMethod" expression="execution(* x.y.service.FooService.getFoo(String,int)) and args(name, age)"/> <aop:around pointcut-ref="theExecutionOfSomeFooServiceMethod" method="profile"/> </aop:aspect> </aop:config> </beans>
若是咱們有下面的驅動腳本,咱們將在標準輸出上獲得以下的輸出:
import org.springframework.beans.factory.BeanFactory; import org.springframework.context.support.ClassPathXmlApplicationContext; import x.y.service.FooService; public final class Boot { public static void main(final String[] args) throws Exception { BeanFactory ctx = new ClassPathXmlApplicationContext("x/y/plain.xml"); FooService foo = (FooService) ctx.getBean("fooService"); foo.getFoo("Pengo", 12); } }
StopWatch 'Profiling for 'Pengo' and '12'': running time (millis) = 0 ----------------------------------------- ms % Task name ----------------------------------------- 00000 ? execution(getFoo)
當同一個切入點(執行方法)上有多個通知須要執行時,執行順序的規則如 Section 6.2.4.7, 「通知順序」所述。切面的優先級經過給切面的支持bean增長 Order註解或者使切面的支持bean實現 Ordered接口來決定。
引入(在AspectJ中稱爲inter-type聲明)容許一個切面聲明一個通知對象實現指定接口, 而且提供了一個接口實現類來表明這些對象。
引入的定義使用aop:aspect中的aop:declare-parents元素。 該元素用於聲明所匹配的類型有一個新的父類型(因此有了這個名字)。 例如,給定接口UsageTracked, 以及這個接口的一個實現類 DefaultUsageTracked, 下面的切面聲明全部實現service接口的類同時實現 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(); }
要實現的接口由implement-interface屬性來指定。 types-matching屬性的值是一個AspectJ類型模式:任何匹配類型的bean都會實現 UsageTracked 接口。注意在上面前置通知的例子中, serevice bean能夠直接用做UsageTracked接口的實現。 若是以編程形式訪問一個bean,你能夠這樣來寫:
UsageTracked usageTracked = (UsageTracked) context.getBean("myService");
"advisor"這個概念來自Spring1.2對AOP的支持,而在AspectJ中沒有等價的概念。 advisor就像一個小的自包含的切面,這個切面只有一個通知。切面自身經過一個bean表示, 而且必須實現一個在Section 7.3.2, 「Spring裏的通知類型」中描述的通知接口。 Advisor能夠很好的利用AspectJ的切入點表達式。
Spring 2.0經過<aop:advisor>元素來支持advisor概念。 你將會發現大多數狀況下它會和transactional advice一塊兒使用,在Spring 2.0中它有本身的命名空間。其格式以下:
<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中 Ordered的值 。
讓咱們看看Section 6.2.7, 「例子」中併發鎖失敗重試的例子, 當使用schema重寫它時是什麼樣子。
由於併發的問題,有時候business services可能會失敗(例如,死鎖失敗)。若是重試操做,下一次極可能就會成功。 對於business services來講,這種狀況下重試是很正常的(Idempotent操做不須要用戶參與,不然會得出矛盾的結論) 咱們可能須要透明的重試操做以免客戶看到一個OptimisticLockingFailureException 異常。很明顯,在一個橫切多層的狀況下,這是很是有必要的,所以經過切面來實現是很理想的。
由於想要重試操做,咱們須要使用環繞通知,這樣就能夠屢次調用proceed()方法。 下面是簡單的切面實現(只是一個schema支持的普通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 這個環繞通知方法中發生。 咱們首先會嘗試處理,若是獲得一個OptimisticLockingFailureException 異常,咱們僅僅重試直到耗盡全部預設的重試次數。
這個類跟咱們在@AspectJ的例子中使用的是相同的,只是沒有使用註解。對應的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>
請注意咱們如今假設全部的bussiness services都是idempotent。若是不是這樣,咱們能夠改寫切面, 經過引入一個Idempotent註解,讓它只調用idempotent:
@Retention(RetentionPolicy.RUNTIME)
public @interface Idempotent {
// marker annotation
}
而且對service操做的實現進行註解。這時若是你只但願改變切面重試idempotent操做, 你只須要改寫切入點表達式,讓其只匹配@Idempotent操做:
<aop:pointcut id="idempotentOperation" expression="execution(* com.xyz.myapp.service.*.*(..)) and @annotation(com.xyz.myapp.service.Idempotent)"/>
當你肯定切面是實現一個給定需求的最佳方法時,你如何選擇是使用Spring AOP仍是AspectJ,以及選擇 Aspect語言(代碼)風格、@AspectJ聲明風格或XML風格?這個決定會受到多個因素的影響,包括應用的需求、 開發工具和小組對AOP的精通程度。
作能起做用的最簡單的事。Spring AOP比徹底使用AspectJ更加簡單, 由於它不須要引入AspectJ的編譯器/織入器到你開發和構建過程當中。 若是你僅僅須要在Spring bean上通知執行操做,那麼Spring AOP是合適的選擇。 若是你須要通知domain對象或其它沒有在Spring容器中管理的任意對象,那麼你須要使用AspectJ。 若是你想通知除了簡單的方法執行以外的鏈接點(如:調用鏈接點、字段get或set的鏈接點等等), 也須要使用AspectJ。
當使用AspectJ時,你能夠選擇使用AspectJ語言(也稱爲「代碼風格」)或@AspectJ註解風格。 很顯然,若是你用的不是Java 5+那麼結論是你只能使用代碼風格。 若是切面在你的設計中扮演一個很大的角色,而且你能在Eclipse中使用AspectJ Development Tools (AJDT), 那麼首選AspectJ語言 :- 由於該語言專門被設計用來編寫切面,因此會更清晰、更簡單。若是你沒有使用 Eclipse,或者在你的應用中只有不多的切面並無做爲一個主要的角色,你或許應該考慮使用@AspectJ風格 並在你的IDE中附加一個普通的Java編輯器,而且在你的構建腳本中增長切面織入(連接)的段落。
若是你選擇使用Spring AOP,那麼你能夠選擇@AspectJ或者XML風格。顯然若是你不是運行 在Java 5上,XML風格是最佳選擇。對於使用Java 5的項目,須要考慮多方面的折衷。
XML風格對現有的Spring用戶來講更加習慣。它可使用在任何Java級別中 (參考鏈接點表達式內部的命名鏈接點,雖然它也須要Java 5+) 而且經過純粹的POJO來支持。當使用AOP做爲工具來配置企業服務時XML會是一個很好的選擇。 (一個好的例子是當你認爲鏈接點表達式是你的配置中的一部分時,你可能想單獨更改它) 對於XML風格,從你的配置中能夠清晰的代表在系統中存在那些切面。
XML風格有兩個缺點。第一是它不能徹底將需求實現的地方封裝到一個位置。 DRY原則中說系統中的每一項知識都必須具備單1、無歧義、權威的表示。 當使用XML風格時,如何實現一個需求的知識被分割到支撐類的聲明中以及XML配置文件中。 當使用@AspectJ風格時就只有一個單獨的模塊 -切面- 信息被封裝了起來。 第二是XML風格同@AspectJ風格所能表達的內容相比有更多的限制:僅僅支持"singleton"切面實例模型, 而且不能在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+ *(..))"/>
可是不能經過組合這些來定義accountPropertyAccess鏈接點
@AspectJ風格支持其它的實例模型以及更豐富的鏈接點組合。它具備將切面保持爲一個模塊單元的優勢。 還有一個優勢就是@AspectJ切面能被Spring AOP和AspectJ二者都理解 - 因此若是稍後你認爲你須要AspectJ的能力去實現附加的需求,那麼你很是容易遷移到基於AspectJ的途徑。 總而言之,咱們更喜歡@AspectJ風格只要你有切面去作超出簡單的「配置」企業服務以外的事情。
咱們徹底能夠混合使用如下幾種風格的切面定義:使用自動代理的@AspectJ風格的切面, schema-defined <aop:aspect>的切面,和用 <aop:advisor>聲明的advisor,甚至是使用Spring 1.2風格的代理和攔截器。 因爲以上幾種風格的切面定義的都使用了相同的底層機制,所以能夠很好的共存。
Spring AOP部分使用JDK動態代理或者CGLIB來爲目標對象建立代理。(建議優先使用JDK的動態代理)
若是被代理的目標對象實現了至少一個接口,則會使用JDK動態代理。全部該目標類型實現的接口都將被代理。 若該目標對象沒有實現任何接口,則建立一個CGLIB代理。
若是你但願強制使用CGLIB代理,(例如:但願代理目標對象的全部方法,而不僅是實現自接口的方法) 那也能夠。可是須要考慮如下問題:
沒法通知(advise)Final方法,由於他們不能被覆寫。
你須要將CGLIB 2二進制發行包放在classpath下面,與之相較JDK自己就提供了動態代理。 當須要CGLIB而在classpath下又沒有找到CGLIB類庫的話,Spring會自動提醒。
代理對象的構造器會被調用兩次。這是很天然的結果由於在CGLIB代理模式下每個代理對象都會 產生一個子類。每個代理實例會生成兩個對象:實際代理對象和它的一個實現了通知的子類實例 而是用JDK代理時不會出現這樣的行爲。一般狀況下,調用代理類型的構造器兩次並非問題, 由於除了會發生指派外沒有任何真正的邏輯被實現。
強制使用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"/>
![]() |
Note |
---|---|
多個<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代理應用於此三者之上。 |
Spring AOP是基於代理機制的。實際上在你編寫本身的切面或者 使用任何由Spring框架提供的基於Spring AOP切面以前,深入領會這一句的意思是很是重要的。
考慮以下場景,當你拿到一個無代理的、無任何特殊之處的POJO對象引用時,如如下代碼段所示
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() 將對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.adddInterface(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框架。
除了在配置文件中使用<aop:config>或者<aop:aspectj-autoproxy>來聲明切面。 一樣能夠經過編程方式來建立代理來通知目標對象。關於Spring AOP API的詳細介紹, 請參看下一章。這裏咱們重點介紹自動建立代理。
類org.springframework.aop.aspectj.annotation.AspectJProxyFactory能夠爲一個或多個 @AspectJ切面通知的目標對象建立一個代理。該類的基本用法很是簡單,示例以下。請參閱Javadoc獲取更詳細的信息。
// 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();
到目前爲止本章討論的一直是純Spring AOP。在這一節裏面咱們將介紹如何使用AspectJ compiler/weaver 來代替Spring AOP或者做爲它的補充,由於有些時候Spring AOP單獨提供的功能也許並不能知足你的須要。
Spring提供了一個小巧的AspectJ aspect library,你能夠在程序發行版本中單獨使用 spring-aspects.jar文件,並將其加入到classpath下以使用其中的切面。Section 6.8.1, 「在Spring中使用AspectJ進行domain object的依賴注入」和Section 6.8.2, 「Spring中其餘的AspectJ切面」 討論了該庫以及如何使用該庫。 Section 6.8.3, 「使用Spring IoC來配置AspectJ的切面」討論瞭如何對經過AspectJ compiler織入的AspectJ切面進行依賴注入。 最後Section 6.8.4, 「在Spring應用中使用AspectJ加載時織入(LTW)」介紹了使用AspectJ的Spring應用程序如何進行加載期織入(load-time weaving)。
Spring容器對application context中定義的bean進行實例化和配置。一樣也能夠經過bean factory 來爲一個已經存在且已經定義爲spring bean的對象應用所包含的配置信息。 spring-aspects.jar中包含了一個annotation-driven的切面, 提供了能爲任何對象進行依賴注入的能力。這樣的支持旨在爲 脫離容器管理而建立的對象進行依賴注入。領域對象常常處於這樣的情形: 它們多是經過new操做符建立的對象,也多是由ORM工具查詢數據庫所返回的結果。
@Configurable註解標記了一個類能夠經過Spring-driven方式來配置。 在最簡單的狀況下,咱們只把它看成標記註解:
package com.xyz.myapp.domain;
import org.springframework.beans.factory.annotation.Configurable;
@Configurable
public class Account {
// ...
}
當只是簡單地做爲一個標記接口來使用的時候,Spring將採用和該已註解的類型 (好比Account類)全名(com.xyz.myapp.domain.Account) 一致的bean原型定義來配置一個新實例。因爲一個bean默認的名字就是它的全名, 因此一個比較方便的辦法就是省略定義中的id屬性:
<bean class="com.xyz.myapp.domain.Account" scope="prototype"> <property name="fundsTransferService" ref="fundsTransferService"/> </bean>
若是你但願明確的指定bean原型定義的名字,你能夠在註解中直接定義:
package com.xyz.myapp.domain;
import org.springframework.beans.factory.annotation.Configurable;
@Configurable("account")
public class Account {
// ...
}
Spring會查找名字爲"account"的bean定義,並使用它做定義來配置一個新的 Account實例。
你也可使用自動裝配來避免手工指定原型定義的名字。只要設置@Configurable 註解中的autowire屬性就可讓Spring進行自動裝配: 指定@Configurable(autowire=Autowire.BY_TYPE)或者 @Configurable(autowire=Autowire.BY_NAME可讓自動裝配分別按照類型或名字進行。 做爲另一種選擇,在Spring2.5中最好是在域或方法級使用@Autowired和 @Resource爲你的@Configurable beans指定 明確的、註解驅動的依賴注入。(詳情請參看Section 3.11, 「基於註解(Annotation-based)的配置」)
最後,你能夠經過使用dependencyCheck 屬性,讓Spring對新建立和配置的對象的對象引用進行 依賴檢查(例如:@Configurable(autowire=Autowire.BY_NAME,dependencyCheck=true))。 若是這個屬性設置爲true,Spring會在配置結束後校驗(除了primitives和collections類型) 全部的屬性是否都被設置。
僅僅使用註解並無作任何事情。可是spring-aspects.jar 中的AnnotationBeanConfigurerAspect會在註解存在時起做用。實質上切面指明: 「在初始化一個由@Configurable 註解的新對象時, Spring按照註解中的屬性來配置這個新建立的對象」。這種狀況下,initialization 指新初始化的(好比用new初始化)的對象以及能進行反序列化的 Serializable對象(例如經過 readResolve()方法)。
![]() |
Note |
---|---|
在上一段中一個關鍵的階段就是「inessence」。多數狀況下,「 當從一個新對象初始化返回以後」的精確語義很不錯...這種語境下, 「初始化以後」的意思是依賴將在對象被構造以後注入 - 這意味着在類的構造器塊中依賴將不可用。若是你但願它能在構造器代碼塊執行 以前被注入,並從而在構造器中使用它, 那麼你須要在@Configurable接口聲明上作相似的定義: @Configurable(preConstruction=true) 你能夠在 AspectJ Programming Guide一書的附錄中 找到更多有關在AspectJ中各類切面類型的語義信息。 |
要實現上述的操做,已註解的類型必須由AspectJ weaver來織入 - 你可使用一個構建時的ant/maven任務來完成 (參見AspectJ Development Environment Guide)或者使用加載時織入(參見 Section 6.8.4, 「在Spring應用中使用AspectJ加載時織入(LTW)」)。 類AnnotationBeanConfigurerAspect自己也須要Spring來配置(得到bean factory的引用,使用bean factory配置新的對象)。爲此Spring的 context命名空間 定義了一個很是方便的標籤。只要簡單的在application context配置中包含下面的內容。
<context:spring-configured/>
若是你使用DTD代替Schema,對應的定義以下:
<bean class="org.springframework.beans.factory.aspectj.AnnotationBeanConfigurerAspect" factory-method="aspectOf"/>
在切面配置完成以前建立的@Configurable 對象實例會致使在log中留下一個warning,而且任何對於該對象的配置都不會生效。 舉一個例子,一個Spring管理配置的bean在被Spring初始化的時候建立了一個domain object。 對於這樣的狀況,你須要定義bean屬性中的"depends-on"屬性來手動指定該bean依賴於configuration切面。
<bean id="myService"
class="com.xzy.myapp.service.MyService"
depends-on="org.springframework.beans.factory.aspectj.AnnotationBeanConfigurerAspect">
<!-- ... -->
</bean>
提供@Configurable支持的一個目的就是使得domain object的單元測試 能夠獨立進行,不須要經過硬編碼查找各類倚賴關係。若是@Configurable 類型沒有經過AspectJ織入,則在單元測試過程當中註解不會起到任何做用, 測試中你能夠簡單的爲對象的mock或者stub屬性賦值,而且和正常狀況同樣去使用該對象。 若是@Configurable類型經過AspectJ織入, 咱們依然能夠脫離容器進行單元測試,不過每次建立一個新的@Configurable 對象時都會看到一個warning,標示該對象沒有被Spring配置。
AnnotationBeanConfigurerAspect經過一個AspectJ singleton切面來實現對 @Configurable的支持。一個singleton切面的做用域和一個 靜態變量的做用域是同樣的,那就是說,對於每個classloader有一個切面來定義類型。 這就意味着若是你在一個classloader層次結構中定義了多個application context的時候就須要考慮 在哪裏定義<aop:spring-configured/> bean和在哪一個classpath下 放置spring-aspects.jar。
考慮一下典型的Spring web項目,通常都是由一個父application context定義大部分business service和 所須要的其餘資源,而後每個servlet擁有一個子application context定義。全部這些context共存於 同一個classloader體系下,所以AnnotationBeanConfigurerAspect僅保持 一個對context的引用。在這樣的狀況下,咱們推薦在父application context中定義 <aop:spring-configured/> bean:這裏所定義的service多是 你但願注入domain object的。這樣作的結果是你不能爲子application context中 使用@Configurable的domain object配置bean引用(可能你也根本就不但願那麼作!)。
當在一個容器中部署多個web-app的時候,請確保每個web-application使用本身的classloader 來加載spring-aspects.jar中的類(例如將spring-aspects.jar放在WEB-INF/lib目錄下)。 若是spring-aspects.jar被放在了容器的classpath下(所以也被父classloader加載),則全部的 web application將共享一個aspect實例,這可能並非你所想要的。
除了@Configurable切面, spring-aspects.jar包含了一個AspectJ切面能夠用來爲 那些使用了@Transactional註解的類型和方法驅動Spring事務管理。 提供這個的主要目的是有些用戶但願脫離Spring容器使用Spring的事務管理。
解析@Transactional註解的切面是 AnnotationTransactionAspect。當使用這個切面時, 你必須註解這個實現類(和/或這個類中的方法),而不是 這個類實現的接口(若是有)。AspectJ容許在接口上註解的Java規則 不被繼承。
類之上的一個@Transactional註解爲該類中任何 public操做的執行指定了默認的事務語義。
類內部方法上的一個@Transactional註解會覆蓋類註解(若是存在) 所給定的默認的事務語義。具備public、protected和default修飾符的方法均可以被註解。 直接註解protected和default方法是讓這個操做的執行得到事務劃分的惟一途徑。
對於AspectJ程序員,但願使用Spring管理配置和事務管理支持,不過他們不想(或者不能)使用註解, spring-aspects.jar也包含了一些抽象 切面供你繼承來提供你本身的切入點定義。參見AbstractBeanConfigurerAspect 和AbstractTransactionAspect的Javadoc獲取更多信息。 做爲一個例子,下面的代碼片段展現瞭如何編寫一個切面,而後經過和類全名匹配的bean原型定義來 配置domian object中定義的全部實例:
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 application中使用AspectJ的時候,很天然的會想到用Spring來管理這些切面。 AspectJ runtime自身負責切面的建立,這意味着經過Spring來管理AspectJ 建立切面依賴於切面所使用的AspectJ instantiation model(per-clause)。
大多數AspectJ切面都是singleton切面。管理這些切面很是容易, 和一般同樣建立一個bean定義引用該切面類型就能夠了,而且在bean定義中包含 'factory-method="aspectOf"'這個屬性。 這確保Spring從AspectJ獲取切面實例而不是嘗試本身去建立該實例。示例以下:
<bean id="profiler" class="com.xyz.profiler.Profiler"
factory-method="aspectOf">
<property name="profilingStrategy" ref="jamonProfilingStrategy"/>
</bean>
non-singleton切面的配置稍難一點,然而它能夠經過定義一個bean原型定義而且使用 spring-aspects.jar中的@Configurable支持, 當切面實例由AspectJ runtime建立後進行配置。
若是你但願一些@AspectJ切面使用AspectJ來織入(例如使用load-time織入domain object) 而另外一些@AspectJ切面使用Spring AOP,而且這些切面都由Spring來管理,那你就須要告訴Spring AOP @AspectJ自動代理支持那些切面須要被自動代理。你能夠經過在 <aop:aspectj-autoproxy>聲明中使用一個或多個 <include/>元素。每一個元素指定了一種命名格式, 只有bean命名至少符合其中一種狀況下才會使用Spring AOP自動代理配置:
<aop:aspectj-autoproxy> <aop:include name="thisBean"/> <aop:include name="thatBean"/> </aop:aspectj-autoproxy>
![]() |
Note |
---|---|
不要被<aop:aspectj-autoproxy/>元素的名字所誤導: 用它會致使Spring AOP 代理的建立。在這中只是使用@AspectJ 類型的切面聲明,但並不會涉及AspectJ運行時。 |
加載時織入(Load-time weaving(LTW))指的是在虛擬機載入字節碼文件時動態織入AspectJ切面。 本 節關注於在Spring Framework中特的定context下配置和使用LTW:並無LTW的介紹。 關於LTW和僅使用AspectJ配置LTW的詳細信息(根本不涉及Spring),請查看 LTW section of the AspectJ Development Environment Guide。
Spring框架的值添加爲AspectJ LTW在動態織入過程當中提供了更細粒度的控制。使用Java(5+)的代理 能使用一個叫‘Vanilla’的AspectJ LTW,這須要在啓動JVM的時候將某個VM參數設置爲開。 這種JVM範圍的設置在一些狀況下或許不錯,但一般狀況下顯得有些粗顆粒。而用Spring的LTW能讓你在 per-ClassLoader的基礎上打開LTW, 這顯然更加細粒度而且對「單JVM多應用」的環境更具意義(例如在一個典型應用服務器環境中同樣)。
另外,在某些環境下,這能讓你使用LTW而 不對應用服務器的啓動腳本作任何改動,否則則須要添加 -javaagent:path/to/aspectjweaver.jar或者(如下將會說起的)-javaagent:path/to/spring-agent.jar。 開發人員只需簡單修改應用上下文的一個或幾個文件就能使用LTW,而不需依靠那些管理着部署配置 好比啓動腳本的系統管理員。
通過以上講解以後,先讓咱們來過一遍一個使用Spring的AspectJ LTW的快速示例,接着是一個 有對元素詳細講解的示例。若是想要一個完整的示例,請參看Petclinic(寵物診所)的應用實例。
假設你是一個應用開人員,被指派診斷一個系統的若干性能問題。與其拿出性能分析工具, 咱們不如開啓一個簡單的分析切面,使咱們能很快地獲得一些性能指標,這樣咱們就能立刻 針對特定區域使用一些較細粒度的分析工具。
這就是一個分析切面。沒什麼特別的,只是一個快餐式的基於時間的模擬分析器, 使用類@AspectJ風格的切面聲明。
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 weaver 咱們要把ProfilingAspect織入到類中。這個文件慣例,即在Java classpath中 出現一個文件稱做「META-INF/aop.xml」是標準的AspectJ。
<!DOCTYPE aspectj PUBLIC "-//AspectJ//DTD//EN" "http://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 http://www.springframework.org/schema/beans/spring-beans-2.5.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-2.5.xsd"> <!-- a service object; we will be profiling it's methods --> <bean id="entitlementCalculationService" class="foo.StubEntitlementCalculationService"/> <!-- this switches on the load-time weaving --> <context:load-time-weaver/> </beans>
如今萬事俱備 - 切面,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的 per-ClassLoader來啓動LTW,並且的確如此。不過,對此例來講, 咱們將使用Java代理(由Spring提供)來啓動LTW。這個就是用以運行上面Main 類的命令行語句:
java -javaagent:C:/projects/foo/lib/global/spring-agent.jar foo.Main
-javaagent是一個Java 5+標記,用來指定和激活 使JVM上的程序運行的代理。Spring框架裝載了一個InstrumentationSavingAgent 代理,在上面的例子中被做爲了-javaagent參數的值打包在 spring-agent.jar中。
Main程序運行的輸出以下所示。(我已經在 calculateEntitlement()的實現中插入了Thread.sleep(..) 語句,以避免讓模擬分析器獲取0毫秒 - 這裏的01234毫秒並不是是AOP引入的系統開銷。)
Calculating entitlement StopWatch 'ProfilingAspect': running time (millis) = 1234 ------ ----- ---------------------------- ms % Task name ------ ----- ---------------------------- 01234 100% calculateEntitlement
由於這個LTW使用成熟的AspectJ,咱們並不侷限於通知Spring beans的方法;接下來這個稍有變化的 Main程序將生成一樣的結果。
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支持都已介紹完了, 此節餘下內容將對使用這些配置和用法背後的理由做詳細解釋。
![]() |
Note |
---|---|
類ProfilingAspect在此例中雖然基本可是頗爲有用。這是一個很好的開發時切面的例子,開發者能夠在開發過程當中使用它(廢話), 而後也能從已部署到UAT或者生產環境的應用中輕易的脫離。 |
你在LTW中使用的切面必須是AspectJ切面。你可使用AspectJ語言或者類@AspectJ風格來編寫你的切面。 後一種方式固然只能在Java 5+中使用,但它說明了你的切面能夠同時對AspectJ和Spring AOP切面有效。 此外,編譯後的切面類須要被註冊到classpath下。
AspectJ LTW的基礎設施是用一個或多個位於Java classpath上的(能夠是直接的文件形式, 也能夠是更典型的jar包形式)META-INF/aop.xml文件配置起來的。
有關文件的結構和內容都在AspectJ的參考文檔中有詳細介紹,有興趣的讀者 請參考這些資源。(很慶幸這一節比較簡短,但aop.xml文件 是100% AspectJ的 - 沒有任何使用Spring特定的信息或語義,所以我也沒有什麼可貢獻的。 與其重寫這些已由AspectJ開發者提供的使人滿意的章節,我不如領你到這裏。)
你至少須要如下類庫來讓Spring框架支持AspectJ LTW:
spring.jar(2.5或更高版本)
aspectjrt.jar (1.5或更高版本)
aspectjweaver.jar (1.5或更高版本)
若是你正在使用 由Spring提供的代理來激活檢測(instrumentation)功能,你會須要:
spring-agent.jar
Spring LTW功能的關鍵組件是LoadTimeWeaver接口 (在org.springframework.instrument.classloading包中), 以及Spring分發包中大量的實現。LoadTimeWeaver的實現負責 在運行時把一個或多個java.lang.instrument.ClassFileTransformers類添加到 ClassLoader中,這能產生各類各樣有趣的應用,LTW切面剛好即是其中之一。
![]() |
Tip |
---|---|
若是你對運行時類文件變換的思想還不熟悉,推薦你在繼續以前閱讀 java.lang.instrument包的Javadoc API文檔。 這其實並不難-反而有些惱人-由於有用的文件並很少...關鍵的接口和類都將會在此節呈現給你。 |
用XML爲ApplicationContext配置一個 LoadTimeWeaver簡單得只須要添加一行。 (請注意幾乎確定你須要使用ApplicationContext做爲你的 Spring容器 - 通常來講只有BeanFactory是不夠的, 由於LTW功能須要用到BeanFactoryPostProcessors。)
當要使用Spring框架的LTW功能時,你須要配置一個LoadTimeWeaver, 通常能夠用<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 http://www.springframework.org/schema/beans/spring-beans-2.5.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-2.5.xsd"> <context:load-time-weaver/> </beans>
上面<context:load-time-weaver/> bean的定義會自動爲你定義和註冊若干 特定LTW的基礎設施beans,好比一個LoadTimeWeaver 和一個AspectJWeavingEnabler。請注意 <context:load-time-weaver/>是怎樣在context 命名空間下被定義的;還要注意被引用的XML Schema文件只在Spring 2.5或更高版本中才可用。
上面的配置爲你定義並註冊了一個默認的LoadTimeWeaver bean。 默認的LoadTimeWeaver是一個 DefaultContextLoadTimeWeaver類,它更傾向於去裝飾一個能自動檢測的LoadTimeWeaver類:LoadTimeWeaver 的確切類型會根據你的運行時環境「自動檢測」出來(概述以下表)。
Table 6.1. DefaultContextLoadTimeWeaver LoadTimeWeaversDefaultContextLoadTimeWeaver類和LoadTimeWeavers接口
運行時環境 | LoadTimeWeaver的接口實現 |
---|---|
WebLogicLoadTimeWeaver |
|
OC4JLoadTimeWeaver |
|
GlassFish環境下 |
GlassFishLoadTimeWeaver |
以SpringInstrumentationSavingAgent 啓動的JVM中(java -javaagent:path/to/spring-agent.jar) |
InstrumentationLoadTimeWeaver |
不過,咱們更但願這些類加載器能遵循共同的規範 (例如適用TomcatInstrumentableClassLoader和Resin) |
ReflectiveLoadTimeWeaver |
請注意當使用DefaultContextLoadTimeWeaver時只有 LoadTimeWeavers實現類能進行自動檢測: 固然,你也能夠經過指定將類的徹底限定名做爲<context:load-time-weaver/> 元素中weaver-class屬性的值 來指定究竟想使用哪一個LoadTimeWeaver的實現。以下例:
<?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 http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-2.5.xsd">
<context:load-time-weaver
weaver-class="org.springframework.instrument.classloading.ReflectiveLoadTimeWeaver"/>
</beans>
在<context:load-time-weaver/>元素上定義和註冊的 LoadTimeWeaver接口能夠在Spring容器中以 loadTimeWeaver名字找到。 記住LoadTimeWeaver接口只是做爲Spring LTW基礎設施的一個機制 用來添加一個或多個ClassFileTransformers的。 ClassFileTransformer類實際是利用ClassPreProcessorAgentAdapter類(包含在 org.aspectj.weaver.loadtime中)來進行LTW的。 有關ClassPreProcessorAgentAdapter的細節請參見 類級別的javadoc,織入實際怎樣生效的具體內容已經超出本節討論範圍。
讓咱們來討論<context:load-time-weaver/>的最後一個屬性: aspectj-weaving。 這是一個簡單的LTW開關,就這麼簡單。 它能夠接受以下所述的三種值,若是不顯示設置此屬性則其默認值爲autodetect
這最後一節包括全部你在諸如應用服務器和web容器中使用Spring的LTW功能時須要的額外設置和配置。
你可能在各類Java應用中經過使用由Spring提供的檢測代理啓用Spring的LTW功能 (獨立應用或者基於應用服務器的應用)。這樣的話,能夠經過指定 -javaagent:path/to/spring-agent.jar選項來啓動虛擬機。 請注意這須要修改虛擬機的啓動腳本,但在某些應用服務器環境下是禁止這麼作的 (這取決於你的操做策略)。
對於部署在Apache Tomcat 5.0或更高版本上的web應用,Spring將一個 TomcatInstrumentableClassLoader註冊成爲web應用的類加載器。 必須的Tomcat設置以下所示,你能夠把它放在Tomcat WAR包根目錄下的核心文件 server.xml中或放到應用特定的META-INF/context.xml文件中。 Spring的spring-tomcat-weaver.jar須要被包含到Tomcat 的common lib路徑下以確保設置生效。
<Context path="/myWebApp" docBase="/my/webApp/location"> <Loader loaderClass="org.springframework.instrument.classloading.tomcat.TomcatInstrumentableClassLoader" useSystemClassLoaderAsParent="false"/> </Context>
注意:當使用LTW時,咱們通常推薦使用Tomcat 5.5.20或更高版本。 先前的版本對定製的ClassLoader設置會產生問題。
另外,請考慮使用在Tomcat啓動腳本中(見上面)指定由Spring提供的通用虛擬機代理。 這樣才能使檢測功能在全部已部署的web應用中可用,不管其上運行的是哪一種類加載器。
有關更多基於Tomcat織入設置的詳細討論,請參考討論各類不一樣Tomcat版本內容的 Section 12.6.1.3.1, 「Tomcat(5.0以上)加載時的織入配置」一節。雖然本節主要關注於 JPA persistence提供者的設置,但也談到了Tomcat各類特定設置適用於通常加載時織入的狀況。
BEA WebLogic(版本10或更高),Oracle的JavaEE容器(OC4J 10.1.3.1或更高)以及 Resin(版本3.1或更高)提供具備本地檢測能力的類加載器。 Srping的原生LTW利用這些類加載器來激活AspectJ織入。你能夠經過簡單地激活以前提到的 context:load-time-weaver來啓動LTW功能。具體來講,即你 不須要經過修改啓動腳原本添加 -javaagent:path/to/spring-agent.jar。
GlassFish一樣也提供了檢測能力的類加載器,不過只能在它的EAR環境下使用。 對於GlassFish的web應用,可使用跟上面tomcat相同的設置。
更多關於AspectJ的信息能夠查看 AspectJ website。
Eclipse AspectJ Adrian Colyer等人編著(Addison-Wesley, 2005) 一書提供了針對AspectJ語言全面的介紹和參考。
AspectJ in Action Ramnivas Laddad編著(Manning, 2003)是一本很是出色介紹AOP的書籍; 全書着重介紹了AspectJ,但也對一些通用的AOP場景進行了比較深刻的研究。