1. 程式人生 > >Spring原理學習系列之三:Spring AOP原理(從原始碼層面分析)-------上部

Spring原理學習系列之三:Spring AOP原理(從原始碼層面分析)-------上部

引言

本文是Spring原理分析的第三篇博文,主要闡述Spring AOP相關概念,同時從原始碼層面分析AOP實現原理。對於AOP原理的理解有利於加深對Spring框架的深入理解。同時我也希望可以探究Spring框架在處理AOP的解決思路,學習框架的時候,有時候需要站在設計者的角度上去考慮,如果自己是設計者遇到同樣需要解決的問題自己會怎麼去處理,然後再對照實際框架中的處理方式,這樣可以發現自己考慮不足之處。
本文側重於找到Spring框架處理AOP的起點,至於涉及到的動態代理相關的問題將在下一篇文中著重介紹。

  • 提問題
  • AOP概念
  • AOP程式碼示例
  • 原始碼分析
  • 總結

一、提問題

到底什麼是AOP

Spring框架到底在什麼階段進行資料織入的?AOP在實際專案開發中到底有什麼作用以及怎樣將AOP的程式設計思想運用到我們的實際專案開發中?

二、AOP概念

AOP(Aspect Orient Programming),我們通常稱為面向切面程式設計。所謂切面是相對於面向物件來說的。面向物件是將實物抽象為物件,這是個縱向的概念。而面向切面是一個橫向的概念,它更加關注那些散落在程式碼中公用的不涉及具體業務邏輯的通用處理方式,例如日誌、許可權驗證以及統一異常處理等等。它是對於面向物件程式設計思想的一種結構化補充。核心思想就是將與業務邏輯無關的進行統一的框架織入,不對原有程式碼以及業務邏輯造成侵入。
Spring AOP

在執行時,能夠動態地將程式碼切入到指定的類的指定方法、指定位置上的程式設計思想就是面向切面程式設計,這種切入的特點是不影響原來的業務邏輯。但是像AspectJ可以在編譯階段以及類載入階段進行織入。

一些概念的說明:

  1. 切面
    所謂切面,按照自己的理解可以把它看作為一把刀,將他橫切於其他物品,通過@Aspect來將類定義一個切面,它就是切點與通知的結合,如下圖所示。
    在這裡插入圖片描述
  2. 切點
    本質上來說,就是需要定義一個切入點表示式,使得可以在增強處理中使用到。通過切點定位和篩選特定的連線點。它關注通知需要織入的一個或者多個連線點,切入點包括兩部分:
    (1)切入點表示式:指定切入點與哪些方法進行匹配;
    (2)切入點名稱:方法簽名
  3. 連線點
    連線點是一個相對虛擬的概念,可以將它理解為切點的集合,也就是Spring允許我們進行通知操作的地方,比如方法、異常丟擲的地方,如果使用aspectj則也支援在構造器中或者屬性中允許通知。
  4. 通知
    通俗地說,通知就是我們需要實現的功能,可以分為前置、後置、異常、最終以及環繞通知這五類。例如日誌,許可權等業務邏輯。
  • 前置通知:在目標方法或者連線點被呼叫前執行通知操作;
  • 後置通知:在某些連線點執行完成之後進行通知操作;
  • 異常通知:在方法丟擲異常退出當前時進行通知操作;
  • 最終通知:當切入點退出時無論是方法正常執行結束還是異常丟擲後退出執行的通知操作;
  • 環繞通知:包圍一個切點的通知,如方法呼叫。這是最強大的一種通知型別。環繞通知可以在方法呼叫前後完成自定義的行為。它也會選擇是否繼續執行連線點或直接返回它自己的返回值或丟擲異常來結束執行。

三、AOP程式碼示例

1.定義一個普通業務邏輯方法

@Component("userDao")
public class UserDao {

    /**
     * @Description: 查詢
     * @Return
     * @Author taomeng
     * @Version V1.0.0
     * @CreateDate: 2018/9/23 11:31
     */
    public void query() {

        System.out.println("query user data!!!");
    }
}

2.定義一個切面類,同時在這個切面類中將切點等進行定義。

/**
 * @Auther: taomeng
 * @Date: 2018/9/17 23:32
 * @Description: 定義切面類
 */
@Configuration
@Aspect
@ComponentScan(basePackages = {"com.tm.springrun.module"})
@ImportResource(locations = {"classpath:spring-context.xml"})
public class AopConfig {

    /**
     * @Description:定義切點
     * @Return
     * @Author taomeng
     * @Version V1.0.0
     * @CreateDate: 2018/9/23 11:32
     */
    @Pointcut("execution(* com.tm.springrun.module.dao.UserDao.query())")
    public void declareJointPointExpression(){}

    @Before("declareJointPointExpression()")
    public  void beforeMethod(JoinPoint joinPoint) {
        String method = joinPoint.getSignature().getName();
        System.out.println("The method of before:" + method);
    }

    @After("declareJointPointExpression()")
    public void afterMethod(JoinPoint joinPoint) {
        String method = joinPoint.getSignature().getName();
        System.out.println("The method of after:" + method );
    }
}

3.在Spring Context中獲取Bean的例項,同時執行Bean例項的方法。

/**
 * @Auther: taomeng
 * @Date: 2018/9/17 23:37
 * @Description: 測試AOP
 */

public class Test {
    public static void main(String[] args) {
        //1.載入spring環境
        AnnotationConfigApplicationContext annotationConfigApplicationContext = new AnnotationConfigApplicationContext(AopConfig.class);
        //2.獲取spring context中的bean的例項
        UserDao userDao = (UserDao)annotationConfigApplicationContext.getBean("userDao");
        //3.執行bean中的方法
        userDao.query();
    }

}

同時需要在配置檔案中開啟AOP,如下所示:

<aop:aspectj-autoproxy/>

四、原始碼分析

Spring AOP進行原始碼分析,就是要找到Spring框架中處理AOP的源頭以及在什麼階段進行 資料織入的,帶著這樣的問題去做原始碼分析才可以做到有的放矢,要不然那麼多原始碼看下去就像無頭蒼蠅一樣不知道如何下手。
如同第二節示例程式碼所示, 在方法執行時,通知已經被執行了。那麼Spring AOP應該是在前兩個步驟中起作用的,要麼是在Bean定義時候,要麼就是在獲取Bean的時候進行的。如下圖所示,我們希望通過debug的方式找到AOP在什麼階段開始起作用。
在這裡插入圖片描述
以下為尋找AOP起作用起點的過程,首先我們猜測Spring在處理AOP的時候是在獲取bean的時候進行資料的織入的,所以在獲取bean的時候進行斷點跟蹤。
在這裡插入圖片描述
進入斷點,進入AbstractBeanFactory類中的getBean方法。
在這裡插入圖片描述
進入getBean方法,在這個方法中
在這裡插入圖片描述

此時發現物件已經被代理了,所以需要到getSingleton(beanName)這個方法中去檢視。
在這裡插入圖片描述
在這個我們可以發現,Spring框架存放bean名稱以及對應的物件的資料結構實際上是一個ConcurrentHashMap。如下所示:

/** Cache of singleton objects: bean name --> bean instance */
	private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);

我們可以看一下在這個map中的存放了Spring中載入的物件,這也是Spring IOC的核心。既然在這個map中獲取到了bean,那肯定是在某個地方將物件進行put操作。全文搜尋後發現,在DefaultSingletonBeanRegistry這個類中的addSingleton方法中進行物件put操作。
在這裡插入圖片描述
從上圖可知,在物件put操作時,userDao物件已經被代理了。這就說明在獲取到bean的時候,對應的物件已經是被代理過的。所以實際資料織入並不是在獲取bean的時候進行的,而是在bean載入到Spring環境中的時候就已經完成了。
那接下來我們需要找出呼叫addSingleton方法的地方,
在這裡插入圖片描述
從下圖可以看出來,此時的mdb還是原生的,那說明此時還沒有進行資料織入,是在下面的方法中進行的。
在這裡插入圖片描述
doCreateBean方法中,我們跟蹤到initializeBean的方法,此時發現bean已經被資料織入了,繼續進入方法。

在這裡插入圖片描述
繼續檢視initializeBean方法內部,如下圖所示:
在這裡插入圖片描述
再繼續檢視
在這裡插入圖片描述
再進入方法進行檢視
在這裡插入圖片描述
至此,我們終於跟蹤到實現代理的最初部分,其中根據條件判斷是jdk代理還是cglib代理。
在這裡插入圖片描述

四、總結

Spring原始碼很複雜,如果不是帶著目標去看,很難抓住重點。在尋找資料織入的部分,需要一步一步進行,每找到一個部分就需要在對應的部分打上斷點,同時去掉之前的斷點,不斷迭代深入,直到獲取到最終的起點,關於代理的這部分內容會放到下一篇文章中進行詳細闡述。
根據第三章中的原始碼分析,我們明確了Spring框架進行資料織入的起點,如下圖所示,是在Spring框架載入Bean的時候進行的。
在這裡插入圖片描述

Spring AOP相關知識樹如下圖所示:
在這裡插入圖片描述
(ps:該圖片來源於網路)