如何實現一個簡易版的 Spring - 如何實現 AOP(上)


前言

本文是「如何實現一個簡易版的 Spring 系列」的第五篇,在之前介紹了 Spring 中的核心技術之一 IoC,從這篇開始我們再來看看 Spring 的另一個重要的技術——AOP。用過 Spring 框架進行開發的朋友們相信或多或少應該接觸過 AOP,用中文描述就是面向切面編程。學習一個新技術了解其產生的背景是至關重要的,在剛開始接觸 AOP 時不知道你有沒有想過這個問題,既然在面向對象的語言中已經有了 OOP 了,為什么還需要 AOP 呢?換個問法也就是說在 OOP 中有哪些場景其實處理得並不優雅,需要重新尋找一種新的技術去解決處理?(P.S. 這里建議暫停十秒鍾,自己先想一想...)

為什么需要 AOP

我們做軟件開發的最終目的是為了解決公司的各種需求,為業務賦能,注意,這里的需求包含了業務需求和系統需求,對於絕大部分的業務需求的普通關注點,都可以通過面向對象(OOP)的方式對其進行很好的抽象、封裝以及模塊化,但是對於系統需求使用面向對象的方式雖然很好的對其進行分解並對其模塊化,但是卻不能很好的避免這些類似的系統需求在系統的各個模塊中到處散落的問題。

why-need-aop.png

因此,需要去重新尋找一種更好的辦法,可以在基於 OOP 的基礎上提供一套全新的方法來處理上面的問題,或者說是對 OOP 面向對象的開發模式做一個補充,使其可以更優雅的處理上面的問題,迄今為止 Spring 提供一個的解決方案就是面向切面編程——AOP。有了 AOP 后,我們可以將這些事務管理、系統日志以及安全檢查等系統需求(橫切關注點:cross-cutting concern)進行模塊化的組織,使得整個系統更加的模塊化方便后續的管理和維護。細心的你應該發現在 AOP 里面引入了一個關鍵的抽象就是切面(Aspect),用於對於系統中的一些橫切關注點進行封裝,要明確的一點是 AOP 和 OOP 不是非此即彼的對立關系,AOP 是對 OOP 的一種補充和完善,可以相互協作來完成需求,Aspect 對於 AOP 的重要程度就像 Class 對 OOP 一樣。

use-aop-arc.png

幾個重要的概念

我們最終的目的是要模仿 Spring 框架自己去實現一個簡易版的 AOP 出來,雖然是簡易版但是會涉及到 Spring AOP 中的核心思想和主要實現步驟,不過在此之前先來看看 AOP 中的重要概念,同時也是為以后的實現打下理論基礎,這里需要說明一點是我不會使用中文翻譯去描述這些 AOP 定義的術語(另外,業界 AOP 術語本來就不太統一),你需要重點理解的是術語在 AOP 中代表的含義,就像我們不會把 Spring 給翻譯成春天一樣,在軟件開發交流你知道它表示一個 Java 開發框架就可以了。下面對其關鍵術語進行逐個介紹:

Joinpoint

A point during the execution of a program, such as the execution of a method or the handling of an exception. In Spring AOP, a join point always represents a method execution. -- Spring Docs

通過之前的介紹可知,在我們的系統運行之前,需要將 AOP 定義的一些橫切關注點(功能模塊)織入(可以簡單理解為嵌入)到系統的一些業務模塊當中去,想要完成織入的前提是我們需要知道可以在哪些執行點上進行操作,這些執行點就是 Joinpoint。下面看個簡單示例:

/**
 * @author mghio
 * @since 2021-05-22
 */
public class Developer {

  private String name;

  private Integer age;

  private String siteUrl;

  private String position;

  public Developer(String name, String siteUrl) {
    this.name = name;
    this.siteUrl = siteUrl;
  }

  public void setSiteUrl(String siteUrl) {
    this.siteUrl = siteUrl;
  }

  public void setAge(Integer age) {
    this.age = age;
  }

  public void setName(String name) {
    this.name = name;
  }

  public void setPosition(String position) {
    this.position = position;
  }

  public void showMainIntro() {
    System.out.printf("name:[%s], siteUrl:[%s]\n", this.name, this.siteUrl);
  }

  public void showAllIntro() {
    System.out.printf("name:[%s], age:[%s], siteUrl:[%s], position:[%s]\n",
        this.name, this.age, this.siteUrl, this.position);
  }

}
/**
 * @author mghio
 * @since 2021-05-22
 */
public class DeveloperTest {

  @Test
  public void test() {
    Developer developer = new Developer("mghio", "https://www.mghio.cn");
    developer.showMainIntro();
    developer.setAge(18);
    developer.setPosition("中國·上海");
    developer.showAllIntro();
  }

}

理論上,在上面示例的這個 test() 方法調用中,我們可以選擇在 Developer 的構造方法執行時進行織入,也可以在 showMainIntro() 方法的執行點上進行織入(被調用的地方或者在方法內部執行的地方),或者在 setAge() 方法設置 sge 字段時織入,實際上,只要你想可以在 test() 方法的任何一個執行點上執行織入,這些可以織入的執行點就是 Joinpoint。
這么說可能比較抽象,下面通過 test() 方法調用的時序圖來直觀的看看:

aop-weaving.png

從方法執行的時序來看不難發現,會有如下的一些常見的 Joinpoint 類型:

  • 構造方法調用(Constructor Call)。對某個對象調用其構造方法進行初始化的執行點,比如以上代碼中的 Developer developer = new Developer("mghio", "https://www.mghio.cn");。
  • 方法調用(Method call)。調用某個對象的方法時所在的執行點,實際上構造方法調用也是方法調用的一種特殊情況,只是這里的方法是構造方法而已,比如示例中的 developer.showMainIntro(); 和 developer.showAllIntro(); 都是這種類型。
  • 方法執行(Method execution)。當某個方法被調用時方法內部所處的程序的執行點,這是被調用方法內部的執行點,與方法調用不同,方法執行入以上方法時序圖中標注所示。
  • 字段設置(Field set)。調用對象 setter 方法設置對象字段的代碼執行點,觸發點是對象的屬性被設置,和設置的方式無關。以上示例中的 developer.setAge(18); 和 developer.setPosition("中國.上海"); 都是這種類型。
  • 類初始化(Class initialization)。類中的一些靜態字段或者靜態代碼塊的初始化執行點,在以上示例中沒有體現。
  • 異常執行(Exception execution)。類的某些方法拋出異常后對應的異常處理邏輯的執行點,在以上示例中沒有這種類型。

雖然理論上,在程序執行中的任何執行點都可以作為 Joinpoint,但是在某些類型的執行點上進行織入操作,付出的代價比較大,所以在 Spring 中的 Joinpoint 只支持方法執行(Method execution)這一種類型(這一點從 Spring 的官方文檔上也有說明),實際上這種類型就可以滿足絕大部分的場景了。

Pointcut

A predicate that matches join points. Advice is associated with a pointcut expression and runs at any join point matched by the pointcut (for example, the execution of a method with a certain name). The concept of join points as matched by pointcut expressions is central to AOP, and Spring uses the AspectJ pointcut expression language by default.-- by Spring Docs

Pointcut 表示的是一類 Jointpoint 的表述方式,在進行織入時需要根據 Pointcut 的配置,然后往那些匹配的 Joinpoint 織入橫切的邏輯。這里面臨的第一個問題:用人類的自然語言可以很快速的表述哪些我們需要織入的 Joinpoint,但是在代碼里要如何去表述這些 Joinpoint 呢?
目前有如下的一些表述 Joinpoint 定義的方式:

  • 直接指定織入的方法名。顯而易見,這種表述方式雖然簡單,但是所支持的功能比較單一,只適用於方法類型的 Joinpoint,而且當我們系統中需要織入的方法比較多時,一個一個的去定義織入的 Pointjoint 時過於麻煩。
  • 正則表達式方式。正則表達式相信大家都有一些了解,功能很強大,可以匹配表示多個不同方法類型的 Jointpoint,Spring 框架的 AOP 也支持這種表述方式。
  • Pointcut 特定語言方式。這個因為是一種特定領域語言(DSL),所以其提供的功能也是最為靈活和豐富的,這也導致了不管其使用和實現復雜度都比較高,像 AspectJ 就是使用的這種表述方式,當然 Spring 也支持。

另外 Pointcut 也支持進行一些簡單的邏輯運算,這時我們就可以將多個簡單的 Pointcut 通過邏輯運算組合為一個比較復雜的 Pointcut 了,比如在 Spring 配置中的 and 和 or 等邏輯運算標識符以及 AspectJ 中的 && 和 || 等邏輯運算符。

Advice

Action taken by an aspect at a particular join point. Different types of advice include “around”, “before” and “after” advice. (Advice types are discussed later.) Many AOP frameworks, including Spring, model an advice as an interceptor and maintain a chain of interceptors around the join point.-- by Spring Docs

Advice 表示的是一個注入到 Joinpoint 的橫切邏輯,是一個橫切關注點邏輯的抽象載體。按照 Advice 的執行點的位置和功能的不同,分為如下幾種主要的類型:

  • Before Advice。Before Advice 表示是在匹配的 Joinpoint 位置之前執行的類型。如果被成功織入到方法類型的 Joinpoint 中,那么 Beofre Advice 就會在這個方法執行之前執行,還有一點需要注意的是,如果需要在 Before Advice 中結束方法的執行,我們可以通過在 Advice 中拋出異常的方式來結束方法的執行。
  • After Advice。顯而易見,After Advice 表示在配置的 Joinpoint 位置之后執行的類型。可以在細分為 After returning Advice、After throwing Advice 和 After finally Advice 三種類型。其中 After returning Advice 表示的是匹配的 Joinpoint 方法正常執行完成(沒有拋出異常)后執行;After throwing Advice 表示匹配的 Joinpoint 方法執行過程中拋出異常沒有正常返回后執行;After finally Advice 表示方法類型的 Joinpoint 的不管是正常執行還是拋出異常都會執行。
    這幾種 Advice 類型在方法類型的 Joinpoint 中執行順序如下圖所示:
    advice-example-location.png
  • Around Advice。這種類型是功能最為強大的 Advice,可以匹配的 Joinpoint 之前、之后甚至終端原來 Joinpoint 的執行流程,正常情況下,會先執行 Joinpoint 之前的執行邏輯,然后是 Joinpoint 自己的執行流程,最后是執行 Joinpoint 之后的執行邏輯。細心的你應該發現了,這不就是上面介紹的 Before Advice 和 After Advice 類型的組合嗎,是的,它可以完成這兩個類型的功能,不過還是要根據具體的場景選擇合適的 Advice 類型。

Aspect

A modularization of a concern that cuts across multiple classes. Transaction management is a good example of a crosscutting concern in enterprise Java applications. In Spring AOP, aspects are implemented by using regular classes (the schema-based approach) or regular classes annotated with the @Aspect annotation (the @AspectJ style). -- Spring Docs

Aspect 是對我們系統里的橫切關注點(crosscutting concern)包裝后的一個抽象概念,可以包含多個 Joinpoint 以及多個 Advice 的定義。Spring 集成了 AspectJ 后,也可以使用 @AspectJ 風格的聲明式指定一個 Aspect,只要添加 @Aspect 注解即可。

Target object

An object being advised by one or more aspects. Also referred to as the “advised object”. Since Spring AOP is implemented by using runtime proxies, this object is always a proxied object. -- by Spring Docs

目標對象一般是指那些可以匹配上 Pointcut 聲明條件,被織入橫切邏輯的對象,正常情況下是由 Pointcut 來確定的,會根據 Pointcut 設置條件的不同而不同。
有了 AOP 這些概念后就可以把上文的例子再次進行整理,各個概念所在的位置如下圖所示:

aop-concept.png

總結

本文首先對 AOP 技術的誕生背景做了簡要介紹,后面介紹了 AOP 的幾個重要概念為后面我們自己實現簡易版 AOP 打下基礎,AOP 是對 OOP 的一種補充和完善,文中列出的幾個概念只是 AOP 中涉及的概念中的冰山一角,想要深入了解更多的相關概念的朋友們可以看 官方文檔 學習,下篇是介紹 AOP 實現依賴的一些基礎技術,敬請期待。轉發、分享都是對我的支持,我將更有動力堅持原創分享!


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM