Spring系列之DI的原理及手動實現

寧願。發表於2018-12-18

目錄

前言

在上一章中,我們介紹和簡單實現了容器的部分功能,但是這裡還留下了很多的問題。比如我們在構造bean例項的時候如果bean是帶引數的我們需要怎麼來進行,當然這個是可以在init方法中進行的,再比如我們平時在Spring中獲取一個物件通過一個註解即可獲取到類的例項,這個例項是怎麼注入的呢?

IOC和DI

依賴注入(DI)和控制反轉(IOC)其實基本上表達的是同一個東西,兩者誰也離不開誰。

假設類a需要依賴類b,但a並不控制b的宣告週期,僅僅在本類中使用b,而對於類b的建立和銷燬等工作交給其他的元件來處理,這叫控制反轉(IOC),而類a要依賴類b,則必須要獲得類b的例項。這個獲得類b的例項的過程則就是依賴注入(DI)。

注入分析

分析我們在那些地方可能會有注入這樣一個行為?

要知道那些地方存在依賴注入首先得明白注入的是什麼,結合上面分析這裡很明顯注入實際就是一個例項化的過程,更廣泛的說是一個賦值的過程。而我們平時那些地方可能會存在賦值的動作呢?首先肯定是建構函式裡,對類的例項化建構函式肯定是跑不了的,大多數賦初值的操作也都在建構函式中完成。然後還有就是另外執行過程中對屬性值修改了。

那麼需要進行依賴注入的地方就很明顯了:

  • 構造引數依賴
  • 屬性依賴

我們賦值的型別有那些呢?

很明顯在Java中我們賦值型別包括基本資料型別(int,double...)和引用型別。

在分析清楚需要注入的行為和型別後又有另外的問題,我們雖然知道注入的型別是基本資料型別和引用型別,但是實際需要注入的型別是無法預料到的,我們事先並不知道某一個引數需要的是int還是boolean或者是引用型別。幸運的是Java實際上已經幫我們解決了這件事情,java中的所有型別都是繼承自Object,我們只需要使用Object來接受值即可。

我們在對值進行注入的時候肯定是需要知道我們注入的具體的型別的,instanceof關鍵字可以幫助我們確定具體的某個型別,但是實際上也就僅僅限於基本資料型別了,因為引用型別實在太多了,使用者自己定義的引用型別我們是沒有辦法事先確定的。所以我們要想辦法能讓我們知道注入的具體是哪一個型別。

BeanReference

我們定義一個用來標識bean型別的類,介面只包含一個標識bean的型別的引數beanName,在注入引數的時候只需要定義好beanName的值即可直接從IOC容器中取相應的bean,而對於基本資料型別和String則直接賦值即可。

Spring系列之DI的原理及手動實現

當然這樣還不能解決所有問題,因為傳入的引數可能攜帶多個引用值,比如引用陣列,List,Map以及屬性檔案讀取(Properties)等,解決這些問題和上面差不多,引用型別還是使用BeanReference,多個值則進行遍歷即可。

構造引數依賴分析

構造引數個數的問題

實際上上面的分析已經將我們的問題解決的差不多了,還存在一個問題就是構造引數的個數是沒有辦法確定的,我們怎麼來儲存一個bean例項化所需的全部引數,又如何值和引數對應。

很明顯需要一個對應關係的話我們立刻能想到的就是key-value形式的Map,實際上我們還能使用List,根據順序來儲存,取得時候也依然是這個順序。

如何匹配建構函式

一個類中方法的過載的個數可以是有多個的,我們如何精確的找到我們需要的方法呢?

在JDK中Class類中為我們提供了一系列的方法:

method 介紹
Constructor getConstructor(Class<?>... parameterTypes) 返回一個 Constructor物件,該物件反映 Constructor物件表示的類的指定的公共 類函式。
Method getMethod(String name, Class<?>... parameterTypes) 返回一個 方法物件,它反映此表示的類或介面的指定公共成員方法 類物件。
Method[] getMethods() 返回包含一個陣列 方法物件反射由此表示的類或介面的所有公共方法 類物件,包括那些由類或介面和那些從超類和超介面繼承的宣告。
Constructor<?>[] getConstructors() 返回包含一個陣列 Constructor物件反射由此表示的類的所有公共構造 類物件。

前面我們已經取到了引數:

  1. 根據引數個數匹配具有同樣個數引數的方法
  2. 根據引數型別精確匹配步驟一種篩選出的方法
單例or原型

簡單來講,單例(Singleton)是指在容器執行過程中,一個bean只建立一次,後面需要使用都是同一個物件。原型(Prototype)在容器執行時不進行建立,只有在使用時才建立,沒用一次就新建立一個。

對於單例模式只建立一次,那麼上面的匹配過程也只會進行一次,對程式的執行不會有影響。但是原型模式每次建立都重新匹配一次這會在一定程度上拖慢程式的執行。所以這裡我們可以考慮將原型bean例項化對應的方法快取起來,那麼後面在同一個地方使用建立時不用重複去匹配。

需要的介面

很明顯上面的分析都是和bean定義有關,相應的方法也應該加在bean定義介面上了。

Spring系列之DI的原理及手動實現

構造引數的注入應當是在bean建立的時候,在前面我們定義類幾種不同的bean建立方式,現在應該在這些方法中加上構造引數了。

程式碼:

BeanReference

public class BeanReference {

    private String beanName;

    public String getBeanName() {
        return beanName;
    }

    public void setBeanName(String beanName) {
        this.beanName = beanName;
    }
}
複製程式碼

DefaultBeanDefinition新增程式碼:

public class DefaultBeanDefinition implements BeanDefinition{

    ...
    
    private Constructor constructor;

    private Method method;

    private List<?> constructorArg;

    ... 

    //getter setter
}

複製程式碼

DefaultBeanFactory新增程式碼

public class DefaultBeanFactory implements BeanFactory, BeanDefinitionRegistry, Closeable {

    //other method

    /**
     * 解析傳入的構造引數值
     * @param constructorArgs
     * @return
     */
    private Object[] parseConstructorArgs(List constructorArgs) throws IllegalAccessException, InstantiationException {

        if(constructorArgs==null || constructorArgs.size()==0){
            return null;
        }

        Object[] args = new Object[constructorArgs.size()];
        for(int i=0;i<constructorArgs.size();i++){
            Object arg = constructorArgs.get(i);
            Object value = null;
            if(arg instanceof BeanReference){
                String beanName = ((BeanReference) arg).getBeanName();
                value = this.doGetBean(beanName);
            }else if(arg instanceof List){
                value = parseListArg((List) arg);
            }else if(arg instanceof Map){
                //todo 處理map
            }else if(arg instanceof Properties){
                //todo 處理屬性檔案
            }else {
                value = arg;
            }
            args[i] = value;
        }
        return args;
    }

    private Constructor<?> matchConstructor(BeanDefinition bd, Object[] args) throws Exception {
        
        if(args == null){
            return bd.getBeanClass().getConstructor(null);
        }
        //如果已經快取了 則直接返回
        if(bd.getConstructor() != null)
            return bd.getConstructor();

        int len = args.length;
        Class[] param = new Class[len];
        //構造引數列表
        for(int i=0;i<len;i++){
            param[i] = args[i].getClass();
        }
        //先進行精確匹配 如果能匹配到相應的構造方法 則後續不用進行
        Constructor constructor = null;
        try {
            constructor = bd.getBeanClass().getConstructor(param);
        } catch (Exception e) {
            //這裡上面的程式碼如果沒匹配到會丟擲空指標異常
            //為了程式碼繼續執行 這裡我們來捕獲 但是不需要做其他任何操作
        }
        if(constructor != null){
            return constructor;
        }

        //未匹配到 繼續匹配
        List<Constructor> firstFilterAfter = new LinkedList<>();
        Constructor[] constructors = bd.getBeanClass().getConstructors();
        //按引數個數匹配
        for(Constructor cons:constructors){
            if(cons.getParameterCount() == len){
                firstFilterAfter.add(cons);
            }
        }

        if(firstFilterAfter.size()==1){
            return firstFilterAfter.get(0);
        }
        if(firstFilterAfter.size()==0){
            log.error("不存在對應的建構函式:" + args);
            throw new Exception("不存在對應的建構函式:" + args);
        }
        //按引數型別匹配
        //獲取所有引數型別
        boolean isMatch = true;
        for(int i=0;i<firstFilterAfter.size();i++){
            Class[] types = firstFilterAfter.get(i).getParameterTypes();
            for(int j=0;j<types.length;j++){
                if(types[j].isAssignableFrom(args[j].getClass())){
                    isMatch = false;
                    break;
                }
            }
            if(isMatch){
                //對於原型bean 快取方法
                if(bd.isPrototype()){
                    bd.setConstructor(firstFilterAfter.get(i));
                }
                return firstFilterAfter.get(i);
            }
        }
        //未能匹配到
        throw new Exception("不存在對應的建構函式:" + args);
    }
    private List parseListArg(List arg) throws Exception {
        //遍歷list
        List param = new LinkedList();
        for(Object value:arg){
            Object res = new Object();
            if(arg instanceof BeanReference){
                String beanName = ((BeanReference) value).getBeanName();
                res = this.doGetBean(beanName);
            }else if(arg instanceof List){
                //遞迴 因為list中可能還存有list
                res = parseListArg(arg);
            }else if(arg instanceof Map){
                //todo 處理map
            }else if(arg instanceof Properties){
                //todo 處理屬性檔案
            }else {
                res = arg;
            }
            param.add(res);
        }
        return param;
    }
}

複製程式碼
相關程式碼已經託管到github:myspring

迴圈依賴

到這裡對建構函式的引數依賴基本完成了,經過測試也基本沒有問題,但是在測試過程中發現如果構造出的引數存在迴圈依賴的話,則會導致整個過程失敗。

什麼是迴圈依賴?如何解決迴圈依賴?

Spring系列之DI的原理及手動實現

如上圖,A依賴B,B依賴C,C又依賴A,在初始化的過程中,A需要載入B,B需要載入C,到了C這一步又來載入A,一直重複上面的過程,這就叫迴圈依賴。

在Spring框架中對Bean進行配置的時候有一個屬性lazy-init。一旦將這個屬性設定為true,那麼迴圈依賴的問題就不存在了,這是為什麼呢?實際上如果配置了懶載入那麼這個bean並不會立刻初始化,而是等到使用時才初始化,而在需要使用時其他的bean都已經初始化好了,這是我們直接取例項,依賴的例項並不需要例項化,所以才不會有迴圈依賴的問題。

那麼我們這裡怎麼解決呢?

根據Spring的啟發,需要解決迴圈依賴那麼主要就是對於已經例項化過的bean不在進行例項化,那麼我們定義一個用於記錄已經例項化後的bean的容器,每一次例項化一個bean是檢測一次,如果已經例項化過的bean直接跳過。

新增的程式碼:

public class DefaultBeanFactory implements BeanFactory, BeanDefinitionRegistry, Closeable {
    //記錄正在建立的bean
    private ThreadLocal<Set<String>> initialedBeans = new ThreadLocal<>();

    public Object doGetBean(String beanName) throws InstantiationException, IllegalAccessException {
    
        //other operation
        // 記錄正在建立的Bean
        Set<String> beans = this.initialedBeans.get();
        if (beans == null) {
            beans = new HashSet<>();
            this.initialedBeans.set(beans);
        }

        // 檢測迴圈依賴
        if (beans.contains(beanName)) {
            throw new Exception("檢測到" + beanName + "存在迴圈依賴:" + beans);
        }

        // 記錄正在建立的Bean
        beans.add(beanName);
        //other operation
        //建立完成 移除該bean的記錄
        beans.remove(beanName);
        return instance;
    }
}

複製程式碼

實際上在單例bean中,對於已經建立好的bean是直接從容器中獲取例項,不需要再次例項化,所以也不會有迴圈依賴的問題。但是對於原型bean,建立好的例項並不放到容器中,而是每一次都重新建立初始化,才會存在迴圈依賴的問題。

屬性依賴

除了在建構函式中初始化引數外,我們還可以對屬性進行賦值,對屬性賦值的好處在於可以在執行中動態的改變屬性的值。

和構造引數依賴有什麼不同

整體來說沒有什麼差別,不同在於對構造引數依賴時有具體的對應方法,可以根據引數的個數和順序來確定構造方法,所以在注入是我們可以使用上面選擇的List根據存入順序作為引數的順序。而對於屬性依賴,我們必須要根據屬性的名稱來注入值才可以,所以在使用list就不行了。

解決:

  1. 使用一個Map容器,key為屬性名,value為屬性值,使用時解析map即可
  2. 自定義一個包裹屬性的類,引數為屬性名和屬性值,然後使用list容納包裹屬性的類,實際上和上面的map差不多。

這裡我使用map類。

然後其他的地方都基本一樣,對於引用型別依舊使用BeanReference。在BeanDefinition中新增獲取和設定屬性值得方法:

    //屬性依賴
    Map<String,Object> getPropertyKeyValue();
    void setPropertyKeyValue(Map<String,Object> properties);
複製程式碼

在BeanFactory的實現中加入解析屬性的方法:

private void parsePropertyValues(BeanDefinition bd, Object instance) throws Exception {
        Map<String, Object> propertyKeyValue = bd.getPropertyKeyValue();
        if(propertyKeyValue==null || propertyKeyValue.size()==0){
            return ;
        }
        Class<?> aClass = instance.getClass();
        Set<Map.Entry<String, Object>> entries = propertyKeyValue.entrySet();
        for(Map.Entry<String, Object> entry:entries){
            //獲取指定的欄位資訊
            Field field = aClass.getDeclaredField(entry.getKey());
            //將訪問許可權設定為true
            field.setAccessible(true);
            Object arg = entry.getValue();
            Object value = null;
            if(arg instanceof BeanReference){
                String beanName = ((BeanReference) arg).getBeanName();
                value = this.doGetBean(beanName);
            }else if(arg instanceof List){
                List param = parseListArg((List) arg);
                value = param;
            }else if(arg instanceof Map){
                //todo 處理map
            }else if(arg instanceof Properties){
                //todo 處理屬性檔案
            }else {
                value = arg;
            }
            field.set(instance, value);
        }
    }
複製程式碼
相關程式碼已經託管到github:myspring

完整類圖

Spring系列之DI的原理及手動實現

相關文章