Spring AOP從零單排-織入時期原始碼分析

油膩的Java發表於2019-03-11

問題:Spring AOP代理中的執行時期,是在初始化時期織入還是獲取物件時期織入?

織入就是代理的過程,指目標物件進行封裝轉換成代理,實現了代理,就可以運用各種代理的場景模式。

何為AOP

簡單點來定義就是切面,是一種程式設計正規化。與OOP對比,它是面向切面,為何需要切面,在開發中,我們的系統從上到下定義的模組中的過程中會產生一些橫切性的問題,這些橫切性的問題和我們的主業務邏輯關係不大,假如不進行AOP,會散落在程式碼的各個地方,造成難以維護。AOP的程式設計思想就是把業務邏輯和橫切的問題進行分離,從而達到解耦的目的,使程式碼的重用性、侵入性低、開發效率高。

AOP使用場景

在這裡插入圖片描述

  • 日誌記錄;記錄呼叫方法的入參和結果返參。
  • 使用者的許可權驗證;驗證使用者的許可權放到AOP中,與主業務進行解耦。
  • 效能監控;監控程式執行方法的耗時,找出專案的瓶頸。
  • 事務管理;控制Spring事務,Mysql事務等。

AOP概念點

AOP和Spring AOP的關係

在這裡問題中,也有一個類似的一對IOC和DI(dependency injection)的關係,AOP可以理解是一種程式設計目標,Spring AOP就是這個實現這個目標的一種手段。同理IOC也是一種程式設計目標,DI就是它的一個手段。

SpringAOP和AspectJ是什麼關係

在這裡插入圖片描述

在Spring官網可以看到,AOP的實現提供了兩種支援分別為@AspectJ、Schema-based AOP。其實在Spring2.5版本時,Spring自己實現了一套AOP開發的規範和語言,但是這一套規範比較複雜,可讀性差。之後,Spring借用了AspectJ程式設計風格,才有了@AspectJ的方式支援,那麼何為程式設計風格。

  • Annotation註解方式;對應@AspectJ
  • JavaConfig;對應Schema-based AOP

SpringAOP和AspectJ的詳細對比,在之後的章節會在進行更加詳細的說明,將會在他們的背景、織入方法、效能做介紹。

Spring AOP的應用

閱讀官網,是我們學習一個新知識的最好途徑,這個就是Spring AOP的核心概念點,跟進它們的重要性,我做了重新的排序,以便好理解,這些會為我們後續的原始碼分析起到作用。

在這裡插入圖片描述

Aspect:切面;使用@Aspect註解的Java類來實現,集合了所有的切點,做為切點的一個載體,做一個比喻就像是我們的一個資料庫。 Tips:這個要實現的話,一定要交給Spirng IOC去管理,也就是需要加入@Component。

Pointcut:切點;表示為所有Join point的集合,就像是資料庫中一個表。

Join point:連線點;俗稱為目標物件,具體來說就是servlet中的method,就像是資料庫表中的記錄。

Advice:通知;這個就是before、after、After throwing、After (finally)。

Weaving:把代理邏輯加入到目標物件上的過程叫做織入。

target:目標物件、原始物件。

aop Proxy:代理物件 包含了原始物件的程式碼和增加後的程式碼的那個物件。

Tips 這個應用點,有很多的知識點可以讓我們去挖掘,比如Pointcut中execution、within的區別,我相信你去針對性搜尋或者官網都未必能有好的解釋,稍後會再專門挑一個文章做重點的使用介紹;

SpringAOP原始碼分析

為了回答我們的一開始的問題,前面的幾個章節我們做了一些簡單的概念介紹做為鋪墊,那麼接下來我們迴歸正題,正面去切入問題。以碼說話,我們以最簡潔的思路把AOP實現,我們先上程式碼。

專案結構介紹

專案目錄結構,比較簡單,5個主要的檔案;

在這裡插入圖片描述
pom.xml核心程式碼;spring-content是核心jar,已經包含了spring所有的基礎jar,aspectjweaver是為了實現AOP。
在這裡插入圖片描述
AppConfig.java;定義一個Annotation,做為我們Spirng IOC容器的啟動類。

package com.will.config;
​
@Configuration
@ComponentScan("com.will")
@EnableAspectJAutoProxy(proxyTargetClass = false)
public class AppConfig {
    
}
複製程式碼

WilAspect.java ;按照官網首推的方式(@AspectJ support),實現AOP代理。

package com.will.config;
​
/**
 * 定義一個切面的載體
 */
@Aspect
@Component
public class WilAspect {
    /**
     * 定義一個切點
     */
    @Pointcut("execution(* com.will.dao.*.*(..))")
    public void pointCutExecution(){
    }
​
    /**
     * 定義一個Advice為Before,並指定對應的切點
     * @param joinPoint
     */
    @Before("pointCutExecution()")
    public  void before(JoinPoint joinPoint){
        System.out.println("proxy-before");
    }
}
複製程式碼

Dao.java

package com.will.dao;
public interface Dao {
    public void query();
}
複製程式碼

UserDao.java

package com.will.dao;
import org.springframework.stereotype.Component;
@Component
public class UserDao implements Dao {
    public void query() {
        System.out.println("query user");
    }
}
複製程式碼

Test.java

package com.will.test;
import com.will.config.AppConfig;
import com.will.dao.Dao;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
public class Test {
    public static void main(String[] args) {
        /**
         * new一個註冊配置類,啟動IOC容器,初始化時期;
         */
        AnnotationConfigApplicationContext annotationConfigApplicationContext = new AnnotationConfigApplicationContext(AppConfig.class);
​
        /**
         * 獲取Dao物件,獲取物件時期,並進行query列印
         */
        Dao dao = annotationConfigApplicationContext.getBean(Dao.class);
        dao.query();
        annotationConfigApplicationContext.start();
    }
}
複製程式碼

好了,這樣我們整體的AOP代理就已經完成。

問題分析測試

究竟是哪個時期進行物件織入的,比如Test類中,究竟是第一行還是第二行進行織入的,我們只能通過原始碼進行分析,假如是你,你會進行如何的分析原始碼解讀。

Spring的程式碼非常優秀,同時也非常複雜,那是一個大專案,裡面進行了很多的程式碼封裝,那麼的程式碼你三天三夜也讀不完,甚至於你都不清楚哪一行的該留意的,哪一行是起到關鍵性作用的,這裡教幾個小技巧。

  • 看方法返回型別;假如是void返回型別的,看都不看跳過。返回結果是物件,比如T果斷進行去進行跟蹤。
  • 假設法;就當前場景,我們大膽假設是第二行進行的織入。
  • 藉助好的IDE;IDEA可以幫我們做很多的事情,它的debug模式中的條件斷點、呼叫鏈(堆疊)會幫助到我們。

假設法原始碼分析

在這裡插入圖片描述

在這裡插入圖片描述

debug模式StepInfo(F5)後,進入 AbstractApplicationContext.getBean方法,這個是Spring應用上下文中最重要的一個類,這個抽象類中提供了幾乎ApplicationContext的所有操作。這裡第一個語句返回void,我們可以直接忽略,看下面的關鍵性程式碼。

在這裡插入圖片描述

繼續debug後,會進入到 DefaultListableBeanFactory 類中,看如下程式碼

return new NamedBeanHolder<>(beanName, getBean
(beanName, requiredType, args));
複製程式碼

在該語句中,這個可以理解為 DefaultListableBeanFactory 容器,幫我們獲取相應的Bean。

在這裡插入圖片描述

進入到AbstractBeanFactory類的doGetBean方法之後,我們執行完。

Object sharedInstance = getSingleton(beanName);
複製程式碼

語句之後,看到sharedInstance 物件列印出&Proxyxxx ,說明在getSingleton 方法的時候就已經獲取到了物件,所以需要跟蹤進入到 getSingleton 方法中,繼續探究。

在這裡插入圖片描述

不方便不方便我們進行問題追蹤到這個步驟之後,我需要引入IDEA的條件斷點,不方便我們進行問題追蹤因為Spring會初始化很多的Bean,我們再ObjectsharedInstance=getSingleton(beanName);加入條件斷點語句。

在這裡插入圖片描述

繼續debug進入到DefaultSingletonBeanRegistrygetSingleton方法。 我們觀察下執行完ObjectsingletonObject=this.singletonObjects.get(beanName); 之後的singletonObject已經變成為&ProxyUserDao,這個時候Spring最關鍵的一行程式碼出現了,請注意這個this.singletonObjects

this.singletonObjects就是相當IOC容器,反之IOC容器就是一個執行緒安全的執行緒安全的HashMap,裡面存放著我們需要Bean。

在這裡插入圖片描述
在這裡插入圖片描述

我們來看下singletonObjects存放著的資料,裡面就有我們的UserDao類。

這就說明,我們的初始化的時期進行織入的,上圖也有整個Debug模式的呼叫鏈。

原始碼深層次探索

通過上一個環節已經得知是在第一行進行初始化的,但是它在初始化的時候是什麼時候完成織入的,抱著求知的心態我們繼續求證。

還是那個問題,那麼多的程式碼,我的切入點在哪裡?

既然singletonObjects是容器,存放我們的Bean,那麼找到關鍵性程式碼在哪裡進行存放(put方法)就可以了。於是我們通過搜尋定位到了。

在這裡插入圖片描述
在這裡插入圖片描述

我們通過debug模式的條件斷點和debug呼叫鏈模式,就可以進行探索。

在這裡插入圖片描述

這個時候藉助上圖中的呼叫鏈,我們把思路放到放到IDEA幫我定位到的兩個方法程式碼上。

DefaultSingletonBeanRegistry.getSingleton

我們一步步斷點,得知,當執行完singletonObject=singletonFactory.getObject();之後,singletonObject已經獲得了代理。

在這裡插入圖片描述

至此我們知道,代理物件的獲取關鍵在於singletonFactory物件,於是又定位到了AbstractBeanFactorydoGetBean方法,發現singletonFactory引數是由createBean方法創造的。這個就是Spring中IOC容器最核心的地方了,這個程式碼的模式也值得我們去學習。

sharedInstance = getSingleton(beanName, () -> {
                        try {
                            return createBean(beanName, mbd, args);
                        }
                        catch (BeansException ex) {
                            // Explicitly remove instance from singleton cache: It might have been put there
                            // eagerly by the creation process, to allow for circular reference resolution.
                            // Also remove any beans that received a temporary reference to the bean.
                            destroySingleton(beanName);
                            throw ex;
                        }
                    });
複製程式碼

這個第二個引數是用到了jdk8中的lambda,這一段的含義是就是為了傳參,重點看下 createBean(beanName,mbd,args);程式碼。隨著斷點,我們進入到這個類方法裡面。

AbstractAutowireCapableBeanFactory.createBean中的;

ObjectbeanInstance=doCreateBean(beanName,mbdToUse,args)方法;

doCreateBean方法中,做了簡化。

Initialize the bean instance.
        Object exposedObject = bean;
        try {
            populateBean(beanName, mbd, instanceWrapper);
            exposedObject = initializeBean(beanName, exposedObject, mbd);
        }
        ...
​
        return exposedObject;
複製程式碼

在這裡插入圖片描述

當執行完 exposedObject=initializeBean(beanName,exposedObject,mbd);之後,我們看到exposedObject已經是一個代理物件,並執行返回。這一行程式碼就是取判斷物件要不要執行代理,要的話就去初始化代理物件,不需要直接返回。後面的initializeBean方法是涉及代理物件生成的邏輯(JDK、Cglib),後續會有一個專門的章節進行詳細介紹。

總結

通過原始碼分析,我們得知,Spring AOP的代理物件的織入時期是在執行Spring初始化的時候就已經完成的織入,並且也分析了Spring是如何完成的織入。

公眾號

相關文章