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

mghio發表於2021-05-23

前言

本文是「如何實現一個簡易版的 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 實現依賴的一些基礎技術,敬請期待。轉發、分享都是對我的支援,我將更有動力堅持原創分享!

相關文章