03.spring framework的AOP

E、作用

AOP是面向對象編程的一個強大補充。通過AOP,我們現在可以把之前分散在應用各處的行爲放入可重用的模塊中。我們顯示地聲明在何處如何應用該行爲。這有效減少了代碼冗餘,並讓我們的類關注自身的主要功能。

一、概念

1、橫切關注點

  • 散佈於應用中多處的功能被稱爲橫切關注點(cross- cutting concern)
  • 通常來講,這些橫切關注點從概念上是與應用的業務邏輯相分離的,但是往往會直接嵌入到應用的業務邏輯之中

2、面向切面編程

在使用面向切面編程時,我們仍然在一個地方定義通用功能,但是可以通過聲明的方式定義這個功能要以何種方式在何處應

3、AOP術語

(1)通知(Advice

         1)定義:切面所要做的工作被稱爲通知
         2)負責:定義切面需要完成什麼工作+何時執行工作(方法執行前?後?....),包含了需要用於多個應用對象的橫切行爲;
         3)spring切面支持通知類型:
前置通知(Before):
在目標方法被調用之前調用通知功能;
後置通知(After):
在目標方法完成之後調用通知,此時不會關
心方法的輸出是什麼;
返回通知(After-returning):
在目標方法成功執行之後調用通
知;
異常通知(After-throwing
在目標方法拋出異常後調用通知;
環繞通知(Around
通知包裹了被通知的方法,在被通知的方
法調用之前和調用之後執行自定義的行爲。

(2)連接點(Join point

          1)定義:應用通知的時機被稱爲連接點。

          2)負責:連接點是在應用執行過程中能夠插入切面的一個點,這個點可以是調用方法時拋出異常時、甚至修改一個字段時

(3)切點(Poincut

          1)定義:(有表達式那個)一個切面並不需要通知應用到所有連接點。切點有助於縮小切面所通知的連接點的範圍。

          2)負責:何處應用切面,切點的定義會匹配通知所要織入的一個或多個連接點;切點定義了通知被應用的 具體位置(在哪些連接點);也就是定義了哪些連接點會得到通知

          3)通常使用明確的類和方法名稱,或是利用正則表達式定義所匹配的類和方法名稱來指定這些切點。有些AOP框架允許我們創建動態的切點,可以根據運行時的決策(比如方法的參數值)來決定是否應用通知。

(4)切面(Aspect

          1)定義:切面是通知和切點的結合

          2)負責:通知和切點共同定義了切面的全部內容——它是什麼,在何時和何處完成其功能。

(5)引入(Introduction

         1)說明:引入允許我們向現有的類添加新方法或屬性。

(5)織入(Weaving

          1)定義:織入是把切面應用到目標對象並創建新的代理對象的過程。

          2)說明:切面在指定的連接點被織入到目標對象中。在目標對象的生命週期裏有多個點可以進行織入:

  • 編譯期:切面在目標類編譯時被織入。這種方式需要特殊的編譯AspectJ的織入編譯器就是以這種方式織入切面的。
  • 類加載期:切面在目標類加載到JVM時被織入。這種方式需要殊的類加載器ClassLoader),它可以在目標類被引入應用之前增強該目標類的字節碼。AspectJ 5的加載時織入(load-time weavingLTW)就支持以這種方式織入切面。
  • 運行期:切面在應用運行的某個時刻被織入。一般情況下,在織入切面時,AOP容器會爲目標對象動態地創建一個代理對象Spring AOP就是以這種方式織入切面的。

二、解決問題

把這些橫切關注點與業務邏輯相分離正是面向切面編程(AOP)所要解決的問題。DI有助於應用對象之間的解耦,而AOP可以較好的實現【橫切關注點與它們所影響的對象之間的解耦】

如果使用面向對象技術解決【橫切關注點與它們所影響的對象之間的解耦】,比如繼承、委託,存在問題:

1、使用繼承:如果在整個應用中都使用相同的基類,繼承往往會導致一個脆弱的對象體系;

2、使用委託:可能需要對委託對象進行復雜的調用。

三、適用場景

1、日誌

2、聲明式事務

3、安全

4、緩存

四、Spring AOP知識點

1、Spring AOP種類

(前3個都是Spring AOP變體:基於代理的AOP

  • 基於代理的經典Spring AOP:(已經過時)
  • Spring聲明式AOP,基於純POJO切面:(Spring的aop命名空間,需要XML配置)
  • @AspectJ註解驅動的切面;依然是Spring基於代理的AOP,編程風格與AspectJ註解基本一致(能夠不使用XML配置):(模仿AspectJ註解)
  • 注入式AspectJ切面(適用於Spring各版本):如果你的AOP需求超過了簡單的方法攔截需求,比如需要構造器或屬性攔截,那麼你需要考慮使用AspectJ來實現切面:(AspectJ的AOP)

2、Spring AOP侷限

由於Spring AOP構建在動態代理基礎之上,因此,SpringAOP的支持侷限於方法攔截

3、Spring AOP框架的一些關鍵知識

  • Spring通知是Java編寫的
  • Spring在運行時通知對象
  • Spring只支持方法連接點:因爲Spring基於動態代理
  • Spring不支持字段和構造器連接點:AspectJ和JBoss支持,方法連接點可以滿足大部分需求,如需要方法攔截之外的連
    接點,則使用Aspect來補充Spring AOP的功能

五、SpringAOP 實踐部分

1、Spring的聲明式AOP

1)定義切點

方式:在Spring AOP中,要使用AspectJ的切點表達式語言來定義切點。(如何具體使用切點,見創建切面部分)

說明:Spring僅支持AspectJ的切點表達式的一個子集,因爲Spring是基於代理的,而某些切點表達式是基於屬性、構造器的

支持的AspectJ的切點表達式(俗稱:AspectJ的切點指示器),如下表:

AspectJ
示器
描 述
arg()
限制連接點匹配方法的參數爲指定類型
@args()
限制連接點匹配方法的參數由指定註解標註
execution()
匹配的連接點類型是:方法
this()
限制連接點匹配AOP代理的bean引用爲指定類型的類
target
限制連接點匹配目標對象爲指定類型的類
@target()
限制連接點匹配特定的執行對象,這些對象對應的類要具有指定類 型的註解
within()
限制連接點匹配指定的類型,限制連接點僅匹配指定的包
@within()
限制連接點匹配指定註解所標註的類型(當使用Spring AOP時,方法定義在由指定的註解所標註的類裏)
@annotation
限定匹配帶有指定註解的連接點
bean('bean的ID或bean的名稱')
在切點表達式中使用bean的ID或bean名稱來標識bean,來限制切點只匹配特定的bean
這個指示器是spring增加的,不是AspectJ中的
  • execution是實際執行匹配的,而其他的指示器都是用來限制匹配的。
  • 這說明execution指示器是我們在編寫切點定義時最主要使用的指示器。在此基礎上,我們使用其他指示器來限制所匹配的切點。

示例:

上面示例中,方法表達式以*號開始,表明了我們不關心方法返回值的類型。然後,我們指定了全限定類名和方法名。對於方法參數列表,我 們使用兩個點號(..)表明切點要選擇任意的perform()方法,無論該方法的入參是什麼。
  • 可以使用&&、||、!  操作符(也可以在SpringXML中直接使用and、or、not代替這三個字符)組合多個切點指示器,分別爲與、或、非關係,切點必須匹配所有的指示器

示例說明:再之前配置的基礎上 and 配置的切點僅匹配concert包中bean

  • bean('bean的ID或bean的名稱')的示例:

示例說明:在執行Performanceperform()方法時應用通知,但限定beanIDwoodstock

2)創建切面(基於註解)(通知、切面、切點結合使用)

  • 基於       【POJO類+類上@AspectJ註解+方法上@各種通知註解 】  可以輕鬆實現切面的定義
  • Spring使用AspectJ註解來聲明通知的註解:因此需要引入AspectJ依賴
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-aspects</artifactId>
    <version>${springframe.version}</version>
</dependency>
  • Spring支持的AspectJ中通知註解如下:
注 解
通 知
@After
通知方法會在目標方法返回或拋出異常後調用,相當於finally
@AfterReturning
通知方法會在目標方法返回後調用
@AfterThrowing
通知方法會在目標方法拋出異常後調用
@Around
通知方法會將目標方法封裝起來
@Before
通知方法會在目標方法調用之前執行
  • 在spring中設置【啓用自動代理功能】
    • 使用JavaConfig的話:在配置類的類級別上通過使用EnableAspectJ-AutoProxy註解啓用自動代理功能
    • 使用XML來裝配bean的話:使用Spring aop命名空間中的<aop:aspectj-autoproxy>元素,見下圖:

  • (非常重要)Spring的AspectJ自動代理僅僅使用@AspectJ作爲創建切面的指導(也就是雖然使用的是AspectJ相關注解:@AspectJ、@Pointcut、@各種通知註解),切面依然是基於代理的。在本質上,它依然是Spring基於代理的切面。這一點非常重要,因爲這意味着儘管使用的是@AspectJ註解,但我們仍然限於代理方法的調用。如果想利用AspectJ的所有能力,我們必須在運行時使用AspectJ並且不依賴Spring來創建基於代理的切面
  • 面向註解的切面缺點:面向註解的切面聲明有一個明顯的劣勢:你必須能夠爲通知類添加註解。爲了做到這一點,必須要有源碼

代碼示例(示例中bean的裝配採用JavaConfig方式):

業務bean接口:

package com.mzj.springframework.aop._01_SpringDeclarativeAOP.bean;

/**
 * 業務bean接口:演出
 */
public interface Performance {

    public void perform();
}

業務bean實現:

package com.mzj.springframework.aop._01_SpringDeclarativeAOP.bean;

import org.springframework.stereotype.Component;

/**
 * 業務bean實現
 *
 * @Auther: mazhongjia
 * @Date: 2020/3/23 13:33
 * @Version: 1.0
 */
@Component
public class PerformanceImpl implements Performance {

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

切面示例一:缺點(相同的切點表達式我們重複了四遍

package com.mzj.springframework.aop._01_SpringDeclarativeAOP.bean;

import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;

/**
 * 切面
 *
 * 使用@AspectJ註解進行了標註。該註解表 明Audience不僅僅是一個POJO,還是一個切面
 *
 * @Auther: mazhongjia
 * @Date: 2020/3/23 12:26
 * @Version: 1.0
 */
//@Component
@Aspect
public class Audience1 {

    /**
     * 手機靜音
     *
     * 通知註解+切點表達式
     */
    @Before("execution(** com.mzj.springframework.aop._01_SpringDeclarativeAOP.bean.Performance.perform(..))")//表演之前
    public void silenceCellPhones(){
        System.out.println("Silenceing cell phones1");
    }

    /**
     * 就坐
     */
    @Before("execution(** com.mzj.springframework.aop._01_SpringDeclarativeAOP.bean.Performance.perform(..))")//表演之前
    public void takeSeats(){
        System.out.println("Taking seats1");
    }

    /**
     * 鼓掌
     */
    @AfterReturning("execution(** com.mzj.springframework.aop._01_SpringDeclarativeAOP.bean.Performance.perform(..))")//表演完成後
    public void applause(){
        System.out.println("CLAP CLAP CLAP1!!!");
    }

    /**
     * 退款
     */
    @AfterThrowing("execution(** com.mzj.springframework.aop._01_SpringDeclarativeAOP.bean.Performance.perform(..))")//表演失敗之後
    public void demandRefund(){
        System.out.println("Demanding a refund1");
    }
}

切面示例二:

package com.mzj.springframework.aop._01_SpringDeclarativeAOP.bean;

import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;

/**
 * 切面
 *
 * 使用@AspectJ註解進行了標註。該註解表 明Audience不僅僅是一個POJO,還是一個切面
 *
 * 通過將相同的切點統一聲明的方式來優化Audience1
 *
 * @Auther: mazhongjia
 * @Date: 2020/3/23 12:26
 * @Version: 1.0
 */
@Component
@Aspect
public class Audience2 {

    /**
     * 定義切點
     *
     * 加@Pointcut註解,我們實際上擴展了切點表達式語言,這樣就可 以在任何的切點表達式中使用performance()了
     */
    @Pointcut("execution(** com.mzj.springframework.aop._01_SpringDeclarativeAOP.bean.Performance.perform(..))")//**與 com.mzj之間必須有空格
    public void performance(){
        //performance()方法的實際內容並不重要
    }

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

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

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

    @AfterThrowing("performance()")
    public void demandRefund(){
        System.out.println("Demanding a refund2");
    }
}

JavaConfig方式:

package com.mzj.springframework.aop._01_SpringDeclarativeAOP.bean;

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

@Configuration
@ComponentScan
@EnableAspectJAutoProxy//在配置類的類級別上通過使 用EnableAspectJ-AutoProxy註解啓用自動代理功能
public class ConcertConfig {

    /**
     * 使用自動掃描:ComponentScan,並不會自動創建切面,所以需要通過@Bean進行創建  或者 在切面類增加@Component註解也行~~~~
     * @return
     */
    @Bean
    public Audience1 audience1() {
        return new Audience1();
    }

//    @Bean
//    public Audience2 audience2() {
//        return new Audience2();
//    }
}

測試類:

package com.mzj.springframework.aop;

import com.mzj.springframework.aop._01_SpringDeclarativeAOP.bean.ConcertConfig;
import com.mzj.springframework.aop._01_SpringDeclarativeAOP.bean.Performance;
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;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes= ConcertConfig.class)
public class Test {

  @Autowired
  private Performance performance;

  @org.junit.Test
  public void play() {
    performance.perform();
  }

}

運行結果:

2-1)Around環繞通知

  • 通過切面中環繞通知方法的ProceedingJoinPoint參數調用業務方法,僅環繞通知有這個參數
package com.mzj.springframework.aop._01_SpringDeclarativeAOP.bean.around;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;

/**
 * 切面
 *
 * 環繞通知
 *
 * @Auther: mazhongjia
 * @Date: 2020/3/23 12:26
 * @Version: 1.0
 */
@Aspect
public class Audience4Around {

    @Pointcut("execution(** com.mzj.springframework.aop._01_SpringDeclarativeAOP.bean.Performance.perform(..)))")
    public void performance(){};

    @Around("performance()")
    public void watchPerformance(ProceedingJoinPoint joinPoint){//ProceedingJoinPoint是在通知中通過它來調用被通知的方法

        try {
            System.out.println("around start.....");
            //有意思的是,你可以不調用proceed()方法,從而阻止對被通知方 法的訪問
            //也可以在通知中對它進行多次調用。要這樣 做的一個場景就是實現重試邏輯
            joinPoint.proceed();
            System.out.println("around end.....");
        } catch (Throwable throwable) {
            System.out.println("around exception.....");
        }
    }
}

2-2)通知中參數(從命名切點到通知方法的參數轉移

  • 在切點表達式中聲明參數,將這個參數傳入到通知方法中
    • 聲明瞭要提供給通知方法的參數:其中方法參數位不再是接收任意類型參數的(..)了,而是接收參數的類型(多個參數以逗號分隔)
    • 切點表達式增加【指定參數】限定符部分:表明傳遞給sing()方法的String、String類型參數也會傳遞到通知中去:&& args(參數),這個參數對應@Pointcut註解修飾的方法參數

  • 詳細說明如下圖:
語法說明:切點表達式中的args(trackNumber)限定符。它表明傳遞給playTrack()方法的int類型參數也會傳遞到通知中去。參數的名稱trackNumber也與切點方法簽名中的參數相匹配。
  • 在通知方法的註解(@Before之類)調用切點方法時設置傳入參數,這個參數與通知方法參數名稱對應、與切點表達式中參數順序一致但是參數名不需要一致

====按照上面幾點規則就完成了從命名切點到 通知方法的參數轉移====

代碼示例如下:
業務bean類:
package com.mzj.springframework.aop._01_SpringDeclarativeAOP.bean.adviceArgs;

public interface SingASong {
  void sing(String SongName,String SongContext);
}

業務bean實現類:

package com.mzj.springframework.aop._01_SpringDeclarativeAOP.bean.adviceArgs;
import org.springframework.stereotype.Component;

@Component
public class SingASongImpl implements SingASong {


  @Override
  public void sing(String songName, String songContext) {
    System.out.println("SingASong " + songName + " : " + songContext);
  }
}

切面類:

package com.mzj.springframework.aop._01_SpringDeclarativeAOP.bean.adviceArgs;

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

import java.util.ArrayList;
import java.util.List;

/**
 * 切面
 *
 * 從命名切點到 通知方法的參數轉移
 *
 * @Auther: mazhongjia
 * @Date: 2020/3/23 12:26
 * @Version: 1.0
 */
@Aspect
public class Audience4AroundArgs {

   private int songCount = 0;
   private List<String> songList = new ArrayList<>();

    @Pointcut("execution(* com.mzj.springframework.aop._01_SpringDeclarativeAOP.bean.adviceArgs.SingASong.sing(String,String))" + "&& args(songName,songContext)")
    public void trackPlayed(String songName,String songContext){};

    @Before("trackPlayed(songName,songContext)")
    public void countTrack(String songName,String songContext){
        songCount++;
        songList.add(songName);
        System.out.println("已經唱了" + songCount + "首歌曲了,他們是:" + songList);
    }
}

JavaConfig類:

package com.mzj.springframework.aop._01_SpringDeclarativeAOP.bean.adviceArgs;

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

/**
 * @Auther: mazhongjia
 * @Date: 2020/3/24 13:11
 * @Version: 1.0
 */
@Configuration
@ComponentScan
@EnableAspectJAutoProxy
public class TrackCounterConfig {

    @Bean
    public SingASong singASong(){
        return new SingASongImpl();
    }

    @Bean
    public Audience4AroundArgs audience1() {
        return new Audience4AroundArgs();
    }
}

測試類:

package com.mzj.springframework.aop;

import com.mzj.springframework.aop._01_SpringDeclarativeAOP.bean.adviceArgs.SingASong;
import com.mzj.springframework.aop._01_SpringDeclarativeAOP.bean.adviceArgs.TrackCounterConfig;
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;

/**
 * @Auther: mazhongjia
 * @Date: 2020/3/24 14:13
 * @Version: 1.0
 */
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes= TrackCounterConfig.class)
public class AdviceTest {

    @Autowired
    private SingASong singASong;

    @org.junit.Test
    public void songTest() {
        String song1 = "let it go...let it go...";
        String song2 = "我愛你中國....我愛你中國....";
        singASong.sing("Let It Go",song1);
        singASong.sing("我愛你中國",song2);
    }
}

運行結果:

已經唱了1首歌曲了,他們是:[Let It Go]
SingASong Let It Go : let it go...let it go...
已經唱了2首歌曲了,他們是:[Let It Go, 我愛你中國]
SingASong 我愛你中國 : 我愛你中國....我愛你中國....

2-3)通過註解引入新功能

目標說明:方法包裝僅僅是切面所能實現的功能之一,通過編寫切面,可以爲被通知的對象引入全新的功能

原理:使用Spring AOP,可以爲bean引入新的方法。代理攔截調用並委託給實現該方法的其他對象,實際上,一個bean的實現被拆分到了多個類中。
 
方式:
  • 爲新功能定義一個接口,並增加功能對應實現類
  • 創建一個切面
  • 切面中定義一個public static變量,變量類型爲新增功能接口
  • 爲變量添加註解:@DeclareParents註解,將新功能接口引入到準備增加功能的目標bean中
  • 和其他的切面一樣,需要在Spring中將切面類聲明爲一個bean(JavaConfig中使用@Bean註解到一個創建切面的方法,或者使用<bean>元素)
示例代碼:
業務bean接口
package com.mzj.springframework.aop._01_SpringDeclarativeAOP.bean.NewFeature;

/**
 * 目標bean接口
 */
public interface SingASong {
  void sing(String SongName, String SongContext);
}

業務bean實現

package com.mzj.springframework.aop._01_SpringDeclarativeAOP.bean.NewFeature;
import org.springframework.stereotype.Component;

/**
 * 目標bean實現
 */
@Component
public class SingASongImpl implements SingASong {

  @Override
  public void sing(String songName, String songContext) {
    System.out.println("SingASong " + songName + " : " + songContext);
  }
}

準備在業務bean增加的新功能接口

package com.mzj.springframework.aop._01_SpringDeclarativeAOP.bean.NewFeature;

/**
 * 增加的新功能接口
 *
 * @Auther: mazhongjia
 * @Date: 2020/3/24 17:55
 * @Version: 1.0
 */
public interface NewFeature {

    void newFeature();
}

新功能實現

package com.mzj.springframework.aop._01_SpringDeclarativeAOP.bean.NewFeature;

/**
 * 增加的新功能實現
 *
 * @Auther: mazhongjia
 * @Date: 2020/3/24 17:55
 * @Version: 1.0
 */
public class NewFeatureImpl implements NewFeature{

    @Override
    public void newFeature() {
        System.out.println("增加新功能。。。");
    }
}

JavaConfig

package com.mzj.springframework.aop._01_SpringDeclarativeAOP.bean.NewFeature;

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

/**
 * JavaConfig類
 */
@Configuration
@ComponentScan
@EnableAspectJAutoProxy
public class NewFeatureConfig {

    @Bean
    public NewFeatureAspect newFeatureAspect() {
        return new NewFeatureAspect();
    }

}

切面

package com.mzj.springframework.aop._01_SpringDeclarativeAOP.bean.NewFeature;

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.DeclareParents;

/**
 * 切面
 *
 * @Auther: mazhongjia
 * @Date: 2020/3/24 17:57
 * @Version: 1.0
 */
@Aspect
public class NewFeatureAspect {

    /**
     * 通過@DeclareParents註解,將NewFeature接口引入到NewFeature bean中
     *
     * @DeclareParents註解由三部分組成:
     *      1)value屬性指定了哪種類型的bean要引入該接口。在本例中,也 就是所有實現Performance的類型。(標記符後面的加號表示 是Performance的所有子類型,而不是Performance本 身。)
     *      2)defaultImpl屬性指定了爲引入功能提供實現的類。在這裏, 我們指定的是DefaultEncoreable提供實現。
     *      3)@DeclareParents註解所標註的靜態屬性指明瞭要引入了接 口。在這裏,我們所引入的是Encoreable接口。
     */
    @DeclareParents(value="com.mzj.springframework.aop._01_SpringDeclarativeAOP.bean.NewFeature.SingASong+",defaultImpl = NewFeatureImpl.class)
    public static NewFeature newFeature;
}

測試代碼:使用新功能時,需要先強制轉換成新功能接口

package com.mzj.springframework.aop;

import com.mzj.springframework.aop._01_SpringDeclarativeAOP.bean.NewFeature.NewFeature;
import com.mzj.springframework.aop._01_SpringDeclarativeAOP.bean.NewFeature.NewFeatureConfig;
import com.mzj.springframework.aop._01_SpringDeclarativeAOP.bean.NewFeature.SingASong;
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;

/**
 * @Auther: mazhongjia
 * @Date: 2020/3/24 14:13
 * @Version: 1.0
 */
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes= NewFeatureConfig.class)
public class NewFeatureTest {

    @Autowired
    private SingASong singASong;

    @org.junit.Test
    public void songTest() {
        String song1 = "let it go...let it go...";
        String song2 = "我愛你中國....我愛你中國....";
        singASong.sing("Let It Go",song1);
        singASong.sing("我愛你中國",song2);
        ((NewFeature)singASong).newFeature();
    }
}
Spring的自動代理機制將會獲取到它的聲明,當Spring發現一個bean使用了@Aspect註解時,Spring就會創建一個代理,然後將調用委託給被代理的bean或被引入的實現,這取決於調用的方法屬於被代理的bean還是屬於被引入的接口。
 

運行結果:

SingASong Let It Go : let it go...let it go...
SingASong 我愛你中國 : 我愛你中國....我愛你中國....
增加新功能。。。

3)創建切面(基於XML)(通知、切面、切點結合使用)

使用說明:基於註解的配置要優於基於Java的配置,基於Java的配置要優於基於XML的配置。但是,如果你需要聲明切面,但是又不能爲通知類添加註解的時候,那麼就必須轉向XML配置了。
 
SpringAOP配置元素能夠以非侵入性的方式聲明切面
 
AOP配置元素
用 途
<aop:advisor>
定義AOP通知器
<aop:after>
定義AOP後置通知(不管被通知的方法是否執行成功)
<aop:after-
returning>
定義AOP返回通知
<aop:after-
throwing>
定義AOP異常通知
<aop:around>
定義AOP環繞通知
<aop:aspect>
定義一個切面
<aop:aspectj-
autoproxy>
啓用@AspectJ註解驅動的切面
<aop:before>
定義一個AOP前置通知
<aop:config>
頂層的AOP配置元素。大多數的<aop:*>元素必須包含
<aop:config>元素內
<aop:declare-
parents>
以透明的方式爲被通知的對象引入額外的接口
<aop:pointcut>
定義一個切點

 

3-1)before、after通知

代碼示例:

業務bean接口:

package com.mzj.springframework.aop._02_XMLAOP;

/**
 * 業務bean接口:演出
 */
public interface Performance {

    public void perform();
}

業務bean實現:

package com.mzj.springframework.aop._02_XMLAOP;

import org.springframework.stereotype.Component;

/**
 * 業務bean實現
 *
 * @Auther: mazhongjia
 * @Date: 2020/3/23 13:33
 * @Version: 1.0
 */
public class PerformanceImpl implements Performance {

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

POJO切面類:

package com.mzj.springframework.aop._02_XMLAOP;

import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;

/**
 *  將一個POJO定義爲一個切面
 *
 *
 * @Auther: mazhongjia
 * @Date: 2020/3/23 12:26
 * @Version: 1.0
 */
public class Audience2 {


    public void silenceCellPhones(){
        System.out.println("Silenceing cell phones223");
    }

    public void takeSeats(){
        System.out.println("Taking seats223");
    }

    public void applause(){
        System.out.println("CLAP CLAP CLAP223!!!");
    }

    public void demandRefund(){
        System.out.println("Demanding a refund223");
    }
}

XML配置:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
		http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd">

    <!--業務bean-->
    <bean id="performance" class="com.mzj.springframework.aop._02_XMLAOP.PerformanceImpl"/>

    <!--切面bean-->
    <bean id="audience2" class="com.mzj.springframework.aop._02_XMLAOP.Audience2"/>

    <aop:config>
        <!--切面類,ref屬性指向POJO切面類-->
        <aop:aspect ref="audience2">
            <!--method:切面類中通知方法,pointcut:切入點(使用AspectJ切點表達式語法所定義的切點。)-->
            <aop:before method="silenceCellPhones" pointcut="execution(** com.mzj.springframework.aop._02_XMLAOP.Performance.perform(..))"></aop:before>
            <aop:before method="takeSeats" pointcut="execution(** com.mzj.springframework.aop._02_XMLAOP.Performance.perform(..))"></aop:before>
            <aop:after-returning method="applause" pointcut="execution(** com.mzj.springframework.aop._02_XMLAOP.Performance.perform(..))"></aop:after-returning>
            <aop:after-throwing method="demandRefund" pointcut="execution(** com.mzj.springframework.aop._02_XMLAOP.Performance.perform(..))"></aop:after-throwing>

        </aop:aspect>
    </aop:config>
</beans>

測試類:

package com.mzj.springframework.aop.xmlaop;

import com.mzj.springframework.aop._02_XMLAOP.Performance;
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;

/**
 * @Auther: mazhongjia
 * @Date: 2020/3/25 13:04
 * @Version: 1.0
 */
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = { "classpath*:com/mzj/springframework/aop/_02_XMLAOP/XMLAOP.xml" })
public class BeforeAfterTest {

    @Autowired
    private Performance performance;

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

執行結果:

Silenceing cell phones223
Taking seats223
PerformanceImpl....
CLAP CLAP CLAP223!!!

可以統一定義切點,在不同通知中使用:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
		http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd">

    <!--業務bean-->
    <bean id="performance" class="com.mzj.springframework.aop._02_XMLAOP.PerformanceImpl"/>

    <!--切面bean-->
    <bean id="audience2" class="com.mzj.springframework.aop._02_XMLAOP.Audience2"/>

    <aop:config>
        <!--定義切點-->
        <aop:pointcut id="performance-cut" expression="execution(** com.mzj.springframework.aop._02_XMLAOP.Performance.perform(..))"/>
        <!--切面類,ref屬性指向POJO切面類-->
        <aop:aspect ref="audience2">
            <!--method:切面類中通知方法,pointcut:切入點(使用AspectJ切點表達式語法所定義的切點。)-->
            <aop:before method="silenceCellPhones" pointcut-ref="performance-cut"></aop:before>
            <aop:before method="takeSeats" pointcut-ref="performance-cut"></aop:before>
            <aop:after-returning method="applause" pointcut-ref="performance-cut"></aop:after-returning>
            <aop:after-throwing method="demandRefund" pointcut-ref="performance-cut"></aop:after-throwing>

        </aop:aspect>
    </aop:config>
</beans>

可以把切點<aop:pointcut>定義在<aop:aspect>內部,也可以把切點定義在<aop:config>內部,區別是範圍可以使用的範圍不同。

3-2)環繞通知

說明:如果不使用成員變量存儲信息的話,在前置通知和後置通知之間共享信息非常麻煩,唯一方式是在前置通知所在的切面類中使用一個成員變量記錄信息,在後置通知中使用這個變量記錄的值。因爲bean默認是單例的,如果像這樣保存 狀態的話,將會存在線程安全問題。 存在這種需求時,環繞通知在這點上有明顯的優勢。

代碼示例:

業務bean:使用之前前置後置通知的bean

切面類:

package com.mzj.springframework.aop._02_XMLAOP.around;

import org.aspectj.lang.ProceedingJoinPoint;

/**
 *  將一個POJO定義爲一個切面
 *
 *
 * @Auther: mazhongjia
 * @Date: 2020/3/23 12:26
 * @Version: 1.0
 */
public class Audience2 {


    public void watchPerformance(ProceedingJoinPoint joinPoint){

        try{
            System.out.println("表演之前.....");
            joinPoint.proceed();//執行被通知的方法
            System.out.println("表演成功後.....");
        }catch (Throwable e){
            System.out.println("表演失敗後....");
        }
    }

}

XML配置:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
		http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd">

    <!--業務bean-->
    <bean id="performance" class="com.mzj.springframework.aop._02_XMLAOP.PerformanceImpl"/>

    <!--普通bean-->
    <bean id="audience2" class="com.mzj.springframework.aop._02_XMLAOP.around.Audience2"/>

    <aop:config>
        <!--定義切點-->
        <aop:pointcut id="performance-cut" expression="execution(** com.mzj.springframework.aop._02_XMLAOP.Performance.perform(..))"/>
        <!--將一個普通bean定義爲切面類,ref屬性指向POJO切面類-->
        <aop:aspect ref="audience2">
            <!--method:切面類中通知方法,pointcut:切入點(使用AspectJ切點表達式語法所定義的切點。)-->
            <aop:around method="watchPerformance" pointcut-ref="performance-cut"></aop:around>
        </aop:aspect>
    </aop:config>
</beans>

測試類:

package com.mzj.springframework.aop.xmlaop;


import com.mzj.springframework.aop._02_XMLAOP.Performance;
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;

/**
 * @Auther: mazhongjia
 * @Date: 2020/3/25 13:04
 * @Version: 1.0
 */
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = {"classpath*:com/mzj/springframework/aop/_02_XMLAOP/around/XMLAOP2.xml"})
public class AroundTest {

    @Autowired
    private Performance performance;

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

執行結果:

表演之前.....
PerformanceImpl....
表演成功後.....

3-3)爲通知傳遞參數

業務bean接口

package com.mzj.springframework.aop._02_XMLAOP.adviceArgs;

public interface SingASong {
  void sing(String SongName, String SongContext);
}

業務bean實現

package com.mzj.springframework.aop._02_XMLAOP.adviceArgs;

public class SingASongImpl implements SingASong {

  @Override
  public void sing(String songName, String songContext) {
    System.out.println("SingASong " + songName + " : " + songContext);
  }
}

切面類(普通POJO,無任何spring註解)

package com.mzj.springframework.aop._02_XMLAOP.adviceArgs;

import java.util.ArrayList;
import java.util.List;

/**
 * 切面
 *
 * 從命名切點到 通知方法的參數轉移
 *
 * @Auther: mazhongjia
 * @Date: 2020/3/23 12:26
 * @Version: 1.0
 */
//@Aspect
public class Audience4AroundArgs {

   private int songCount = 0;
   private List<String> songList = new ArrayList<>();

//    @Pointcut("execution(* com.mzj.springframework.aop._01_SpringDeclarativeAOP.bean.adviceArgs.SingASong.sing(String,String))" + "&& args(songName,songContext)")
//    public void trackPlayed(String songName,String songContext){};

//    @Before("trackPlayed(songName,songContext)")
    public void countTrack(String songName,String songContext){
        songCount++;
        songList.add(songName);
        System.out.println("已經唱了" + songCount + "首歌曲了,他們是:" + songList);
    }
}

配置文件

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
		http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd">

    <!--普通bean-->
    <bean id="audience2" class="com.mzj.springframework.aop._02_XMLAOP.adviceArgs.Audience4AroundArgs"/>

    <!--業務bean-->
    <bean id="song" class="com.mzj.springframework.aop._02_XMLAOP.adviceArgs.SingASongImpl"/>

    <aop:config>
        <!--將一個普通bean聲明爲一個切面-->
        <aop:aspect ref="audience2">
            <!--定義切點,通過新增的args(songName,songContext)將參數傳遞給通知上,其中ongName,songContext這兩個變量對應通知方法countTrack的參數名-->
            <aop:pointcut id="trackPlayed"
                          expression="execution(* com.mzj.springframework.aop._02_XMLAOP.adviceArgs.SingASong.sing(String,String)) and args(songName,songContext)"/>
            <!--method:切面類中通知方法,pointcut:切入點(使用AspectJ切點表達式語法所定義的切點。)-->
            <aop:before method="countTrack" pointcut-ref="trackPlayed"></aop:before>
        </aop:aspect>
    </aop:config>

</beans>

測試類:

package com.mzj.springframework.aop.xmlaop;

import com.mzj.springframework.aop._02_XMLAOP.adviceArgs.SingASong;
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;

/**
 * @Auther: mazhongjia
 * @Date: 2020/3/25 13:04
 * @Version: 1.0
 */
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = {"classpath*:com/mzj/springframework/aop/_02_XMLAOP/adviceArgs/XMLAOP2.xml"})
public class AdviceTest {

    @Autowired
    private SingASong singASong;

    @org.junit.Test
    public void songTest() {
        String song1 = "let it go...let it go...";
        String song2 = "我愛你中國....我愛你中國....";
        singASong.sing("Let It Go",song1);
        singASong.sing("我愛你中國",song2);
    }
}

運行結果:

已經唱了1首歌曲了,他們是:[Let It Go]
SingASong Let It Go : let it go...let it go...
已經唱了2首歌曲了,他們是:[Let It Go, 我愛你中國]
SingASong 我愛你中國 : 我愛你中國....我愛你中國....

3-4)通過切面引入新的功能

使用Spring aop命名空間中的<aop:declare-parents>元素,可以通過XML配置方式,實現@DeclareParents註解(AspectJ的註解)的功能。
 
代碼示例:
業務bean接口
package com.mzj.springframework.aop._02_XMLAOP.NewFeature;

/**
 * 目標bean接口
 */
public interface SingASong {
  void sing(String SongName, String SongContext);
}

業務bean實現:

package com.mzj.springframework.aop._02_XMLAOP.NewFeature;

/**
 * 目標bean實現
 */
public class SingASongImpl implements SingASong {

  @Override
  public void sing(String songName, String songContext) {
    System.out.println("SingASong " + songName + " : " + songContext);
  }
}

新功能接口:

package com.mzj.springframework.aop._02_XMLAOP.NewFeature;

/**
 * 增加的新功能接口
 *
 * @Auther: mazhongjia
 * @Date: 2020/3/24 17:55
 * @Version: 1.0
 */
public interface NewFeature {
    void newFeature();
}

新功能實現:

package com.mzj.springframework.aop._02_XMLAOP.NewFeature;

/**
 * 增加的新功能實現
 *
 * @Auther: mazhongjia
 * @Date: 2020/3/24 17:55
 * @Version: 1.0
 */
public class NewFeatureImpl implements NewFeature {

    @Override
    public void newFeature() {
        System.out.println("增加新功能。。。");
    }
}

spring配置文件:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
		http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd">

    <!--業務bean-->
    <bean id="song" class="com.mzj.springframework.aop._02_XMLAOP.NewFeature.SingASongImpl"/>

    <!--準備增加的新功能bean-->
<!--    <bean id="newFeature" class="com.mzj.springframework.aop._02_XMLAOP.NewFeature.NewFeatureImpl"/>-->

    <aop:config>
        <!--將一個普通bean聲明爲一個切面-->
        <aop:aspect>
            <!--types-matching:準備增加新功能的業務bean,其中+代表配置bean的子類-->
            <!--implement-interface:新功能接口-->
            <!--default-impl:新功能實現,也可以使用delegate-ref指定新功能實現類bean的id,使用default-impl來直接標識委託和間接使用delegate-ref的 區別在於後者是Spring bean,它本身可以被注入、通知或使用其他的 Spring配置。而使用default-impl不需要配置新功能實現類爲一個bean-->
            <aop:declare-parents types-matching="com.mzj.springframework.aop._02_XMLAOP.NewFeature.SingASong+" implement-interface="com.mzj.springframework.aop._02_XMLAOP.NewFeature.NewFeature" default-impl="com.mzj.springframework.aop._02_XMLAOP.NewFeature.NewFeatureImpl"/>
        </aop:aspect>
    </aop:config>

</beans>

測試代碼:

package com.mzj.springframework.aop.xmlaop;

import com.mzj.springframework.aop._02_XMLAOP.NewFeature.NewFeature;
import com.mzj.springframework.aop._02_XMLAOP.NewFeature.SingASong;
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;

/**
 * @Auther: mazhongjia
 * @Date: 2020/3/25 13:04
 * @Version: 1.0
 */
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = {"classpath*:com/mzj/springframework/aop/_02_XMLAOP/NewFeature/XMLAOP2.xml"})
public class NewFutureTest {

    @Autowired
    private SingASong singASong;

    @org.junit.Test
    public void songTest() {
        String song1 = "let it go...let it go...";
        String song2 = "我愛你中國....我愛你中國....";
        singASong.sing("Let It Go",song1);
        singASong.sing("我愛你中國",song2);

        ((NewFeature)singASong).newFeature();
    }
}

運行結果:

SingASong Let It Go : let it go...let it go...
SingASong 我愛你中國 : 我愛你中國....我愛你中國....
增加新功能。。。

六、AspectJ AOP

1、說明

1、AspectJ AOP功能比SpringAOP強大:
  • 切點支持:構造函數
    • Spring基於代理的AOP無法把通知應用於對象的創建過程
  • 切點支持:成員變量

​​​​​​​2、AspectJ AOP通常來說不需要spring,但是AspectJ AOP的實現過程可能需要藉助一些對象提供的功能,這些對象可以

  • AspectJ AOP切面中自行new
  • 由Spring IOC進行管理(將這些對象裝配到AspectJ AOP切面中)(推薦)
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章