Java開發學習(十五)----AOP入門案例及其工作流程解析

2022-07-22 06:01:06

一、AOP簡介

1.1 什麼是AOP

  • AOP(Aspect Oriented Programming)面向切面程式設計,一種程式設計正規化,指導開發者如何組織程式結構。

    • OOP(Object Oriented Programming)物件導向程式設計

我們都知道OOP是一種程式設計思想,那麼AOP也是一種程式設計思想,程式設計思想主要的內容就是指導程式設計師該如何編寫程式,所以它們兩個是不同的程式設計正規化

1.2 AOP作用

  • 作用:在不驚動原始設計的基礎上為其進行功能增強。

1.3 AOP核心概念

為了能更好的理解AOP的相關概念,觀察下面的類:BookDaoImpl

@Repository
public class BookDaoImpl implements BookDao {
    public void save() {
        //記錄程式當前執行執行(開始時間)
        Long startTime = System.currentTimeMillis();
        //業務執行萬次
        for (int i = 0;i<10000;i++) {
            System.out.println("book dao save ...");
        }
        //記錄程式當前執行時間(結束時間)
        Long endTime = System.currentTimeMillis();
        //計算時間差
        Long totalTime = endTime-startTime;
        //輸出資訊
        System.out.println("執行萬次消耗時間:" + totalTime + "ms");
    }
    public void update(){
        System.out.println("book dao update ...");
    }
    public void delete(){
        System.out.println("book dao delete ...");
    }
    public void select(){
        System.out.println("book dao select ...");
    }
}

程式碼的內容相信大家都能夠讀懂,對於save方法中有計算萬次執行消耗的時間。

當在App類中從容器中獲取bookDao物件後,分別執行其save,delete,updateselect方法後會有如下的列印結果:

這個時候,我們就應該有些疑問?

  • 對於計算萬次執行消耗的時間只有save方法有,為什麼delete和update方法也會有呢?

  • delete和update方法有,那什麼select方法為什麼又沒有呢?

這個其實就使用了Spring的AOP,在不驚動(改動)原有設計(程式碼)的前提下,想給誰新增功能就給誰新增。這個也就是Spring的理念:

  • 無入侵式/無侵入式

說了這麼多,Spring到底是如何實現的呢?

(1)前面一直在強調,Spring的AOP是對一個類的方法在不進行任何修改的前提下實現增強。對於上面的BookServiceImpl中有save,update,deleteselect方法,這些方法我們給起了一個名字叫連線點

(2)在BookServiceImpl的四個方法中,updatedelete沒有計算萬次執行消耗時間,但是在執行的時候已經有該功能,那也就是說updatedelete方法都已經被增強,所以對於需要增強的方法我們給起了一個名字叫切入點

(3)執行BookServiceImpl的update和delete方法的時候都被新增了一個計算萬次執行消耗時間的功能,將這個功能抽取到一個方法中,換句話說就是存放共性功能的方法,我們給起了個名字叫通知

(4)通知是要增強的內容,會有多個,切入點是需要被增強的方法,也會有多個,那哪個切入點需要新增哪個通知,就需要提前將它們之間的關係描述清楚,那麼對於通知和切入點之間的關係描述,我們給起了個名字叫切面

(5)通知是一個方法,方法不能獨立存在需要被寫在一個類中,這個類我們也給起了個名字叫通知類

至此AOP中的核心概念就已經介紹完了,總結下:

  • 連線點(JoinPoint):程式執行過程中的任意位置,粒度為執行方法、丟擲異常、設定變數等

    • 這個概念很大,在SpringAOP中,一般理解為方法的執行

  • 切入點(Pointcut):匹配連線點的式子

    • 在SpringAOP中,一個切入點可以描述一個具體方法,也可也匹配多個方法

      • 一個具體的方法:如dao包下的BookDao介面中的無形參無返回值的save方法

      • 匹配多個方法:所有的save方法,所有的get開頭的方法,所有以Dao結尾的介面中的任意方法,所有帶有一個引數的方法

    • 連線點範圍要比切入點範圍大,是切入點的方法也一定是連線點,但是是連線點的方法就不一定要被增強,所以可能不是切入點。

  • 通知(Advice):在切入點處執行的操作,也就是共性功能

    • 在SpringAOP中,功能最終以方法的形式呈現

  • 通知類:定義通知的類

  • 切面(Aspect):描述通知與切入點的對應關係。

二、AOP入門案例

2.1 需求分析

案例設定:測算介面執行效率,但是這個稍微複雜了點,我們對其進行簡化。

簡化設定:在方法執行前輸出當前系統時間。

2.2 思路分析

需求明確後,具體該如何實現,有哪些步驟?

1.匯入座標(pom.xml)

2.製作連線點(原始操作,Dao介面與實現類)

3.製作共性功能(通知類與通知)

4.定義切入點

5.繫結切入點與通知關係(切面)

2.3 環境準備

  • 建立一個Maven專案

  • pom.xml新增Spring依賴

    <dependencies>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>5.2.10.RELEASE</version>
        </dependency>
    </dependencies>
  • 新增BookDao和BookDaoImpl類

    public interface BookDao {
        public void save();
        public void update();
    }
    ​
    @Repository
    public class BookDaoImpl implements BookDao {
    ​
        public void save() {
            System.out.println(System.currentTimeMillis());
            System.out.println("book dao save ...");
        }
    ​
        public void update(){
            System.out.println("book dao update ...");
        }
    }
  • 建立Spring的設定類

    @Configuration
    @ComponentScan("com.itheima")
    public class SpringConfig {
    }
  • 編寫App執行類

    public class App {
        public static void main(String[] args) {
            ApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig.class);
            BookDao bookDao = ctx.getBean(BookDao.class);
            bookDao.save();
        }
    }

最終建立好的專案結構如下:

說明:

  • 目前列印save方法的時候,因為方法中有列印系統時間,所以執行的時候是可以看到系統時間

  • 對於update方法來說,就沒有該功能

  • 我們要使用SpringAOP的方式在不改變update方法的前提下讓其具有列印系統時間的功能。

2.4 AOP實現步驟

步驟1:新增依賴

pom.xml

<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjweaver</artifactId>
    <version>1.9.4</version>
</dependency>

  • 因為spring-context中已經匯入了spring-aop,所以不需要再單獨匯入spring-aop

  • 匯入AspectJ的jar包,AspectJ是AOP思想的一個具體實現,Spring有自己的AOP實現,但是相比於AspectJ來說比較麻煩,所以我們直接採用Spring整合ApsectJ的方式進行AOP開發。

步驟2:定義介面與實現類

環境準備的時候,BookDaoImpl已經準備好,不需要做任何修改

步驟3:定義通知類和通知

通知就是將共性功能抽取出來後形成的方法,共性功能指的就是當前系統時間的列印。

public class MyAdvice {
    public void method(){
        System.out.println(System.currentTimeMillis());
    }
}

類名和方法名沒有要求,可以任意。

步驟4:定義切入點

BookDaoImpl中有兩個方法,分別是save和update,我們要增強的是update方法,該如何定義呢?

public class MyAdvice {
    @Pointcut("execution(void com.itheima.dao.BookDao.update())")
    private void pt(){}
    public void method(){
        System.out.println(System.currentTimeMillis());
    }
}

說明:

  • 切入點定義依託一個不具有實際意義的方法進行,即無引數、無返回值、方法體無實際邏輯。

  • execution及後面編寫的內容,後面在介紹,這裡只是介紹下用法。

步驟5:製作切面

切面是用來描述通知和切入點之間的關係,如何進行關係的繫結?

public class MyAdvice {
    @Pointcut("execution(void com.itheima.dao.BookDao.update())")
    private void pt(){}
    
    @Before("pt()")
    public void method(){
        System.out.println(System.currentTimeMillis());
    }
}

繫結切入點與通知關係,並指定通知新增到原始連線點的具體執行位置

說明:@Before翻譯過來是之前,也就是說通知會在切入點方法執行之前執行,除此之前還有其他四種型別,後面會介紹。

步驟6:將通知類配給容器並標識其為切面類

@Component
@Aspect
public class MyAdvice {
    @Pointcut("execution(void com.itheima.dao.BookDao.update())")
    private void pt(){}
    
    @Before("pt()")
    public void method(){
        System.out.println(System.currentTimeMillis());
    }
}

步驟7:開啟註解格式AOP功能

@Configuration
@ComponentScan("com.itheima")
@EnableAspectJAutoProxy
public class SpringConfig {
}

步驟8:執行程式

public class App {
    public static void main(String[] args) {
        ApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig.class);
        BookDao bookDao = ctx.getBean(BookDao.class);
        bookDao.update();
    }
}

看到在執行update方法之前列印了系統時間戳,說明對原始方法進行了增強,AOP程式設計成功。

知識點1:@EnableAspectJAutoProxy

名稱 @EnableAspectJAutoProxy
型別 設定類註解
位置 設定類定義上方
作用 開啟註解格式AOP功能

知識點2:@Aspect

名稱 @Aspect
型別 類註解
位置 切面類定義上方
作用 設定當前類為AOP切面類

知識點3:@Pointcut

名稱 @Pointcut
型別 方法註解
位置 切入點方法定義上方
作用 設定切入點方法
屬性 value(預設):切入點表示式

知識點4:@Before

名稱 @Before
型別 方法註解
位置 通知方法定義上方
作用 設定當前通知方法與切入點之間的繫結關係,當前通知方法在原始切入點方法前執行

三、AOP工作流程

AOP的入門案例已經完成,對於剛才案例的執行過程,我們就得來分析分析,主要是兩個知識點:AOP工作流程AOP核心概念

3.1 AOP工作流程

由於AOP是基於Spring容器管理的bean做的增強,所以整個工作過程需要從Spring載入bean說起:

流程1:Spring容器啟動

  • 容器啟動就需要去載入bean,哪些類需要被載入呢?

  • 需要被增強的類,如:BookServiceImpl

  • 通知類,如:MyAdvice

  • 注意此時bean物件還沒有建立成功

流程2:讀取所有切面設定中的切入點

  • 上面這個例子中有兩個切入點的設定,但是第一個ptx()並沒有被使用,所以不會被讀取。

流程3:初始化bean

判定bean對應的類中的方法是否匹配到任意切入點

  • 注意第1步在容器啟動的時候,bean物件還沒有被建立成功。

  • 要被範例化bean物件的類中的方法和切入點進行匹配

    • 匹配失敗,建立原始物件,如UserDao

      • 匹配失敗說明不需要增強,直接呼叫原始物件的方法即可。

    • 匹配成功,建立原始物件(目標物件)的代理物件,如:BookDao

      • 匹配成功說明需要對其進行增強

      • 對哪個類做增強,這個類對應的物件就叫做目標物件

      • 因為要對目標物件進行功能增強,而採用的技術是動態代理,所以會為其建立一個代理物件

      • 最終執行的是代理物件的方法,在該方法中會對原始方法進行功能增強

流程4:獲取bean執行方法

  • 獲取的bean是原始物件時,呼叫方法並執行,完成操作

  • 獲取的bean是代理物件時,根據代理物件的執行模式執行原始方法與增強的內容,完成操作

驗證容器中是否為代理物件

為了驗證IOC容器中建立的物件和我們剛才所說的結論是否一致,首先先把結論理出來:

  • 如果目標物件中的方法會被增強,那麼容器中將存入的是目標物件的代理物件

  • 如果目標物件中的方法不被增強,那麼容器中將存入的是目標物件本身。

驗證思路

1.要執行的方法,不被定義的切入點包含,即不要增強,列印當前類的getClass()方法

2.要執行的方法,被定義的切入點包含,即要增強,列印出當前類的getClass()方法

3.觀察兩次列印的結果

步驟1:修改App類,獲取類的型別
public class App {
    public static void main(String[] args) {
        ApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig.class);
        BookDao bookDao = ctx.getBean(BookDao.class);
        System.out.println(bookDao);
        System.out.println(bookDao.getClass());
    }
}
步驟2:修改MyAdvice類,不增強

因為定義的切入點中,被修改成update1,所以BookDao中的update方法在執行的時候,就不會被增強,

所以容器中的物件應該是目標物件本身。

@Component
@Aspect
public class MyAdvice {
    @Pointcut("execution(void com.itheima.dao.BookDao.update1())")
    private void pt(){}
    
    @Before("pt()")
    public void method(){
        System.out.println(System.currentTimeMillis());
    }
}
步驟3:執行程式

步驟4:修改MyAdvice類,增強

因為定義的切入點中,被修改成update,所以BookDao中的update方法在執行的時候,就會被增強,

所以容器中的物件應該是目標物件的代理物件

@Component
@Aspect
public class MyAdvice {
    @Pointcut("execution(void com.itheima.dao.BookDao.update())")
    private void pt(){}
    
    @Before("pt()")
    public void method(){
        System.out.println(System.currentTimeMillis());
    }
}
步驟5:執行程式

至此對於剛才的結論,我們就得到了驗證,這塊大家需要注意的是:

不能直接列印物件,從上面兩次結果中可以看出,直接列印物件走的是物件的toString方法,不管是不是代理物件列印的結果都是一樣的,原因是內部對toString方法進行了重寫。

3.2 AOP核心概念

在上面介紹AOP的工作流程中,我們提到了兩個核心概念,分別是:

  • 目標物件(Target):原始功能去掉共性功能對應的類產生的物件,這種物件是無法直接完成最終工作的

  • 代理(Proxy):目標物件無法直接完成工作,需要對其進行功能回填,通過原始物件的代理物件實現

上面這兩個概念比較抽象,簡單來說,

目標物件就是要增強的類[如:BookServiceImpl類]對應的物件,也叫原始物件,不能說它不能執行,只能說它在執行的過程中對於要增強的內容是缺失的。

SpringAOP是在不改變原有設計(程式碼)的前提下對其進行增強的,它的底層採用的是代理模式實現的,所以要對原始物件進行增強,就需要對原始物件建立代理物件,在代理物件中的方法把通知[如:MyAdvice中的method方法]內容加進去,就實現了增強,這就是我們所說的代理(Proxy)。