在軟件開發中,散佈於應用中多處的功能被稱爲橫切關注點(crosscutting concern)。一般來說,這些橫切關注點從概念上是與應用的業務邏輯相分離的(可是每每會直接嵌入到應用的業務邏輯之中)。把這些橫切關注點與業務邏輯相分離正是面向切面編程(AOP)所要解決的問題java
下圖展示了一個被劃分爲模塊的典型應用。每一個模塊的核心功能都是爲特定業務領域提供服務,可是這些模塊都須要相似的輔助功能,例如安全和事務管理正則表達式
切面提供了取代繼承和委託的方案,且在不少場景下更清晰簡潔。使用面向切面編程時,仍然在一個地方定義通用功能,可是能夠經過聲明的方式定義這個功能要以何種方式在何處應用,而無需修改受影響的類。橫切關注點能夠被模塊化爲特殊的類,這些類被稱爲切面(aspect)spring
這樣作有兩個好處:首先,如今每一個關注點都集中於一個地方,而不是分散到多處代碼中;其次,服務模塊更簡潔,由於它們只包含主要關注點(或核心功能)的代碼,而次要關注點的代碼被轉移到切面中編程
描述切面的經常使用術語有通知(advice)、切點(pointcut)和鏈接點(join point)。下圖展現了這些概念的關聯方式安全
切面的工做被稱爲通知。通知定義了切面是什麼以及什麼時候使用app
Spring切面能夠應用5種類型的通知:框架
前置通知(Before):在目標方法被調用以前調用通知功能dom
後置通知(After):在目標方法完成以後調用通知,此時不會關心方法的輸出是什麼模塊化
返回通知(After-returning):在目標方法成功執行以後調用通知工具
異常通知(After-throwing):在目標方法拋出異常後調用通知
環繞通知(Around):通知包裹了被通知的方法,在被通知的方法調用以前和調用以後執行自定義的行爲
鏈接點是在應用執行過程當中可以插入切面的一個點。這個點能夠是調用方法時、拋出異常時、甚至修改一個字段時。切面代碼能夠利用這些點插入到應用的正常流程之中,並添加新的行爲
通知定義了切面的「什麼」和「什麼時候」的話,切點定義了「何處」。切點的定義會匹配通知所要織入的一個或多個鏈接點
一般使用明確的類和方法名稱,或是利用正則表達式定義所匹配的類和方法名稱來指定這些切點。有些AOP框架容許建立動態的切點,能夠根據運行時的決策(好比方法的參數值)來決定是否應用通知
切面是通知和切點的結合。通知和切點共同定義了切面的所有內容——它是什麼,在什麼時候和何處完成其功能
引入容許咱們向現有的類添加新方法或屬性。例如,咱們能夠建立一個Auditable通知類,該類記錄了對象最後一次修改時的狀態。這很簡單,只需一個方法,setLastModified(Date),和一個實例變量來保存這個狀態。而後,這個新方法和實例變量就能夠被引入到現有的類中,從而能夠在無需修改這些現有的類的狀況下,讓它們具備新的行爲和狀態
織入是把切面應用到目標對象並建立新的代理對象的過程。切面在指定的鏈接點被織入到目標對象中。在目標對象的生命週期裏有多個點能夠進行織入:
編譯期:切面在目標類編譯時被織入。這種方式須要特殊的編譯器。AspectJ的織入編譯器就是以這種方式織入切面的
類加載期:切面在目標類加載到JVM時被織入。這種方式須要特殊的類加載器(ClassLoader),它能夠在目標類被引入應用以前加強該目標類的字節碼。AspectJ 5的加載時織入(load-time weaving,LTW)就支持以這種方式織入切面
運行期:切面在應用運行的某個時刻被織入。通常狀況下,在織入切面時,AOP容器會爲目標對象動態地建立一個代理對象。Spring AOP就是以這種方式織入切面的
Spring提供了4種類型的AOP支持:
基於代理的經典Spring AOP
純POJO切面
@AspectJ註解驅動的切面
注入式AspectJ切面(適用於Spring各版本)
Spring所建立的通知都是用標準的Java類編寫的。並且,定義通知所應用的切點一般會使用註解或在Spring配置文件裏採用XML來編寫
AspectJ與之相反。雖然AspectJ如今支持基於註解的切面,但AspectJ最初是以Java語言擴展的方式實現的。這種方式有優勢也有缺點。經過特有的AOP語言,咱們能夠得到更強大和細粒度的控制,以及更豐富的AOP工具集,可是咱們須要額外學習新的工具和語法
經過在代理類中包裹切面,Spring在運行期把切面織入到Spring管理的bean中。如圖所示,代理類封裝了目標類,並攔截被通知方法的調用,再把調用轉發給真正的目標bean。當代理攔截到方法調用時,在調用目標bean方法以前,會執行切面邏輯
Spring的切面由包裹了目標對象的代理類實現。代理類處理方法的調用,執行額外的切面邏輯,並調用目標方法
直到應用須要被代理的bean時,Spring才建立代理對象。若是使用的是ApplicationContext的話,在ApplicationContext從BeanFactory中加載全部bean的時候,Spring纔會建立被代理的對象。由於Spring運行時才建立代理對象,因此咱們不須要特殊的編譯器來織入Spring AOP的切面
經過使用各類AOP方案能夠支持多種鏈接點模型。由於Spring基於動態代理,因此Spring只支持方法鏈接點。Spring缺乏對字段鏈接點的支持,沒法讓咱們建立細粒度的通知,例如攔截對象字段的修改。並且它不支持構造器鏈接點,咱們就沒法在bean建立時應用通知。可是方法攔截能夠知足絕大部分的需求。若是須要方法攔截以外的鏈接點攔截功能,那麼能夠利用Aspect來補充Spring AOP的功能
關於Spring AOP的AspectJ切點,Spring僅支持AspectJ切點指示器(pointcut designator)的一個子集。下表列出了Spring AOP所支持的AspectJ切點指示器:
AspectJ指示器 | 描 述 |
---|---|
arg() | 限制鏈接點匹配參數爲指定類型的執行方法 |
@args() | 限制鏈接點匹配參數由指定註解標註的執行方法 |
execution() | 用於匹配是鏈接點的執行方法 |
this() | 限制鏈接點匹配AOP代理的bean引用爲指定類型的類 |
target | 限制鏈接點匹配目標對象爲指定類型的類 |
@target() | 限制鏈接點匹配特定的執行對象,這些對象對應的類要具備指定類型的註解 |
within() | 限制鏈接點匹配指定的類型 |
@within() | 限制鏈接點匹配指定註解所標註的類型(當使用Spring AOP時,方法定義在由指定的註解所標註的類裏) |
@annotation | 限定匹配帶有指定註解的鏈接點 |
在Spring中嘗試使用AspectJ其餘指示器時,將會拋出IllegalArgument-Exception異常
注:只有execution指示器是實際執行匹配的,而其餘的指示器都是用來限制匹配的。說明execution指示器是在編寫切點定義時最主要使用的指示器
package concert; public interface Performance { public void perform(); }
Performance能夠表明任何類型的現場表演,如舞臺劇、電影或音樂會。設想編寫Performance的perform()方法觸發的通知。下圖展示了一個切點表達式,這個表達式可以設置當perform()方法執行時觸發通知的調用
使用AspectJ切點表達式來選擇Performance的perform()方法:
使用execution()指示器選擇Performance的perform()方法。方法表達式以「*」號開始,代表了不關心方法返回值的類型。而後指定全限定類名和方法名。對於方法參數列表,使用兩個點號(..)代表切點要選擇任意的perform()方法,不管該方法的入參是什麼
設現需配置的切點僅匹配concert包。在此場景下,可使用within()指示器限制切點範圍:
and代替「&&」,or代替「||」,not代替「!」
Spring引入新的bean()指示器,它容許在切點表達式中使用bean的ID來標識bean。bean()使用bean ID或bean名稱做爲參數來限制切點只匹配特定的bean
執行Performance的perform()方法時應用通知,但限定bean的ID爲woodstock:
execution(* concert.Performance.perform()) and bean('woodstock')
使用非操做爲除了特定ID之外的其餘bean應用通知:
execution(* concert.Performance.perform()) and !bean('woodstock')
//Audience類:觀看演出的切面 package concert; import org.aspectj.lang.annotation.AfterReturning; import org.aspectj.lang.annotation.AfterThrowing; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Before; @Aspect public class Audience { @Before("execution(** concert.Performance.perform(..))") // 表演以前 public void silenceCellPhones() { System.out.println("Silencing cell phones"); } @Before("execution(** concert.Performance.perform(..))") // 表演以前 public void takeSeats() { System.out.println("Taking seats"); } @AfterReturning("execution(** concert.Performance.perform(..))") // 表演以後 public void applause() { System.out.println("CLAP CLAP CLAP!!!"); } @AfterThrowing("execution(** concert.Performance.perform(..))") // 表演失敗以後 public void demandRefound() { System.out.println("Demanding a refund"); } }
Audience類使用@AspectJ註解進行了標註。該註解代表Audience不只僅是一個POJO,仍是一個切面。Audience類中的方法都使用註解來定義切面的具體行爲
Spring使用AspectJ註解來聲明通知方法:
注 解 | 通 知 |
---|---|
@After | 通知方法會在目標方法返回或拋出異常後調用 |
@AfterReturning | 通知方法會在目標方法返回後調用 |
@AfterThrowing | 通知方法會在目標方法拋出異常後調用 |
@Around | 通知方法會將目標方法封裝起來 |
@Before | 通知方法會在目標方法調用以前執行 |
爲@Pointcut註解設置的值是一個切點表達式,就像以前在通知註解上所設置的那樣。經過在performance()方法上添加@Pointcut註解,實際上擴展了切點表達式語言,這樣就能夠在任何的切點表達式中使用performance()了,若是不這樣作的話,須要在這些地方使用那個更長的切點表達式
如今把全部通知註解中的長表達式都替換成了performance(),該方法的實際內容並不重要,在這裏其實是空的。其實該方法自己只是一個標識,供@Pointcut註解依附
// 經過@Pointcut註解聲明頻繁使用的切點表達式 package concert; import org.aspectj.lang.annotation.AfterReturning; import org.aspectj.lang.annotation.AfterThrowing; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Before; import org.aspectj.lang.annotation.Pointcut; @Aspect public class Audience { @Pointcut("execution(** concert.Performance.perform(..))") //定義命名的切點 public void performance(){} @Before("performance()") public void silenceCellPhones() { System.out.println("Silencing cell phones"); } @Before("execution(** concert.Performance.perform(..))") // 表演以前 public void takeSeats() { System.out.println("Taking seats"); } @AfterReturning("performance()") // 表演以後 public void applause() { System.out.println("CLAP CLAP CLAP!!!"); } @AfterThrowing("performance()") // 表演失敗以後 public void demandRefound() { System.out.println("Demanding a refund"); } }
像其餘的Java類同樣,它能夠裝配爲Spring中的bean:
@Bean public Audience audience() { return new Audience(); }
經過上述操做,Audience只會是Spring容器中的一個bean。即使使用了AspectJ註解,但它並不會被視爲切面,這些註解不會解析,也不會建立將其轉換爲切面的代理
使用JavaConfig能夠在配置類的類級別上經過使用EnableAspectJ-AutoProxy註解啓用自動代理功能:
package concert; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.EnableAspectJAutoProxy; // 啓動AspectJ自動代理 @Configuration @EnableAspectJAutoProxy @ComponentScan public class ConcertConfig { @Bean public Audience audience() { return new Audience(); } }
使用XML來裝配bean須要使用Springaop命名空間中的<aop:aspectj-autoproxy>元素
// 在XML中,經過Spring的aop命名空間啓用AspectJ自動代理 <?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" xmlns:aop="http://www.springframework.org/schema/aop" // 聲明Spring的aop命名空間 xsi:schemaLocation="http://www.springframework.org/schema/aop http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"> <context:component-scan base-package = "concert" /> <aop:aspectj-autoproxy /> // 啓動AspectJ自動代理 <bean class= "concert.Audience" /> // 聲明Audience bean </beans>
JavaConfig和XML配置,AspectJ自動代理都會爲使用@Aspect註解的bean建立一個代理,這個代理會圍繞着全部該切面的切點所匹配的bean。在這種狀況下,將會爲Concert bean建立一個代理,Audience類中的通知方法將會在perform()調用先後執行
Spring的AspectJ自動代理僅僅使用@AspectJ做爲建立切面的指導,切面依然是基於代理的。在本質上,它依然是
Spring基於代理的切面。這一點很是重要,由於這意味着儘管使用的是@AspectJ註解,但咱們仍然限於代理方法的調用。若是想利用AspectJ的全部能力,咱們必須在運行時使用AspectJ而且不依賴Spring來建立基於代理的切面
環繞通知是最爲強大的通知類型。可以讓所編寫的邏輯被通知的目標方法徹底包裝起來。實際上就像在一個通知方法中同時編寫前置通知和後置通知
// 使用環繞通知從新實現Audience切面 package concert; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Pointcut; @Aspect public class Audience { @Pointcut("execution(** concert.Performance.perform(..))") //定義命名的切點 public void performance(){} @Around("performance()") public void watchPerformance(ProceedingJoinPoint jp) { try{ System.out.println("Silencing cell phones"); System.out.println("Taking seats"); jp.proceed(); System.out.println("CLAP CLAP CLAP!!!"); } catch(Throwable e){ System.out.println("Demanding a refund"); } } }
@Around註解代表watchPerformance()方法會做爲performance()切點的環繞通知。首先接受ProceedingJoinPoint做爲參數。這個對象是必需要有的,由於在通知中須要經過它來調用被通知的方法。通知方法中能夠作任何的事情,當要將控制權交給被通知的方法時,它須要調用ProceedingJoinPoint的proceed()方法
注:調用proceed()方法。如不調該方法,那麼通知實際上會阻塞對被通知方法的調用;若不調用proceed()方法,會阻塞對被通知方法的訪問,與之相似,也能夠在通知中對它進行屢次調用
建立TrackCounter類,用來記錄每一個磁道所播放的次數,是通知playTrack()方法的一個切面。下面的程序清單展現了這個切面:
// 使用參數化的通知來記錄磁道播放的次數 package soundsystem; import java.util.HashMap; import java.util.Map; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Before; import org.aspectj.lang.annotation.Pointcut; @Aspect public class TrackCounter { private Map<Integer, Integer> TrackCounts = new HashMap<Integer, Integer>(); // 通知playTrack()方法 @Pointcut( "execution(* soundsystem.CompactDisc.playTrack(int))" + "&& args(trackNumber)" ) public void trackPlayed(int trackNumber){} @Before("trackPlayed(trackNumber)") // 在播放前,爲該磁道計數 public void countTrack(int trackNumber) { int currentCount = getPlayCount(trackNumber); trackCounts.put(trackNumber, currentCount + 1); } public int getPlayCount(int trackNumber) { return trackCounts.containsKey(trackNumber) ? trackCounts.get(trackNumber) : 0; } }
在切點表達式中聲明參數,這個參數傳入到通知方法中:
切點表達式中的args(trackNumber)限定符。代表傳遞給playTrack()方法的int類型參數也會傳遞到通知中去。參數的名稱trackNumber也與切點方法簽名中的參數相匹配
這個參數會傳遞到通知方法中,這個通知方法是經過@Before註解和命名切點trackPlayed(trackNumber)定義的。切點定義中的參數與切點方法中的參數名稱是同樣的,這樣就完成了從命名切點到通知方法的參數轉移
// 配置TrackCount記錄每一個磁道播放的次數 package soundsystem; import java.util.ArrayList; import java.List; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.EnableAspectJAutoProxy; @Configuration @EnableAspectJAutoProxy // 啓用AspectJ自動代理 public class TrackCounterConfig { @Bean public CompactDisc sgtPeppers() // CompactDisc bean { BlankDisc cd = new BlankDisc(); cd.setTitle("Sgt. Pepper's Lonely Hearts Club Band"); cd.setArtist("The Beales"); List<String> tracks = new ArrayList<String>(); tracks.add("Sgt. Pepper's Lonely Hearts Club Band"); tracks.add("With a Little Help from My Friends"); tracks.add("Lucy in the Sky with Diamonds"); tracks.add("Getting Better"); tracks.add("Fixing a Hole"); // ...other tracks omitted for brevity... cd.setTracks(tracks); return cd; } @Bean public TrackCounter trackCounter() // TrackCounter bean { return new TrackCounter(); } }
使用Spring AOP,咱們能夠爲bean引入新的方法。代理攔截調用並委託給實現該方法的其餘對象
爲示例中的全部的Performance實現引入下面的Encoreable接口:
package concert; public interface Encoreable { void performEncore(); }
藉助於AOP的引入功能,沒必要在設計上妥協或者侵入性地改變現有的實現。爲了實現該功能,建立一個新
的切面:
package concert; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.DeclareParents; @Aspect public class EncoreableIntroducer { @DeclareParents(value = "concert.Performance+", defaultImpl = DefaultEncoreable.class) public static Encoreable encoreable; }
EncoreableIntroducer是一個切面。可是它與以前所建立的切面不一樣,並無提供前置、後置或環繞通知,而是
經過@DeclareParents註解,將Encoreable接口引入到Performance bean中
@DeclareParents註解由三部分組成:
value屬性指定了哪一種類型的bean要引入該接口。在本例中,也就是全部實現Performance的類型。(標記符後面的加號表示是Performance的全部子類型,而不是Performance自己。)
defaultImpl屬性指定了爲引入功能提供實現的類。這裏指定的是DefaultEncoreable提供實現
@DeclareParents註解所標註的靜態屬性指明瞭要引入了接口。這裏所引入的是Encoreable接口
和其餘的切面同樣,須要在Spring應用中將EncoreableIntroducer聲明爲一個bean:
<bean class = "concert.EncoreableIntroducer" />
Spring的自動代理機制將會獲取到它的聲明,當Spring發現一個bean使用了@Aspect註解時,Spring就會建立一個代理,而後將調用委託給被代理的bean或被引入的實現,這取決於調用的方法屬於被代理的bean仍是屬於被引入的接口
爲演出建立一個新切面。具體來說,以切面的方式建立一個評論員的角色,他會觀看演出而且會在演出以後提供一些批評意見
// 使用AspectJ實現表演的評論員 package concert; public aspect CriticAspect{ public CriticAspect(){} pointcut performance() : execution(* perform(..)); afterReturning() : performance() { System.out.println(criticismEngine.getCriticism()); } private CriticismEngine criticismEngine; public void setCriticismEngine(CriticismEngine criticismEngine) // 注入CriticismEngine { this.criticismEngine = criticismEngine; } }
上述程序中的performance()切點匹配perform()方法。當它與afterReturning()通知一塊兒配合使用時,可讓該切面在表演結束時起做用
實際上,CriticAspect與一個CriticismEngine對象相協做,在表演結束時,調用該對象的getCriticism()方法來發表一個苛刻的評論。爲了不CriticAspect和CriticismEngine之間產生沒必要要的耦合,咱們經過Setter依賴注入爲CriticAspect設置CriticismEngine。此關係以下圖所示:
切面也須要注入。像其餘的bean同樣,Spring能夠爲AspectJ切面注入依賴
\\ 要注入到CriticAspect中的CriticismEngine實現 package com.springinaction.springidol; public class CriticismEngineImpl implements CriticismEngine { public CriticismEngineImpl(){} public String getCriticism() { int i = (int) (Math.random() * criticismPool.length) return criticismPool[i]; } // injected private String[] criticismPool; public void setCriticismPool(String[] criticismPool) { this.criticismPool = criticismPool; } }
CriticismEngineImpl實現了CriticismEngine接口,經過從注入的評論池中隨機選擇一個苛刻的評論
使用XML聲明Spring bean:
<bean id = "criticismEngine" class = "com.springinaction.springidol.CriticismEngineImpl"> <property name = "criticisms"> <list> <value>Worst performance ever!</value> <value>I laughed, I cried, then I realized I was at the wrong show.</value> <value>A must see show!</value> </list> </property> </bean>
如今有了一個要賦予CriticAspect的Criticism-Engine實現。剩下的就是爲CriticAspect裝配CriticismEngineImple。在展現如何實現注入以前,必須清楚AspectJ切面根本不須要Spring就能夠織入到咱們的應用中。若是想使用Spring的依賴注入爲AspectJ切面注入協做者,那咱們就須要在Spring配置中把切面聲明爲一個Spring配置中的<bean>。以下的<bean>聲明會把criticismEnginebean注入到CriticAspect中:
<bean class= "com.springinaction.springidol.CriticAspect" factory-method = "aspectOf"> <property name = "criticismEngine" ref = "criticismEngine" /> </bean>
Spring須要經過aspectOf()工廠方法得到切面的引用,而後像<bean>元素規定的那樣在該對象上執行依賴注入