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

目錄

前言

在上一章中,咱們介紹和簡單實現了容器的部分功能,可是這裏還留下了不少的問題。好比咱們在構造bean實例的時候若是bean是帶參數的咱們須要怎麼來進行,固然這個是能夠在init方法中進行的,再好比咱們平時在Spring中獲取一個對象經過一個註解便可獲取到類的實例,這個實例是怎麼注入的呢?html

IOC和DI

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

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

注入分析

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

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

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

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

咱們賦值的類型有那些呢?緩存

很明顯在Java中咱們賦值類型包括基本數據類型(int,double...)和引用類型。框架

在分析清楚須要注入的行爲和類型後又有另外的問題,咱們雖然知道注入的類型是基本數據類型和引用類型,可是實際須要注入的類型是沒法預料到的,咱們事先並不知道某一個參數須要的是int仍是boolean或者是引用類型。幸運的是Java實際上已經幫咱們解決了這件事情,java中的全部類型都是繼承自Object,咱們只須要使用Object來接受值便可。函數

咱們在對值進行注入的時候確定是須要知道咱們注入的具體的類型的,instanceof關鍵字能夠幫助咱們肯定具體的某個類型,可是實際上也就僅僅限於基本數據類型了,由於引用類型實在太多了,用戶本身定義的引用類型咱們是沒有辦法事先肯定的。因此咱們要想辦法能讓咱們知道注入的具體是哪個類型。測試

BeanReference

咱們定義一個用來標識bean類型的類,接口只包含一個標識bean的類型的參數beanName,在注入參數的時候只須要定義好beanName的值便可直接從IOC容器中取相應的bean,而對於基本數據類型和String則直接賦值便可。

固然這樣還不能解決全部問題,由於傳入的參數可能攜帶多個引用值,好比引用數組,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定義接口上了。

構造參數的注入應當是在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

循環依賴

到這裏對構造函數的參數依賴基本完成了,通過測試也基本沒有問題,可是在測試過程當中發現若是構造出的參數存在循環依賴的話,則會致使整個過程失敗。

什麼是循環依賴?如何解決循環依賴?

如上圖,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

完整類圖

相關文章
相關標籤/搜索