《spring 實戰 第四版》第四章 面向切面的spring

spring 的面向切面

面向切面編程AOP

相關概念

  1. 切面(Aspect): 一個關注點的模塊化,這個關注點可能會橫切多個對象。事務管理是J2EE應用中一個關於橫切關注點的很好的例子。由通知(什麼時候,做什麼)和切入點(在什麼地方)組合而成。
  2. 連接點(Joinpoint): 在程序執行過程中某個特定的點,比如某方法調用的時候或者處理異常的時候。 在Spring AOP中,一個連接點 總是 代表一個方法的執行。
  3. 通知(Advice): 在切面的某個特定的連接點(Joinpoint)上執行的動作。
  4. 切入點(Pointcut): 匹配連接點(Joinpoint)的斷言,也就是定義了切面要切入的連接點。通知和切入點表達式關聯,並在滿足這個切入點的連接點上運行(例如,當執行某個特定名稱的方法時)。 切入點表達式如何和連接點匹配是AOP的核心:Spring缺省使用AspectJ切入點語法。
  5. 引入(Introduction): (也被稱爲內部類型聲明(inter-type declaration))。聲明額外的方法或者某個類型的字段。 Spring允許引入新的接口(以及一個對應的實現)到任何被代理的對象。 例如,你可以使用一個引入來使bean實現 IsModified 接口,以便簡化緩存機制。
  6. 目標對象(Target Object): 被一個或者多個切面(aspect)所通知(advise)的對象。也有人把它叫做 被通知(advised) 對象。 既然Spring AOP是通過運行時代理實現的,這個對象永遠是一個 被代理(proxied) 對象。
  7. AOP代理(AOP Proxy): AOP框架創建的對象,用來實現切面契約(aspect contract)(包括通知方法執行等功能)。 在Spring中,AOP代理可以是JDK動態代理或者CGLIB代理。 注意:Spring 2.0最新引入的基於模式(schema-based)風格和@AspectJ註解風格的切面聲明,對於使用這些風格的用戶來說,代理的創建是透明的。
  8. 織入(Weaving): 把切面(aspect)連接到其它的應用程序類型或者對象上,並創建一個被通知(advised)的對象。 這些可以在編譯時(例如使用AspectJ編譯器),類加載時和運行時完成。 Spring和其他純Java AOP框架一樣,在運行時完成織入。

通知的五種類型

  • 前置通知(Before advice): 在某連接點(join point)之前執行的通知,但這個通知不能阻止連接點前的執行(除非它拋出一個異常)。
  • 返回後通知(After returning advice): 在某連接點(join point)正常完成後執行的通知:例如,一個方法沒有拋出任何異常,正常返回。
  • 拋出異常後通知(After throwing advice): 在方法拋出異常退出時執行的通知。
  • 後通知(After (finally) advice): 當某連接點退出的時候執行的通知(不論是正常返回還是異常退出)。
  • 環繞通知(Around Advice): 包圍一個連接點(join point)的通知,如方法調用。這是最強大的一種通知類型。 環繞通知可以在方法調用前後完成自定義的行爲。它也會選擇是否繼續執行連接點或直接返回它們自己的返回值或拋出異常來結束執行。

spring AOP

  1. spring AOP是構建在動態代理的基礎上,並且該支持僅限於方法攔截。
  2. 如圖示,代理類封裝包含了目標bean,當外部調用了切入點,即目標對象的某個方法,代理類攔截該調用,執行切面邏輯(即通知),再將其轉發給目標對象調用方法。(PS:僅支持方法連接點,不提供字段和構造器的接入點)
    spring AOP 原理

通過切點選擇連接點

spring支持的切點指示器

切點指示器

編寫切點

  • 當perform()方法執行時觸發通知

    在這裏插入圖片描述
execution(* concert.Performance.perform())
  • 加上限制條件 within() 和 與或判斷(&& 、 || !以及xml使用的 and、or、not)

    在這裏插入圖片描述

切點中選擇bean

  • 使用bean()方法,通過bean ID 或者bean 名稱來匹配bean
execution(* concert.Performance.perform())
    and bean('woodstock')
匹配ID爲woodstock的bean

使用註解

目標對象

接口 Performance.java

public interface Performance {
    public void perform();
}

目標類 Concert.java

@Component
public class Concert implements Performance {

    public void perform() {
        System.out.println("Performing...");
    }

}

定義切面

Audience.java

@Component
@Aspect
public class Audience {

    @Before("execution(* com.zexing.aspectj.Performance.perform(..))")//表演之前手機靜音
    public void silenceCellPhones(){
        System.out.println("silencing cell phones");
    }

    @Before("execution(* com.zexing.aspectj.Performance.perform(..))")//表演之前坐好位置
    public void takeSeats(){
        System.out.println("Taking seats");
    }

    @AfterReturning("execution(* com.zexing.aspectj.Performance.perform(..))")//表演成功後鼓掌吶喊
    public void applause(){
        System.out.println("CLAP CLAP CLAP!");
    }

    @AfterThrowing("execution(* com.zexing.aspectj.Performance.perform(..))")//表演失敗後要求退款
    public void demandRefun(){
        System.out.println("Demanding a refun");
    }
    
}
  • @Aspect

將POJO類定義爲一個切面(如Audience類)

  • @Before

目標方法之前執行註解的通知方法(如在perform()之前執行silenCellPhones()和takeSeats())

  • @After

目標方法返回或拋出異常後執行通知方法

  • @AfterReturning

目標方法返回後執行

  • @AfterThrowing

目標方法拋出異常後執行

  • @Around

通知方法將目標方法封裝

可以使用@Pointcut 進行重構,抽出重複的切點表達式

    @Pointcut("execution(* com.zexing.aspectj.Performance.perform(..))")
    public void performance(){
    
    }

    @Before("performance()")//表演之前手機靜音
    public void silenceCellPhones(){
        System.out.println("silencing cell phones");
    }

配置類或者xml文件啓用自動代理

  • 註解方式

啓動後這裏spring容器纔會將audience bean 創建爲切面

@Configuration
@EnableAspectJAutoProxy     //啓用自動代理
@ComponentScan
public class ConcertConfig {
}

  • xml方式
    <context:component-scan base-package="com.zexing.aspectj" />

    <aop:aspectj-autoproxy />

    <bean id="audience" class="com.zexing.aspectj.Audience" />

AspecJ自動代理都會使用@Aspect註解的bean創建一個代理,而這個代理會圍繞着所有該切面的切點所匹配的bean。

測試

測試類 ConcertConfigTest.java

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = ConcertConfig.class)
//@ContextConfiguration(locations = "classpath*:app.xml")
public class ConcertConfigTest {

    @Autowired
    private Performance concert;

    @Test
    public void concertStart(){

        assertNotNull(concert);
        concert.perform();
    }
}

結果

silencing cell phones
Taking seats
Performing...
CLAP CLAP CLAP!

環繞通知

Audience.java

@Aspect
public class Audience {

    @Pointcut("execution(* com.zexing.aspectj.Performance.perform(..))")
    public void performance(){
    }
    
    @Around("performance()")
    public void watchPerformance(ProceedingJoinPoint jp) {
        try {
            System.out.println("手機靜音");
            System.out.println("得到座位");
            jp.proceed();
            System.out.println("鼓掌!!!");
        } catch (Throwable e) {
            System.out.println("這演的啥啊!退票");
        }
    }
    
}

這裏的 @Around註解就聲明瞭watchPerformance()方法成爲了一個環繞通知切點。他的運行結果與上例使用@Before,@After等等的效果相同。可以發現watchPerformance()方法中給出了一個PreceedingJoinPoint類型的參數,這個是必須的,因爲你需要告訴該切點中的任務相對與你的工作所處的位置。使用ProceedingJoinPoint’s proceed()方法將你的任務放在你想要的位置

測試結果

手機靜音
得到座位
Performing...
鼓掌!!!

處理通知中的參數

通知裏面獲取被通知方法的參數並進行處理

在CDPlayer播放中,記錄磁道的播放次數與播放本身是不同的關注點,因此不應該屬於playTrack()方法,故統計次數是切面的任務,切面獲取磁道名稱並記錄其出現的次數。

目標類 CDPlayer.java

@Component
public class CDPlayer {
    /**
     *
     * Created by Joson on 2/25/2019.
     */
    @Autowired
    private CompactDisc cd;

    public void playTrack() {
        System.out.println(cd);
    }

    public void play(int index) {
        System.out.println(cd.getTracks().get(index-1) + " is playing....");
    }
}

CompactDisc.java

@Component
public class CompactDisc {
    /**
     *
     * Created by Joson on 2/25/2019.
     */
    private String title;
    private List<String> tracks;

    public CompactDisc(){} // 如果自定義了構造方法,必須顯式地定義默認構造方法,否則 Spring 無法實現自動注入

    public CompactDisc(String title, List<String> tracks) {
        this.title = title;
        this.tracks = tracks;
    }

    public List<String> getTracks() {
        return tracks;
    }
}

切面類 TrackCounter.java

@Aspect
@Component
public class TrackCounter {
    /**
     *
     * Created by Joson on 2/25/2019.
     */
    private Map<Integer, Integer> map = new HashMap<Integer, Integer>();

    // execution(...play(int))的 int 是被通知的方法的獲得的參數的類型
    // 通過 && args(trackNumber) 表示被通知方法的實參也將傳遞給通知方法
    @Pointcut("execution(* com.zexing.aspectj.trackPlayCount.CDPlayer.play(int)) && args(trackNumber)")
    public void pointcut(int trackNumber) { // 形參名必須和 args()一致
    }

    // @Around("trackPlayed(trackNumber)")中的 "trackNumber"
    // 不必與 args() 相同 ,可以另外命名的,但必須保證本通知內一致即可。
    @Around("trackPlayed(trackNumber)")
    public void countTrack(ProceedingJoinPoint pjp, int trackNumber) {
        try {
            pjp.proceed(); //調用被通知方法
            // 每次調用被通知方法成功之後,音軌的播放次數+1
            int currentCount = getTrackCurrentCount(trackNumber);
            map.put(trackNumber, ++currentCount);
        } catch (Throwable e) {
            // 調用出現異常後的代碼
            System.out.println("CDPlayer 播放異常!");
        }
    }

    public int getTrackCurrentCount(int trackNumber) {
        return map.containsKey(trackNumber) ? map.get(trackNumber) : 0;
    }
}

這裏我們用 @PointCut 註解了一個帶有int參數的方法 trackPlayed() ,將其定義爲一個切點。在 @PointCut 中我們去匹配 CompactDisc 類中的 play() 方法,並且定製了其參數類型爲int,這裏當這樣限制了被切入方法的類型,就必須同時使用 args() 標識符來指明變量的名字。

配置類 CompactDiscConfig.java

@Configuration
@EnableAspectJAutoProxy     //啓用自動代理
@ComponentScan
public class CompactDiscConfig {
    /**
     *
     * Created by Joson on 2/25/2019.
     */
    /*
    由於CompactDisc類中有兩個構造方法,Spring在匹配 bean 時出現衝突,所以必須顯式指定一個bean。
    否則將出現異常,大概就是說,我只一個 bean 就夠了,但給我兩個,叫我怎麼選啊:
    No qualifying bean of type 'com.san.spring.aop.CD' available:
    expected single matching bean but found 2: CD,setCD
    */
    @Bean
    @Primary //首選bean
    public CompactDisc compactDisc(){
        String title = "唐朝";
        List<String> tracks = new ArrayList<String>();
        tracks.add("夢迴唐朝");
        tracks.add("太陽");
        tracks.add("九拍");
        tracks.add("天堂");
        tracks.add("選擇");
        tracks.add("飛翔鳥");
        tracks.add("世紀末之夢");
        tracks.add("月夢");
        tracks.add("不要逃避");
        tracks.add("傳說");

        return new CompactDisc(title, tracks);
    }
}

測試類

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = CompactDiscConfig.class)
public class CompactDiscConfigTest {

    @Autowired
    private CDPlayer player;

    @Autowired
    private TrackCounter trackCounter;

    @Test
    public void testTrackCounter() {

        // 執行 play()一次,該音軌的播放次數加1
        player.play(1);
        player.play(1);
        player.play(2);
        player.play(3);
        player.play(5);

        // 與期待的次數一致,則測試通過
        assertEquals(2, trackCounter.getTrackCurrentCount(1));
        assertEquals(1, trackCounter.getTrackCurrentCount(2));
        assertEquals(1, trackCounter.getTrackCurrentCount(3));
        assertEquals(0, trackCounter.getTrackCurrentCount(4));
        assertEquals(1, trackCounter.getTrackCurrentCount(5));
    }

}

通過註解引入新功能

上面我們都是在方法上新增功能方法,同樣的,切面可以實現類級別上新增方法。
如圖示,切面引入新方法流程

在這裏插入圖片描述

當引入接口的方法被調用時,代理會把此調用委 託給實現了新接口的某個其他對象。實際上,一個bean的實現被拆分到了多個類中。

場景描述

在一場音樂會中,添加潮劇表演的環節,但在原來的音樂會表演類(Concert.java)中沒有這個方法,所以需要爲這個表演類再新加一個方法。

  1. 創建表演者接口
public interface Performer {
    public void performAdded();
}
  1. 指派一個表演者去表演潮劇
@Component
public class OnePerformer implements Performer {

    /**
     *
     * Created by Joson on 2/26/2019.
     */
    public void performAdded() {
        System.out.println("潮劇表演...");
    }
}
  1. 通過主持人(切面類)的介紹
@Component
@Aspect
public class PerformIntroducer {
    /**
     *
     * Created by Joson on 2/26/2019.
     */
    //通過 PerformIntroducer 的介紹,潮劇表演者進入音樂會進行加場表演
    @DeclareParents(value = "com.zexing.aspectj.concert.Performance+",defaultImpl = OnePerformer.class )
    public static Performer performer;
}

通過@DeclareParents註解,將Performer接口引入 到Performance bean中。

@DeclareParents註解由三部分組成:

  • value屬性指定了哪種類型的bean要引入該接口。在本例中,也 就是所有實現Performance的類型。(標記符後面的加號表示 是Performance的所有子類型,而不是Performance本 身。)
  • defaultImpl屬性指定了爲引入功能提供實現的類。在這裏, 我們指定的是OnePerformer提供實現。
  • @DeclareParents註解所標註的靜態屬性指明瞭要引入的接 口。在這裏,我們所引入的是Performer接口。
  1. 開始音樂會
    @Test
    public void addPerform(){
        assertNotNull(concert);
        concert.perform();
        System.out.println("--下面進行臨時加場表演--");
        Performer p = (Performer) concert;
        p.performAdded();
    }
  1. 音樂會
手機靜音
得到座位
silencing cell phones
Taking seats
Performing...
鼓掌!!!
CLAP CLAP CLAP!
--下面進行臨時加場表演--
潮劇表演...

在Spring中,註解和自動代理提供了一種便利的方式來創建切面,它非常簡單,並且只設計最少的Spring配置,但是,面向註解的切面有一個明顯的不足點:你必須能夠爲通知類添加註解,爲了做到這一點,必須要有源碼。

使用XML

  • xml中切面的聲明標籤
  1. aop:advisor :定義AOP通知器
  2. aop:after :定義AOP後置通知
  3. aop:after-returning :定義AOP返回通知
  4. aop:after-throwing :定義AOP異常通知
  5. aop:around :定義AOP環繞通知
  6. aop:aspect :定義一個切面
  7. aop:aspectj-autoproxy :啓用@AspectJ註解
  8. aop:before :定義一個AOP前置通知
  9. aop:poiontcut :定義一個切點
  • 聲明前後置通知
<!--聲明切面 -->
    <aop:config>
        <aop:aspect ref="audience">       <!--引用audience Bean-->

            <aop:before pointcut="execution(* com.zexing.aspectj.concert.Performance.perform(..))" method="silenceCellPhones"/>

            <aop:before pointcut="execution(* com.zexing.aspectj.concert.Performance.perform(..))" method="takeSeats"/>

            <aop:after-returning pointcut="execution(* com.zexing.aspectj.concert.Performance.perform(..))" method="applause"/>

            <aop:after-throwing pointcut="execution(* com.zexing.aspectj.concert.Performance.perform(..))" method="demandRefun"/>

        </aop:aspect>
    </aop:config>
  • 聲明切點
<!--聲明切點 -->
    <aop:config>
        <aop:aspect ref="audience">
            <aop:pointcut id="performance" expression="execution(* com.zexing.aspectj.concert.Performance.perform(..))" />

            <aop:before pointcut-ref="performance" method="silenceCellPhones"/>

            <aop:before pointcut-ref="performance" method="takeSeats"/>

            <aop:after-returning pointcut-ref="performance" method="applause"/>

            <aop:after-throwing pointcut-ref="performance" method="demandRefun"/>

        </aop:aspect>
    </aop:config>
  • 聲明環繞通知
<!-- 聲明環繞通知-->
    <aop:config>
        <aop:aspect ref="audience">       <!--引用audience Bean-->
            <aop:pointcut id="performance" expression="execution(* com.zexing.aspectj.concert.Performance.perform(..))"  />

            <aop:around pointcut-ref="performance" method="watchPerformance"/>

        </aop:aspect>
    </aop:config>
  • 爲通知傳遞參數
<!-- 爲通知傳遞參數-->
    <bean id="player" class="com.zexing.aspectj.trackPlayCount.CDPlayer" />
    <!-- 構造器注入屬性-->
    <bean id="cd" class="com.zexing.aspectj.trackPlayCount.CompactDisc" >
        <constructor-arg name="title" value="唐朝" />
        <constructor-arg name="tracks">
            <list>
                <value>夢迴唐朝</value>
                <value>太陽</value>
                <value>九拍</value>
                ...
            </list>
        </constructor-arg>

    </bean>
    <bean id="trackCounter" class="com.zexing.aspectj.trackPlayCount.TrackCounter" />

    <aop:config>
        <aop:aspect ref="trackCounter">
            <aop:pointcut id="trackPlayed" expression="execution(* com.zexing.aspectj.trackPlayCount.CDPlayer.play(int)) and args(trackNumber)" />
            <!-- 這裏args()的trackNumber 必須與類中countTrack()方法的參數名保持一致-->
            <aop:around pointcut-ref="trackPlayed" method="countTrack" />
        </aop:aspect>
    </aop:config>
  • 爲通知新增方法
<!-- 爲bean 新增方法-->
    <bean id="onePerformer" class="com.zexing.aspectj.concert2.OnePerformer" />

    <aop:config>
        <aop:aspect ref="audience">       <!--引用audience Bean-->
            <aop:declare-parents types-matching="com.zexing.aspectj.concert.Performance"
                                 implement-interface="com.zexing.aspectj.concert2.Performer"
                                 delegate-ref="onePerformer" />
        </aop:aspect>
    </aop:config>
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章