在軟件開發中,散佈於應用中多處的功能被稱爲橫切關注點。把這些橫切關注點與業務邏輯相分離正是面向切面編程(AOP)所要解決的問題。
依賴注入(DI)有助於應用對象之間的解耦,而AOP可以實現橫切關注點與它們所影響的對象之間的解耦。
1.什麼是面向切面編程
在使用面向切面編程時,我們仍然在一個地方定義通用功能,但是可以通過聲明的方式定義這個功能要以何種方式在何處應用,而無需修改受影響的類。
橫切關注點可以被模塊化爲特殊的類,這些類被稱爲切面(aspect)。
描述切面的常用術語有通知(advice)、切點(pointcut)、連接點(join point)。
在AOP術語中,切面的工作被稱爲通知。通知定義了切面是什麼以及何時使用。除了描述切面要完成的工作,通知還解決了何時執行這個工作的問題。
Spring切面可以應用5種類型的通知:
- 前置通知(Before):在目標方法被調用之前執行;
- 後置通知(After):在目標方法完成之後調用通知,此時不會關心方法的輸出是什麼;
- 返回通知(After-returning):在目標方法成功執行之後調用通知;
- 異常通知(After-throwing):在目標方法拋出異常之後調用通知;
- 環繞通知(Around):通知包裹了被通知的方法,在被通知的方法調用之前和調用之後執行自定義的行爲。
連接點
連接點是在應用執行過程中能夠插入切面的一個點。這個點可以是調用方法時、拋出異常時、甚至修改一個字段時。切面代碼可以利用這些點插入到應用的正常流程之中,並添加新的行爲。
切點(Poincut)
如果說通知定義了切面的“什麼”和“何時”的話,那麼切點就定義了“何處”。切點的定義會匹配所要織入的一個或多個連接點。我們通常使用明明確的類和方法名稱,或是利用正則表達式定義所匹配的類和方法名稱來指定這些切點。
切面(Aspect)
切面是通知和切點的結合。通知和切點共同定義了切面的全部內容——它是什麼、在何時何處完成其功能。
引入(Introduction)
引入允許我們向現有的類添加新方法或屬性。
織入(Weaving)
織入是把切面應用到目標對象並創建新的代理對象的過程。切面在指定的連接點唄織入到目標對象中。在目標對象的生命週期裏有多個點可以進行織入:
- 編譯期:切面在目標類編譯時被織入。這種方式需要特殊的編譯器。AspectJ的織入編譯器就是一這種方法織入切面的。
- 類加載期:切面在目標類加載到JVM時被織入。這種方式需要特殊的類加載器(ClassLoader),它可以在目標類被引入應用之前增強該目標了IDE字節碼。AspectJ 5的加載時織入就支持以這種方式織入切面。
- 運行期:切面在應用運行的某個時刻被織入。一般情況下,在織入切面時,AOP容器會爲目標對象動態創建一個代理對象。Spring AOP就是以這種方式織入切面。
Spring對AOP的支持
創建切點來定義切面所織入的連接點是AOP框架的基本功能。
Spring提供了4中類型的AOP支持:
- 基於代理的經典Spring AOP;
- 純POJO切面;
- @AspectJ註解驅動的切面;
- 注入式AspectJ切面(適用於Spring各版本)。
前三種都是Spring AOP實現的變體,Spring AOP構建在動態代理基礎之上,因此,Spring對AOP的支持侷限於方法攔截。
Spring所創建的通知都是用標準的Java類編寫的。
通過在代理類中包裹切面,Spring在運行期把切面織入到Spring管理的bean中。Spring的切面包裹了目標對象的代理類實現。代理類處理方法的調用,執行額外的切面邏輯,並調用目標方法。直到應用需要被代理的bean時,Spring才創建代理對象。因爲Spring運行時才創建代理對象,因此我們不需要特殊的編譯器來織入Spring AOP的切面。
Spring只支持方法級別的連接點。
2.通過切點來選擇連接點
在Spring AOP中,要使用AspectJ的切點表達式語言來定義切點。關於Spring AOP的AspectJ切點,最重要的一點就是Spring僅支持AspectJ切點指示器的一個子集。因爲Spring是基於代理的,而某些切點表達式與基於代理的AOP無關。
下圖爲Spring AOP所支持的AspectJ切點指示器:
execution指示器執行匹配,其他指示器來限制匹配的切點。
除此之外,Spring還引入一個新的bean()指示器,它允許我們在切點表達式中使用bean的ID來標示bean。
例如,我們要使用AspectJ切點表達式來選擇一個名爲Performance類中的perform()方法:
execution(* concert.Perfoemance.perform(..))
/**我們使用execution()指示器來選擇Performance的perform()方法。方法表達式以“ * ”號開始,表明我們不關心方法返回值的類型。然後,指定了全限定類名和方法名。對於參數列表,我們使用兩個點號(..)表明切點要選擇任意的perform()方法,無論該方法的入參是什麼。**/
/**如果我們需要配置的切點僅匹配concert包,我們可以使用within()指示器來限制。**/
execution(* concert.Perfoemance.perform(..)) && within(concert.*)
因爲“&”在XML中有特殊含義,所以Spring的XML配置裏面描述切點時,我們可以使用and、or、not來代替“&&”、“||”、“!”。
execution(* concert.Performance.perform(..)) and bean('woodstock')
3.使用註解創建切面
如果我們把Performance類中perform()方法看成是一個表演的話,那麼下面這個切面定義的是在表演之前、表演之後、以及表演失敗之後觀衆的反映(即程序的輸出)。
@Aspect //表明Audience是一個切面
public class Audience {
//表演之前
@Before("execution(** concert.Performance.perform(..))")
public void silenceCellPhone() {
System.out.println("將手機調至靜音狀態");
}
//表演之前
@Before("execution(** concert.Performance.perform(..))")
public void takeSeats() {
System.out.println("就坐");
}
//表演之後
@AfterReturning("execution(** concert.Performance.perform(..))")
public void applause() {
System.out.println("鼓掌喝彩");
}
//表演之後
@AfterThrowing("execution(** concert.Performance.perform(..))")
public void demandrefund() {
System.out.println("要求退款");
}
}
此外,@Pointcut註解能夠在一個@Aspect切面內定義可重用的切點
@Aspect //表明Audience是一個切面
public class Audience {
//定義命名的切點
@Pointcut("execution(** concert.Performance.perform(..))")
public void performance() {}
//表演之前
@Before("performance()")
public void silenceCellPhone() {
System.out.println("將手機調至靜音狀態");
}
//表演之前
@Before("performance()")
public void takeSeats() {
System.out.println("就坐");
}
//表演之後
@AfterReturning("performance()")
public void applause() {
System.out.println("鼓掌喝彩");
}
//表演之後
@AfterThrowing("performance()")
public void demandrefund() {
System.out.println("要求退款");
}
}
在這裏,Audience仍然是一個Java類,只不過它通過註解表明會作爲切面使用而已。同其他類一樣,可以裝配爲Spring中bean。
除此之外,你還需要對AspectJ註解進行配置,不然這些代碼不會生效。如果使用JavaConfig的話,可以在配置類的類級別上通過使用@EnableAspectJAutoProxy註解啓用自動代理:
@Configuration
@EnableAspectJAutoProxy //啓用AspectJ自動代理
@Component
public class ConcertConfg{
//聲明Audience bean
@Bean
public Audience audience() {
return new Audience();
}
}
如果使用xml裝配bean的話,那麼需要使用Spring aop命名空間的<aop:aspectj-autoproxy>
元素:
<beans
...
記得加命名空間哦>
<context:component-scan base-package="concert" />
<aop:aspectj-autoproxy>
<bean class="concert.Audience" />
</beans>
需要記住的是,Spring的AspectJ自動代理僅僅使用@AspectJ作爲創建切面的指導,切面依然是基於代理的。在本質上,它依然是Spring基於代理的切面。這一點非常重要,因爲這意味着儘管使用的是@AspectJ註解,但我們仍然限於代理方法的調用。如果想利用AspectJ的所有能力,我們必須在運行時使用AspectJ並且不依賴Spring來創建基於代理的切面。
環繞通知是最爲強大的通知類型。它能夠讓你所編寫的邏輯將被通知的目標方法完全包裝起來。實際上就像在一個通知方法彙總同時編寫前置通知和後置通知。
@Aspect //表明Audience是一個切面
public class Audience {
//定義命名的切點
@Pointcut("execution(** concert.Performance.perform(..))")
public void performance() {}
@Around("performance()")
public void watchPerformance(ProceedingJoinPoint jp) {
try {
System.out.println("將手機調至靜音狀態");
System.out.println("就坐");
jp.proceed();//調用被通知的方法
System.out.println("鼓掌喝彩");
}catch (Throwable e) {
System.out.println("要求退款");
}
}
}
ProceedingJoinPoint 對象參數是必須的。通知中通過他來調用被通知的方法。
處理通知中的參數
@Aspect //表明Audience是一個切面
public class Audience {
//定義命名的切點
@Pointcut("execution(** concert.Performance.perform(String))"
+"&& args(songName)")
public void performance(String songName) {}
@Before("performance(songName)")
public void watchPerformance(String songName) {
System.out.println("演唱的歌曲是"+songName);
}
}
這樣就可以將方法中的參數傳達到通知中。
通過註解引入新功能
當引入接口的方法被調用時,代理會把此調用委託給實現了新接口的某個其他對象。也就是說,一個bean的實現被拆分到多個類中。
爲了實現該功能,我們需要創建一個新的切面:
@Aspect
public class EncoreableIntroducer {
@DeclareParents(value="concert.Performance+",
defaultImpl=DefaultEncoreable.class)
public static Encoreable encoreable;
}
通過@DeclareParents註解,將Encoreable 接口引入到Performance bean中。
@DeclareParents註解由三部分組成:
- value屬性指定了哪種類型的bean要引入該接口。在本例中,也就是所有顯示Performance的類型。(標記符後面的加號表示是Performance的所有子類型,而不是Performance本身。)
- defaultImpl屬性指定了爲引入功能提供實現的類。在這裏,我們指定的是DefaultEncoreable提供實現。
- @DeclareParents註解所標註的靜態屬性指明瞭要引入的接口。在這裏,我們所引入的是Encoreable 接口。
4.在XML中聲明切面
在Spring的aop命名空間中,提供了多個元素用來在XML中聲明切面
去掉Audience所有的Aspect註解
public class Audience {
public void silenceCellPhone() {
System.out.println("將手機調至靜音狀態");
}
public void takeSeats() {
System.out.println("就坐");
}
public void applause() {
System.out.println("鼓掌喝彩");
}
public void demandrefund() {
System.out.println("要求退款");
}
}
聲明前置通知和後置通知
<aop:config>
<!-- 引用audience Bean -->
<aop:aspect ref="audience">
<aop:before
pointcut="execution(** concert.Performance.perform(..))"
method="silenceCellPhone" />
<aop:before
pointcut="execution(** concert.Performance.perform(..))"
method="takeSeats" />
<aop:after-returning
pointcut="execution(** concert.Performance.perform(..))"
method="applause" />
<aop:after-throwing
pointcut="execution(** concert.Performance.perform(..))"
method="demandrefund" />
</aop:aspect>
</aop:config>
使用<aop:pointcut>
定義切點
<aop:config>
<!-- 引用audience Bean -->
<aop:aspect ref="audience">
<!-- 定義切點 -->
<aop:pointcut
id="performance"
expression="execution(** concert.Performance.perform(..))" />
<aop:before
pointcut-ref="performance"
method="silenceCellPhone" />
<aop:before
pointcut-ref="performance"
method="takeSeats" />
<aop:after-returning
pointcut-ref="performance"
method="applause" />
<aop:after-throwing
pointcut-ref="performance"
method="demandrefund" />
</aop:aspect>
</aop:config>
聲明環繞通知
public class Audience {
public void watchPerformance(ProceedingJoinPoint jp) {
try {
System.out.println("將手機調至靜音狀態");
System.out.println("就坐");
jp.proceed();//調用被通知的方法
System.out.println("鼓掌喝彩");
}catch (Throwable e) {
System.out.println("要求退款");
}
}
}
<aop:config>
<!-- 引用audience Bean -->
<aop:aspect ref="audience">
<!-- 定義切點 -->
<aop:pointcut
id="performance"
expression="execution(** concert.Performance.perform(..))" />
<!-- 聲明環繞通知 -->
<aop:around
pointcut-ref="performance"
method="watchPerformance" />
</aop:aspect>
</aop:config>
爲通知傳遞參數
public class Audience {
//要聲明爲前置通知的方法
public void watchPerformance(String songName) {
System.out.println("演唱的歌曲是"+songName);
}
}
<aop:config>
<!-- 引用audience Bean -->
<aop:aspect ref="audience">
<!-- 定義切點 -->
<aop:pointcut
id="performance"
expression="execution(** concert.Performance.perform(String))
and args(songName)" />
<aop:before
pointcut-ref="performance"
method="watchPerformance" />
</aop:aspect>
</aop:config>
通過切面引入新的功能
<aop:aspect>
<aop:declare-parents
types-matching="concert.Performance+"
implement-interface="concert.Encoreable"
default-impl="concert.DefaultEncoteable"
/>
</aop:aspect>