Spring入門(十):Spring AOP使用講解

  • 2019 年 10 月 3 日
  • 筆記

1. 什麼是AOP?

AOP是Aspect Oriented Programming的縮寫,意思是:面向切面編程,它是通過預編譯方式和運行期動態代理實現程式功能的統一維護的一種技術。

可以認為AOP是對OOP(Object Oriented Programming 面向對象編程)的補充,主要使用在日誌記錄,性能統計,安全控制等場景,使用AOP可以使得業務邏輯各部分之間的耦合度降低,只專註於各自的業務邏輯實現,從而提高程式的可讀性及維護性。

比如,我們需要記錄項目中所有對外介面的入參和出參,以便出現問題時定位原因,在每一個對外介面的程式碼中添加程式碼記錄入參和出參當然也可以達到目的,但是這種硬編碼的方式非常不友好,也不夠靈活,而且記錄日誌本身和介面要實現的核心功能沒有任何關係。

此時,我們可以將記錄日誌的功能定義到1個切面中,然後通過聲明的方式定義要在何時何地使用這個切面,而不用修改任何1個外部介面。

在講解具體的實現方式之前,我們先了解幾個AOP中的術語。

1.1 通知(Advice)

在AOP術語中,切面要完成的工作被稱為通知,通知定義了切面是什麼以及何時使用。

Spring切面有5種類型的通知,分別是:

  • 前置通知(Before):在目標方法被調用之前調用通知功能
  • 後置通知(After):在目標方法完成之後調用通知,此時不關心方法的輸出結果是什麼
  • 返回通知(After-returning):在目標方法成功執行之後調用通知
  • 異常通知(After-throwing):在目標方法拋出異常後調用通知
  • 環繞通知(Around):通知包裹了被通知的方法,在被通知的方法調用之前和調用之後執行自定義的行為

1.2 連接點(Join point)

連接點是在應用執行過程中能夠插入切面的一個點,這個點可以是調用方法時、拋出異常時、修改某個欄位時。

1.3 切點(Pointcut)

切點是為了縮小切面所通知的連接點的範圍,即切面在何處執行。我們通常使用明確的類和方法名稱,或者利用正則表達式定義所匹配的類和方法名稱來指定切點。

1.4 切面(Aspect)

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

1.5 引入(Introduction)

引入允許我們在不修改現有類的基礎上,向現有類添加新方法或屬性。

1.6 織入(Weaving)

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

切面在指定的連接點被織入到目標對象中,在目標對象的生命周期里,有以下幾個點可以進行織入:

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

2. Spring 對AOP的支援

2.1 動態代理

Spring AOP構建在動態代理之上,也就是說,Spring運行時會為目標對象動態創建代理對象。

代理類封裝了目標類,並攔截被通知方法的調用,再把調用轉發給真正的目標bean。

當代理類攔截到方法調用時,在調用目標bean方法之前,會執行切面邏輯。

2.2 織入切面時機

通過在代理類中包裹切面,Spring在運行期把切面織入到Spring 管理的bean中,也就是說,直到應用需要被代理的bean時,Spring才會創建代理對象。

因為Spring運行時才創建代理對象,所以我們不需要特殊的編譯器來織入Spring AOP切面。

2.3 連接點限制

Spring只支援方法級別的連接點,如果需要欄位級別或者構造器級別的連接點,可以利用AspectJ來補充Spring AOP的功能。

3. Spring AOP使用

假設我們有個現場表演的介面Performance和它的實現類SleepNoMore:

package chapter04.concert;    /**   * 現場表演,如舞台劇,電影,音樂會   */  public interface Performance {      void perform();  }
package chapter04.concert;    import org.springframework.stereotype.Component;    /**   * 戲劇:《不眠之夜Sleep No More》   */  @Component  public class SleepNoMore implements Performance {      @Override      public void perform() {          System.out.println("戲劇《不眠之夜Sleep No More》");      }  }

既然是演出,就需要觀眾,假設我們的需求是:在看演出之前,觀眾先入座並將手機調整至靜音,在觀看演出之後觀眾鼓掌,如果演出失敗觀眾退票,我們當然可以把這些邏輯寫在上面的perform()方法中,但不推薦這麼做,因為這些邏輯理論上和演出的核心無關,就算觀眾不將手機調整至靜音或者看完演出不鼓掌,都不影響演出的進行。

針對這個需求,我們可以使用AOP來實現。

3.1 定義切面

首先,在pom.xml文件中添加如下依賴:

<!--spring aop支援-->  <dependency>      <groupId>org.springframework</groupId>      <artifactId>spring-aop</artifactId>      <version>5.1.8.RELEASE</version>  </dependency>  <!--aspectj支援-->  <dependency>      <groupId>org.aspectj</groupId>      <artifactId>aspectjrt</artifactId>      <version>1.8.5</version>  </dependency>  <dependency>      <groupId>org.aspectj</groupId>      <artifactId>aspectjweaver</artifactId>      <version>1.8.9</version>  </dependency>

然後,定義一個觀眾的切面如下:

package chapter04.concert;    import org.aspectj.lang.annotation.Aspect;    /**   * 觀眾   * 使用@Aspect註解定義為切面   */  @Aspect  public class Audience {  }

注意事項:@Aspect註解表明Audience類是一個切面。

3.2 定義前置通知

在Audience切面中定義前置通知如下所示:

/**   * 表演之前,觀眾就座   */  @Before("execution(* chapter04.concert.Performance.perform(..))")  public void takeSeats() {      System.out.println("Taking seats");  }    /**   * 表演之前,將手機調至靜音   */  @Before("execution(* chapter04.concert.Performance.perform(..))")  public void silenceCellPhones() {      System.out.println("Silencing cell phones");  }

這裡的重點程式碼是@Before("execution(* chapter04.concert.Performance.perform(..))"),它定義了1個前置通知,其中execution(* chapter04.concert.Performance.perform(..))被稱為AspectJ切點表達式,每一部分的講解如下:

  • @Before:該註解用來定義前置通知,通知方法會在目標方法調用之前執行
  • execution:在方法執行時觸發
  • *:表明我們不關心方法返回值的類型,即可以是任意類型
  • chapter04.concert.Performance.perform:使用全限定類名和方法名指定要添加前置通知的方法
  • (..):方法的參數列表使用(..),表明我們不關心方法的入參是什麼,即可以是任意類型

3.3 定義後置通知

在Audience切面中定義後置通知如下所示:

/**   * 表演結束,不管表演成功或者失敗   */  @After("execution(* chapter04.concert.Performance.perform(..))")  public void finish() {      System.out.println("perform finish");  }

注意事項:@After註解用來定義後置通知,通知方法會在目標方法返回或者拋出異常後調用

3.4 定義返回通知

在Audience切面中定義返回通知如下所示:

/**   * 表演之後,鼓掌   */  @AfterReturning("execution(* chapter04.concert.Performance.perform(..))")  public void applause() {      System.out.println("CLAP CLAP CLAP!!!");  }

注意事項:@AfterReturning註解用來定義返回通知,通知方法會在目標方法返回後調用

3.5 定義異常通知

在Audience切面中定義異常通知如下所示:

/**   * 表演失敗之後,觀眾要求退款   */  @AfterThrowing("execution(* chapter04.concert.Performance.perform(..))")  public void demandRefund() {      System.out.println("Demanding a refund");  }

注意事項:@AfterThrowing註解用來定義異常通知,通知方法會在目標方法拋出異常後調用

3.6 定義可復用的切點表達式

細心的你可能會發現,我們上面定義的5個切點中,切點表達式都是一樣的,這顯然是不好的,好在我們可以使用@Pointcut註解來定義可重複使用的切點表達式:

/**   * 可復用的切點   */  @Pointcut("execution(* chapter04.concert.Performance.perform(..))")  public void perform() {  }

然後之前定義的5個切點都可以引用這個切點表達式:

/**   * 表演之前,觀眾就座   */  @Before("perform()")  public void takeSeats() {      System.out.println("Taking seats");  }    /**   * 表演之前,將手機調至靜音   */  @Before("perform()")  public void silenceCellPhones() {      System.out.println("Silencing cell phones");  }    /**   * 表演結束,不管表演成功或者失敗   */  @After("perform()")  public void finish() {      System.out.println("perform finish");  }    /**   * 表演之後,鼓掌   */  @AfterReturning("perform()")  public void applause() {      System.out.println("CLAP CLAP CLAP!!!");  }    /**   * 表演失敗之後,觀眾要求退款   */  @AfterThrowing("perform()")  public void demandRefund() {      System.out.println("Demanding a refund");  }

3.7 單元測試

新建配置類ConcertConfig如下所示:

package chapter04.concert;    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  @EnableAspectJAutoProxy  @ComponentScan  public class ConcertConfig {      @Bean      public Audience audience() {          return new Audience();      }  }

注意事項:和以往不同的是,我們使用了@EnableAspectJAutoProxy註解,該註解用來啟用自動代理功能。

新建Main類,在其main()方法中添加如下測試程式碼:

package chapter04.concert;    import org.springframework.context.annotation.AnnotationConfigApplicationContext;    public class Main {      public static void main(String[] args) {          AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(ConcertConfig.class);            Performance performance = context.getBean(Performance.class);          performance.perform();            context.close();      }  }

運行程式碼,輸出結果如下所示:

Silencing cell phones

Taking seats

戲劇《不眠之夜Sleep No More》

perform finish

CLAP CLAP CLAP!!!

稍微修改下SleepNoMore類的perform()方法,讓它拋出一個異常:

@Override  public void perform() {      int number = 3 / 0;      System.out.println("戲劇《不眠之夜Sleep No More》");  }

再次運行程式碼,輸出結果如下所示:

Silencing cell phones

Taking seats

perform finish

Demanding a refund

Exception in thread "main" java.lang.ArithmeticException: / by zero

由此也可以說明,不管目標方法是否執行成功,@After註解都會執行,但@AfterReturning註解只會在目標方法執行成功時執行。

值得注意的是,使用@Aspect註解的切面類必須是一個bean(不管以何種方式聲明),否則切面不會生效,因為AspectJ自動代理只會為使用@Aspect註解的bean創建代理類。

也就是說,如果我們將ConcertConfig配置類中的以下程式碼刪除或者注釋掉:

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

運行結果將變為:

戲劇《不眠之夜Sleep No More》

3.8 創建環繞通知

我們可以使用@Around註解創建環繞通知,該註解能夠讓你在調用目標方法前後,自定義自己的邏輯。

因此,我們之前定義的5個切點,現在可以定義在一個切點中,為不影響之前的切面,我們新建切面AroundAudience,如下所示:

package chapter04.concert;    import org.aspectj.lang.ProceedingJoinPoint;  import org.aspectj.lang.annotation.Around;  import org.aspectj.lang.annotation.Aspect;  import org.aspectj.lang.annotation.Pointcut;    @Aspect  public class AroundAudience {      /**       * 可重用的切點       */      @Pointcut("execution(* chapter04.concert.Performance.perform(..))")      public void perform() {      }        @Around("perform()")      public void watchPerform(ProceedingJoinPoint joinPoint) {          try {              System.out.println("Taking seats");              System.out.println("Silencing cell phones");                joinPoint.proceed();                System.out.println("CLAP CLAP CLAP!!!");          } catch (Throwable throwable) {              System.out.println("Demanding a refund");          } finally {              System.out.println("perform finish");          }      }  }

這裡要注意的是,該方法有個ProceedingJoinPoint類型的參數,在方法中可以通過調用它的proceed()方法來調用目標方法。

然後修改下ConcertConfig類的程式碼:

package chapter04.concert;    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  @EnableAspectJAutoProxy  @ComponentScan  public class ConcertConfig {      /*@Bean      public Audience audience() {          return new Audience();      }*/        @Bean      public AroundAudience aroundAudience() {          return new AroundAudience();      }  }

運行結果如下所示:

Taking seats

Silencing cell phones

戲劇《不眠之夜Sleep No More》

CLAP CLAP CLAP!!!

perform finish

4. 源碼及參考

源碼地址:https://github.com/zwwhnly/spring-action.git,歡迎下載。

Craig Walls 《Spring實戰(第4版)》

AOP(面向切面編程)_百度百科

5. 最後

歡迎掃碼關注微信公眾號:「申城異鄉人」,定期分享Java技術乾貨,讓我們一起進步。