面向切面編程(AOP)提供另外一種角度來(lái)思考程序結構,通過(guò)這種方式彌補了面向對象編程(OOP)的不足。 除了類(lèi)(classes)以外,AOP提供了 切面。切面對關(guān)注點(diǎn)進(jìn)行模塊化,例如橫切多個(gè)類(lèi)型和對象的事務(wù)管理。 (這些關(guān)注點(diǎn)術(shù)語(yǔ)通常稱(chēng)作 橫切(crosscutting) 關(guān)注點(diǎn)。)
Spring的一個(gè)關(guān)鍵的組件就是 AOP框架。 盡管如此,Spring IoC容器并不依賴(lài)于A(yíng)OP,這意味著(zhù)你可以自由選擇是否使用AOP,AOP提供強大的中間件解決方案,這使得Spring IoC容器更加完善。
Spring中所使用的AOP:
提供聲明式企業(yè)服務(wù),特別是為了替代EJB聲明式服務(wù)。 最重要的服務(wù)是 聲明性事務(wù)管理(declarative transaction management) , 這個(gè)服務(wù)建立在Spring的抽象事務(wù)管理(transaction abstraction)之上。
允許用戶(hù)實(shí)現自定義的切面,用AOP來(lái)完善OOP的使用。
這樣你可以把Spring AOP看作是對Spring的一種增強,它使得Spring可以不需要EJB就能提供聲明式事務(wù)管理; 或者也可以使用Spring AOP框架的全部功能來(lái)實(shí)現自定義的切面。
本章首先 介紹了AOP的概念,無(wú)論你打算采用哪種風(fēng)格的切面聲明,這個(gè)部分都值得你一讀。 本章剩下的部分將著(zhù)重于Spring 2.0對AOP的支持; 下一章 提供了關(guān)于Spring 1.2風(fēng)格的AOP概述,也許你已經(jīng)在其他書(shū)本,文章以及已有的應用程序中碰到過(guò)這種AOP風(fēng)格。
如果你只打算使用通用的聲明式服務(wù)或者預先打包的聲明式中間件服務(wù),例如緩沖池(pooling), 那么你不必直接使用Spring AOP,而本章的大部分內容也可以直接跳過(guò)。首先讓我們從定義一些重要的AOP概念開(kāi)始。這些術(shù)語(yǔ)不是Spring特有的。 不幸的是,AOP術(shù)語(yǔ)并不是特別的直觀(guān);如果Spring使用自己的術(shù)語(yǔ),將會(huì )變得更加令人困惑。
切面(Aspect): 一個(gè)關(guān)注點(diǎn)的模塊化,這個(gè)關(guān)注點(diǎn)可能會(huì )橫切多個(gè)對象。事務(wù)管理是J2EE應用中一個(gè)關(guān)于橫切關(guān)注點(diǎn)的很好的例子。 在Spring AOP中,切面可以使用通用類(lèi)(基于模式的風(fēng)格) 或者在普通類(lèi)中以 @Aspect 注解(@AspectJ風(fēng)格)來(lái)實(shí)現。
連接點(diǎn)(Joinpoint): 在程序執行過(guò)程中某個(gè)特定的點(diǎn),比如某方法調用的時(shí)候或者處理異常的時(shí)候。 在Spring AOP中,一個(gè)連接點(diǎn) 總是 代表一個(gè)方法的執行。 通過(guò)聲明一個(gè)org.aspectj.lang.JoinPoint類(lèi)型的參數可以使通知(Advice)的主體部分獲得連接點(diǎn)信息。
通知(Advice): 在切面的某個(gè)特定的連接點(diǎn)(Joinpoint)上執行的動(dòng)作。通知有各種類(lèi)型,其中包括“around”、“before”和“after”等通知。 通知的類(lèi)型將在后面部分進(jìn)行討論。許多AOP框架,包括Spring,都是以攔截器做通知模型, 并維護一個(gè)以連接點(diǎn)為中心的攔截器鏈。
切入點(diǎn)(Pointcut): 匹配連接點(diǎn)(Joinpoint)的斷言。通知和一個(gè)切入點(diǎn)表達式關(guān)聯(lián),并在滿(mǎn)足這個(gè)切入點(diǎn)的連接點(diǎn)上運行(例如,當執行某個(gè)特定名稱(chēng)的方法時(shí))。 切入點(diǎn)表達式如何和連接點(diǎn)匹配是AOP的核心:Spring缺省使用AspectJ切入點(diǎn)語(yǔ)法。
引入(Introduction): (也被稱(chēng)為內部類(lèi)型聲明(inter-type declaration))。聲明額外的方法或者某個(gè)類(lèi)型的字段。 Spring允許引入新的接口(以及一個(gè)對應的實(shí)現)到任何被代理的對象。 例如,你可以使用一個(gè)引入來(lái)使bean實(shí)現 IsModified 接口,以便簡(jiǎn)化緩存機制。
目標對象(Target Object): 被一個(gè)或者多個(gè)切面(aspect)所通知(advise)的對象。也有人把它叫做 被通知(advised) 對象。 既然Spring AOP是通過(guò)運行時(shí)代理實(shí)現的,這個(gè)對象永遠是一個(gè) 被代理(proxied) 對象。
AOP代理(AOP Proxy): AOP框架創(chuàng )建的對象,用來(lái)實(shí)現切面契約(aspect contract)(包括通知方法執行等功能)。 在Spring中,AOP代理可以是JDK動(dòng)態(tài)代理或者CGLIB代理。 注意:Spring 2.0最新引入的基于模式(schema-based)風(fēng)格和@AspectJ注解風(fēng)格的切面聲明,對于使用這些風(fēng)格的用戶(hù)來(lái)說(shuō),代理的創(chuàng )建是透明的。
織入(Weaving): 把切面(aspect)連接到其它的應用程序類(lèi)型或者對象上,并創(chuàng )建一個(gè)被通知(advised)的對象。 這些可以在編譯時(shí)(例如使用AspectJ編譯器),類(lèi)加載時(shí)和運行時(shí)完成。 Spring和其他純Java AOP框架一樣,在運行時(shí)完成織入。
通知的類(lèi)型:
前置通知(Before advice): 在某連接點(diǎn)(join point)之前執行的通知,但這個(gè)通知不能阻止連接點(diǎn)前的執行(除非它拋出一個(gè)異常)。
返回后通知(After returning advice): 在某連接點(diǎn)(join point)正常完成后執行的通知:例如,一個(gè)方法沒(méi)有拋出任何異常,正常返回。
拋出異常后通知(After throwing advice): 在方法拋出異常退出時(shí)執行的通知。
后通知(After (finally) advice): 當某連接點(diǎn)退出的時(shí)候執行的通知(不論是正常返回還是異常退出)。
環(huán)繞通知(Around Advice): 包圍一個(gè)連接點(diǎn)(join point)的通知,如方法調用。這是最強大的一種通知類(lèi)型。 環(huán)繞通知可以在方法調用前后完成自定義的行為。它也會(huì )選擇是否繼續執行連接點(diǎn)或直接返回它們自己的返回值或拋出異常來(lái)結束執行。
環(huán)繞通知是最常用的一種通知類(lèi)型。大部分基于攔截的AOP框架,例如Nanning和JBoss4,都只提供環(huán)繞通知。
跟AspectJ一樣,Spring提供所有類(lèi)型的通知,我們推薦你使用盡量簡(jiǎn)單的通知類(lèi)型來(lái)實(shí)現需要的功能。 例如,如果你只是需要用一個(gè)方法的返回值來(lái)更新緩存,雖然使用環(huán)繞通知也能完成同樣的事情, 但是你最好使用After returning通知而不是環(huán)繞通知。 用最合適的通知類(lèi)型可以使得編程模型變得簡(jiǎn)單,并且能夠避免很多潛在的錯誤。 比如,你不需要調用 JoinPoint(用于A(yíng)round Advice)的 proceed() 方法,就不會(huì )有調用的問(wèn)題。
在Spring 2.0中,所有的通知參數都是靜態(tài)類(lèi)型,因此你可以使用合適的類(lèi)型(例如一個(gè)方法執行后的返回值類(lèi)型)作為通知的參數而不是使用一個(gè)對象數組。
切入點(diǎn)(pointcut)和連接點(diǎn)(join point)匹配的概念是AOP的關(guān)鍵,這使得AOP不同于其它僅僅提供攔截功能的舊技術(shù)。 切入點(diǎn)使得定位通知(advice)可獨立于OO層次。 例如,一個(gè)提供聲明式事務(wù)管理的around通知可以被應用到一組橫跨多個(gè)對象中的方法上(例如服務(wù)層的所有業(yè)務(wù)操作)。
Spring AOP用純Java實(shí)現。它不需要專(zhuān)門(mén)的編譯過(guò)程。Spring AOP不需要控制類(lèi)裝載器層次,因此它適用于J2EE web容器或應用服務(wù)器。
Spring目前僅支持使用方法調用作為連接點(diǎn)(join point)(在Spring bean上通知方法的執行)。 雖然可以在不影響到Spring AOP核心API的情況下加入對成員變量攔截器支持,但Spring并沒(méi)有實(shí)現成員變量攔截器。 如果你需要把對成員變量的訪(fǎng)問(wèn)和更新也作為通知的連接點(diǎn),可以考慮其它語(yǔ)法的Java語(yǔ)言,例如AspectJ。
Spring實(shí)現AOP的方法跟其他的框架不同。Spring并不是要嘗試提供最完整的AOP實(shí)現(盡管Spring AOP有這個(gè)能力), 相反的,它其實(shí)側重于提供一種AOP實(shí)現和Spring IoC容器的整合,用于幫助解決在企業(yè)級開(kāi)發(fā)中的常見(jiàn)問(wèn)題。
因此,Spring AOP通常都和Spring IoC容器一起使用。 Aspect使用普通的bean定義語(yǔ)法(盡管Spring提供了強大的“自動(dòng)代理(autoproxying)”功能): 與其他AOP實(shí)現相比這是一個(gè)顯著(zhù)的區別。有些事使用Spring AOP是無(wú)法輕松或者高效的完成的,比如說(shuō)通知一個(gè)細粒度的對象。 這種時(shí)候,使用AspectJ是最好的選擇。不過(guò)經(jīng)驗告訴我們: 于大多數在J2EE應用中遇到的問(wèn)題,只要適合AOP來(lái)解決的,Spring AOP都沒(méi)有問(wèn)題,Spring AOP提供了一個(gè)非常好的解決方案。
Spring AOP從來(lái)沒(méi)有打算通過(guò)提供一種全面的AOP解決方案來(lái)取代AspectJ。 我們相信無(wú)論是基于代理(proxy-based )的框架比如說(shuō)Spring亦或是full-blown的框架比如說(shuō)是AspectJ都是很有價(jià)值的,他們之間的關(guān)系應該是互補而不是競爭的關(guān)系。 Spring 2.0可以無(wú)縫的整合Spring AOP,IoC 和AspectJ,使得所有的AOP應用完全融入基于Spring的應用體系。 這樣的集成不會(huì )影響Spring AOP API或者AOP Alliance API;Spring AOP保留了向下兼容性。接下來(lái)的一章會(huì )詳細討論Spring AOP API。
Spring缺省使用J2SE 動(dòng)態(tài)代理(dynamic proxies)來(lái)作為AOP的代理。這樣任何接口都可以被代理。
Spring也支持使用CGLIB代理. 對于需要代理類(lèi)而不是代理接口的時(shí)候CGLIB代理是很有必要的。 如果一個(gè)業(yè)務(wù)對象并沒(méi)有實(shí)現一個(gè)接口,默認就會(huì )使用CGLIB。 作為面向接口編程的最佳實(shí)踐,業(yè)務(wù)對象通常都會(huì )實(shí)現一個(gè)或多個(gè)接口。但也有可能會(huì ) 強制使用CGLIB, 在這種情況(希望不常有)下,你可能需要通知一個(gè)沒(méi)有在接口中聲明的方法,或者需要傳入一個(gè)代理對象給方法作為具體類(lèi)型
在Spring 2.0之后,Spring可能會(huì )提供多種其他類(lèi)型的AOP代理,包括了完整的生成類(lèi)。這不會(huì )影響到編程模型。"@AspectJ"使用了Java 5的注解,可以將切面聲明為普通的Java類(lèi)。 AspectJ 5發(fā)布的 AspectJ project 中引入了這種@AspectJ風(fēng)格。 Spring 2.0 使用了和AspectJ 5一樣的注解,使用了AspectJ 提供的一個(gè)庫來(lái)做切點(diǎn)(pointcut)解析和匹配。 但是,AOP在運行時(shí)仍舊是純的Spring AOP,并不依賴(lài)于A(yíng)spectJ 的編譯器或者織入器(weaver)。
使用AspectJ的編譯器或者織入器(weaver)的話(huà)就可以使用完整的AspectJ 語(yǔ)言,我們將在 Section 6.8, “在Spring應用中使用AspectJ” 中討論這個(gè)問(wèn)題。為了在Spring配置中使用@AspectJ aspects,你必須首先啟用Spring對基于@AspectJ aspects的配置支持,自動(dòng)代理(autoproxying)基于通知是否來(lái)自這些切面。 自動(dòng)代理是指Spring會(huì )判斷一個(gè)bean是否使用了一個(gè)或多個(gè)切面通知,并據此自動(dòng)生成相應的代理以攔截其方法調用,并且確認通知是否如期進(jìn)行。
通過(guò)在你的Spring的配置中引入下列元素來(lái)啟用Spring對@AspectJ的支持:
<aop:aspectj-autoproxy/>
我們假使你正在使用 Appendix A, XML Schema-based configuration 所描述的schema支持。 關(guān)于如何在aop的命名空間中引入這些標簽,請參見(jiàn) Section A.2.6, “The aop schema”
如果你正在使用DTD,你仍舊可以通過(guò)在你的application context中添加如下定義來(lái)啟用@AspectJ支持:
<bean class="org.springframework.aop.aspectj.annotation.AnnotationAwareAspectJAutoProxyCreator" />
你需要在你的應用程序的classpath中引入兩個(gè)AspectJ庫:aspectjweaver.jar 和 aspectjrt.jar。 這些庫可以在A(yíng)spectJ的安裝包(1.5.1或者之后的版本)中的 lib 目錄里找到,或者也可以在Spring依賴(lài)庫的 lib/aspectj 目錄下找到。
在啟用@AspectJ支持的情況下,在application context中定義的任意帶有一個(gè)@Aspect切面(擁有@Aspect注解)的bean都將被Spring自動(dòng)識別并用于配置在Spring AOP。 以下例子展示了為了完成一個(gè)不是非常有用的切面所需要的最小定義:
下面是在application context中的一個(gè)常見(jiàn)的bean定義,這個(gè)bean指向一個(gè)使用了 @Aspect 注解的bean類(lèi):
<bean id="myAspect" class="org.xyz.NotVeryUsefulAspect"><!-- configure properties of aspect here as normal --></bean>下面是 NotVeryUsefulAspect 類(lèi)定義,使用了 org.aspectj.lang.annotation.Aspect 注解。
package org.xyz;import org.aspectj.lang.annotation.Aspect;@Aspectpublic class NotVeryUsefulAspect {}切面(用 @Aspect 注解的類(lèi))和其他類(lèi)一樣有方法和字段定義。他們也可能包括切入點(diǎn),通知和引入(inter-type)聲明。
回想一下,切入點(diǎn)決定了連接點(diǎn)關(guān)注的內容,使得我們可以控制通知什么時(shí)候執行。 Spring AOP 只支持 Spring bean 方法執行連接點(diǎn)。所以你可以把切入點(diǎn)看做是匹配 Spring bean 上方法的執行。 一個(gè)切入點(diǎn)聲明有兩個(gè)部分:一個(gè)包含名字和任意參數的簽名,還有一個(gè)切入點(diǎn)表達式,該表達式?jīng)Q定了我們關(guān)注那個(gè)方法的執行。 在 @AspectJ 注解風(fēng)格的 AOP 中,一個(gè)切入點(diǎn)簽名通過(guò)一個(gè)普通的方法定義來(lái)提供,并且切入點(diǎn)表達式使用 @Pointcut 注解來(lái)表示(作為切入點(diǎn)簽名的方法必須返回 void 類(lèi)型)。
用一個(gè)例子會(huì )幫助我們區分切入點(diǎn)簽名和切入點(diǎn)表達式之間的差別,下面的例子定義了一個(gè)切入點(diǎn)‘a(chǎn)nyOldTransfer‘, 這個(gè)切入點(diǎn)將匹配任何名為 "transfer" 的方法的執行:
@Pointcut("execution(* transfer(..))")// the pointcut expressionprivate void anyOldTransfer() {}// the pointcut signature切入點(diǎn)表達式,也就是 @Pointcut 注解的值,是正規的AspectJ 5切入點(diǎn)表達式。 如果你想要更多了解AspectJ的 切入點(diǎn)語(yǔ)言,請參見(jiàn) AspectJ 編程指南(如果要了解基于Java 5的擴展請參閱 AspectJ 5 開(kāi)發(fā)手冊) 或者其他人寫(xiě)的關(guān)于A(yíng)spectJ的書(shū),例如Colyer et. al.著(zhù)的《Eclipse AspectJ》或者Ramnivas Laddad著(zhù)的《AspectJ in Action》。
Spring AOP 支持在切入點(diǎn)表達式中使用如下的AspectJ切入點(diǎn)指定者:
execution - 匹配方法執行的連接點(diǎn),這是你將會(huì )用到的Spring的最主要的切入點(diǎn)指定者。
within - 限定匹配特定類(lèi)型的連接點(diǎn)(在使用Spring AOP的時(shí)候,在匹配的類(lèi)型中定義的方法的執行)。
this - 限定匹配特定的連接點(diǎn)(使用Spring AOP的時(shí)候方法的執行),其中bean reference(Spring AOP 代理)是指定類(lèi)型的實(shí)例。
target - 限定匹配特定的連接點(diǎn)(使用Spring AOP的時(shí)候方法的執行),其中目標對象(被代理的appolication object)是指定類(lèi)型的實(shí)例。
args - 限定匹配特定的連接點(diǎn)(使用Spring AOP的時(shí)候方法的執行),其中參數是指定類(lèi)型的實(shí)例。
@target - 限定匹配特定的連接點(diǎn)(使用Spring AOP的時(shí)候方法的執行),其中執行的對象的類(lèi)已經(jīng)有指定類(lèi)型的注解。
@args - 限定匹配特定的連接點(diǎn)(使用Spring AOP的時(shí)候方法的執行),其中實(shí)際傳入參數的運行時(shí)類(lèi)型有指定類(lèi)型的注解。
@within - 限定匹配特定的連接點(diǎn),其中連接點(diǎn)所在類(lèi)型已指定注解(在使用Spring AOP的時(shí)候,所執行的方法所在類(lèi)型已指定注解)。
@annotation - 限定匹配特定的連接點(diǎn)(使用Spring AOP的時(shí)候方法的執行),其中連接點(diǎn)的主題有某種給定的注解。
切入點(diǎn)表達式可以使用using ‘&‘, ‘||‘ 和 ‘!‘來(lái)合并.還可以通過(guò)名字來(lái)指向切入點(diǎn)表達式。 以下的例子展示了三種切入點(diǎn)表達式: anyPublicOperation(在一個(gè)方法執行連接點(diǎn)代表了任意public方法的執行時(shí)匹配); inTrading(在一個(gè)代表了在交易模塊中的任意的方法執行時(shí)匹配) 和 tradingOperation(在一個(gè)代表了在交易模塊中的任意的公共方法執行時(shí)匹配)。
@Pointcut("execution(public * *(..))")private void anyPublicOperation() {}@Pointcut("within(com.xyz.someapp.trading..*")private void inTrading() {}@Pointcut("anyPublicOperation() && inTrading()")private void tradingOperation() {}就上所示的,從更小的命名組件來(lái)構建更加復雜的切入點(diǎn)表達式是一種最佳實(shí)踐。 當用名字來(lái)指定切入點(diǎn)時(shí)使用的是常見(jiàn)的Java成員可視性訪(fǎng)問(wèn)規則。 (比如說(shuō),你可以在同一類(lèi)型中訪(fǎng)問(wèn)私有的切入點(diǎn),在繼承關(guān)系中訪(fǎng)問(wèn)受保護的切入點(diǎn),可以在任意地方訪(fǎng)問(wèn)公共切入點(diǎn)。 成員可視性訪(fǎng)問(wèn)規則不影響到切入點(diǎn)的 匹配。
當開(kāi)發(fā)企業(yè)級應用的時(shí)候,你通常會(huì )想要從幾個(gè)切面來(lái)參考模塊化的應用和特定操作的集合。 我們推薦定義一個(gè)“SystemArchitecture”切面來(lái)捕捉常見(jiàn)的切入點(diǎn)表達式。一個(gè)典型的切面可能看起來(lái)像下面這樣:
package com.xyz.someapp;import org.aspectj.lang.annotation.Aspect;import org.aspectj.lang.annotation.Pointcut;@Aspectpublic 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.*/@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() {}}示例中的切入點(diǎn)定義了一個(gè)你可以在任何需要切入點(diǎn)表達式的地方可引用的切面。比如,為了使service層事務(wù)化,你可以寫(xiě)成:
<aop:config><aop:advisorpointcut="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-based AOP support” 中討論 <aop:config> 和 <aop:advisor>標簽。 在 Chapter 9, 事務(wù)管理 中討論事務(wù)標簽。
Spring AOP 用戶(hù)可能會(huì )經(jīng)常使用 execution pointcut designator。執行表達式的格式如下:
execution(modifiers-pattern? ret-type-pattern declaring-type-pattern? name-pattern(param-pattern) throws-pattern?)
除了返回類(lèi)型模式(上面代碼片斷中的ret-type-pattern),名字模式和參數模式以外,所有的部分都是可選的。 返回類(lèi)型模式?jīng)Q定了方法的返回類(lèi)型必須依次匹配一個(gè)連接點(diǎn)。 你會(huì )使用的最頻繁的返回類(lèi)型模式是 *,它代表了匹配任意的返回類(lèi)型。 一個(gè)全稱(chēng)限定的類(lèi)型名將只會(huì )匹配返回給定類(lèi)型的方法。名字模式匹配的是方法名。 你可以使用 * 通配符作為所有或者部分命名模式。 參數模式稍微有點(diǎn)復雜:() 匹配了一個(gè)不接受任何參數的方法, 而 (..) 匹配了一個(gè)接受任意數量參數的方法(零或者更多)。 模式 (*) 匹配了一個(gè)接受一個(gè)任何類(lèi)型的參數的方法。 模式 (*,String) 匹配了一個(gè)接受兩個(gè)參數的方法,第一個(gè)可以是任意類(lèi)型,第二個(gè)則必須是String類(lèi)型。 請參見(jiàn)AspectJ編程指南的 Language Semantics 部分。
下面給出一些常見(jiàn)切入點(diǎn)表達式的例子。
任意公共方法的執行:
execution(public * *(..))
任何一個(gè)以“set”開(kāi)始的方法的執行:
execution(* set*(..))
AccountService 接口的任意方法的執行:
execution(* com.xyz.service.AccountService.*(..))
定義在service包里的任意方法的執行:
execution(* com.xyz.service.*.*(..))
定義在service包或者子包里的任意方法的執行:
execution(* com.xyz.service..*.*(..))
在service包里的任意連接點(diǎn)(在Spring AOP中只是方法執行) :
within(com.xyz.service.*)
在service包或者子包里的任意連接點(diǎn)(在Spring AOP中只是方法執行) :
within(com.xyz.service..*)
實(shí)現了 AccountService 接口的代理對象的任意連接點(diǎn)(在Spring AOP中只是方法執行) :
this(com.xyz.service.AccountService)‘this‘在binding form中用的更多:- 請常見(jiàn)以下討論通知的章節中關(guān)于如何使得代理對象可以在通知體內訪(fǎng)問(wèn)到的部分。
實(shí)現了 AccountService 接口的目標對象的任意連接點(diǎn)(在Spring AOP中只是方法執行) :
target(com.xyz.service.AccountService)‘target‘在binding form中用的更多:- 請常見(jiàn)以下討論通知的章節中關(guān)于如何使得目標對象可以在通知體內訪(fǎng)問(wèn)到的部分。
任何一個(gè)只接受一個(gè)參數,且在運行時(shí)傳入的參數實(shí)現了 Serializable 接口的連接點(diǎn) (在Spring AOP中只是方法執行)
args(java.io.Serializable)‘a(chǎn)rgs‘在binding form中用的更多:- 請常見(jiàn)以下討論通知的章節中關(guān)于如何使得方法參數可以在通知體內訪(fǎng)問(wèn)到的部分。
請注意在例子中給出的切入點(diǎn)不同于 execution(* *(java.io.Serializable)): args只有在動(dòng)態(tài)運行時(shí)候傳入參數是可序列化的(Serializable)才匹配,而execution 在傳入參數的簽名聲明的類(lèi)型實(shí)現了 Serializable 接口時(shí)候匹配。
有一個(gè) @Transactional 注解的目標對象中的任意連接點(diǎn)(在Spring AOP中只是方法執行)
@target(org.springframework.transaction.annotation.Transactional)‘@target‘ 也可以在binding form中使用:請常見(jiàn)以下討論通知的章節中關(guān)于如何使得annotation對象可以在通知體內訪(fǎng)問(wèn)到的部分。
任何一個(gè)目標對象聲明的類(lèi)型有一個(gè) @Transactional 注解的連接點(diǎn)(在Spring AOP中只是方法執行)
@within(org.springframework.transaction.annotation.Transactional)‘@within‘也可以在binding form中使用:- 請常見(jiàn)以下討論通知的章節中關(guān)于如何使得annotation對象可以在通知體內訪(fǎng)問(wèn)到的部分。
任何一個(gè)執行的方法有一個(gè) @Transactional annotation的連接點(diǎn)(在Spring AOP中只是方法執行)
@annotation(org.springframework.transaction.annotation.Transactional)‘@annotation‘ 也可以在binding form中使用:- 請常見(jiàn)以下討論通知的章節中關(guān)于如何使得annotation對象可以在通知體內訪(fǎng)問(wèn)到的部分。
任何一個(gè)接受一個(gè)參數,并且傳入的參數在運行時(shí)的類(lèi)型實(shí)現了 @Classified annotation的連接點(diǎn)(在Spring AOP中只是方法執行)
@args(com.xyz.security.Classified)‘@args‘也可以在binding form中使用:- 請常見(jiàn)以下討論通知的章節中關(guān)于如何使得annotation對象可以在通知體內訪(fǎng)問(wèn)到的部分。
通知是跟一個(gè)切入點(diǎn)表達式關(guān)聯(lián)起來(lái)的,并且在切入點(diǎn)匹配的方法執行之前或者之后或者之前和之后運行。 切入點(diǎn)表達式可能是指向已命名的切入點(diǎn)的簡(jiǎn)單引用或者是一個(gè)已經(jīng)聲明過(guò)的切入點(diǎn)表達式。
一個(gè)切面里使用 @Before 注解聲明前置通知:
import org.aspectj.lang.annotation.Aspect;import org.aspectj.lang.annotation.Before;@Aspectpublic class BeforeExample {@Before("com.xyz.myapp.SystemArchitecture.dataAccessOperation()")public void doAccessCheck() {// ...}}如果使用一個(gè)in-place 的切入點(diǎn)表達式,我們可以把上面的例子換個(gè)寫(xiě)法:
import org.aspectj.lang.annotation.Aspect;import org.aspectj.lang.annotation.Before;@Aspectpublic class BeforeExample {@Before("execution(* com.xyz.myapp.dao.*.*(..))")public void doAccessCheck() {// ...}}返回后通知通常在一個(gè)匹配的方法返回的時(shí)候執行。使用 @AfterReturning 注解來(lái)聲明:
import org.aspectj.lang.annotation.Aspect;import org.aspectj.lang.annotation.AfterReturning;@Aspectpublic class AfterReturningExample {@AfterReturning("com.xyz.myapp.SystemArchitecture.dataAccessOperation()")public void doAccessCheck() {// ...}}說(shuō)明:你可以在同一個(gè)切面里定義多個(gè)通知,或者其他成員。我們只是在展示如何定義一個(gè)簡(jiǎn)單的通知。這些例子主要的側重點(diǎn)是正在討論的問(wèn)題。 有時(shí)候你需要在通知體內得到返回的值。你可以使用以 @AfterReturning 接口的形式來(lái)綁定返回值:
import org.aspectj.lang.annotation.Aspect;import org.aspectj.lang.annotation.AfterReturning;@Aspectpublic class AfterReturningExample {@AfterReturning(pointcut="com.xyz.myapp.SystemArchitecture.dataAccessOperation()",returning="retVal")public void doAccessCheck(Object retVal) {// ...}}在 returning 屬性中使用的名字必須對應于通知方法內的一個(gè)參數名。 當一個(gè)方法執行返回后,返回值作為相應的參數值傳入通知方法。 一個(gè) returning 子句也限制了只能匹配到返回指定類(lèi)型值的方法。 (在本例子中,返回值是 Object 類(lèi),也就是說(shuō)返回任意類(lèi)型都會(huì )匹配)
拋出后通知在一個(gè)方法拋出異常后執行。使用 @AfterThrowing 注解來(lái)聲明:
import org.aspectj.lang.annotation.Aspect;import org.aspectj.lang.annotation.AfterThrowing;@Aspectpublic class AfterThrowingExample {@AfterThrowing("com.xyz.myapp.SystemArchitecture.dataAccessOperation()")public void doRecoveryActions() {// ...}}你通常會(huì )想要限制通知只在某種特殊的異常被拋出的時(shí)候匹配,你還希望可以在通知體內得到被拋出的異常。 使用 throwing 屬性不光可以限制匹配的異常類(lèi)型(如果你不想限制,請使用 Throwable 作為異常類(lèi)型),還可以將拋出的異常綁定到通知的一個(gè)參數上。
import org.aspectj.lang.annotation.Aspect;import org.aspectj.lang.annotation.AfterThrowing;@Aspectpublic class AfterThrowingExample {@AfterThrowing(pointcut="com.xyz.myapp.SystemArchitecture.dataAccessOperation()",throwing="ex")public void doRecoveryActions(DataAccessException ex) {// ...}}在 throwing 屬性中使用的名字必須與通知方法內的一個(gè)參數對應。 當一個(gè)方法因拋出一個(gè)異常而中止后,這個(gè)異常將會(huì )作為那個(gè)對應的參數送至通知方法。 throwing 子句也限制了只能匹配到拋出指定異常類(lèi)型的方法(上面的示例為 DataAccessException)。
不論一個(gè)方法是如何結束的,在它結束后(finally)后通知(After (finally) advice)都會(huì )運行。 使用 @After 注解來(lái)聲明。這個(gè)通知必須做好處理正常返回和異常返回兩種情況。通常用來(lái)釋放資源。
import org.aspectj.lang.annotation.Aspect;import org.aspectj.lang.annotation.After;@Aspectpublic class AfterFinallyExample {@After("com.xyz.myapp.SystemArchitecture.dataAccessOperation()")public void doReleaseLock() {// ...}}最后一種通知是環(huán)繞通知。環(huán)繞通知在一個(gè)方法執行之前和之后執行。 它使得通知有機會(huì )既在一個(gè)方法執行之前又在執行之后運行。并且,它可以決定這個(gè)方法在什么時(shí)候執行,如何執行,甚至是否執行。 環(huán)繞通知經(jīng)常在在某線(xiàn)程安全的環(huán)境下,你需要在一個(gè)方法執行之前和之后共享某種狀態(tài)的時(shí)候使用。 請盡量使用最簡(jiǎn)單的滿(mǎn)足你需求的通知。(比如如果前置通知(before advice)也可以適用的情況下不要使用環(huán)繞通知)。
環(huán)繞通知使用 @Around 注解來(lái)聲明。通知的第一個(gè)參數必須是 ProceedingJoinPoint 類(lèi)型。 在通知體內,調用 ProceedingJoinPoint 的 proceed() 方法將會(huì )導致潛在的連接點(diǎn)方法執行。 proceed 方法也可能會(huì )被調用并且傳入一個(gè) Object[] 對象-該數組將作為方法執行時(shí)候的參數。
當傳入一個(gè) Object[] 對象的時(shí)候,處理的方法與通過(guò)AspectJ編譯器處理環(huán)繞通知略有不同。 對于使用傳統AspectJ語(yǔ)言寫(xiě)的環(huán)繞通知來(lái)說(shuō),傳入參數的數量必須和傳遞給環(huán)繞通知的參數數量匹配(不是后臺的連接點(diǎn)接受的參數數量),并且特定順序的傳入參數代替了將要綁定給連接點(diǎn)的原始值(如果你看不懂不用擔心)。 Spring采用的方法更加簡(jiǎn)單并且更好得和他的基于代理(proxy-based),只匹配執行的語(yǔ)法相適用。 如果你適用AspectJ的編譯器和編織器來(lái)編譯為Spring而寫(xiě)的@AspectJ切面和處理參數,你只需要了解這一區別即可。 有一種方法可以讓你寫(xiě)出100%兼容Spring AOP和AspectJ的,我們將會(huì )在后續的通知參數(advice parameters)的章節中討論它。import org.aspectj.lang.annotation.Aspect;import org.aspectj.lang.annotation.Around;import org.aspectj.lang.ProceedingJoinPoint;@Aspectpublic class AroundExample {@Around("com.xyz.myapp.SystemArchitecture.businessService()")public Object doBasicProfiling(ProceedingJoinPoint pjp) throws Throwable {// start stopwatchObject retVal = pjp.proceed();// stop stopwatchreturn retVal;}}方法的調用者得到的返回值就是環(huán)繞通知返回的值。 例如:一個(gè)簡(jiǎn)單的緩存切面,如果緩存中有值,就返回該值,否則調用proceed()方法。 請注意proceed可能在通知體內部被調用一次,許多次,或者根本不被調用。
Spring 2.0 提供了完整的通知類(lèi)型 - 這意味著(zhù)你可以在通知簽名中聲明所需的參數,(就像在以前的例子中我們看到的返回值和拋出異常一樣)而不總是使用Object[]。 我們將會(huì )看到如何在通知體內訪(fǎng)問(wèn)參數和其他上下文相關(guān)的值。首先讓我們看以下如何編寫(xiě)普通的通知以找出正在被通知的方法。
任何通知方法可以將第一個(gè)參數定義為 org.aspectj.lang.JoinPoint 類(lèi)型 (環(huán)繞通知需要定義為 ProceedingJoinPoint 類(lèi)型的, 它是 JoinPoint 的一個(gè)子類(lèi)。) JoinPoint 接口提供了一系列有用的方法, 比如 getArgs()(返回方法參數)、getThis()(返回代理對象)、getTarget()(返回目標)、getSignature()(返回正在被通知的方法相關(guān)信息)和 toString()(打印出正在被通知的方法的有用信息)。詳細的內容請參考Javadocs。
我們已經(jīng)看到了如何綁定返回值或者異常(使用后置通知(after returning)和異常后通知(after throwing advice)。 為了可以在通知(adivce)體內訪(fǎng)問(wèn)參數,你可以使用 args 來(lái)綁定。 如果在一個(gè)參數表達式中應該使用類(lèi)型名字的地方使用一個(gè)參數名字,那么當通知執行的時(shí)候對應的參數值將會(huì )被傳遞進(jìn)來(lái)。 可能給出一個(gè)例子會(huì )更好理解。假使你想要通知(advise)接受某個(gè)Account對象作為第一個(gè)參數的DAO操作的執行,你想要在通知體內也能訪(fǎng)問(wèn)到account對象,你可以寫(xiě)如下的代碼:
@Before("com.xyz.myapp.SystemArchitecture.dataAccessOperation() &&" +"args(account,..)")public void validateAccount(Account account) {// ...}切入點(diǎn)表達式的 args(account,..) 部分有兩個(gè)目的: 首先它保證了只會(huì )匹配那些接受至少一個(gè)參數的方法的執行,而且傳入的參數必須是 Account 類(lèi)型的實(shí)例, 其次它使得可以在通知體內通過(guò) account 參數來(lái)訪(fǎng)問(wèn)那個(gè)account參數。
另外一個(gè)辦法是定義一個(gè)切入點(diǎn),這個(gè)切入點(diǎn)在匹配某個(gè)連接點(diǎn)的時(shí)候“提供”了一個(gè)Account對象, 然后直接從通知中訪(fǎng)問(wèn)那個(gè)命名的切入點(diǎn)。你可以這樣寫(xiě):
@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)都可以用一種簡(jiǎn)單格式綁定。 以下的例子展示了如何使用 @Auditable 注解來(lái)匹配方法執行,并提取AuditCode。
首先是 @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();// ...}綁定在通知上的參數依賴(lài)切入點(diǎn)表達式的匹配名,并借此在(通知(advice)和切入點(diǎn)(pointcut))的方法簽名中聲明參數名。 參數名 無(wú)法 通過(guò)Java反射來(lái)獲取,所以Spring AOP使用如下的策略來(lái)決定參數名字:
如果參數名字已經(jīng)被用戶(hù)明確指定,則使用指定的參數名: 通知(advice)和切入點(diǎn)(pointcut)注解有一個(gè)額外的"argNames"屬性,該屬性用來(lái)指定所注解的方法的參數名 - 這些參數名在運行時(shí)是 可以 訪(fǎng)問(wèn)的。例子如下:
@Before( value="com.xyz.lib.Pointcuts.anyPublicMethod() && " + "@annotation(auditable)", argNames="auditable") public void audit(Auditable auditable) { AuditCode code = auditable.value(); // ... } 如果一個(gè)@AspectJ切面已經(jīng)被AspectJ編譯器(ajc)編譯過(guò)了,那么就不需要再添加 argNames 參數了,因為編譯器會(huì )自動(dòng)完成這一工作。 使用 ‘a(chǎn)rgNames‘ 屬性有點(diǎn)不那么優(yōu)雅,所以如果沒(méi)有指定‘a(chǎn)rgNames‘ 屬性, Spring AOP 會(huì )尋找類(lèi)的debug信息,并且嘗試從本地變量表(local variable table)中來(lái)決定參數名字。 只要編譯的時(shí)候使用了debug信息(至少要使用 ‘-g:vars‘ ),就可獲得這些信息。 使用這個(gè)flag編譯的結果是: (1)你的代碼將能夠更加容易的讀懂(反向工程), (2)生成的class文件會(huì )稍許大一些(通常是不重要的), (3)移除不被使用的本地變量的優(yōu)化功能將會(huì )失效。 換句話(huà)說(shuō),你在使用這個(gè)flag的時(shí)候不會(huì )遇到任何困難。
如果不加上debug信息來(lái)編譯的話(huà),Spring AOP將會(huì )嘗試推斷參數的綁定。 (例如,要是只有一個(gè)變量被綁定到切入點(diǎn)表達式(pointcut expression)、通知方法(advice method)將會(huì )接受這個(gè)參數, 這是顯而易見(jiàn)的)。 如果變量的綁定不明確,將會(huì )拋出一個(gè) AmbiguousBindingException 異常。
如果以上所有策略都失敗了,將會(huì )拋出一個(gè) IllegalArgumentException 異常。
我們之前提過(guò)我們將會(huì )討論如何編寫(xiě)一個(gè) 帶參數的 的proceed()調用,使得不論在Spring AOP中還是在A(yíng)spectJ都能正常工作。 解決方法是保證通知簽名依次綁定方法參數。比如說(shuō):
@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});}大多數情況下你都會(huì )這樣綁定(就像上面的例子那樣)。
如果有多個(gè)通知想要在同一連接點(diǎn)運行會(huì )發(fā)生什么?Spring AOP 的執行通知的順序跟AspectJ的一樣。 在“進(jìn)入”連接點(diǎn)的情況下,最高優(yōu)先級的通知會(huì )先執行(所以上面給出的兩個(gè)前置通知(before advice)中,優(yōu)先級高的那個(gè)會(huì )先執行)。 在“退出”連接點(diǎn)的情況下,最高優(yōu)先級的通知會(huì )最后執行。(所以上面給出的兩個(gè)前置通知(before advice)中,優(yōu)先級高的那個(gè)會(huì )第二個(gè)執行)。 對于定義在相同切面的通知,根據聲明的順序來(lái)確定執行順序。比如下面這個(gè)切面:
@Aspectpublic class AspectWithMultipleAdviceDeclarations {@Pointcut("execution(* foo(..))")public void fooExecution() {}@Before("fooExecution()")public void doBeforeOne() {// ...}@Before("fooExecution()")public void doBeforeTwo() {// ...}@AfterReturning("fooExecution()")public void doAfterOne() {// ...}@AfterReturning("fooExecution()")public void doAfterTwo() {// ...}}這樣,假使對于任何一個(gè)名字為foo的方法的執行, doBeforeOne、doBeforeTwo、doAfterOne 和 doAfterTwo 通知方法都需要運行。 執行順序將按照聲明的順序來(lái)確定。在這個(gè)例子中,執行的結果會(huì )是:
doBeforeOnedoBeforeTwofoodoAfterOnedoAfterTwo
換言之,因為doBeforeOne先定義,它會(huì )先于doBeforeTwo執行,而doAfterTwo后于doAfterOne定義,所以它會(huì )在doAfterOne之后執行。 只需要記住通知是按照定義的順序來(lái)執行的就可以了。 - 如果想要知道更加詳細的內容,請參閱AspectJ編程指南。
當定義在 不同的 切面里的兩個(gè)通知都需要在一個(gè)相同的連接點(diǎn)中運行,那么除非你指定,否則執行的順序是未知的。 你可以通過(guò)指定優(yōu)先級來(lái)控制執行順序。在Spring中可以在切面類(lèi)中實(shí)現 org.springframework.core.Ordered 接口做到這一點(diǎn)。 在兩個(gè)切面中,Ordered.getValue() 方法返回值較低的那個(gè)有更高的優(yōu)先級。
引入(Introductions)(在A(yíng)spectJ中被稱(chēng)為inter-type聲明)使得一個(gè)切面可以定義被通知對象實(shí)現一個(gè)給定的接口,并且可以代表那些對象提供具體實(shí)現。
使用 @DeclareParents注解來(lái)定義引入。這個(gè)注解被用來(lái)定義匹配的類(lèi)型擁有一個(gè)新的父親。 比如,給定一個(gè)接口 UsageTracked,然后接口的具體實(shí)現 DefaultUsageTracked 類(lèi), 接下來(lái)的切面聲明了所有的service接口的實(shí)現都實(shí)現了 UsageTracked 接口。(比如為了通過(guò)JMX輸出統計信息)。
@Aspectpublic 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();}}實(shí)現的接口通過(guò)被注解的字段類(lèi)型來(lái)決定。@DeclareParents 注解的 value 屬性是一個(gè)AspectJ的類(lèi)型模式:- 任何匹配類(lèi)型的bean都會(huì )實(shí)現 UsageTracked 接口。 請注意,在上面的前置通知(before advice)的例子中,service beans 可以直接用作 UsageTracked 接口的實(shí)現。 如果需要編程式的來(lái)訪(fǎng)問(wèn)一個(gè)bean,你可以這樣寫(xiě):
UsageTracked usageTracked = (UsageTracked) context.getBean("myService");默認情況下,在application context中每一個(gè)切面都會(huì )有一個(gè)實(shí)例。 AspectJ 把這個(gè)叫做單個(gè)實(shí)例化模型(singleton instantiation model)。 也可以用其他的生命周期來(lái)定義切面:- Spring支持AspectJ的 perthis 和 pertarget 實(shí)例化模型 (現在還不支持percflow、percflowbelow 和 pertypewithin )。
一個(gè)"perthis" 切面的定義:在 @Aspect 注解中指定perthis 子句。 讓我們先來(lái)看一個(gè)例子,然后解釋它是如何運作的:
@Aspect("perthis(com.xyz.myapp.SystemArchitecture.businessService())")public class MyAspect {private int someState;@Before(com.xyz.myapp.SystemArchitecture.businessService())public void recordServiceUsage() {// ...}}這個(gè)perthis子句的效果是每個(gè)獨立的service對象執行時(shí)都會(huì )創(chuàng )建一個(gè)切面實(shí)例(切入點(diǎn)表達式所匹配的連接點(diǎn)上的每一個(gè)獨立的對象都會(huì )綁定到‘this‘上)。 service對象的每個(gè)方法在第一次執行的時(shí)候創(chuàng )建切面實(shí)例。切面在service對象失效的同時(shí)失效。 在切面實(shí)例被創(chuàng )建前,所有的通知都不會(huì )被執行,一旦切面對象創(chuàng )建完成,定義的通知將會(huì )在匹配的連接點(diǎn)上執行,但是只有當service對象是和切面關(guān)聯(lián)的才可以。 如果想要知道更多關(guān)于per-clauses的信息,請參閱 AspectJ 編程指南。
‘pertarget‘實(shí)例模型的跟“perthis”完全一樣,只不過(guò)是為每個(gè)匹配于連接點(diǎn)的獨立目標對象創(chuàng )建一個(gè)切面實(shí)例。
現在你已經(jīng)看到了每個(gè)獨立的部分是如何運作的了,是時(shí)候把他們放到一起做一些有用的事情了!
因為并發(fā)的問(wèn)題,有時(shí)候business services可能會(huì )失?。ɡ?,死鎖失?。?。如果重新嘗試一下,很有可能就會(huì )成功。 對于business services來(lái)說(shuō),重試幾次是很正常的(Idempotent操作不需要用戶(hù)參與,否則會(huì )得出矛盾的結論) 我們可能需要透明的重試操作以避免讓客戶(hù)看見(jiàn) PessimisticLockingFailureException 例外被拋出。 很明顯,在一個(gè)橫切多層的情況下,這是非常有必要的,因此通過(guò)切面來(lái)實(shí)現是很理想的。
因為我們想要重試操作,我們會(huì )需要使用到環(huán)繞通知,這樣我們就可以多次調用proceed()方法。下面是簡(jiǎn)單的切面實(shí)現:
@Aspectpublic 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;}}請注意切面實(shí)現了 Ordered 接口,這樣我們就可以把切面的優(yōu)先級設定為高于事務(wù)通知(我們每次重試的時(shí)候都想要在一個(gè)全新的事務(wù)中進(jìn)行)。 maxRetries 和 order 屬性都可以在Spring中配置。 主要的動(dòng)作在 doConcurrentOperation 這個(gè)環(huán)繞通知中發(fā)生。 請注意這個(gè)時(shí)候我們所有的 businessService() 方法都會(huì )使用這個(gè)重試策略。 我們首先會(huì )嘗試處理,然后如果我們得到一個(gè) PessimisticLockingFailureException 意外,我們只需要簡(jiǎn)單的重試,直到我們耗盡所有預設的重試次數。
對應的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>
為了改進(jìn)切面,使之僅僅重試idempotent操作,我們可以定義一個(gè) Idempotent 注解:
@Retention(RetentionPolicy.RUNTIME)public @interface Idempotent {// marker annotation}并且對service操作的實(shí)現進(jìn)行注解。 這樣如果你只希望改變切面使得idempotent的操作會(huì )嘗試多次,你只需要改寫(xiě)切入點(diǎn)表達式,這樣只有 @Idempotent 操作會(huì )匹配:
@Around("com.xyz.myapp.SystemArchitecture.businessService() && " +"@annotation(com.xyz.myapp.service.Idempotent)")public Object doConcurrentOperation(ProceedingJoinPoint pjp) throws Throwable {...}如果你無(wú)法使用Java 5,或者你比較喜歡使用XML格式,Spring2.0也提供了使用新的"aop"命名空間來(lái)定義一個(gè)切面。 和使用@AspectJ風(fēng)格完全一樣,切入點(diǎn)表達式和通知類(lèi)型同樣得到了支持,因此在這一節中我們將著(zhù)重介紹新的 語(yǔ)法 和回顧前面我們所討論的如何寫(xiě)一個(gè)切入點(diǎn)表達式和通知參數的綁定(Section 6.2, “@AspectJ支持”)。
使用本章所介紹的aop命名空間標簽(aop namespace tag),你需要引入Appendix A, XML Schema-based configuration中提及的spring-aop schema。 參見(jiàn)Section A.2.6, “The aop schema”。
在Spring的配置文件中,所有的切面和通知器都必須定義在 <aop:config> 元素內部。 一個(gè)application context可以包含多個(gè) <aop:config>。 一個(gè) <aop:config> 可以包含pointcut,advisor和aspect元素(注意它們必須按照這樣的順序進(jìn)行聲明)。
![]() | Warning |
|---|---|
| <aop:config>風(fēng)格的配置使得對Spring auto-proxying 機制的使用變得很笨重。如果你已經(jīng)通過(guò)BeanNameAutoProxyCreator或類(lèi)似的東西使用顯式的auto-proxying將會(huì )引發(fā)問(wèn)題 (例如通知沒(méi)有被織入)。推薦的使用模式是只使用<aop:config>風(fēng)格或只使用 AutoProxyCreator風(fēng)格 | |
有了schema的支持,切面就和常規的Java對象一樣被定義成application context中的一個(gè)bean。 對象的字段和方法提供了狀態(tài)和行為信息,XML文件則提供了切入點(diǎn)和通知信息。
切面使用<aop:aspect>來(lái)聲明,backing bean(支持bean)通過(guò) ref 屬性來(lái)引用:
<aop:config><aop:aspect id="myAspect" ref="aBean">...</aop:aspect></aop:config><bean id="aBean" class="...">...</bean>
切面的支持bean(上例中的"aBean")可以象其他Spring bean一樣被容器管理配置以及依賴(lài)注入。
切入點(diǎn)可以在切面里面聲明,這種情況下切入點(diǎn)只在切面內部可見(jiàn)。切入點(diǎn)也可以直接在<aop:config>下定義,這樣就可以使多個(gè)切面和通知器共享該切入點(diǎn)。
一個(gè)描述service層中表示所有service執行的切入點(diǎn)可以如下定義:
<aop:config><aop:pointcut id="businessService"expression="execution(* com.xyz.myapp.service.*.*(..))"/></aop:config>
注意切入點(diǎn)表達式本身使用了 Section 6.2, “@AspectJ支持” 中描述的AspectJ 切入點(diǎn)表達式語(yǔ)言。 如果你在Java 5環(huán)境下使用基于schema的聲明風(fēng)格,可參考切入點(diǎn)表達式類(lèi)型中定義的命名式切入點(diǎn),不過(guò)這在JDK1.4及以下版本中是不被支持的(因為依賴(lài)于Java 5中的AspectJ反射API)。 所以在JDK 1.5中,上面的切入點(diǎn)的另外一種定義形式如下:
<aop:config><aop:pointcut id="businessService"expression="com.xyz.myapp.SystemArchitecture.businessService()"/></aop:config>
假定你有 Section 6.2.3.3, “共享常見(jiàn)的切入點(diǎn)(pointcut)定義”中說(shuō)描述的 SystemArchitecture 切面。
在切面里面聲明一個(gè)切入點(diǎn)和聲明一個(gè)頂級的切入點(diǎn)非常類(lèi)似:
<aop:config><aop:aspect id="myAspect" ref="aBean"><aop:pointcut id="businessService"expression="execution(* com.xyz.myapp.service.*.*(..))"/>...</aop:aspect></aop:config>
當需要連接子表達式的時(shí)候,‘&‘在XML中用起來(lái)非常不方便,所以關(guān)鍵字‘a(chǎn)nd‘, ‘or‘ 和 ‘not‘可以分別用來(lái)代替‘&‘, ‘||‘ 和 ‘!‘。
注意這種方式定義的切入點(diǎn)通過(guò)XML id來(lái)查找,并且不能定義切入點(diǎn)參數。在基于schema的定義風(fēng)格中命名切入點(diǎn)支持較之@AspectJ風(fēng)格受到了很多的限制。
和@AspectJ風(fēng)格一樣,基于schema的風(fēng)格也支持5種通知類(lèi)型并且兩者具有同樣的語(yǔ)義。
Before通知在匹配方法執行前進(jìn)入。在<aop:aspect>里面使用<aop:before>元素進(jìn)行聲明。
<aop:aspect id="beforeExample" ref="aBean"><aop:beforepointcut-ref="dataAccessOperation"method="doAccessCheck"/>...</aop:aspect>
這里 dataAccessOperation 是一個(gè)頂級(<aop:config>)切入點(diǎn)的id。 要定義內置切入點(diǎn),可將 pointcut-ref 屬性替換為 pointcut 屬性:
<aop:aspect id="beforeExample" ref="aBean"><aop:beforepointcut="execution(* com.xyz.myapp.dao.*.*(..))"method="doAccessCheck"/>...</aop:aspect>
我們已經(jīng)在@AspectJ風(fēng)格章節中討論過(guò)了,使用命名切入點(diǎn)能夠明顯的提高代碼的可讀性。
Method屬性標識了提供了通知的主體的方法(doAccessCheck)。這個(gè)方法必須定義在包含通知的切面元素所引用的bean中。 在一個(gè)數據訪(fǎng)問(wèn)操作執行之前(執行連接點(diǎn)和切入點(diǎn)表達式匹配),切面中的"doAccessCheck"會(huì )被調用。
After returning通知在匹配的方法完全執行后運行。和Before通知一樣,可以在<aop:aspect>里面聲明。例如:
<aop:aspect id="afterReturningExample" ref="aBean"><aop:after-returningpointcut-ref="dataAccessOperation"method="doAccessCheck"/>...</aop:aspect>
和@AspectJ風(fēng)格一樣,通知主體可以接收返回值。使用returning屬性來(lái)指定接收返回值的參數名:
<aop:aspect id="afterReturningExample" ref="aBean"><aop:after-returningpointcut-ref="dataAccessOperation"returning="retVal"method="doAccessCheck"/>...</aop:aspect>
doAccessCheck方法必須聲明一個(gè)名字叫 retVal 的參數。 參數的類(lèi)型強制匹配,和先前我們在@AfterReturning中講到的一樣。例如,方法簽名可以這樣聲明:
public void doAccessCheck(Object retVal) {...After throwing通知在匹配方法拋出異常退出時(shí)執行。在 <aop:aspect> 中使用after-throwing元素來(lái)聲明:
<aop:aspect id="afterThrowingExample" ref="aBean"><aop:after-throwingpointcut-ref="dataAccessOperation"method="doRecoveryActions"/>...</aop:aspect>
和@AspectJ風(fēng)格一樣,可以從通知體中獲取拋出的異常。 使用throwing屬性來(lái)指定異常的名稱(chēng),用這個(gè)名稱(chēng)來(lái)獲取異常:
<aop:aspect id="afterThrowingExample" ref="aBean"><aop:after-throwingpointcut-ref="dataAccessOperation"thowing="dataAccessEx"method="doRecoveryActions"/>...</aop:aspect>
doRecoveryActions方法必須聲明一個(gè)名字為 dataAccessEx 的參數。 參數的類(lèi)型強制匹配,和先前我們在@AfterThrowing中講到的一樣。例如:方法簽名可以如下這般聲明:
public void doRecoveryActions(DataAccessException dataAccessEx) {...After (finally)通知在匹配方法退出后執行。使用 after 元素來(lái)聲明:
<aop:aspect id="afterFinallyExample" ref="aBean"><aop:afterpointcut-ref="dataAccessOperation"method="doReleaseLock"/>...</aop:aspect>
Around通知是最后一種通知類(lèi)型。Around通知在匹配方法運行期的“周?chē)眻绦小?它有機會(huì )在目標方法的前面和后面執行,并決定什么時(shí)候運行,怎么運行,甚至是否運行。 Around通知經(jīng)常在需要在一個(gè)方法執行前或后共享狀態(tài)信息,并且是線(xiàn)程安全的情況下使用(啟動(dòng)和停止一個(gè)計時(shí)器就是一個(gè)例子)。 注意選擇能滿(mǎn)足你需求的最簡(jiǎn)單的通知類(lèi)型(i.e.如果簡(jiǎn)單的before通知就能做的事情絕對不要使用around通知)。
Around通知使用 aop:around 元素來(lái)聲明。 通知方法的第一個(gè)參數的類(lèi)型必須是 ProceedingJoinPoint 類(lèi)型。 在通知的主體中,調用 ProceedingJoinPoint的proceed() 方法來(lái)執行真正的方法。 proceed 方法也可能會(huì )被調用并且傳入一個(gè) Object[] 對象 - 該數組將作為方法執行時(shí)候的參數。 參見(jiàn) Section 6.2.4.5, “環(huán)繞通知(Around Advice)” 中提到的一些注意點(diǎn)。
<aop:aspect id="aroundExample" ref="aBean"><aop:aroundpointcut-ref="businessService"method="doBasicProfiling"/>...</aop:aspect>
doBasicProfiling 通知的實(shí)現和@AspectJ中的例子完全一樣(當然要去掉注解):
public Object doBasicProfiling(ProceedingJoinPoint pjp) throws Throwable {// start stopwatchObject retVal = pjp.proceed();// stop stopwatchreturn retVal;}Schema-based聲明風(fēng)格和@AspectJ支持一樣,支持通知的全名形式 - 通過(guò)通知方法參數名字來(lái)匹配切入點(diǎn)參數。 參見(jiàn) Section 6.2.4.6, “通知參數(Advice parameters)” 獲取詳細信息。
如果你希望顯式指定通知方法的參數名(而不是依靠先前提及的偵測策略),可以通過(guò) arg-names 屬性來(lái)實(shí)現。示例如下:
<aop:beforepointcut="com.xyz.lib.Pointcuts.anyPublicMethod() and @annotation(auditable)"method="audit"arg-names="auditable"/>
The arg-names attribute accepts a comma-delimited list of parameter names.
arg-names屬性接受由逗號分割的參數名列表。
請看下面這個(gè)基于XSD風(fēng)格的更復雜一些的實(shí)例,它展示了關(guān)聯(lián)多個(gè)強類(lèi)型參數的環(huán)繞通知的使用。
首先,服務(wù)接口及它的實(shí)現將被通知:
package x.y.service;public interface FooService {Foo getFoo(String fooName, int age);}// the attendant implementation (defined in another file of course)public class DefaultFooService implements FooService {public Foo getFoo(String name, int age) {return new Foo(name, age);}}下一步(無(wú)可否認的)是切面。注意實(shí)際上profile(..)方法 接受多個(gè)強類(lèi)型(strongly-typed)參數,第一個(gè)參數是方法調用時(shí)要執行的連接點(diǎn),該參數指明了 profile(..)方法被用作一個(gè)環(huán)繞通知:
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());}}}最后,下面是為一個(gè)特定的連接點(diǎn)執行上面的通知所必需的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.0.xsdhttp://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-2.0.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>
如果使用下面的驅動(dòng)腳本,我們將在標準輸出上得到如下的輸出:
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)
當同一個(gè)切入點(diǎn)(執行方法)上有多個(gè)通知需要執行時(shí),執行順序規則在 Section 6.2.4.7, “通知(Advice)順序” 已經(jīng)提及了。 切面的優(yōu)先級通過(guò)切面的支持bean是否實(shí)現了Ordered接口來(lái)決定。
Intrduction (在A(yíng)spectJ中成為inter-type聲明)允許一個(gè)切面聲明一個(gè)通知對象實(shí)現指定接口,并且提供了一個(gè)接口實(shí)現類(lèi)來(lái)代表這些對象。
在 aop:aspect 內部使用 aop:declare-parents 元素定義Introduction。 該元素用于用來(lái)聲明所匹配的類(lèi)型有了一個(gè)新的父類(lèi)型(所以有了這個(gè)名字)。 例如,給定接口 UsageTracked,以及這個(gè)接口的一個(gè)實(shí)現類(lèi) DefaultUsageTracked, 下面聲明的切面所有實(shí)現service接口的類(lèi)同時(shí)實(shí)現 UsageTracked 接口。(比如為了通過(guò)JMX暴露statistics。)
<aop:aspect id="usageTrackerAspect" ref="usageTracking"><aop:declare-parentstypes-matching="com.xzy.myapp.service.*+",implement-interface="UsageTracked"default-impl="com.xyz.myapp.service.tracking.DefaultUsageTracked"/><aop:beforepointcut="com.xyz.myapp.SystemArchitecture.businessService()and this(usageTracked)"method="recordUsage"/></aop:aspect>
usageTracking bean的支持類(lèi)可以包含下面的方法:
public void recordUsage(UsageTracked usageTracked) {usageTracked.incrementUseCount();}欲實(shí)現的接口由 implement-interface 屬性來(lái)指定。 types-matching 屬性的值是一個(gè)AspectJ類(lèi)型模式:- 任何匹配類(lèi)型的bean會(huì )實(shí)現 UsageTracked 接口。 注意在Before通知的例子中,srevice bean可以用作 UsageTracked 接口的實(shí)現。 如果編程形式訪(fǎng)問(wèn)一個(gè)bean,你可以這樣來(lái)寫(xiě):
UsageTracked usageTracked = (UsageTracked) context.getBean("myService");"advisors"這個(gè)概念來(lái)自Spring1.2對AOP的支持,在A(yíng)spectJ中是沒(méi)有等價(jià)的概念。 advisor就像一個(gè)小的自包含的切面,這個(gè)切面只有一個(gè)通知。 切面自身通過(guò)一個(gè)bean表示,并且必須實(shí)現一個(gè)通知接口, 在 Section 7.3.2, “Spring里的通知類(lèi)型” 中我們會(huì )討論相應的接口。Advisors可以很好的利用AspectJ切入點(diǎn)表達式。
Spring 2.0 通過(guò) <aop:advisor> 元素來(lái)支持advisor 概念。 你將會(huì )發(fā)現它大多數情況下會(huì )和transactional advice一起使用,transactional advice在Spring 2.0中有自己的命名空間。格式如下:
<aop:config><aop:pointcut id="businessService"expression="execution(* com.xyz.myapp.service.*.*(..))"/><aop:advisorpointcut-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 屬性來(lái)定義一個(gè)內聯(lián)的切入點(diǎn)表達式。
為了定義一個(gè)advisord的優(yōu)先級以便讓通知可以有序,使用 order 屬性來(lái)定義 advisor的值 Ordered 。
讓我們來(lái)看看在 Section 6.2.7, “例子” 提過(guò)并發(fā)鎖失敗重試的例子,如果使用schema對這個(gè)例子進(jìn)行重寫(xiě)是什么效果。
因為并發(fā)鎖的關(guān)系,有時(shí)候business services可能會(huì )失?。ɡ?,死鎖失?。?。 如果重新嘗試一下,很有可能就會(huì )成功。對于business services來(lái)說(shuō),重試幾次是很正常的(Idempotent操作不需要用戶(hù)參與,否則會(huì )得出矛盾的結論) 我們可能需要透明的重試操作以避免讓客戶(hù)看見(jiàn) PessimisticLockingFailureException 例外被拋出。 很明顯,在一個(gè)橫切多層的情況下,這是非常有必要的,因此通過(guò)切面來(lái)實(shí)現是很理想的。
因為我們想要重試操作,我們會(huì )需要使用到環(huán)繞通知,這樣我們就可以多次調用proceed()方法。 下面是簡(jiǎn)單的切面實(shí)現(只是一個(gè)schema支持的普通Java 類(lèi)):
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;}}請注意切面實(shí)現了 Ordered 接口,這樣我們就可以把切面的優(yōu)先級設定為高于事務(wù)通知(我們每次重試的時(shí)候都想要在一個(gè)全新的事務(wù)中進(jìn)行)。 maxRetries 和 order 屬性都可以在Spring中配置。 主要的動(dòng)作在 doConcurrentOperation 這個(gè)環(huán)繞通知中發(fā)生。 請注意這個(gè)時(shí)候我們所有的 businessService() 方法都會(huì )使用這個(gè)重試策略。 我們首先會(huì )嘗試處理,然后如果我們得到一個(gè) PessimisticLockingFailureException 異常,我們只需要簡(jiǎn)單的重試,直到我們耗盡所有預設的重試次數。
這個(gè)類(lèi)跟我們在@AspectJ的例子中使用的是相同的,只是沒(méi)有使用注解。對應的Spring配置如下:
<aop:config><aop:aspect id="concurrentOperationRetry" ref="concurrentOperationExecutor"><aop:pointcut id="idempotentOperation"expression="execution(* com.xyz.myapp.service.*.*(..))"/><aop:aroundpointcut-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。如果不是這樣,我們可以改寫(xiě)切面,加上 Idempotent 注解,讓它只調用idempotent:
@Retention(RetentionPolicy.RUNTIME)public @interface Idempotent {// marker annotation}并且對service操作的實(shí)現進(jìn)行注解。這樣如果你只希望改變切面使得idempotent的操作會(huì )嘗試多次,你只需要改寫(xiě)切入點(diǎn)表達式,這樣只有 @Idempotent 操作會(huì )匹配:
<aop:pointcut id="idempotentOperation"expression="execution(* com.xyz.myapp.service.*.*(..)) and@annotation(com.xyz.myapp.service.Idempotent)"/>
當你確定切面是實(shí)現一個(gè)給定需求的最佳方法時(shí),你如何選擇是使用Spring AOP還是AspectJ,以及選擇 Aspect語(yǔ)言(代碼)風(fēng)格、@AspectJ聲明風(fēng)格或XML風(fēng)格?這個(gè)決定會(huì )受到多個(gè)因素的影響,包括應用的需求、 開(kāi)發(fā)工具和小組對AOP的精通程度。
做能起作用的最簡(jiǎn)單的事。Spring AOP比完全使用AspectJ更加簡(jiǎn)單,因為它不需要引入AspectJ的編譯器/織入器到你開(kāi)發(fā)和構建過(guò)程中。 如果你僅僅需要在Spring bean上通知執行操作,那么Spring AOP是合適的選擇。如果你需要通知domain對象或其它沒(méi)有在Spring容器中 管理的任意對象,那么你需要使用AspectJ。如果你想通知除了簡(jiǎn)單的方法執行之外的連接點(diǎn)(如:調用連接點(diǎn)、字段get或set的連接點(diǎn)等等), 也需要使用AspectJ。
當使用AspectJ時(shí),你可以選擇使用AspectJ語(yǔ)言(也稱(chēng)為“代碼風(fēng)格”)或@AspectJ注解風(fēng)格。 如果切面在你的設計中扮演一個(gè)很大的角色,并且你能在Eclipse中使用AspectJ Development Tools (AJDT), 那么首選AspectJ語(yǔ)言 :- 因為該語(yǔ)言專(zhuān)門(mén)被設計用來(lái)編寫(xiě)切面,所以會(huì )更清晰、更簡(jiǎn)單。如果你沒(méi)有使用 Eclipse,或者在你的應用中只有很少的切面并沒(méi)有作為一個(gè)主要的角色,你或許應該考慮使用@AspectJ風(fēng)格 并在你的IDE中附加一個(gè)普通的Java編輯器,并且在你的構建腳本中增加切面織入(鏈接)的段落。
如果你選擇使用Spring AOP,那么你可以選擇@AspectJ或者XML風(fēng)格??偟膩?lái)說(shuō),如果你使用Java 5, 我們建議使用@AspectJ風(fēng)格。顯然如果你不是運行在Java 5上,XML風(fēng)格是最佳選擇。XML和@AspectJ 之間權衡的細節將在下面進(jìn)行討論。
XML風(fēng)格對現有的Spring用戶(hù)來(lái)說(shuō)更加習慣。它可以使用在任何Java級別中(參考連接點(diǎn)表達式內部的命名連接點(diǎn),雖然它也需要Java 5) 并且通過(guò)純粹的POJO來(lái)支持。當使用AOP作為工具來(lái)配置企業(yè)服務(wù)時(shí)(一個(gè)好的例子是當你認為連接點(diǎn)表達式是你的配置中的一部分時(shí), 你可能想單獨更改它)XML會(huì )是一個(gè)很好的選擇。對于XML風(fēng)格,從你的配置中可以清晰的表明在系統中存在那些切面。
XML風(fēng)格有兩個(gè)缺點(diǎn)。第一是它不能完全將需求實(shí)現的地方封裝到一個(gè)位置。DRY原則中說(shuō)系統中的每一項知識都必須具有單一、無(wú)歧義、權威的表示。 當使用XML風(fēng)格時(shí),如何實(shí)現一個(gè)需求的知識被分割到支撐類(lèi)的聲明中以及XML配置文件中。當使用@AspectJ風(fēng)格時(shí)就只有一個(gè)單獨的模塊 -切面- 信息被封裝了起來(lái)。 第二是XML風(fēng)格同@AspectJ風(fēng)格所能表達的內容相比有更多的限制:僅僅支持"singleton"切面實(shí)例模型,并且不能在XML中組合命名連接點(diǎn)的聲明。 例如,在@AspectJ風(fēng)格中我們可以編寫(xiě)如下的內容:
@Pointcut(execution(* get*()))public void propertyAccess() {}@Pointcut(execution(org.xyz.Account+ *(..))public void operationReturningAnAccount() {}@Pointcut(propertyAccess() && operationReturningAnAccount())public void accountPropertyAccess() {}在XML風(fēng)格中能聲明開(kāi)頭的兩個(gè)連接點(diǎn):
<aop:pointcut id="propertyAccess"expression="execution(* get*())"/><aop:pointcut id="operationReturningAnAccount"expression="execution(org.xyz.Account+ *(..))"/>
但是不能通過(guò)組合這些來(lái)定義accountPropertyAccess連接點(diǎn)
@AspectJ風(fēng)格支持其它的實(shí)例模型以及更豐富的連接點(diǎn)組合。它具有將將切面保持為一個(gè)模塊單元的優(yōu)點(diǎn)。 還有一個(gè)優(yōu)點(diǎn)就是@AspectJ切面能被Spring AOP和AspectJ兩者都理解 - 所以如果稍后你認為你需要AspectJ 的能力去實(shí)現附加的需求,那么你非常容易轉移到基于A(yíng)spectJ的途徑??偠灾?,我們更喜歡@AspectJ風(fēng)格只要你有切面 去做超出簡(jiǎn)單的“配置”企業(yè)服務(wù)之外的事情。
我們完全可以混合使用以下幾種風(fēng)格的切面定義:使用自動(dòng)代理的@AspectJ 風(fēng)格的切面,schema-defined <aop:aspect> 的切面,和用 <aop:advisor> 聲明的advisor,甚至是使用Spring 1.2風(fēng)格的代理和攔截器。 由于以上幾種風(fēng)格的切面定義的都使用了相同的底層機制,因此可以很好的共存。
Spring AOP部分使用JDK動(dòng)態(tài)代理或者CGLIB來(lái)為目標對象創(chuàng )建代理。(建議盡量使用JDK的動(dòng)態(tài)代理)
如果被代理的目標對象實(shí)現了至少一個(gè)接口,則會(huì )使用JDK動(dòng)態(tài)代理。所有該目標類(lèi)型實(shí)現的接口都將被代理。若該目標對象沒(méi)有實(shí)現任何接口,則創(chuàng )建一個(gè)CGLIB代理。
如果你希望強制使用CGLIB代理,(例如:希望代理目標對象的所有方法,而不只是實(shí)現自接口的方法)那也可以。但是需要考慮以下問(wèn)題:
無(wú)法通知(advise)Final 方法,因為他們不能被覆寫(xiě)。
你需要將CGLIB 2二進(jìn)制發(fā)行包放在classpath下面,與之相較JDK本身就提供了動(dòng)態(tài)代理
強制使用CGLIB代理需要將 <aop:config> 的 proxy-target-class 屬性設為true:
<aop:config proxy-target-class="true">...</aop:config>當需要使用CGLIB代理和@AspectJ自動(dòng)代理支持,請按照如下的方式設置 <aop:aspectj-autoproxy> 的 proxy-target-class 屬性:
<aop:aspectj-autoproxy proxy-target-class="true"/>除了在配置文件中使用 <aop:config> 或者 <aop:aspectj-autoproxy> 來(lái)聲明切面。 同樣可以通過(guò)編程方式來(lái)創(chuàng )建代理通知(advise)目標對象。關(guān)于Spring AOP API的詳細介紹,請參看下一章。這里我們重點(diǎn)介紹自動(dòng)創(chuàng )建代理。
類(lèi) org.springframework.aop.aspectj.annotation.AspectJProxyFactory 可以為@AspectJ切面的目標對象創(chuàng )建一個(gè)代理。該類(lèi)的基本用法非常簡(jiǎn)單,示例如下。請參看Javadoc獲取更詳細的信息。
// create a factory that can generate a proxy for the given target objectAspectJProxyFactory 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 aspectsfactory.addAspect(SecurityManager.class);// you can also add existing aspect instances, the type of the object supplied must be an @AspectJ aspectfactory.addAspect(usageTracker);// now get the proxy object...MyInterfaceType proxy = factory.getProxy();
到目前為止本章討論的一直是純Spring AOP。 在這一節里面我們將介紹如何使用AspectJ compiler/weaver來(lái)代替Spring AOP或者作為它的補充,因為有些時(shí)候Spring AOP單獨提供的功能也許并不能滿(mǎn)足你的需要。
Spring提供了一個(gè)小巧的AspectJ aspect library (你可以在程序發(fā)行版本中單獨使用 spring-aspects.jar 文件,并將其加入到classpath下以使用其中的切面)。 Section 6.8.1, “在Spring中使用AspectJ來(lái)為domain object進(jìn)行依賴(lài)注入” 和 Section 6.8.2, “Spring中其他的AspectJ切面” 討論了該庫和如何使用該庫。 Section 6.8.3, “使用Spring IoC來(lái)配置AspectJ的切面” 討論了如何對通過(guò)AspectJ compiler織入的AspectJ切面進(jìn)行依賴(lài)注入。 最后Section 6.8.4, “在Spring應用中使用AspectJ Load-time weaving(LTW)”介紹了使用AspectJ的Spring應用程序如何裝載期織入(load-time weaving)。
Spring容器對application context中定義的bean進(jìn)行實(shí)例化和配置。 同樣也可以通過(guò)bean factory來(lái)為一個(gè)已經(jīng)存在且已經(jīng)定義為spring bean的對象應用所包含的配置信息。 spring-aspects.jar中包含了一個(gè)annotation-driven的切面,提供了能為任何對象進(jìn)行依賴(lài)注入的能力。 這樣的支持旨在為 脫離容器管理 創(chuàng )建的對象進(jìn)行依賴(lài)注入。 Domain object經(jīng)常處于這樣的情形:它們可能是通過(guò) new 操作符創(chuàng )建的對象, 也可能是ORM工具查詢(xún)數據庫的返回結果對象。
包 org.springframework.orm.hibernate.support 中的類(lèi) DependencyInjectionInterceptorFactoryBean 可以讓Spring為Hibernate創(chuàng )建并且配置prototype類(lèi)型的domain object(使用自動(dòng)裝配或者確切命名的bean原型定義)。 當然,攔截器不支持配置你編程方式創(chuàng )建的對象而非檢索數據庫返回的對象。 其他framework也會(huì )提供類(lèi)似的技術(shù)。仍是那句話(huà),Be Pragramatic選擇能滿(mǎn)足你需求的方法中最簡(jiǎn)單的那個(gè)。 請注意前面提及的類(lèi) 沒(méi)有 隨Spring發(fā)行包一起發(fā)布。 如果你希望使用該類(lèi),需要從Spring CVS Respository上下載并且自行編譯。 你可以在Spring CVS respository下的 ‘sandbox‘ 目錄下找到該文件。
@Configurable 注解標記了一個(gè)類(lèi)可以通過(guò)Spring-driven方式來(lái)配置。 在最簡(jiǎn)單的情況下,我們只把它當作標記注解:
package com.xyz.myapp.domain;import org.springframework.beans.factory.annotation.Configurable;@Configurablepublic class Account {...}當只是簡(jiǎn)單地作為一個(gè)標記接口來(lái)使用的時(shí)候,Spring將采用和該已注解的類(lèi)型(比如Account類(lèi))全名 (com.xyz.myapp.domain.Account)一致的bean原型定義來(lái)配置一個(gè)新實(shí)例。 由于一個(gè)bean默認的名字就是它的全名,所以一個(gè)比較方便的辦法就是省略定義中的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會(huì )查找名字為"account"的bean定義,并使用它作為原型定義來(lái)配置一個(gè)新的Account對象。
你也可以使用自動(dòng)裝配來(lái)避免手工指定原型定義的名字。 只要設置 @Configurable 注解中的autowire屬性就可以讓Spring來(lái)自動(dòng)裝配了: @Configurable(autowire=Autowire.BY_TYPE) 或者 @Configurable(autowire=Autowire.BY_NAME,這樣就可以按類(lèi)型或者按名字自動(dòng)裝配了。
最后,你可以設置 dependencyCheck 屬性,通過(guò)設置,Spring對新創(chuàng )建和配置的對象的對象引用進(jìn)行校驗 (例如:@Configurable(autowire=Autowire.BY_NAME,dependencyCheck=true) )。 如果這個(gè)屬性被設為true,Spring會(huì )在配置結束后校驗除了primitives和collections類(lèi)型的所有的屬性是否都被賦值了。
僅僅使用注解并沒(méi)有做任何事情。但當注解存在時(shí),spring-aspects.jar中的 AnnotationBeanConfigurerAspect 就起作用了。 實(shí)質(zhì)上切面做了這些:當初始化一個(gè)有 @Configurable 注解的新對象時(shí),Spring按照注解中的屬性來(lái)配置這個(gè)新創(chuàng )建的對象。 要實(shí)現上述的操作,已注解的類(lèi)型必須由AspectJ weaver來(lái)織入 - 你可以使用一個(gè) build-time ant/maven任務(wù)來(lái)完成 (參見(jiàn)AspectJ Development Environment Guide) 或者使用load-time weaving(參見(jiàn) Section 6.8.4, “在Spring應用中使用AspectJ Load-time weaving(LTW)”)。
類(lèi) AnnotationBeanConfigurerAspect 本身也需要Spring來(lái)配置(獲得bean factory的引用,使用bean factory配置新的對象)。 為此Spring AOP命名空間定義了一個(gè)非常方便的標簽。如下所示,可以很簡(jiǎn)單的在application context配置文件包含這個(gè)標簽中。
<aop:spring-configured/>
如果你使用DTD代替Schema,對應的定義如下:
<beanclass="org.springframework.beans.factory.aspectj.AnnotationBeanConfigurerAspect"factory-method="aspectOf"/>
在切面配置完成 之前 創(chuàng )建的@Configurable對象實(shí)例會(huì )導致在log中留下一個(gè)warning,并且任何對于該對象的配置都不會(huì )生效。 舉一個(gè)例子,一個(gè)Spring管理配置的bean在被Spring初始化的時(shí)候創(chuàng )建了一個(gè)domain object。 對于這樣的情況,你需要定義bean屬性中的"depends-on"屬性來(lái)手動(dòng)指定該bean依賴(lài)于configuration切面。
<bean id="myService"class="com.xzy.myapp.service.MyService"depends-on="org.springframework.beans.factory.aspectj.AnnotationBeanConfigurerAspect">...</bean>
提供 @Configurable 支持的一個(gè)目的就是使得domain object的單元測試可以獨立進(jìn)行,不需要通過(guò)硬編碼查找各種倚賴(lài)關(guān)系。 如果 @Configurable 類(lèi)型沒(méi)有通過(guò)AspectJ織入, 則在單元測試過(guò)程中注解不會(huì )起到任何作用,測試中你可以簡(jiǎn)單的為對象的mock或者stub屬性賦值,并且和正常情況一樣的去使用該對象。 如果 @Configurable 類(lèi)型通過(guò)AspectJ織入, 我們依然可以脫離容器進(jìn)行單元測試,不過(guò)每次創(chuàng )建一個(gè)新的 @Configurable 對象時(shí)都會(huì )看到一個(gè)warning標示該對象不受Spring管理配置。
AnnotationBeanConfigurerAspect 通過(guò)一個(gè)AspectJ singleton切面來(lái)實(shí)現對 @Configurable 的支持。 一個(gè)singleton切面的作用域和一個(gè)靜態(tài)變量的作用域是一樣的,例如,對于每一個(gè)classloader有一個(gè)切面來(lái)定義類(lèi)型。 這就意味著(zhù)如果你在一個(gè)classloader層次結構中定義了多個(gè)application context的時(shí)候就需要考慮在哪里定義 <aop:spring-configured/> bean和在哪個(gè)classpath下放置Srping-aspects.jar。
考慮一下典型的Spring web項目,一般都是由一個(gè)父application context定義大部分business service和所需要的其他資源,然后每一個(gè)servlet擁有一個(gè)子application context定義。 所有這些context共存于同一個(gè)classloader hierarchy下,因此對于全體context,AnnotationBeanConfigurerAspect 僅可以維護一個(gè)引用。 在這樣的情況下,我們推薦在父application context中定義 <aop:spring-configured/> bean: 這里所定義的service可能是你希望注入domain object的。 這樣做的結果是你不能為子application context中使用@Configurable的domain object配置bean引用(可能你也根本就不希望那么做?。?。
當在一個(gè)容器中部署多個(gè)web-app的時(shí)候,請確保每一個(gè)web-application使用自己的classloader來(lái)加載spring-aspects.jar中的類(lèi)(例如將spring-aspects.jar放在WEB-INF/lib目錄下)。 如果spring-aspects.jar被放在了容器的classpath下(因此也被父classloader加載),則所有的web application將共享一個(gè)aspect實(shí)例,這可能并不是你所想要的。
除了 @Configurable 支持,spring-aspects.jar包含了一個(gè)AspectJ切面可以用來(lái)為那些使用了 @Transactional annotation 的類(lèi)型和方法驅動(dòng)Spring事務(wù)管理(參見(jiàn) Chapter 9, 事務(wù)管理)。 提供這個(gè)的主要目的是有些用戶(hù)希望脫離Spring容器使用Spring的事務(wù)管理。
解析@Transactional annotations的切面是AnnotationTransactionAspect。 當使用這個(gè)切面時(shí),你必須注解這個(gè)實(shí)現類(lèi)(和/或這個(gè)類(lèi)中的方法),而不是這個(gè)類(lèi)實(shí)現的接口(如果有)。 AspectJ允許在接口上注解的Java規則 不被繼承。
類(lèi)之上的一個(gè)@Transactional注解為該類(lèi)中任何public操作的執行指定了默認的事務(wù)語(yǔ)義。
類(lèi)內部方法上的一個(gè)@Transactional注解會(huì )覆蓋類(lèi)注解(如果存在)所給定的默認的事務(wù)語(yǔ)義。 具有public、protected和default修飾符的方法都可以被注解。直接注解protected和default方法是讓這個(gè)操作的執行 獲得事務(wù)劃分的唯一途徑。
對于A(yíng)spectJ程序員,希望使用Spring管理配置和事務(wù)管理支持,不過(guò)他們不想(或者不能)使用注解,spring-aspects.jar也包含了一些抽象切面供你繼承來(lái)提供你自己的切入點(diǎn)定義。 參見(jiàn) AbstractBeanConfigurerAspect 和 AbstractTransactionAspect 的Javadoc獲取更多信息。 作為一個(gè)例子,下面的代碼片斷展示了如何編寫(xiě)一個(gè)切面,然后通過(guò)bean原型定義中和類(lèi)全名匹配的來(lái)配置domian object中所有的實(shí)例:
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的時(shí)候,很自然的會(huì )想到用Spring來(lái)管理這些切面。 AspectJ runtime自身負責切面的創(chuàng )建,這意味著(zhù)通過(guò)Spring來(lái)管理AspectJ 創(chuàng )建切面依賴(lài)于切面所使用的AspectJ instantiation model(per-clause)。
大多數AspectJ切面都是 singleton 切面。 管理這些切面非常容易,和通常一樣創(chuàng )建一個(gè)bean定義引用該切面類(lèi)型就可以了,并且在bean定義中包含 ‘factory-method="aspectOf"‘ 這個(gè)屬性。 這確保Spring從AspectJ獲取切面實(shí)例而不是嘗試自己去創(chuàng )建該實(shí)例。示例如下:
<bean id="profiler" class="com.xyz.profiler.Profiler"factory-method="aspectOf"><property name="profilingStrategy" ref="jamonProfilingStrategy"/></bean>
對于non-singleton的切面,最簡(jiǎn)單的配置管理方法是定義一個(gè)bean原型定義并且使用@Configurable支持,這樣就可以在切面被AspectJ runtime創(chuàng )建后管理它們。
如果你希望一些@AspectJ切面使用AspectJ來(lái)織入(例如使用load-time織入domain object) 和另一些@AspectJ切面使用Spring AOP,而這些切面都是由Spring來(lái)管理的,那你就需要告訴Spring AOP @AspectJ自動(dòng)代理支持那些切面需要被自動(dòng)代理。 你可以通過(guò)在 <aop:aspectj-autoproxy> 聲明中使用一個(gè)或多個(gè) <include/>。 每一個(gè)指定了一種命名格式,只有bean命名至少符合其中一種情況下才會(huì )使用Spring AOP自動(dòng)代理配置:
<aop:aspectj-autoproxy><include name="thisBean"/><include name="thatBean"/></aop:aspectj-autoproxy>
Load-time weaving(LTW)指的是在虛擬機載入字節碼文件時(shí)動(dòng)態(tài)織入AspectJ切面。 關(guān)于LTW的詳細信息,請查看 LTW section of the AspectJ Development Environment Guide。 在這里我們重點(diǎn)來(lái)看一下Java 5環(huán)境下Spring應用如何配置LTW。
LTW需要定義一個(gè) aop.xml,并將其置于META-INF目錄。 AspectJ會(huì )自動(dòng)查找所有可見(jiàn)的classpath下的META-INF/aop.xml文件,并且通過(guò)定義內容的合集來(lái)配置自身。
一個(gè)基本的META-INF/aop.xml文件應該如下所示:
<!DOCTYPE aspectj PUBLIC"-//AspectJ//DTD//EN" "http://www.eclipse.org/aspectj/dtd/aspectj.dtd"><aspectj><weaver><include within="com.xyz.myapp..*"/></weaver></aspectj>
‘<include/>‘的內容告訴AspectJ那些類(lèi)型需要被納入織入過(guò)程。使用包名前綴并加上"..*"(表示該子包中的所有類(lèi)型)是一個(gè)不錯的默認設定。 使用include元素是非常重要的,不然AspectJ會(huì )查找每一個(gè)應用里面用到的類(lèi)型(包括Spring的庫和其它許多相關(guān)庫)。通常你并不希望織入這些類(lèi)型并且不愿意承擔AspectJ嘗試去匹配的開(kāi)銷(xiāo)。
希望在日志中記錄LTW的活動(dòng),請添加如下選項:
<!DOCTYPE aspectj PUBLIC"-//AspectJ//DTD//EN" "http://www.eclipse.org/aspectj/dtd/aspectj.dtd"><aspectj><weaveroptions="-showWeaveInfo-XmessageHandlerClass:org.springframework.aop.aspectj.AspectJWeaverMessageHandler"><include within="com.xyz.myapp..*"/></weaver></aspectj>
最后,如果希望精確的控制使用哪些切面,可以使用 aspects。 默認情況下所有定義的切面都將被織入(spring-aspects.jar包含了META-INF/aop.xml,定義了配置管理和事務(wù)管理切面)。 如果你在使用spring-aspects.jar,但是只希望使用配制管理切面而不需要事務(wù)管理的話(huà),你可以像下面那樣定義:
<!DOCTYPE aspectj PUBLIC"-//AspectJ//DTD//EN" "http://www.eclipse.org/aspectj/dtd/aspectj.dtd"><aspectj><aspects><include within="org.springframework.beans.factory.aspectj.AnnotationBeanConfigurerAspect"/></aspects><weaveroptions="-showWeaveInfo -XmessageHandlerClass:org.springframework.aop.aspectj.AspectJWeaverMessageHandler"><include within="com.xyz.myapp..*"/></weaver></aspectj>
在Java 5平臺下,LTW可以通過(guò)虛擬機的參數來(lái)啟用。
-javaagent:<path-to-ajlibs>/aspectjweaver.jar
更多關(guān)于A(yíng)spectJ的信息可以查看 AspectJ home page。
Eclipse AspectJ by Adrian Colyer et. al. (Addison-Wesley, 2005)全面介紹并提供了AspectJ語(yǔ)言參考。
AspectJ in Action by Ramnivas Laddad (Manning, 2003)是一本非常出色介紹AOP的書(shū)籍;全書(shū)著(zhù)重介紹了AspectJ,但也對一些通用的AOP場(chǎng)景進(jìn)行了比較深入的研究。
聯(lián)系客服