1. 程式人生 > 程式設計 >手把手教你實現spring-beans (一)

手把手教你實現spring-beans (一)

系列文章

手把手教你實現spring-beans (一)
手把手教你實現spring-beans (二)
手把手教你實現spring-context
手把手教你實現spring-aop (TODO)

關於

  本系列是對tiny-spring專案的詳細解讀,聚焦spring-beans的基本實現,對應著(first~sixth)-stage這六個構建過程。這部分實現了基礎的IoC容器,DI是它的核心(控制反轉和依賴注入的相關概念可以看這裡)。

spring-beans的使用流程

  回想一下在使用BeanFactory.getBean(...)之前,我們要做些什麼?首先,定義xml配置檔案,告訴Spring我們需要什麼樣的物件以及它們之前的關係,接著初始化BeanFactory

讀取配置檔案、載入其中的定義資訊,最後才是呼叫BeanFactory.getBean(),根據定義資訊初始化bean並返回。

  舉個例子,假設我們有如下兩個類:

    public class Car {
    
        private double price;
        
        private String brand;
    
        public double getPrice() {
            return price;
        }
    
        public void setPrice(double price) {
            this.price = price;
        }
    
        public String getBrand
() { return brand; } public void setBrand(String brand) { this.brand = brand; } } public class Person { private String name; private Car car; public String getName() { return name; } public void set
Name(String name) { this.name = name; } public Car getCar() { return car; } public void setCar(Car car) { this.car = car; } } 複製程式碼

  現在有一個叫Saber的妹子有一輛BYD產的價值240000.00的車,如果用Spring來管理的話,大概是這樣:

    // 1、定義配置檔案,描述需求
    
    <beans>
    
        <bean id="byd" class="test.Car">
            <property name="price">
                <value>240000.00</value>
            </property>
            <property name="brand">
                <value>BYD</value>
            </property>
        </bean>
        
        <bean id="saber" class="test.Person">
            <property name="name">
                <value>Saber</value>
            </property>
            <property name="car">
                <ref bean="byd"/>
            </property>
        </bean>
        
    </beans>
    
    // 2、讀取配置檔案,載入定義資訊
    
    Resource resource = new ClassPathResource("test/config.xml");
    DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory();
    XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(beanFactory);
    reader.loadBeanDefinitions(resource);
    
    // 3、呼叫getBean()
    
    Person saber = (Person) beanFactory.getBean("saber");
    Car byd = saber.getCar();
    System.out.println("name = " + saber.getName() + ",car-brand = " + byd.getBrand() + ",car-price = " + byd.getPrice());
    
    // 4、得到列印結果
    
    name = Saber,car-brand = BYD,car-price = 240000.0
複製程式碼

一切從Resource開始

  快速瀏覽完使用流程,我們知道首先是要有xml配置檔案來告訴Spring我們需要的物件以及它們之間的關係。同時,真實的Spring不僅僅支援xml這一種格式,還支援properties檔案格式甚至是自定義的格式。Spring是怎麼做到的呢?這是因為Spring引入了一層對資源的抽象——Resource介面。Resource介面解決的是配置檔案從哪裡來、怎麼讀取它的問題。

    // 在tiny-spring目錄下輸入命令 git checkout first-stage,切換到第一階段,可以看到對Resource介面的描述(這個介面對真實的Resource介面進行了大幅精簡)。
    // 其中直接定義在Resource介面中的方法抽象了資源從哪裡來,定義在父介面中的方法抽象了資源怎麼讀。

    public interface InputStreamSource {
        /**
         * 返回代表資源的輸入流。
         */
        @Nullable
        InputStream getInputStream() throws IOException;
    }
    
    public interface Resource extends InputStreamSource {
        /**
         * 從類路徑載入的偽URL協議字首。
         */
        String CLASSPATH_URL_PREFIX = "classpath:";
        
        /**
         * 檔案系統中檔案的URL協議名。
         */
        String FILESYSTEM_URL_PROTOCOL = "file";
        
        /**
         * 檢查資源是否真實存在。
         */
        boolean exists();
    
        /**
         * 返回指向此資源的URL。
         */
        @Nullable
        URL getURL() throws IOException;
    
        /**
         * 返回表示此資源的檔案。
         */
        @Nullable
        File getFile() throws IOException;
    }

複製程式碼

  在tiny-spring的實現中,我們採用xml格式來作為配置檔案,並且對支援的功能(也就是對應的xml標籤)進行了刪減,因此僅實現了ClassPathResource用來載入classpath下的xml配置檔案,作為原始碼解析來說應該是夠用了。

    // 輸入命令git checkout second-stage,切換到第二階段,
    // 在DefaultXMLBeanDefinitionParser.java中檢視tiny-spring支援的xml標籤及屬性。

    private static final String TRUE_VALUE = "true";

    private static final String BEAN_ELEMENT = "bean";
    private static final String CLASS_ATTRIBUTE = "class";
    private static final String ID_ATTRIBUTE = "id";
    private static final String NAME_ATTRIBUTE = "name";
    private static final String SINGLETON_ATTRIBUTE = "singleton";
    private static final String DEPENDS_ON_ATTRIBUTE = "depends-on";
    private static final String INIT_METHOD_ATTRIBUTE = "init-method"; 
    private static final String DESTROY_METHOD_ATTRIBUTE = "destroy-method";
    private static final String CONSTRUCTOR_ARG_ELEMENT = "constructor-arg";
    private static final String INDEX_ATTRIBUTE = "index";
    private static final String TYPE_ATTRIBUTE = "type";
    private static final String PROPERTY_ELEMENT = "property";
    private static final String REF_ELEMENT = "ref";
    private static final String BEAN_REF_ATTRIBUTE = "bean";
    private static final String LIST_ELEMENT = "list";
    private static final String VALUE_ELEMENT = "value";
    private static final String NULL_ELEMENT = "null";

    private static final String LAZY_INIT_ATTRIBUTE = "lazy-init";

    private static final String AUTOWIRE_ATTRIBUTE = "autowire";
    private static final String AUTOWIRE_BY_NAME_VALUE = "byName";
    private static final String AUTOWIRE_BY_TYPE_VALUE = "byType";
    private static final String AUTOWIRE_CONSTRUCTOR_VALUE = "constructor";
    private static final String AUTOWIRE_AUTODETECT_VALUE = "autodetect";
複製程式碼

  可以看到,tiny-spring復刻了真實Spring IoC Container的功能子集:每一個<bean>標籤都定義了一個容器管理的物件,同時支援setter注入和建構函式注入兩種方式,分別由<property><constructor-arg>標籤代表,bean之間的引用由<ref>標籤代表,注入的值可以是簡單型別,由<value>標籤代表,也可以是複合型別,比如陣列,由<list>標籤代表(map/set在tiny-spring中就不做支援了)。其他的諸如自動裝配、懶載入、生命週期回撥等屬性也同樣支援。

XML配置檔案到BeanDefinition的轉換

  在讀取配置檔案之前,思考一下提取出來的資訊如何儲存?我們說過,每一個<bean>標籤都定義了一個容器管理的物件,自然就引出了BeanDefinition,可以說一個<bean>標籤就對應著一個BeanDefinition例項,singletonautowire等等都是它的屬性。

    // 輸入命令git checkout third-stage,切換到第三階段,檢視BeanDefinition的具體定義。

    /**
     * 儲存從xml中解析出來的bean的定義資訊。
     */
    public class BeanDefinition {
    
        // 不進行自動裝配
        public static final int AUTOWIRE_NO = 0;
        // 通過bean名稱自動裝配
        public static final int AUTOWIRE_BY_NAME = 1;
        // 通過bean型別自動裝配
        public static final int AUTOWIRE_BY_TYPE = 2;
        // 自動裝配建構函式
        public static final int AUTOWIRE_CONSTRUCTOR = 3;
        // 自適應裝配模式
        public static final int AUTOWIRE_AUTODETECT = 4;
    
        // bean所屬的類,bean的名稱由BeanFactoryRegistry管理
        private final Class<?> beanClass;
    
        // 是單例項還是每次獲取都建立,預設為true
        private boolean singleton = true;
    
        // 對單例項的bean是否需要懶載入,
        // 預設為false,在BeanFactory初始化時就
        // 初始化所有單例項bean
        private boolean lazyInit = false;
    
        // 自動裝配的模式
        private int autowireMode = AUTOWIRE_NO;
    
        // 所依賴的其他bean的名稱
        // dependsOn所代表的bean會在
        // 當前bean初始化之前得到初始化
        private String[] dependsOn;
    
        // 自定義的初始化方法名,要求無參
        private String initMethodName;
    
        // 自定義的銷燬方法名,要求無參
        private String destroyMethodName;
    
        // setter注入的相關資訊
        private MutablePropertyValues propertyValues;
    
        // 建構函式注入的相關資訊
        private ConstructorArgumentValues constructorArgumentValues;
        
        // 省略若干
        ......
    }

複製程式碼

而對於<property><constructor-arg>標籤,它們也有著各自的子標籤和屬性,因此分別由MutablePropertyValuesConstructorArgumentValues兩個類來表示。很簡單的兩個類,各位同學自行檢視一下third-stage的程式碼即可,這裡就不貼了。

BeanDefinition的註冊

  概念上我們知道了一個<bean>標籤等於一個BeanDefinition例項,那麼tiny-spring怎麼實現從XML配置檔案到BeanDefinition例項的轉換呢?這就引出了XMLBeanDefinitionReader介面,它只有一個方法,從Resource中提取出BeanDefinition(s)

    /**
     * 對xml配置檔案讀取器的抽象。
     * 讀取器最主要的目的是讀取一個個<bean>標籤,
     * 解析出其中的資訊,生成對應的BeanDefinition。
     */
    public interface XMLBeanDefinitionReader {
        /**
         * 載入bean的定義資訊。
         * @param resource 代表一個xml配置檔案
         */
        void loadBeanDefinition(@NotNull Resource resource);
    }
複製程式碼

  回想一下BeanFactory.getBean(...)的呼叫場景,我們傳入一個bean name,容器根據bean name找到對應的BeanDefinition,通過BeanDefinition描述的資訊生成物件並返回。也就是說容器持有著beanName -> BeanDefinition的對應關係,這一層抽象出來也就是BeanDefinitionRegistry

    /**
     * 這個介面管理著BeanFactory中BeanDefinition註冊
     * 的相關事宜,因此BeanFactory的實現類也會實現這個介面。
     * 單獨抽取出這個介面,是為了讓BeanFactory的職責更清晰,
     * 避免成為上帝介面。BeanFactory就是一個bean工廠,司職於bean的獲取查詢。
     */
    public interface BeanDefinitionRegistry {
        /**
         * 向BeanFactory中註冊bean的定義資訊
         */
        void registerBeanDefinition(String beanName,BeanDefinition beanDefinition);
    }
複製程式碼

顯然,這兩個介面是要組合使用的。XMLBeanDefinitionReader載入出BeanDefinition(s)之後,由BeanDefinitionRegistry來執行註冊。Spring在實現時,額外提供了一個策略介面XMLBeanDefinitionParser來進行真正的解析,tiny-springDefaultXMLBeanDefinitionReader也是這麼實現的。

    /**
     * 對xml配置檔案解析器的抽象。
     * 這是一個策略介面,XMLBeanDefinitionReader通過
     * XMLBeanDefinitionParser來做具體的解析。
     */
    public interface XMLBeanDefinitionParser {
        /**
         * 讀取<bean>標籤的定義生成BeanDefinition,再通過
         * BeanDefinitionRegistry註冊進BeanFactory。
         * @param document 代表xml配置檔案的Document物件
         * @param classLoader 載入<bean>標籤對應JavaBean的類載入器
         * @param registry 用來註冊BeanDefinition的註冊器
         */
        void registerBeanDefinitions(@NotNull Document document,@NotNull ClassLoader classLoader,@NotNull BeanDefinitionRegistry registry);
    }
複製程式碼

XMLBeanDefinitionReader將真正的解析行為代理給了XMLBeanDefinitionParser。NOTE:說是策略模式可以,說是代理模式也可以。具體如何解析,只是一個對應的演演算法,從這個層面說是策略模式,ok;XMLBeanDefinitionReader本身不進行xml檔案的解析,而是將這個行為委託給了XMLBeanDefinitionParser,這麼說是代理也沒啥毛病吧。設計模式吧,大多都是語意上的區別,理解就好,犯不著鑽牛角尖,Spring中有很多地方用到了這種模式。

XML配置檔案的解析

  以上都理解了之後,下面就進入DefaultXMLBeanDefinitionParser執行真正的配置檔案解析了。按照Spring xml配置檔案的格式,首先獲取最頂層標籤<beans>(這裡其實是什麼標籤都可以),<bean>標籤是<beans>的子標籤,因此我們逐個遍歷<beans>的子標籤找到其中的<bean>標籤,因此重心便轉到了解析<bean>標籤上。

    @Override
    public void registerBeanDefinitions(Document document,ClassLoader classLoader,BeanDefinitionRegistry registry) {
        // 獲取頂層元素(也就是<beans>標籤)
        Element root = document.getDocumentElement();
        // 獲取<beans>下的子標籤列表
        NodeList nodes = root.getChildNodes();
        // 統計<bean>標籤的數量
        int numberOfBeans = 0;
        // 遍歷子標籤列表
        for (int i = 0; i < nodes.getLength(); ++i) {
            Node node = nodes.item(i);
            // 找到<bean>標籤
            if (node instanceof Element &&
                    BEAN_ELEMENT.equals(node.getNodeName())) {
                // 每一個<bean>標籤就對應一個BeanDefinition
                numberOfBeans++;
                // 載入其配置資訊
                loadBeanDefinition((Element) node,classLoader,registry);
            }
        }
        System.out.println("一共找到" + numberOfBeans + "個<bean>標籤");
    }
複製程式碼

每個<bean>標籤對應著一個BeanDefinition,因此在解析的過程中我們建立了一個BeanDefinition例項來儲存解析的結果。tiny-spring並不支援bean name aliasinner bean,也不支援BeanFactory的層級結構,因此<bean>標籤必須指定id屬性和class屬性,解析出來的BeanDefinition就直接交給BeanDefinitionRegistry註冊了。

    /**
     * 解析並註冊<bean>標籤
     */
    private void loadBeanDefinition(Element element,BeanDefinitionRegistry registry) {
        // tiny spring不支援inner bean,也不支援bean的別名,
        // 因此獲取到的id就是bean的名稱,也是關聯對應BeanDefinition的key
        String beanName = element.getAttribute(ID_ATTRIBUTE);
        if (!StringUtils.hasLength(beanName)) {
            throw new BeansException("每個<bean>標籤都必須明確指定id屬性");
        }
        // 解析出對應的BeanDefinition
        BeanDefinition beanDefinition = parseBeanDefinition(beanName,element,classLoader);
        // 檢驗一下是否有效
        beanDefinition.validate();
        // 並註冊進BeanFactory
        registry.registerBeanDefinition(beanName,beanDefinition);
        System.out.println("已解析出[" + beanName + "]對應的bean定義[" + beanDefinition + "]");
    }
複製程式碼

解析的過程是非常直白的,檢視<bean>標籤有沒有定義lazy-initsingletoninit-method等屬性,有的話提取出來儲存進BeanDefinition

    /**
     * 解析<bean>標籤
     */
    private BeanDefinition parseBeanDefinition(String beanName,Element element,ClassLoader classLoader) {
        // tiny spring也沒有支援BeanFactory的層次結構,
        // 因此每個bean也需要明確指明其所屬的類
        String beanClassName = element.getAttribute(CLASS_ATTRIBUTE);
        if (!StringUtils.hasLength(beanClassName)) {
            throw new BeansException("每個<bean>標籤都必須明確指定class屬性");
        }
        try {
            // 載入這個類
            Class<?> beanClass = Class.forName(beanClassName,true,classLoader);
            // 獲取所有<property>標籤的內容
            MutablePropertyValues propertyValues = parseAllPropertyElements(beanName,element);
            // 獲取所有<constructor-arg>標籤的內容
            ConstructorArgumentValues constructorArgumentValues = parseAllConstructorArgElements(beanName,element);
            // 生成bean的定義資訊
            BeanDefinition beanDefinition = new BeanDefinition(beanClass,propertyValues,constructorArgumentValues);
            // 獲取依賴資訊
            if (element.hasAttribute(DEPENDS_ON_ATTRIBUTE)) {
                String dependsOn = element.getAttribute(DEPENDS_ON_ATTRIBUTE);
                beanDefinition.setDependsOn(StringUtils.split(dependsOn,",; ",true));
            }
            // 獲取自動裝配模式
            String autowire = element.getAttribute(AUTOWIRE_ATTRIBUTE);
            beanDefinition.setAutowireMode(getAutowireMode(autowire));
            // 獲取自定義的初始化方法名
            String initMethodName = element.getAttribute(INIT_METHOD_ATTRIBUTE);
            if (StringUtils.hasLength(initMethodName)) {
                beanDefinition.setInitMethodName(initMethodName);
            }
            // 獲取自定義的銷燬方法名
            String destroyMethodName = element.getAttribute(DESTROY_METHOD_ATTRIBUTE);
            if (StringUtils.hasLength(destroyMethodName)) {
                beanDefinition.setDestroyMethodName(destroyMethodName);
            }
            // 獲取是否配置成單例
            if (element.hasAttribute(SINGLETON_ATTRIBUTE)) {
                beanDefinition.setSingleton(TRUE_VALUE.equals(element.getAttribute(SINGLETON_ATTRIBUTE)));
            }
            // 獲取是否配置成懶載入
            String lazyInit = element.getAttribute(LAZY_INIT_ATTRIBUTE);
            if (beanDefinition.isSingleton()) { // 此屬性對單例的bean才有效
                beanDefinition.setLazyInit(TRUE_VALUE.equals(lazyInit));
            }
            return beanDefinition;
        } catch (ClassNotFoundException e) {
            throw new BeansException("找不到[" + beanClassName + "]對應的類",e);
        }
    }
複製程式碼

<property><constructor-arg>因為是<bean>的子標籤而不是屬性,因此需要單獨處理。<property><constructor-arg>標籤的解析過程基本是一致的,這裡就以<property>來作為說明。首先也是要遍歷出<bean>下的所有<property>標籤,轉換成對應的PropertyValue,然後存入MutablePropertyValues,最後歸入BeanDefinition。在tiny-spring中,<property>下可能存在<value><list><ref>中的一個來表示要注入屬性的值,具體是怎麼處理的呢?

    /**
     * 解析帶有屬性值的標籤,提取值
     */
    private Object parsePropertySubElement(String beanName,Element element) {
        // <property>標籤下有<value>/<list>/<ref>三種標籤標識了屬性值
        // <set>/<map>/inner bean這些這裡就不做支援了
        if (element.getTagName().equals(REF_ELEMENT)) {
            // 如果是<ref>,它指向另一個bean的定義
            String beanRef = element.getAttribute(BEAN_REF_ATTRIBUTE);
            if (!StringUtils.hasLength(beanRef)) {
                throw new BeansException("[" + beanName + "] - <ref>標籤必須通過bean屬性指明引用的其他bean");
            }
            // 返回一個包裝引用的物件
            return new RuntimeBeanReference(beanRef);
        } else if (element.getTagName().equals(LIST_ELEMENT)) {
            // 是一個List
            return getList(beanName,element);
        } else if (element.getTagName().equals(VALUE_ELEMENT)) {
            // 是字面值
            return getTextValue(beanName,element);
        } else if (element.getTagName().equals(NULL_ELEMENT)) {
            // 是一個null標籤
            return null;
        }
        throw new BeansException("[" + beanName + "] - 發現一個<property>標籤下未知的子標籤<" + element.getTagName() + ">");
    }
複製程式碼

對於<ref>標籤,我們用RuntimeBeanReference來標識它是一個對其它bean的引用,後續通過RuntimeBeanReference中儲存的bean name,使用BeanFactorygetBean(...)就能獲取到對應的bean並設定進去;對於<list>標籤,我們同樣用一個標記類ManagedList來標識它,至於<value>,在xml中只是普通的字串,直接提取出來儲存即可,後續根據屬性的實際型別來進行轉換,當然,這是後面的故事了。

總結

  至此我們完成了配置檔案的抽象和讀取過程,接下來就是BeanFactory的戲份了,下一篇將會詳細介紹BeanFactory如何利用這些配置資訊來幫我們管理物件。我是愛呆毛的小士郎,歡迎交流~