Spring AOP 掃盲

關於AOP

面向切面編程(Aspect-oriented Programming,俗稱AOP)提供了一種面向對象編程(Object-oriented Programming,俗稱OOP)的補充,面向對象編程最核心的單元是類(class),然而面向切面編程最核心的單元是切面(Aspects)。與面向對象的順序流程不同,AOP採用的是橫向切面的方式,注入與主業務流程無關的功能,例如事務管理和日誌管理。

Spring的一個關鍵組件是AOP框架。 雖然Spring IoC容器不依賴於AOP(意味著你不需要在IOC中依賴AOP),但AOP為Spring IoC提供了非常強大的中間件解決方案。

AOP 是一種編程範式,最早由 AOP 聯盟的組織提出的,通過預編譯方式和運行期動態代理實現程式功能的統一維護的一種技術。它是 OOP的延續。利用 AOP 可以對業務邏輯的各個部分進行隔離,從而使得業務邏輯各部分之間的耦合度降低,提高程式的可重用性,同時提高了開發的效率

我們之間的開發流程都是使用順序流程,那麼使用 AOP 之後,你就可以橫向抽取重複程式碼,什麼叫橫向抽取呢?或許下面這幅圖你能理解,先來看一下傳統的軟體開發存在什麼樣風險。

縱向繼承體系

在改進方案之前,我們或許都遇到過 IDEA 對你輸出 Duplicate Code 的時候,這個時候的類的設計是很糟糕的,程式碼寫的也很冗餘,基本上 if…else… 完成所有事情,這個時候就需要把相同的程式碼抽取出來成為公共的方法,降低耦合性。這種提取程式碼的方式是縱向抽取,縱向抽取的程式碼之間的關聯關係非常密切。
橫向抽取也是程式碼提取的一種方式,不過這種方式不會修改主要業務邏輯程式碼,只是在此基礎上添加一些與主要的業務邏輯無關的功能,AOP 採取橫向抽取機制,補充了傳統縱向繼承體系(OOP)無法解決的重複性 程式碼優化(性能監視、事務管理、安全檢查、快取),將業務邏輯和系統處理的程式碼(關閉連接、事務管理、操作日誌記錄)解耦。

AOP 的概念

在深入學習SpringAOP 之前,讓我們先對AOP的幾個基本術語有個大致的概念,這些概念不是很容易理解,比較抽象,可以知道有這麼幾個概念,下面一起來看一下:

  • 切面(Aspect): Aspect 聲明類似於 Java 中的類聲明,事務管理是AOP一個最典型的應用。在AOP中,切面一般使用 @Aspect 註解來使用,在XML 中,可以使用 <aop:aspect> 來定義一個切面。
  • 連接點(Join Point): 一個在程式執行期間的某一個操作,就像是執行一個方法或者處理一個異常。在Spring AOP中,一個連接點就代表了一個方法的執行。
  • 通知(Advice): 在切面中(類)的某個連接點(方法出)採取的動作,會有四種不同的通知方式: around(環繞通知),before(前置通知),after(後置通知), exception(異常通知),return(返回通知)。許多AOP框架(包括Spring)將建議把通知作為為攔截器,並在連接點周圍維護一系列攔截器。
  • 切入點(Pointcut):表示一組連接點,通知與切入點表達式有關,並在切入點匹配的任何連接點處運行(例如執行具有特定名稱的方法)。由切入點表達式匹配的連接點的概念是AOP的核心,Spring默認使用AspectJ切入點表達式語言。
  • 介紹(Introduction): introduction可以為原有的對象增加新的屬性和方法。例如,你可以使用introduction使bean實現IsModified介面,以簡化快取。
  • 目標對象(Target Object): 由一個或者多個切面代理的對象。也被稱為”切面對象”。由於Spring AOP是使用運行時代理實現的,因此該對象始終是代理對象。
  • AOP代理(AOP proxy): 由AOP框架創建的對象,在Spring框架中,AOP代理對象有兩種:JDK動態代理和CGLIB代理
  • 織入(Weaving): 是指把增強應用到目標對象來創建新的代理對象的過程,它(例如 AspectJ 編譯器)可以在編譯時期,載入時期或者運行時期完成。與其他純Java AOP框架一樣,Spring AOP在運行時進行織入。

Spring AOP 中通知的分類

  • 前置通知(Before Advice): 在目標方法被調用前調用通知功能;相關的類org.springframework.aop.MethodBeforeAdvice
  • 後置通知(After Advice): 在目標方法被調用之後調用通知功能;相關的類org.springframework.aop.AfterReturningAdvice
  • 返回通知(After-returning): 在目標方法成功執行之後調用通知功能;
  • 異常通知(After-throwing): 在目標方法拋出異常之後調用通知功能;相關的類org.springframework.aop.ThrowsAdvice
  • 環繞通知(Around): 把整個目標方法包裹起來,在被調用前和調用之後分別調用通知功能相關的類org.aopalliance.intercept.MethodInterceptor

Spring AOP 中織入的三種時期

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

AOP 的兩種實現方式

AOP 採用了兩種實現方式:靜態織入(AspectJ 實現)和動態代理(Spring AOP實現)

AspectJ

AspectJ 是一個採用Java 實現的AOP框架,它能夠對程式碼進行編譯(一般在編譯期進行),讓程式碼具有AspectJ 的 AOP 功能,AspectJ 是目前實現 AOP 框架中最成熟,功能最豐富的語言。ApectJ 主要採用的是編譯期靜態織入的方式。在這個期間使用 AspectJ 的 acj 編譯器(類似 javac)把 aspect 類編譯成 class 位元組碼後,在 java 目標類編譯時織入,即先編譯 aspect 類再編譯目標類。

Spring AOP 實現

Spring AOP 是通過動態代理技術實現的,而動態代理是基於反射設計的。Spring AOP 採用了兩種混合的實現方式:JDK 動態代理和 CGLib 動態代理,分別來理解一下

  • JDK動態代理:Spring AOP的首選方法。 每當目標對象實現一個介面時,就會使用JDK動態代理。目標對象必須實現介面
  • CGLIB代理:如果目標對象沒有實現介面,則可以使用CGLIB代理。

Spring 對 AOP的支援

Spring 提供了兩種AOP 的實現:基於註解式配置和基於XML配置

@AspectJ 支援

為了在Spring 配置中使用@AspectJ ,你需要啟用Spring支援,以根據@AspectJ切面配置Spring AOP,並配置自動代理。自動代理意味著,Spring 會根據自動代理為 Bean 生成代理來攔截方法的調用,並確保根據需要執行攔截。

可以使用XML或Java樣式配置啟用@AspectJ支援。 在任何一種情況下,都還需要確保AspectJ的aspectjweaver.jar 第三方庫位於應用程式的類路徑中(版本1.8或更高版本)。

開啟@AspectJ 支援

使用@Configuration 支援@AspectJ 的時候,需要添加 @EnableAspectJAutoProxy 註解,就像下面例子展示的這樣來開啟 AOP代理

@Configuration
@EnableAspectJAutoProxy
public class AppConfig {}

也可以使用XML配置來開啟@AspectJ 支援

<aop:aspectj-autoproxy/>

默認你已經添加了 aop 的schema 空間,如果沒有的話,你需要手動添加

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

    <!-- bean definitions here -->
</beans>

聲明一個切面

在啟用了@AspectJ支援的情況下,在應用程式上下文中定義的任何bean都具有@AspectJ方面的類(具有@Aspect注釋),Spring會自動檢測並用於配置Spring AOP。

使用XML 配置的方式定義一個切面

<aop:aspect />

使用註解的方式定義一個切面

@Aspect
public class MyAspect {}

切面(也就是用@Aspect註解的類)就像其他類一樣有屬性和方法。它們能夠包含切入點,通知和介紹聲明。

通過自動掃描檢測切面

你可以在Spring XML 配置中將切面類註冊為常規的bean,或者通過類路徑掃描自動檢測它們 – 與任何其他Spring管理的bean相同。然而,只是註解了@Aspect 的類不會被當作bean 進行管理,你還需要在類上面添加 @Component 註解,把它當作一個組件交給 Spring 管理。

定義一個切點

一個切點由兩部分組成:包含名稱和任何參數以及切入點表達式的簽名,該表達式能夠確定我們想要執行的方法。在@AspectJ注釋風格的AOP中,切入點表達式需要用@Pointcut註解標註(這個表達式作為方法的簽名,它的返回值必須是 void)。

@Pointcut("execution(* transfer(..))") // 切入點表達式
private void definePointcut() {}// 方法簽名

切入點表達式的編寫規則如下:

現在假設我們需要配置的切點僅僅匹配指定的包,就可以使用 within() 限定符來表示,如下表達式所述:

請注意我們使用了 && 操作符把 execution() 和 within() 指示器連接在一起,表示的是 的關係,類似的,你還可以使用 || 操作來表示 的關係, 使用 ! 表示 的關係。

除了within() 表示的限定符外,還有其它的限定符,下面是一個限定符表

AspectJ 描述符 描述
arg() 限制連接點匹配參數為指定類型的執行方法
@args() 限制連接點匹配參數由指定註解標註的執行方法
execution() 用於匹配是連接點的執行方法
this() 限制連接點匹配的AOP代理的bean引用為指定類型的類
target 限制連接點匹配目標對象為指定類型的類
@target() 限制連接點匹配特定的執行對象,這些對象對應的類要具有指定類型的註解
within() 限制連接點匹配指定的類型
@within() 限制連接點匹配指定註解所標註的類型
@annotationn 限定匹配帶有指定註解的連接點

使用XML配置來配置切點

<aop:config>
	<aop:aspect ref = "">
  	<aop:poincut id = "" expression="execution(** com.cxuan.aop.definePointcut(......))"/>
  </aop:aspect>
</aop:config>

聲明一個通知

通知是和切入點表達式相互關聯,用於在方法執行之前,之後或者方法前後,方法返回,方法拋出異常時調用通知的方法,切入點表達式可以是對命名切入點的簡單引用,也可以是在適當位置聲明的切入點表達式。下面以一個例子來演示一下這些通知都是如何定義的:

上面的例子就很清晰了,定義了一個 Audience 切面,並在切面中定義了一個performance() 的切點,下面各自定義了表演之前、表演之後返回、表演失敗的時候進行通知,除此之外,你還需要在main 方法中開啟 @EnableAspectJAutoProxy 來開啟自動代理。

除了使用Java Config 的方式外,你還可以使用基於XML的配置方式

當然,這種切點定義的比較冗餘,為了解決這種類似 if...else... 災難性的業務邏輯,你需要單獨定義一個<aop:pointcut>,然後使用 pointcut-ref 屬性指向上面那個標籤,就像下面這樣

環繞通知

在目標方法執行之前和之後都可以執行額外程式碼的通知。在環繞通知中必須顯式的調用目標方法,目標方法才會執行,這個顯式調用時通過ProceedingJoinPoint來實現的,可以在環繞通知中接收一個此類型的形參,spring容器會自動將該對象傳入,注意這個參數必須處在環繞通知的第一個形參位置。

環繞通知需要返回返回值,否則真正調用者將拿不到返回值,只能得到一個null。下面是環繞通知的一個示例

 <aop:around method="around" pointcut-ref="pc1"/>
 public Object around(ProceedingJoinPoint jp) throws Throwable{
   System.out.println("1 -- around before...");
   Object obj = jp.proceed(); //--顯式的調用目標方法
   System.out.println("1 -- around after...");
   return obj;
 }

文章參考:

//juejin.im/post/5a695b3cf265da3e47449471

《Spring In Action》

//docs.spring.io/spring/docs/5.1.9.RELEASE/spring-framework-reference/core.html

Spring AOP 五大通知類型

Tags: