Spring AOP 掃盲

程式設計師cxuan發表於2020-06-07

關於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="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:aop="http://www.springframework.org/schema/aop" xsi:schemaLocation="
        http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/aop https://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;
 }

文章參考:

https://juejin.im/post/5a695b3cf265da3e47449471

《Spring In Action》

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

Spring AOP 五大通知型別

相關文章