Spring使用註解式聲明切面與使用

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
來源:慕課網

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章