背景概念:
1)橫切關注點:散布在應用中多處的功能稱為橫切關注點
2)通知(Advice):切面完成的工作。通知定了了切面是什么及何時調用。
5中可以應用的通知:
前置通知(Before):在目標方法被調用前調用通知功能。
后置通知(After):在目標方法完成后調用通知,此時不會關系方法輸出什么。
返回通知(After-returning):在目標方法成功執行后調用通知。
異常通知(After-throwing):在目標方法拋出異常后調用通知。
環繞通知(Around):通知包裹了被通知的方法,在被通知的方法調用之前和之后執行自定義的方法。
3)連接點(Join Point):調用通知的時機稱為連接點。
4)切點(Pointcut):通知要織入的連接點的范圍。
5)切面(Aspect):切面:通知和切點的結合。
6)引入(Introduction):允許我們向現有的類添加新方法或屬性。
7)織入(weaving):把切面應用到目標對象並創建新的代理對象的過程。
切面在指定的連接點被織入到目標對象中,在目標對象的生命周期里有多個點可以進行織入:
編譯期:切面在目標類編譯時織入。這種方式需要特殊的編譯器。AspectJ的織入編譯器就是以這種方式織入切面的。
類加載期:切面在目標類被加載到JVM時織入。這種方式需要特殊的類加載器,他可以在目標類被引入應用之前增加該目標類的字節碼。AspectJ5的加載時織入,就支持以這種方式織入。
運行期:切面在應用運行的某個時刻被織入。一般情況下,在切面被織入時,AOP容器會為目標對象動態創建代理對象,SpringAOP就是以這種方式進行織入的。
注:注解的解釋:注解本身是沒有功能的,就和XML一樣,注解和XML都是一種元數據,元數據就是解釋數據的數據,這里就是所謂的配置。
注解的功能來自用注解的這個地方
Spring對AOP的支持
SpringAOP存在的目的:解耦。
AOP可以讓一組類共享相同的行為。避免通過繼承等高耦合方式實現為類添加功能。
Spring支持4中類型的AOP支持:
1)基於代理的經典SpringAOP;
2)純POJO切面;
3)@AspectJ注解驅動的切面;
4)注入式AspectJ切面(適用於各個Spring版本)。
注:前三種都是SpringAOP實現的變體,SpringAOP構建在動態代理的基礎之上,因此,Spring對AOP的支持局限於方法攔截。
如果AOP需求超過了簡單的方法調用(如構造器或屬性攔截),那么需要使用第四種方式。
Spring通知是java編寫的
Spring的通知是POJO實現的,可以基於注解和XML實現,相對簡單便捷。
AspectJ以java擴展的方式實現的,優點:特有的AOP語言可以獲得更強大的細粒度的控制以及更豐富的AOP工作集,但學習成本大。
Spring在運行時通知對象
Spring運行時才會創建代理對象,所以我們不需要特殊的編譯器來織入SpringAOP的切面。
Spring只支持方法級別的連接點
如果需要使用除方法攔截之外的連接點攔截功能,那么我們可以利用Aspect來補充Spring AOP的功能。
通過切點來選擇連接點
注:只有execution指示器是實際執行匹配的,其他指示器都是限定匹配的,我們在編寫切點定義時最主要使用的指示器應當是:execution指示器,在此基礎上使用其他指示器來限制所匹配的切點。
編寫切點:
定義一個performance接口:
package com.spring.learn.index;
public interface Performance { public void perform(); }
則我們想在表演接口中的表演方法觸發我們的通知時,需要的切點表達式:
execution(* com.spring.learn.index.Performance.perform(..))
若我們僅僅需要的是com包下的類,可以使用within()來限制匹配:
execution(* com.spring.learn.index.Performance.perform(..)) && within(com.*)
使用 && 來表示與關系;同理 || 表示或關系;!表示非。
注在XML中使用and or not 來替換(因為符號在XML中有特殊含義)。
在切點中選擇bean
除了上述指示器外Spring還引入了bean()指示器,bean()使用bean ID或者bean名稱來作為參數限制切點只匹配特定的bean。
使用注解創建切面
已經定義了Performance接口,它是切面中目標對象。使用AspecJ注解來定義切面。
定義切面:
使用Audience類:觀看演出的切面:
@Aspect public class Audience { //這是表演之前
@Before("execution(* com.spring.learn.index.Performance.perform(..))") public void silenceCellPhones(){ System.out.println("觀眾的手機靜音"); } //表演之前
@Before("execution(* com.spring.learn.index.Performance.perform(..))") public void takeSeats() { System.out.println("觀眾落座"); } //表演之后
@AfterReturning("execution(* com.spring.learn.index.Performance.perform(..))") public void applause() { System.out.println("掌聲雷動"); } //表演失敗
@AfterThrowing("execution(* com.spring.learn.index.Performance.perform(..))") public void demandRefund() { System.out.println("要求退款"); } }
@Aspect注解,表明了Audience不僅僅是一個POJO,還是一個切面。
Audience中的方法使用注解的方式定義了通知何時調用。AspectJ中提供了五個注解來定義通知:
每一個注解都使用了切點表達式來作為他的值。但是我們的切點表達式重復使用了四次,其實我們可以只寫一次,然后使用引用的方式實現同樣的操作。
@PointCut注解能夠在一個@Aspect定義的切面內定義可重復使用的切點:
@Aspect public class Audience { //定義命名的切點
@Pointcut("execution(* com.spring.learn.index.Performance.perform(..))") public void myPointCut(){} @Before("myPointCut()") public void silenceCellPhones(){ System.out.println("觀眾的手機靜音"); } @Before("myPointCut()") public void takeSeats() { System.out.println("觀眾落座"); } @AfterReturning("myPointCut()") public void applause() { System.out.println("掌聲雷動"); } @AfterThrowing("myPointCut()") public void demandRefund() { System.out.println("要求再來一場新的表演"); } }
到此為止Audience仍只是一個普通的POJO類,即使使用了@Aspect注解也不會被視為切面,這些注解不會解析,也不會創建將其轉化為切面的代理。
需要額外的配置類,並在配置類上使用EnableAspectJ-AutoProxy 注解啟動自動代理功能:
//啟用AspectJ自動代理
@Configuration @EnableAspectJAutoProxy @ComponentScan public class AspectConfig { //聲明Audience bean
@Bean public Audience audience() { return new Audience(); } }
如果使用的是XML來裝配bean的話,需要使用Spring AOP命名空間的<aop:aspectj-autoproxy> 元素:
不論哪種方式,AspectJ自動代理都會使用@Aspect注解的bean創建一個代理,這個代理會圍繞所有該切面的切點所匹配的bean。將會為bean創建一個代理,使通知的方法在切點前后被調用。
創建環繞通知
@Aspect public class Audience { //定義命名的切點
@Pointcut("execution(* com.spring.learn.index.Performance.perform(..))") public void myPointCut(){} @Around("myPointCut()") public void watchPerformance(ProceedingJoinPoint jp){ try { System.out.println("觀眾的手機靜音"); System.out.println("觀眾落座"); jp.proceed(); System.out.println("掌聲雷動"); }catch (Throwable e) { System.out.println("退票退票"); } } }
可以看到環繞方法可以實現之前的幾個注解的所有功能。
注:ProceedingJoinPoint參數,這個參數必須有,因為要在通知中通過它來調用被通知的方法。通知方法可以做任何事,當要將控制權交給被通知的方法時,需要調用ProceedingJoinPoint的proceed()方法。
注: 一定要調用proceed()方法,否則通知將會阻塞被通知方法的調用。
實際上你也可以對被通知方法進行多次調用。這一般是為了實現重試邏輯。
處理通知中的參數
場景:磁帶中不同的磁道有多種歌曲,調用playTrack()方法可以實現播放。要記錄每個磁道被播放的次數,使用切面來完成:
通過上述的方式可以將切面上的參數同步到通知上。
通過注解引入新功能
通過引入的AOP概念,切面可以為Spring bean添加新的方法。
我們為Performance接口引入下面的接口:
public interface Encoreable { void perforEncore(); }
則定義的切面如下:
@Aspect public class EncoreableIntroducer { @DeclareParents(value = "com.spring.learn.index.Performance+", defaultImpl = DefaultEncoreable.class) public static Encoreable encoreable; }
注:和其他切面一樣,我們需要將在Spring應用中將Encoreableintroducer聲明為一個bean:
//啟用AspectJ自動代理
@Configuration @EnableAspectJAutoProxy @ComponentScan public class AspectConfig { //聲明Audience bean
@Bean public Audience audience() { return new Audience(); } //聲明EncoreableIntroducer bean
@Bean public EncoreableIntroducer encoreableIntroducer() { return new EncoreableIntroducer(); } }
或:
由於目前已經不接觸XML配置了。
所以XML配置與AspectJ的更強大面向切面實現,等過一陣再來補充。
AspectJ的全面學習博客地址。
AspectJ切入點語法詳解地址。