Spring之旅第七站:面向切面程式設計(AOP)

guoxiaoxu發表於2018-02-23

面向切面的Spring

本章主要內容:

  • 面向切面程式設計的基本原理
  • 通過POJO建立切面
  • 使用@Aspect註解
  • 為AspectJ切面注入依賴。

說明

如果你有幸能看到。

  • 1、本文參考了《Spring 實戰》重點內容,參考了GitHub上的程式碼
  • 2、本文只為記錄作為以後參考,要想真正領悟Spring的強大,請看原書。
  • 3、在一次佩服老外,國外翻譯過來的書,在GiuHub上大都有例項。看書的時候,跟著敲一遍,效果很好。
  • 4、程式碼和筆記在這裡GitHub,對你有幫助的話,歡迎點贊。
  • 5、每個人的學習方式不一樣,找到合適自己的就行。2018,加油。
  • 6、問候了下Java 8 In Action 的作者Mario Fusco,居然回覆了。
  • 7、Spring In Action 、Spring Boot In Action的作者Craig Walls老忙了,沒理睬。
  • 8、知其然,也要知其所以然。

談一些個人感受

  • 1、趕快學習Spring吧,Spring MVC 、Spring Boot 、微服務。
  • 2、重點中的重點,學習JDK 8 Lambda,Stream,Spring 5 最低要求JDK1.8.
  • 3、還有Netty、放棄SH吧,不然你會落伍的。
  • 4、多看一些國外翻譯過來的書,例如 Xxx In Action 系列。權威指南系列。用Kindle~

軟體系統中的一些功能就像我們家裡的電錶一樣。則核心功能需要用到應用程式的多個地方。但是我們又不想在每個點都明確呼叫它。日誌、安全、事務管理的確很重要。但它們是否為應用物件主動參與的行為呢?如果讓應用物件只關注與自己所針對的業務領域問題,而其他方面的問題由其他應用物件來處理,這樣不更好嗎?

在軟體開發中,散佈於應用中多出功能被稱為橫切關注點(crosscutting concern)。通常來講橫切關注點從概念上是與應用的業務邏輯分離的。但往往是耦合在一起的,把這些橫切關注點與業務邏輯相分離正是面向切面程式設計(AOP)所要解決的問題。

依賴注入(DI)管理我們的應用物件,DI有助於應用物件之間解耦。而AOP可以實現橫切關注點與它們所影響的物件之間的耦合。

4.1 什麼是面向切面程式設計

切面能夠幫我們模組化橫切關注點。簡而言之,橫切關注點可以被描述為影響應用多處的功能。例如 安全,事務、日誌等功能。

如果要重用物件的話,最常見的物件導向技術是繼承、委託、組合。但是,如果整個應用中都使用相同的基類,繼承往往會導致一個脆弱的物件體系。而使用委託可能需要委託物件進行復雜的呼叫。

切面提供了取代繼承和委託的另一種可選方案。在使用面向切面程式設計時,我們仍然在一個地方定義通知功能,而無需修改受影響的類。橫切關注點可以被模組化為特殊的類,這些類被稱為切面(aspect). 這樣做帶來兩個好處:每個關注點都集中到一個地方,而不是分散到多處程式碼中:其次,服務模組更簡潔,因為它只包含了主要關注點(核心功能)的程式碼。而次要關注的程式碼被移到切面中了。

4.1.1 定義AOP術語

描述切面的常用術語有:通知(advice)、切點(pointcut)、(連線點)。

通知(advice)

通知定義了切面是什麼以及何時使用。除了描述切面要完成的工作外,通知還解決了何時執行這個工作問題。它應該在某個方法被呼叫之前?之後?之前和之後都呼叫?還是隻在方法丟擲異常時呼叫?

Spring切面可以應用5中型別的通知:

  • 前置通知(Before):在目標方法被呼叫之前呼叫通知功能。
  • 後置通知(After):在目標方法完成之後呼叫通知
  • 返回通知(After-returning):在目標方法成功執行之後呼叫通知
  • 異常通知(After-throwing):在目標方法丟擲異常後呼叫通知
  • 環繞通知(Around):在被通知方法呼叫之前和呼叫之後執行自定義的行為

連線點

我們的應用可能有數以千計的時機應用通知,這些時機被稱為連線點。連線點是在應用執行過程中能夠插入的一個點。這個點可以是呼叫方法時,丟擲異常時,甚至修改一個欄位時。切面可以利用這些點插入到應用的正常流程之中,並新增新的行為。

切點 如果說通知定義了切面的的“什麼”和“何時”,那麼切點定義了“何處”。切點的定義會匹配通知所要織入的一個或多個連線點。

切面 切面是通知和切點的結合。通知和切點通過定義了切面的全部 內容——他是什麼,在什麼時候和在哪裡完成其功能。

引入 引入允許我們向現有的類新增新的方法或者屬性。

織入

織入是把切面應用到目標物件並建立新的代理物件的過程。切面在指定的連線點被織入到目標物件。在目標物件的生命週期裡有多個點可以進行織入:

  • 編譯器:切面在目標類編譯時被織入。Aspect的織入編譯器就是以這種方式織入切面的。
  • 類載入器:切面在目標類載入到JVM時被織入。需要特殊的類載入(Classloader),它可以在目標類被引入之前增強該目標類的位元組碼(CGlib)
  • 執行期:切面在應用執行時的某個時刻被織入。AOP會為目標物件建立一個代理物件

通知包含了需要用於多個應用物件的橫切關注點。連線點是程式執行過程中能夠應用通知的所有點。切點定義了通知被應用的具體位置(在哪些連線點),其中關鍵是切點定義了哪些連線點會得到通知。

4.1.2 Spring對AOP的支援

並不是所有的AOP框架都是相同的,他們在連線點模型上可能有強弱之分。有些允許在欄位修飾符級別的通知,而另一些只支援與方法呼叫相關的連線點。它們織入切面的方式和時機也有所不同。但是,無論如何,建立切點來定義切面所織入的連線點是AOP的基本功能。

Spring提供了4種型別的AOP支援:

  • 基於代理的經典Spring AOP
  • 純POJO切面
  • @AspectJ註解驅動的切面
  • 注入式AspectJ切面

前三種都是Spirng AOP實現的變體,Spring AOP構建在動態代理基礎上。因此,Spring對AOP的支援侷限於方法攔截。

引入了簡單的宣告式AOP與基於註解的AOP之後,Spring經典的看起來就顯得非常笨拙和過於複雜話,直接使用ProxyFactory bean 會讓人感覺厭煩。

藉助於Spring的aop名稱空間,我們可以將純POJO轉為切面。

Spring借鑑了AspectJ的切面,以提供註解驅動的AOP。本質上,它依然是Spring基於代理的AOP,但是程式設計模型幾乎與編寫成熟的AspectJ註解切面完全一致。這種AOP風格的好處在於能夠不使用XML來完成功能。

Spring所建立的通知都是用標準的Java類編寫的,定義通知所應用的切點通常會使用註解或在Spring配置檔案裡採用XML來編寫

通知帶代理類中包裹切面,Spring在執行時把切面織入到Spring所管理的bean中。代理類封裝了目標類,並攔截被通知方法的呼叫。再把呼叫轉發給真正的目標bean。當代理攔截到方法呼叫時,在呼叫目標bean方法之前,會執行切面邏輯。直到應用需要被代理bean時,Spring才會建立代理物件。如果使用ApplicationContext的話,在ApplicationContext從BeanFactory中載入所有的bean的時候,Spring才會建立被代理的物件。因為Spirng執行時才建立代理物件,所以我們不需要特殊的編譯器來織入Spring AOP的切面。

Spring基於動態代理,所以Spring只支援方法連線點。方便攔截可以滿足大部分的需求。

4.2 通過切點來選擇連線點

切點用於準確定位應該在什麼地方應用切面的通知。通知和切點是切面最基本的元素。

Spring僅支援AspectJ切點指示器的一個子集。Spring是基於代理的,而某些切點表示式是基於代理的AOP無關的。

Spring支援的指示器,只有execution指示器是實際執行匹配的,而其他的指示器都是用來限制匹配的。這說明execution指示器是我們在編寫切點定義時最主要的指示器。

4.2.1編寫切點

為了闡述Spring中的切面, 我們需要有個主題來定義切面的切點。

package com.guo.cocert;
public interface Performance {
  public void perform();
}
execution(* concert.Performance.perform(..))

我們使用execution()指示器選擇Performance的perform()方法,方法表示式以”*”號開始,表明了我們不關心方法返回值的型別。然後指明瞭全限定類名和方法名,對於方法引數列表,我們使用了兩個點號(..)表明切點要選擇任意的perform()方法,無論該方法的入參是什麼。

現在假設我們需要配置的切點僅匹配concert包,可以使用within()指示器

execution(* concert.Performance.perform(..)) && within(concert.*)

因為“&”在XMl中有特殊的含義,所以在Spring和XML配置中,描述切點時,可以使用and代替“&&”。

4.2.2 在切點中選擇bean

Spring引入了一個新的bean()指示器,它允許我們在切點表示式中使用bean的ID來標識bean。bean()使用bean ID 或 bean 名稱作為引數來限制切點只匹配特定的bean。

execution(* concert.Performance.perform(..)) and bean("woodsotck")

也可以這樣

execution(* concert.Performance.perform(..)) and !bean("woodsotck")

切面的通知會被編織到所有ID不為woodsotck的bean中。

4.3使用註解建立切面

使用註解來建立切面是AspectJ 5所引入的關鍵特性。

4.3.1 定義切面

如果一場演出沒有觀眾的話,那不能稱之為演出。

@AspectJ
public class Audience {

}

Audience類使用@AspectJ註解進行了標註。該註解表明Audience不僅僅是一個POJO,還是一個切面。Audience類中的方法都是使用註解來定義切面的具體行為。

@AspectJ
public class Audience {
  @Pointcut("execution(* * concern.Performance.perform(..))")
  public void performance() {};
}

在Autience中,performance()方法使用了@Pointcut註解。為@Pointcut註解設定的值是一個切點表示式,就像之前在通知註解上所設定的那樣。

需要注意的是,除了註解和沒有實際操作的performa()方法,Audience類依然是一個POJO,我們能夠像使用其他的Java類那樣呼叫它的方法,它的方法也能獨立的進行單元測試。與其他Java類沒有什麼區別。

像其他的Java類一樣,它可以裝配為Spring中的bean

@Bean
public Audience audience() {
  return new Audience();
}

如果你就此止步的話,Audience只會是Spring容器中的一個bean。即便使用了AspectJ註解,但它並不會被視為切面,這些註解不會解析,也不會建立將其轉化為切面的代理。

如果你使用JavaConfig的話,可以在配置類的級別上通過使用EnableAspectJ-AutoProxy註解啟用自動代理功能。

@Configuration
@EnableAspectJAutoProxy             //啟用AspectJ自動代理
@ComponentScan
public class ConcertConfig {
  @Bean
  public Audience autidence() {     //宣告Audience bean
    return new Audience();
  }
}

假如你在Spring中使用XMl來裝配bean的話,那麼需要使用Spring aop名稱空間中的<aop:aspect-autoproxy>元素

<?xml version="1.0" encoding="UTF-8"?>

、、、、、、、、、、、、、、、、、、、、、、、、

<context:component-scan base-package="com.guo.concert"/>
<aop:aspect-autoproxy/>
<bean class="com.guo.concert.Audience"/>

不管你使用JavaConfig還是XML,AspecJ自動代理都會使用@Aspect註解的bean建立一個代理。這個代理會圍繞著所有該切面的切點所匹配的bean。

我們需要記住的是,Spring的AspectJ自動代理僅僅使用@AspectJ作為建立切面的指導,切面依然是基於代理的。本質上它依然是Spring基於代理的切面。

4.3.2 建立環繞通知

環繞通知是最為強大的通知型別,它能夠讓你編寫的邏輯將被通知的目標方法安全包裝起來,實際上就像在一個通知方法中同時編寫前置通知和後置通知。

@AspectJ
public class Audience {
  @Pointcut("execution(* * concern.Performance.perform(..))")
  public void performance() {};
  @Around
  public void xx(Xxx jp) {
    .......
    jp.proced()
  }
}

在這裡,@Around註解,表明這個xx()方法會作為performance()切點的環繞通知。

這個通知所達到的效果與之前的前置通知和後置通知是一樣的。

需要注意的是,別忘記呼叫proceed()方法,如果不呼叫這個方法,那麼你的通知實際上會阻塞對被通知方法的呼叫,有意思的是,你可以不呼叫proceed方法,從而阻塞對被通知方法的反問,

4.3.4 通過註解引入新功能

一些程式語言,例如:Ruby和Groovy,有開放來的理念,它們可以不直接使用修改物件或類的定義就能夠為物件或類增加新的方法。不過Java並不是動態語言,一旦編譯完成了,就很難在為該類新增新的功能了。

如果切面能夠為現有的方法增加額外的功能,為什麼不恩那個為一個物件增加新的方法呢?利用引入AOP的概念,切面可以為Spring bean 新增新的方法。

在Spring中,註解和自動代理提供了一種便利的方式來建立切面,它非常簡單,並且只設計最少的Spring配置,但是,面向註解的切面有一個明顯的不足點:你必須能夠為通知類新增註解,為了做到這一點,必須要有原始碼。

4.4 在XML中宣告切面

之前,有這樣一條原則:那就是基於註解的配置要優於Java的配置,基於Java的配置要優於XMl的配置,但是,如果你需要宣告切面,但是又不能為通知類新增註解的時候 ,那麼就必須轉向XML配置了。

在Spring的aop名稱空間中,提供了多個元素用來在XML中宣告切面,

  • <aop:advisor> :定義AOP通知器
  • <aop:after> :定義AOP後置通知
  • <aop:after-returning> :定義AOP返回通知
  • <aop:after-throwing> :定義AOP異常通知
  • <aop:around> :定義AOP環繞通知
  • <aop:aspect> :定義一個切面
  • <aop:aspectj-autoproxy> :啟用@AspectJ註解
  • <aop:before> :定義一個AOP前置通知
  • <aop:poiontcut> :定義一個切點

4.4.1 宣告前置通知和後置通知

我們會使用Spring aop名稱空間中的一些元素,將沒有註解的Aurience類轉為切面

<aop:config>
    <aop:aspect ref="audience">       <!--引用audience Bean-->

        <aop:before pointcut="execution(* * concert.Performance.perform(..))" method="silenceCellIphones"/>

        <aop:before pointcut="execution(* * concert.Performance.perform(..))" method="takeSeats"/>

        <aop:after-returning pointcut="execution(* * concert.Performance.perform(..))" method="applause"/>

        <aop:after-throwing pointcut="execution(* * concert.Performance.perform(..))" method="demandRefund"/>

    </aop:aspect>
</aop:config>

第一需要注意的就是大多數AOP配置元素必須在<aop:config>元素的上下文中使用。

在所有的通知元素中,pointcut屬性定義了通知所應用的切點,它的值是使用AspectJ切點表示式語法所定義的切點。

在基於Aspectj註解的通知中,當發現在這些型別的重複時,使用@Pointcut註解來消除這些重複的內容。

如下的XMl配置展示瞭如何將通用的切點表示式抽取到一個切點宣告中,這樣,這個宣告就能在所有的通知元素中使用了

<aop:config>
    <aop:aspect ref="audience">       <!--引用audience Bean-->
        <aop:pointcut id="performance" expression="execution(* * concert.Performance.perform(..))"  />

        <aop:before pointcut="" method="silenceCellIphones"/>

        <aop:before pointcut-ref="performance" method="takeSeats"/>

        <aop:after-returning pointcut-ref="performance" method="applause"/>

        <aop:after-throwing pointcut-ref="performance" method="demandRefund"/>

    </aop:aspect>
</aop:config>

現在的切點是一個地方定義的,並且被多個通知元素所引用,<aop:pointcut>元素定義了一個id為performance的切點,同時修改所有的通知元素,用pointcut0ref來引用這個命名切點。

4.4.2 宣告環繞通知

相比於前置通知和後置通知,環繞通知在這點上有明顯的優勢。使用環繞通知,我們可以完成前置通知和後置通知所實現的相同功能,而且只需要在一個方法中實現。因為整個通知邏輯都是在一個方法中實現的。

<aop:config>
    <aop:aspect ref="audience">       <!--引用audience Bean-->
        <aop:pointcut id="performance" expression="execution(* * concert.Performance.perform(..))"  />

        <aop:around pointcut-ref="performance" method="watchPerformance"/>

    </aop:aspect>
</aop:config>

像其他通知的XML元素一樣,<aop:around>指定了一個切點和一個通知方法的名字。

4.4.3 為通知傳遞引數

區別在於切點表示式中包含了一個引數,這個引數傳遞到通知方法中。還有區別就是這裡使用了and關鍵字

4.4.4 通過切面引入新的功能

藉助於AspectJ的@DeclareParents註解為被通知的方法引入新的方法。但是AOP引入並不是Aspectj特有的。使用Spring aop名稱空間中的<aop:declare-parents>元素,我們可以實現相同的功能

<aop:config>
     <aop:aspect ref="audience">       <!--引用audience Bean-->
        <aop:declare-parents types-matching="concert.Performance"
                             implement-interface="concert.Encoreable"
                             default-impl="concert.DefaoultEncoreable"

     </aop:aspect>
 </aop:config>

4.5 注入AspectJ切面

雖然Spring AOP能夠滿足許多應用的切面需求,但是與AspectJ相比,Spring AOP是一個功能比較弱的AOP解決方案,ASpect提供了Spring AOP 所不能支援的許多型別的切點。

Spring不能像之前那樣使用<bean>宣告來建立一個例項—-它已經在執行時由AspectJ建立完成了,Spring需要通過工廠方法獲取切面的引用。然後像<bean>元素規定的那樣在該物件上執行依賴注入

4.6 小節(重點中的重點)

AOP是物件導向程式設計的一個強大補充,通過AspectJ,我們現在可以把之前分散在應用各處的行為放入可重用的模組中。我們顯示地宣告在何處如何應用該行為。這樣有效減少了程式碼冗餘,並讓我們的類關注自身的主要功能。

Spring提供了一個AOP框架,讓我們把切面插入到方法執行的周圍。現在我們已經學會了如何把通知織入前置,後置和環繞方法的呼叫中,以及為處理異常增加自定義行為。

關於在Spirng應用中如何使用切面 ,我們可以有多種選擇。通過使用@AspectJ註解和簡化的配置名稱空間,在Spring中裝配通知和切點變得非常簡單

最後,當Spring不能滿足需求時,我們必須轉向更為強大的AspectJ。對於這些場景,我們瞭解瞭如何使用Spring為AspectJ切面注入依賴。

此時此刻,我們已經覆蓋了Spring框架的基礎知識,瞭解到如何配置Spring容器以及如何為Spring管理的物件應用切面,這些技術為建立高內聚,低耦合的應用奠定了堅實的基礎。

從下一章開始,首先看到的是如何使用Spring構建Web應用。。

期待……

相關文章