在軟件開發中,散布於應用中多處的功能被稱為橫切關注點。通常來說,這些橫切關注點從概念上是與應用的業務邏輯相分離的。把這些橫切關注點與業務邏輯相分離正是面向切面編程(AOP)所要解決的問題。DI有助於應用對象之間的解耦,而AOP可以實現橫切關注點與他們所影響的對象之間解耦。
AOP的術語
切面(Aspect)
橫切關注點可以被模塊化為特殊的類,這些類可以稱為切面(aspect)。這樣做有兩個好處:首先,現在每個關注點都集中於一個地方,而不是分散到多處代碼中;其次,服務模塊更加簡潔,因為它們主要關注業務代碼,而次要關注的代碼被移入切面中。
通知(Advice)
在AOP術語中,切面的工作就被稱為通知。除了描述切面要完成的工作,通知還解決了何時執行這個工作的問題。Spring切面可以應用5種類型的通知:
- 前置通知(Before):在目標方法被調用之前調用通知功能
- 后置通知(After):在目標方法完成之后調用通知,不關心方法的輸出是什么
- 返回通知(After-returning):在目標方法成功執行之后調用
- 異常通知(After-throwing):在目標方法拋出異常之后調用
- 環繞通知(Around):通知包裹被通知方法,在被通知的繁華調用之前和之后執行自定義的行為
連接點(Join point)
連接點是應用執行過程中能夠插入切面的一個點,這個點可以是調用方法時,拋出異常時,甚至修改一個字段時。切點代碼可以利用這些點插入到應用的正常流程中,並添加新的行為。
切點(Poincut)
一個切點並不需要通知應用的所有連接點,切點有助於縮小切面所通知的連接點的范圍。切點的定義,我們需要使用明確的類和方法名稱或者利用正則表達式來指定切點(切點表達式)
織入(Weaving)
織入是把切面應用到目標對象並創建新的代理的過程。切面在指定的連接點被織入到目標對象中。目標對象的生命周期你有多個階段可以被織入:
- 編譯期:切面在目標類編譯階段被織入。這種方式需要特殊的編譯器。
- 類加載期:切面在目標類加載到JVM時被織入。這種方式需要特殊的類加載器。
- 運行期:切面在應用運行的某個階段被織入。一般情況下,在織入切面時,AOP容器會為目標對象動態創建一個代理對象。
AspectJ這三種方式方式都支持,Spring AOP只支持在運行期織入切面。
引入(introduction)
AOP的作用就在於增強目標對象現有的屬性或方法,而引入允許我們向現有的類中添加新方法或屬性。
Spring 對AOP的支持
並不是所有AOP框架都是相同的,它們在連接點模型上可能有強弱之分,它們織入切面的方式和時機也會有不同。但是無論如何,創建切點來定義切面所織入的連接點是所有AOP框架的基本功能。Spring AOP構建在動態代理基礎上,因此,Spring對AOP的支持局限於方法攔截,這是Spring作為AOP框架的局限性。
如果AOP的需求超過了簡單的方法調用(如構造器或屬性攔截),那么就需要考慮使用AspectJ來實現切面。
Spring只支持方法級別的連接點
Spring基於動態代理,所以Spring只支持方法級別的連接點,缺少對字段連接點的支持,無法讓我們創建細顆粒度的通知,例如攔截對象字段的修改;而且它不支持構造器連接點,我們無法在bean創建的時候應用通知。
雖然方法攔截可以滿足大部分的需求,但要攔截其他,就需要利用AspectJ來補充Spring AOP的功能。
通過切點來選擇連接點
在Spring AOP中,要使用AspectJ的切點表達式語言來定義切點。AspectJ的切點指示器只有execution是實際用於執行匹配的,其他的只是限制匹配的。execution指示器是我們在編寫切點表達式時最主要使用的。
編寫切點
execution(返回類型 全限定的類.方法(參數))
例如: execution(* cn.lynu.Performance.perform(..))
表達式以"*"號開始,表明可以返回任意類型,然后我們使用全限定的類名和方法名,對於方法參數列表,我們使用兩個點號(..)表明該方法可以使用任意入參
我們還可以使用限制的指示器來匹配,例如使用execution()和within()限制切點。使用的是“&&”操作符進行連接:
execution(* cn.lynu.Performance.perform(..) && within(cn.*))
類似的還可以使用“||”運算符表示或的關系,“!”運算符表示非的關系。
但是因為“&&”在XML中有特殊含義,Spring的XML配置里面使用切點可以使用and替代“&&”,or和not分別替代“||” 和“!”.
使用Java代碼定義切面
使用AspectJ的@Aspect注解表明一個Jav類作為切面,這個類中的方法都可以使用注解來定義切面的具體行為。AspectJ使用5個注解來對應5中通知方式:
- @After 后置通知
- @Before 前置通知
- @AfterReturning 返回通知
- @AfterThrowing 異常通知
- @Around 環繞通知
所有的這些通知注解都可以使用一個切點表達式作為它的值。
@Aspect public class Audience { @Before("execution(* test04.Performance.perform(..))") public void silenceCellPhone() { System.out.println("將手機調置靜音"); } @Before("execution(* test04.Performance.perform(..))") public void taskSeats() { System.out.println("觀眾就坐"); } @AfterReturning("execution(* test04.Performance.perform(..))") public void applause() { System.out.println("鼓掌"); } @AfterThrowing("execution(* test04.Performance.perform(..))") public void demandRefund() { System.out.println("表演失敗,觀眾要求退款"); } }
如果所有的這些切點表達式都是相同的,我們可以使用@Pointcut注解定義個可重用的切點:
@Pointcut("execution(* test04.Performance.perform(..))") public void performance() {} @Before("performance()") public void silenceCellPhone() { System.out.println("將手機調置靜音"); }
performance方法是一個空方法,其本身只是作為一個標識,供@Pointcut注解依附。其實這個已經是切面的Audience,我們依然可以像其他Java類那樣使用它的方法,它的方法也可以獨立地進行測試,這與其他Java類並沒有什么不同。只是使用了@Aspect注解,並不會被視為切面,這些注解也不會解析,也不會轉換為切面的代理,還需要啟動自動代理功能。
如果使用的是JavaConfig的話,可以在配置類的類級別上使用@EnableAspectJAutoProxy注解啟用:
@Configuration @EnableAspectJAutoProxy @ComponentScan(basePackageClasses= {Performance.class}) public class Config { @Bean public Audience audience() { return new Audience(); } }
如果使用XML來裝配bean,就需要使用aop命名空間的<aop:aspect-autoproxy>元素
<!--啟用AspectJ自動代理-->
<aop:aspectj-autoproxy />
<bean class="cn.Audience"/>
不要忘了將切面聲明為一個Spring bean,不論是用JavaConfig還是XML。
雖然我們使用了AspectJ的注解來創建切面,但是這個切面依然是基於代理的,它依然是Spring基於代理的切面,仍然受限於代理方法的調用,並不能利用AspectJ所有的能力。
接下來,我們可以測試這個切面的效果了:
@RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(classes= {Config.class}) public class Test { @Autowired private Performance performance; @org.junit.Test public void test01() { performance.perform(); } }
創建環繞通知
環繞通知是最為強大的通知類型,它能夠讓所編寫的邏輯將被通知的目標方法完全包裝起來,事實上就像在一個通知方法中同時編寫前置和后置通知。
@Aspect public class Audience { @Pointcut("execution(* test04.Performance.perform(..))") public void performance() {} //環繞通知 @Around("performance()") public void watchPerformance(ProceedingJoinPoint pj) { try { System.out.println("關閉手機"); System.out.println("就坐"); //調用被通知方法 pj.proceed(); System.out.println("鼓掌"); } catch (Throwable e) { System.out.println("表演失敗,觀眾要求退款"); } } }
注意環繞通知方法的參數是ProceedingJoinPoint作為入參的,這個對象是必須的,因為需要在環繞通知方法中通過它來調用目標方法,使用的是它的proceed()方法,不要忘記調用這個方法,如果不調這個方法,則會阻塞被通知方法的調用。
處理通知中的參數
切面所通知的法拉伐確實有參數該怎么辦?如何在切面中訪問和使用傳遞給被通知方法的參數?
我們換一個有參數的切點,並改造切點表達式:
@Pointcut("execution(* cn.lynu.CompactDisc.playTrack(int)) && args(trackNumber)") public void trackPlayed(int trackNumber){} @Before("trackPlayed(trackNumber)") public void before(int trackNumber){ system.out.print(trackNumber); }
被通知的方法入參是int類型,並使用args限制器,參數的名稱是與切點方法簽名中的參數名相匹配的。這樣一來,就可以在通知方法中使用傳遞給切點方法的參數了
通過注解引入新功能
之前,我們一直是為目標對象以擁有的方法添加新功能,實際上,利用引入的概念,AOP可以為對象添加新的方法。在Spring中,切面只是實現了它們所包裹的bean所現有接口的代理。如果這些代理可以暴露新的接口,那么目標類看起來也實現了新的接口,即使底層實現類並沒有實現這些接口。但調用這個新引入的方法時,代理會把調用傳遞給實現了新接口的某個對象。
@Aspect public class EncoreableIntroducer { @DeclareParents(value="test04.Performance+",defaultImpl=DefaultEncorable.class) public static Encoreable encoreable; }
通過@DeclareParents注解,將Encoredable接口引入到Performance bean中。這個注解有三個部分組成:value屬性指定了哪種類型bean要引入該接口,標記符后面的加號表示是Performance的所有子類型,而不是Performance本身。defaultImpl屬性指定為引入功能提供實現的類。注解所標注的靜態屬性指明了要引入的接口。
接下來,我們可以測試調用這個引入的新方法:
@RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(classes= {Config.class}) public class Test { @Autowired private Performance performance; @org.junit.Test public void test01() { //需要先強轉為引入的接口類型,再調用新方法 Encoreable encoreable=(Encoreable) performance; encoreable.performEncore(); } }
運行之后,可以正常使用這個新方法,如果不通過引入的方法,直接強轉會出現ClassCaseException。
使用注解的方式真的太方便了,但是這種方式由一個明顯的缺點:必須可以看到和修改源碼。如果沒有源碼,或不想將AspectJ的注解放在代碼中,我們就需要使用XML的方式。
在XML中聲明切面
在Spring aop命名空間中,提供了多個元素用在XML中聲明切面:
AOP配置元素 | 用途 |
<aop:advisor> | 定義AOP通知器 |
<aop:after> | 定義AOP后置通知 |
<aop:after-returning> | 定義AOP返回通知 |
<aop:after-throwing> | 定義AOP異常通知 |
<aop:around> | 定義環繞通知 |
<aop:aspect> | 定義切面 |
<aop:before> | 定義前置通知 |
<aop:aspectj-autoproxy> | 這個之前就見過,是為啟用@Aspec注解 |
<aop:config> | 頂層到的AOP配置,大多數<aop:*>元素必須包裹在<aop:config>元素內 |
<aop:declare-parents> | 引入 |
<aop:pointcut> | 定義切點 |
<bean id="audience" class="test04.Audience"></bean> <aop:config> <aop:aspect ref="audience"> <aop:pointcut expression="execution(* test04.Performance.perform(..))" id="pointcut"/> <aop:before pointcut-ref="pointcut" method="silenceCellPhone"/> <aop:before pointcut-ref="pointcut" method="taskSeats"/> <aop:after-returning pointcut-ref="pointcut" method="applause"/> <aop:after-throwing pointcut-ref="pointcut" method="demandRefund"/> <aop:around pointcut-ref="pointcut" method="watchPerformance"/> </aop:aspect> </aop:config>
關於Spring AOP配置元素,注意的是大多數AOP配置元素必須在<aop:config>元素上下文內使用.這里使用<aop:pointcut>將相同的切點抽取出來,如果通知的切點不一致,在通知中使用pointcut屬性而不是pointcut-ref。<aop:pointcut>元素還可以放在<aop:config>元素范圍內,提供其他切面使用。
為通知傳遞參數
在AspectJ注解的方式中,我們可以獲得目標方法的參數,使用XML的方式也可以:
<aop:pointcut expression="execution(* test04.CompactDisc.playTrack(int)) and args(trackNumber)" id="pointcut"/>
只不過在XML中用and or not表示與或非,而不是&& || !
在XML中引入新功能
AspectJ中使用的是@DeclareParents注解,在XML中對應的就是Spring aop命名空間中的<aop:declare-parents>元素:
<bean id="audience" class="test04.Audience"></bean> <aop:config> <aop:aspect ref="audience"> <aop:declare-parents types-matching="test04.Performance+" implement-interface="test04.Encoreable" delegate-ref="myPerformance"/> </aop:aspect> </aop:config>
最后再說一點,相比較AspectJ,SpringAOP只局限與對方法的增強,AOP的功能較弱,如果需要對對於構造器,屬性等類型的切點,就需要直接使用AspectJ。