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

前言

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

為什么需要 AOP

我們做軟件開發(fā)的最終目的是為了解決公司的各種需求,為業(yè)務賦能,注意,這里的需求包含了業(yè)務需求系統(tǒng)需求,對于絕大部分的業(yè)務需求的普通關注點,都可以通過面向?qū)ο螅?code>OOP)的方式對其進行很好的抽象、封裝以及模塊化,但是對于系統(tǒng)需求使用面向?qū)ο蟮姆绞诫m然很好的對其進行分解并對其模塊化,但是卻不能很好的避免這些類似的系統(tǒng)需求在系統(tǒng)的各個模塊中到處散落的問題。

why-need-aop.png

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

use-aop-arc.png

幾個重要的概念

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

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

通過之前的介紹可知,在我們的系統(tǒng)運行之前,需要將 AOP 定義的一些橫切關注點(功能模塊)織入(可以簡單理解為嵌入)到系統(tǒng)的一些業(yè)務模塊當中去,想要完成織入的前提是我們需要知道可以在哪些執(zhí)行點上進行操作,這些執(zhí)行點就是 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() 方法調(diào)用中,我們可以選擇在 Developer 的構(gòu)造方法執(zhí)行時進行織入,也可以在 showMainIntro() 方法的執(zhí)行點上進行織入(被調(diào)用的地方或者在方法內(nèi)部執(zhí)行的地方),或者在 setAge() 方法設置 sge 字段時織入,實際上,只要你想可以在 test() 方法的任何一個執(zhí)行點上執(zhí)行織入,這些可以織入的執(zhí)行點就是 Joinpoint
這么說可能比較抽象,下面通過 test() 方法調(diào)用的時序圖來直觀的看看:

aop-weaving.png

從方法執(zhí)行的時序來看不難發(fā)現(xiàn),會有如下的一些常見的 Joinpoint 類型:

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

雖然理論上,在程序執(zhí)行中的任何執(zhí)行點都可以作為 Joinpoint,但是在某些類型的執(zhí)行點上進行織入操作,付出的代價比較大,所以在 Spring 中的 Joinpoint 只支持方法執(zhí)行(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 的表述方式,在進行織入時需要根據(jù) Pointcut 的配置,然后往那些匹配的 Joinpoint 織入橫切的邏輯。這里面臨的第一個問題:用人類的自然語言可以很快速的表述哪些我們需要織入的 Joinpoint,但是在代碼里要如何去表述這些 Joinpoint 呢?
目前有如下的一些表述 Joinpoint 定義的方式:

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

另外 Pointcut 也支持進行一些簡單的邏輯運算,這時我們就可以將多個簡單的 Pointcut 通過邏輯運算組合為一個比較復雜的 Pointcut 了,比如在 Spring 配置中的 andor 等邏輯運算標識符以及 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 的執(zhí)行點的位置和功能的不同,分為如下幾種主要的類型:

  • Before Advice。Before Advice 表示是在匹配的 Joinpoint 位置之前執(zhí)行的類型。如果被成功織入到方法類型的 Joinpoint 中,那么 Beofre Advice 就會在這個方法執(zhí)行之前執(zhí)行,還有一點需要注意的是,如果需要在 Before Advice 中結(jié)束方法的執(zhí)行,我們可以通過在 Advice 中拋出異常的方式來結(jié)束方法的執(zhí)行。
  • After Advice。顯而易見,After Advice 表示在配置的 Joinpoint 位置之后執(zhí)行的類型??梢栽诩毞譃?After returning Advice、After throwing AdviceAfter finally Advice 三種類型。其中 After returning Advice 表示的是匹配的 Joinpoint 方法正常執(zhí)行完成(沒有拋出異常)后執(zhí)行;After throwing Advice 表示匹配的 Joinpoint 方法執(zhí)行過程中拋出異常沒有正常返回后執(zhí)行;After finally Advice 表示方法類型的 Joinpoint 的不管是正常執(zhí)行還是拋出異常都會執(zhí)行。
    這幾種 Advice 類型在方法類型的 Joinpoint 中執(zhí)行順序如下圖所示:
    advice-example-location.png
  • Around Advice。這種類型是功能最為強大的 Advice,可以匹配的 Joinpoint 之前、之后甚至終端原來 Joinpoint 的執(zhí)行流程,正常情況下,會先執(zhí)行 Joinpoint 之前的執(zhí)行邏輯,然后是 Joinpoint 自己的執(zhí)行流程,最后是執(zhí)行 Joinpoint 之后的執(zhí)行邏輯。細心的你應該發(fā)現(xiàn)了,這不就是上面介紹的 Before AdviceAfter Advice 類型的組合嗎,是的,它可以完成這兩個類型的功能,不過還是要根據(jù)具體的場景選擇合適的 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 是對我們系統(tǒng)里的橫切關注點(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 來確定的,會根據(jù) Pointcut 設置條件的不同而不同。
有了 AOP 這些概念后就可以把上文的例子再次進行整理,各個概念所在的位置如下圖所示:

aop-concept.png

總結(jié)

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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容