1 什麼是面向切面
這種在運行時,動態地將代碼切入到類的指定方法、指定位置上的編程思想就是面向切面的編程。
AOP是Spring提供的關鍵特性之一。AOP即面向切面編程,是OOP編程的有效補充。使用AOP技術,可以將一些系統性相關的編程工作,獨立提取出來,獨立實現,然後通過切面切入進系統。從而避免了在業務邏輯的代碼中混入很多的系統相關的邏輯——比如權限管理,事物管理,日誌記錄等等。這些系統性的編程工作都可以獨立編碼實現,然後通過AOP技術切入進系統即可。從而達到了 將不同的關注點分離出來的效果。
2 AOP術語
2.1 通知(Advice)
切面必須要完成的工作即稱爲通知。通知定義了切面是什麼以及什麼時候實用。
spring切面可以實用的5種類型通知:
- 前置通知(Before):在目標方法被調用之前調用通知功能;
- 後置通知(After):在目標方法完成之後調用通知,此時不會關心方法的輸出是什麼;
- 返回通知(After-returning):在目標方法成功執行之後調用通知;
- 異常通知(After-throwing):在目標方法拋出異常後調用通知;
- 環繞通知(Around):通知包裹了被通知的方法,在被通知的方法調用之前和調用之後執行自定義的行爲。
2.2 連接點(Join point)
我們的應用可能有數以千計的時機應用通知。這些時機被稱
爲連接點。連接點是在應用執行過程中能夠插入切面的一個點。這個點可以是調用方法時、拋出異常時、甚至修改一個字段時。切面代碼可以利用這些點插入到應用的正常流程之中,並添加新的行爲。
2.3 切點(Poincut)
切點定義了從何處切入。切點的定義會匹配通知所要織入的一個或多個連接點。通常使用明確的類和方法名稱,或是利用正則表達式定義所匹配的類和方法名稱來指定這些切點。
2.4 切面(Aspect)
切面是通知和切點的結合。通知和切點共同定義了切面的全部內容----它是什麼,在何時和何處完成其功能。
2.5 引入(Introduction)
引入允許我們向現有的類添加新方法或屬性。
2.6 織入(Weaving)
織入是把切面應用到目標對象並創建新的代理對象的過程。切面在指定的連接點被織入到目標對象中。
- 編譯期:切面在目標類編譯時被織入。這種方式需要特殊的編譯器。AspectJ的織入編譯器就是以這種方式織入切面的。
- 類加載期:切面在目標類加載到JVM時被織入。這種方式需要特殊的類加載器(ClassLoader),它可以在目標類被引入應用之前增強該目標類的字節碼。AspectJ 5的加載時織入(load-timeweaving,LTW)就支持以這種方式織入切面。
- 運行期:切面在應用運行的某個時刻被織入。一般情況下,在織入切面時,AOP容器會爲目標對象動態地創建一個代理對象。Spring AOP就是以這種方式織入切面的。
3 Spring對切面的支持
Spring提供了4種類型的AOP支持:
- 基於代理的經典Spring AOP;
- 純POJO切面;
- @AspectJ註解驅動的切面;
- 注入式AspectJ切面(適用於Spring各版本)。
前三種都是Spring AOP實現的變體,Spring AOP構建在動態代理基礎之上,因此,Spring對AOP的支持侷限於方法攔截。
4 認識切點
在Spring AOP中,要使用AspectJ的切點表達式語言來定義切點。
首先定義一個接口來作爲切點:
public interface Performance {
void perform();
}
假設我們想編寫Performance的perform()方法觸發的通
知。下面的表達式能夠設置當perform()方法執行時觸發通知的調用。
execution(* com.wtj.springlearn.aop.Performance.perform(..))
execution()指示器選擇Performance的perform()方法。方法表達式以“*”號開始,表明了不關心方法返回值的類型。然後指定了全限定類名和方法名。對於方法參數列表,使用兩個點號(…)表明切點要選擇任意的perform()方法,無論該方法的入參是什麼。
如果我們需要設置切點匹配com.wtj.springlearn.aop包,可以使用within()來限定匹配。
execution(* com.wtj.springlearn.aop.Performance.perform(..)) && within(com.wtj.springlearn.aop.*)
表示com.wtj.springlearn.aop包下任意類的方法被調用時。
使用“&&”操作符把execution()和within()指示器連接在一起形成與(and)關係(切點必須匹配所有的指示器)。類似地,我們可以使用“||”操作符來標識或(or)關係,而使用“!”操作符來標識非(not)操作。
因爲“&”在XML中有特殊含義,所以在Spring的XML配置裏面描述切點時,我們可以使用and來代替“&&”。同樣,or和not可以分別用來代替“||”和“!”。
還可以使用bean的ID來標識bean。bean()使用bean ID或bean名稱作爲參數來限制切點只匹配特定的bean。
execution(* com.wtj.springlearn.aop.Performance.perform(..)) && bean('book')
這裏表示執行perform方法時通知,但是只限於bean的ID爲book。
5 通過註解創建切面
本篇主要介紹註解方式的切面定義方式
通過@Aspect進行標註,表示該Audience不僅是一個POJO還是一個切面。類中的方法表示了切面的具體行爲。
Spring提供了五種註解來定義通知時間:
首先創建一個切面:
@Aspect
public class Audience {
//表演前 手機靜音
@Before("execution(* com.wtj.springlearn.aop.Performance.perform(..))")
public void silenceCellPhone(){
System.out.println("silence Cell Phone");
}
//表演成功-clap
@AfterReturning("execution(** com.wtj.springlearn.aop.Performance.perform(..))")
public void clap(){
System.out.println("clap clap clap");
}
//表演失敗-退款
@AfterThrowing("execution(** com.wtj.springlearn.aop.Performance.perform(..))")
public void refund(){
System.out.println("refund refund refund");
}
}
Performance的實現類:
@Component
public class PerformanceImpl implements Performance {
public void perform() {
System.out.println("the perform is good");
}
}
最後還需要開啓自動代理功能,通過JavaConfig進行配置,使用@EnableAspectJAutoProxy
標籤開啓。
@Configuration
@EnableAspectJAutoProxy
@ComponentScan
public class AudienceConfig {
@Bean
public Audience audience(){
return new Audience();
}
}
最後通過一個簡單的測試用例就可以來驗證了。
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = AudienceConfig.class)
public class PerformanceTest {
@Autowired
private Performance performance;
@Test
public void perTest(){
performance.perform();
}
}
打印結果:
silence Cell Phone
the perform is good
clap clap clap
5.1 @PointCut聲明切點
你會發現上面切面的方法中,切點的聲明都是一樣的,這種情況下可以使用@Pointcut
註解來定義切點。
@Pointcut("execution(* com.wtj.springlearn.aop.Performance.perform(..))")
public void per(){};
//表演前 手機靜音
@Before("per()")
public void silenceCellPhone(){
System.out.println("silence Cell Phone");
}
per()方法本身並不重要,該方法只是一個標識,供@PointCut註解依附。
5.2 環繞通知
環繞通知是最爲強大的通知類型。它能夠讓你所編寫的邏輯將被通知的目標方法完全包裝起來。實際上就像在一個通知方法中同時編寫前置通知和後置通知。
重寫Audience切面,使用環繞通知替代之前多個不同的前置通知和後置通知。
@Around("per()")
public void watch(ProceedingJoinPoint point) throws Throwable {
try{
System.out.println("silence Cell Phone");
point.proceed();
System.out.println("clap clap clap");
}catch (Exception e){
System.out.println("refund refund refund");
}
}
首先注意到的可能是它接受ProceedingJoinPoint作爲參數。這個對象是必須要有的,因爲你要在通知中通過它來調用被通知的方法。通知方法中可以做任何的事情,當要將控制權交給被通知的方法時,它需要調用ProceedingJoinPoint的proceed()方法。
如果不調proceed()這個方法的話,那麼你的通知實際上會阻塞對被通知方法的調用。同樣的,你也可以調用多次。
5.3 向通知中傳入參數
上面我們創建的切面都很簡單,沒有任何參數。那麼切面能訪問和使用傳遞給被通知方法的參數麼?
Performance中新增方法:
void perform(String name);
實現類:
public void perform(String name) {
System.out.println("下面請 "+name+" 開始他的表演");
}
修改Audience中的切點和切面
@Pointcut("execution(* com.wtj.springlearn.aop.Performance.perform(String)) && args(name)")
public void per(String name){};
@Around("per(name)")
public void toWatch(ProceedingJoinPoint point,String name) throws Throwable {
try{
point.proceed();
System.out.println(name +" 上場啦");
System.out.println(name +" 演出結束");
}catch (Exception e){
System.out.println("refund refund refund");
}
}
表達式args(name)
限定符,它表示傳遞給perform(String name)方法的String類型參數也會傳到通知中去,參數名與切點中的參數名相同。perform(String)
指明瞭傳入參數的類型。
然後在@Around
註解中指明切點與參數名,這樣就完成了參數轉移。
最後修改一下測試用例就完成了
@Test
public void perTest(){
performance.perform("渣渣輝");
}
打印輸出:
下面請 渣渣輝 開始他的表演
渣渣輝 上場啦
渣渣輝 演出結束
5.4 通過註解@DeclareParents引入新方法
如果我們想在一個類上新增方法,通常情況下我們會怎麼做呢?最簡單的辦法就是在此目標類上增加此方法,但是如果原目標類非常複雜,動一發而牽全身。並且有些時候我們是沒有目標類的源碼的,哪這個時候怎麼辦呢?
我們可以爲需要添加的方法建立一個類,然後建一個代理類,同時代理該類和目標類。用一個圖來表示
當引入接口的方法被調用時,代理會把此調用委託給實現了新接口的某個其他對象。
還是上面的例子,假設我們需要讓表演者跳起來。
新建Jump接口以及實現類:
public interface Jump {
void duJump();
}
public class JumpImpl implements Jump {
public void duJump() {
System.out.println("do Jump");
}
}
然後我們代理兩個類:
@Aspect
public class JumpIntroducer {
@DeclareParents(value = "com.wtj.springlearn.aop.Performance+",defaultImpl = JumpImpl.class)
public static Jump jump;
}
@DeclareParents註解由三部分組成:
- value屬性指定了哪種類型的bean要引入該接口。在本例中,也就是所有實現Performance的類型。(標記符後面的加號表示是Performance的所有子類型,而不是Performance本
身。) - defaultImpl屬性指定了爲引入功能提供實現的類。
- @DeclareParents註解所標註的靜態屬性指明瞭要引入了接
口。
通過配置將JumpIntroducer聲明
@ComponentScan
@Configuration
@EnableAspectJAutoProxy
public class JumpConfig {
@Bean
public JumpIntroducer jumpIntroducer(){
return new JumpIntroducer();
}
}
或者你也可以在JumpIntroducer類上加入@Component
註解,就可以不用聲明bean了。
最後通過測試用例進行測試:
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = JumpConfig.class)
public class PerformanceTest {
@Autowired
private Performance performance;
@Test
public void perTest(){
//類型轉換
Jump jump = (Jump) performance;
jump.duJump();
}
}
打印結果:
do Jump
作者:王同學灬
鏈接:http://www.imooc.com/article/details/id/292931
來源:慕課網