Spring AOP淺析

什麼是AOP?

AOP Aspect-oriented Programing,也就是面向切面編程,那麼什麼是面向切面編程呢?
大家對OOP一定很熟悉,也就是面向對象編程。OOP主要是爲了解決代碼的可重用行,可擴展性等而引入的。它的特徵是繼承,多態,封裝和抽象。
AOP和OOP不在一個層次上,解決的是不同的問題,兩者可以一起使用。AOP解決什麼問題呢?
我們一定有這樣的需求,在一個項目工程中,有些代碼是用來實現業務邏輯的,而有些代碼實現一些其他的功能,如日誌,安全等。通常,我們要實現日誌記錄,都是在需要的地方new一個Log對象,然後調用這個日誌對象的方法實現日誌記錄等功能。
這樣會有一個問題,業務邏輯代碼和日誌代碼混在一起,即便有依賴注入實現解耦,但這種方式依然不完美,不便於調試和修改。那麼,有沒有一種方法可以使業務邏輯的代碼和其它業務無關的代碼完全分離開呢?Spring的AOP就是實現這個功能的。

關於AOP編程,有幾個重要概念需要介紹:
- Advice
- Joint Points
- Pointcuts
- Aspects
- Weaving

Advice

切面要完成的工作就是Advice,他定義了what和when兩個方面,即什麼時候完成什麼動作。
一共有5種類型的Advice,分別是:
1. Before–在某個指定的方法執行前執行;
2. After–在指定的方法完成後執行,無論成功或失敗;
3. After-returning–在指定的方法成功返回後執行
4. After-throwing–在指定的方法拋出異常後執行;
5. Around–可以同時完成上面四個動作;

Joint Points

一個可以插入額外執行流程的位置就是一個Joint Point,它可以是一個方法被調用,一個異常被拋出,甚至是一個變量被改變。以Log爲例,每個可以添加日誌的地方就是一個Joint Point。Spring只支持方法的調用作爲Joint Point,在大部分情況下,這是夠用的,如果需要其他的比如參數的更改作爲Joint Point,可以考慮其他的AOP框架,如AspectJ。

Pointcuts

不是每個Joint Point都要插入一個advice,那些真正插入了advice,也就是真正加入了額外執行流程的地方就是一個Pointcut。雖然有很多地方可以添加日誌,但不是所有日誌都是必須的,只有那些真正添加了日誌的地方纔是Pointcut。
如果說advice定義了when和what,那麼pointcut定義了where。即在什麼地方執行額外的流程。

Aspects

一個Aspect是advice和pointcut的集合,也就是什麼時候(when)在哪兒(where)做什麼事(what)。

Weaving

weaving是將aspect編織到pointcut的過程,Spring的這個過程可以發生在不同的時段,什麼時候編織有如下幾個:
1. Compile time:在目標class(也就是提供pointcut的那個class)編譯時將aspect編織進去,這需要特殊的編譯器,AspectJ 的編譯器以這種方式工作。
2. Class load time:在目標class被加載入jvm時編織。這需要一個特殊的ClassLoader。AspectJ5 以這種方式工作。
3. Runtime:程序執行時編織,這是Spring AOP的工作方式。

Spring AOP 示例

說了這麼多,下面通過一個例子說明Spring的用法。
首先創建一個performance 接口,如下:

package demo;

public interface Performance {
    public void perform();
}
接口很簡單,只有一個perform方法,我們希望捕獲所有這個接口的實現類的perform方法的執行,然後添加相應的代碼,我們需要創建下面這樣的檢查點(pointcut),

execution(* concert.Performance.perform(..))
以上的pointcut只是簡單的捕獲了perform方法的執行,如果我們要添加更多的限制條件,可以使用AspectJ的檢查點擴展語言(AspectJ’s pointcut expression language),它有如下幾個方法:

AspectJ designator Description
args() 僅當檢查點的方法的參數和給定的類型相同時才觸發advice
@args()
execution() 限制檢查點只能是方法的執行
this()
target()
@target()
within()
@within()
@annotation 限制僅當檢查點的方法帶有某個註解時才觸發

擴展語言之間使用&&and 連接可以添加多個限制。
除此之外,還可以限制當具體哪個bean的特定方法執行時觸發advice,比如:
execution(* demo.Performance.perform(..)) && bean('hello')
這就限制只有當id爲“hello”執行perform方法時纔會觸發advice。

接着上面的例子,下面創建Aspect,編寫一個如下的class

@Aspect
public class Audience {
    @Pointcut("execution(* cap4.Performance.perform(..))")
    public void performance(){}

    @Before("performance()")
    public void silenceCellPhones() {
        System.out.println("Silencing cell phones");
    }

    @Before("performance()")
    public void takeSeats() {
        System.out.println("taking seats");
    }

    @AfterReturning("performance()")
    public void applause() {
        System.out.println("Clap Clap Clap");
    }

    @AfterThrowing("performance()")
    public void demandRefund() {
        System.out.println("Demanding a refund");
    }
}
這裏,我們使用`@Pointcut` 註解創建了一個Pointcut,捕獲所有實現了Performance接口的bean的perform方法的執行,然後分別使用三種不同的註解定義了四個方法。程序的大意是:表演開始前觀衆要關閉手機並就坐,表演結束後要鼓掌,而如果演出出現意外,需要退款。
需要指出的是,這裏我們使用`@Pointcut` 註解定義了一個切入點,也可以在每個方法頭上的註解後面的括號裏直接添加切入點,只不過上面的例子需要寫四次,爲了方便,我們使用註解定義了pointcut。

邊寫好了Audience 類後,需要在配置文件中定義bean,如下:

@Configuration
@ComponentScan(basePackageClasses = Audience.class)
@EnableAspectJAutoProxy
public class DemoConfig {

    @Bean
    public  Audience audience(){
        return new Audience();
    }
}

注意,在配置class的上方,我們使用了@EnableAspectJAutoProxy 註解開啓了auto-proxy。如果使用xml的配置方式,使用 <aop:aspectj-autoproxy /> 標籤。

爲了測試,我們編寫Concert class實現Performance接口,代碼如下:

package demo;

import org.springframework.stereotype.Component;

/**
 * Created by chyzh on 2016/3/21.
 */
@Component
public class Concert implements Performance{

    @Override
    public void perform() {
        System.out.println("singing");
    }
}

然後編寫如下的測試class:

package demo;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

/**
 * Created by chyzh on 2016/3/21.
 */
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = DemoConfig.class)
public class TestDemo {
    @Autowired
    private Performance performance;

    @Test
    public void test() {
        performance.perform();
    }

}

執行測試,可以看到如下輸出:

Silencing cell phones
taking seats
singing
Clap Clap Clap

高級特性

處理advice中的參數

上面的例子非常簡單,perform方法沒有任何參數。但如果perform方法有參數,那麼其advice能否獲得其參數呢?如果能,我們可以做更多的事。
考慮如下場景,某一天有一個演唱會,表演結束後評論家需要給出最演唱會的評價,這就非常適合使用SpringAOP實現,首先我們把Performance 接口改成下面這樣,使perform方法接受一個參數,在這裏可以是表演的節目的名字:

package demo;

/**
 * Created by chyzh on 2016/3/21.
 */
public interface Performance {
    public void perform(String name);
}

然後編寫Concert 類實現這個接口:

package demo;


import org.springframework.stereotype.Component;

/**
 * Created by chyzh on 2016/3/21.
 */
@Component
public class Concert implements Performance{

    public void perform(String name) {
        System.out.println("performing " + name);
    }
}

接着編寫評論家的Critic 類,代碼如下:

package demo;

import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;

/**
 * Created by chyzh on 2016/3/21.
 */
@Aspect
public class Critic {
    @Pointcut("execution(* demo.Performance.perform(String)) && args(name)")
    public void performance(String name){}

    @AfterReturning("performance(name)")
    public void comment(String name) {
        System.out.println("The performance  " + name + " is very good!");
    }
}

我們創建了一個Pointcut,同樣是監聽perform方法,切使用args()獲取了它的參數,然後在評論方法comment中使用額這個參數。

接着是使用java配置的配置文件:

package demo;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;

/**
 * Created by chyzh on 2016/3/21.
 */
@Configuration
@ComponentScan(basePackageClasses = Critic.class)
@EnableAspectJAutoProxy
public class DemoConfig {

    @Bean
    public Critic critic() {
        return  new Critic();
    }
}

因爲Concert bean使用了自動裝入,這裏就只有Critic bean的定義,接着測試這個例子,測試類如下:

package demo;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

/**
 * Created by chyzh on 2016/3/21.
 */
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = DemoConfig.class)
public class TestDemo {
    @Autowired
    private Performance performance;

    @Test
    public void test() {
        performance.perform("Uptown Funk");
    }
}

測試類自動裝入了一個Performance類型的bean,在test方法中調用它的perform方法,並傳入一個字符串作爲表演節目的名字。運行測試,可以看到如下的輸出:

performing Uptown Funk
The performance  Uptown Funk is very good!

可以看到aspect Critic成功獲得了傳入的參數。

有關AOP的內容介紹的差不多了,有關xml配置AOP的部分,可以參考《Spring in Action》這本書,裏面的介紹很詳細。

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