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

系列文章

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

關(guān)于

??本系列是對tiny-spring項目的詳細(xì)解讀,聚焦spring-beans的基本實現(xiàn),對應(yīng)著(first~sixth)-stage這六個構(gòu)建過程。這部分實現(xiàn)了基礎(chǔ)的IoC容器,DI是它的核心(控制反轉(zhuǎn)和依賴注入的相關(guān)概念可以看這里)。

spring-beans的使用流程

??回想一下在使用BeanFactory.getBean(...)之前,我們要做些什么?首先,定義xml配置文件,告訴Spring我們需要什么樣的對象以及它們之前的關(guān)系,接著初始化BeanFactory讀取配置文件、加載其中的定義信息,最后才是調(diào)用BeanFactory.getBean(),根據(jù)定義信息初始化bean并返回。

??舉個例子,假設(shè)我們有如下兩個類:

    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 setName(String name) {
            this.name = name;
        }
    
        public Car getCar() {
            return car;
        }
    
        public void setCar(Car car) {
            this.car = car;
        }
    }

??現(xiàn)在有一個叫Saber的妹子有一輛BYD產(chǎn)的價值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、調(diào)用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、得到打印結(jié)果
    
    name = Saber, car-brand = BYD, car-price = 240000.0

一切從Resource開始

??快速瀏覽完使用流程,我們知道首先是要有xml配置文件來告訴Spring我們需要的對象以及它們之間的關(guān)系。同時,真實的Spring不僅僅支持xml這一種格式,還支持properties文件格式甚至是自定義的格式。Spring是怎么做到的呢?這是因為Spring引入了一層對資源的抽象——Resource接口。Resource接口解決的是配置文件從哪里來、怎么讀取它的問題。

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

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

??在tiny-spring的實現(xiàn)中,我們采用xml格式來作為配置文件,并且對支持的功能(也就是對應(yīng)的xml標(biāo)簽)進(jìn)行了刪減,因此僅實現(xiàn)了ClassPathResource用來加載classpath下的xml配置文件,作為源碼解析來說應(yīng)該是夠用了。

    // 輸入命令git checkout second-stage,切換到第二階段,
    // 在DefaultXMLBeanDefinitionParser.java中查看tiny-spring支持的xml標(biāo)簽及屬性。

    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復(fù)刻了真實Spring IoC Container的功能子集:每一個<bean>標(biāo)簽都定義了一個容器管理的對象,同時支持setter注入和構(gòu)造函數(shù)注入兩種方式,分別由<property><constructor-arg>標(biāo)簽代表,bean之間的引用由<ref>標(biāo)簽代表,注入的值可以是簡單類型,由<value>標(biāo)簽代表,也可以是復(fù)合類型,比如數(shù)組,由<list>標(biāo)簽代表(map/set在tiny-spring中就不做支持了)。其他的諸如自動裝配、懶加載、生命周期回調(diào)等屬性也同樣支持。

XML配置文件到BeanDefinition的轉(zhuǎn)換

??在讀取配置文件之前,思考一下提取出來的信息如何保存?我們說過,每一個<bean>標(biāo)簽都定義了一個容器管理的對象,自然就引出了BeanDefinition,可以說一個<bean>標(biāo)簽就對應(yīng)著一個BeanDefinition實例,singleton、autowire等等都是它的屬性。

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

    /**
     * 保存從xml中解析出來的bean的定義信息。
     */
    public class BeanDefinition {
    
        // 不進(jìn)行自動裝配
        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;
        // 自動裝配構(gòu)造函數(shù)
        public static final int AUTOWIRE_CONSTRUCTOR = 3;
        // 自適應(yīng)裝配模式
        public static final int AUTOWIRE_AUTODETECT = 4;
    
        // bean所屬的類, bean的名稱由BeanFactoryRegistry管理
        private final Class<?> beanClass;
    
        // 是單實例還是每次獲取都創(chuàng)建,默認(rèn)為true
        private boolean singleton = true;
    
        // 對單實例的bean是否需要懶加載,
        // 默認(rèn)為false,在BeanFactory初始化時就
        // 初始化所有單實例bean
        private boolean lazyInit = false;
    
        // 自動裝配的模式
        private int autowireMode = AUTOWIRE_NO;
    
        // 所依賴的其他bean的名稱
        // dependsOn所代表的bean會在
        // 當(dāng)前bean初始化之前得到初始化
        private String[] dependsOn;
    
        // 自定義的初始化方法名,要求無參
        private String initMethodName;
    
        // 自定義的銷毀方法名,要求無參
        private String destroyMethodName;
    
        // setter注入的相關(guān)信息
        private MutablePropertyValues propertyValues;
    
        // 構(gòu)造函數(shù)注入的相關(guān)信息
        private ConstructorArgumentValues constructorArgumentValues;
        
        // 省略若干
        ......
    }

而對于<property><constructor-arg>標(biāo)簽,它們也有著各自的子標(biāo)簽和屬性,因此分別由MutablePropertyValuesConstructorArgumentValues兩個類來表示。很簡單的兩個類,各位同學(xué)自行查看一下third-stage的代碼即可,這里就不貼了。

BeanDefinition的注冊

??概念上我們知道了一個<bean>標(biāo)簽等于一個BeanDefinition實例,那么tiny-spring怎么實現(xiàn)從XML配置文件到BeanDefinition實例的轉(zhuǎn)換呢?這就引出了XMLBeanDefinitionReader接口,它只有一個方法,從Resource中提取出BeanDefinition(s)

    /**
     * 對xml配置文件讀取器的抽象。
     * 讀取器最主要的目的是讀取一個個<bean>標(biāo)簽,
     * 解析出其中的信息,生成對應(yīng)的BeanDefinition。
     */
    public interface XMLBeanDefinitionReader {
        /**
         * 加載bean的定義信息。
         * @param resource 代表一個xml配置文件
         */
        void loadBeanDefinition(@NotNull Resource resource);
    }

??回想一下BeanFactory.getBean(...)的調(diào)用場景,我們傳入一個bean name,容器根據(jù)bean name找到對應(yīng)的BeanDefinition,通過BeanDefinition描述的信息生成對象并返回。也就是說容器持有著beanName -> BeanDefinition的對應(yīng)關(guān)系,這一層抽象出來也就是BeanDefinitionRegistry

    /**
     * 這個接口管理著BeanFactory中BeanDefinition注冊
     * 的相關(guān)事宜,因此BeanFactory的實現(xiàn)類也會實現(xiàn)這個接口。
     * 單獨抽取出這個接口,是為了讓BeanFactory的職責(zé)更清晰,
     * 避免成為上帝接口。BeanFactory就是一個bean工廠,司職于bean的獲取查詢。
     */
    public interface BeanDefinitionRegistry {
        /**
         * 向BeanFactory中注冊bean的定義信息
         */
        void registerBeanDefinition(String beanName, BeanDefinition beanDefinition);
    }

顯然,這兩個接口是要組合使用的。XMLBeanDefinitionReader加載出BeanDefinition(s)之后,由BeanDefinitionRegistry來執(zhí)行注冊。Spring在實現(xiàn)時,額外提供了一個策略接口XMLBeanDefinitionParser來進(jìn)行真正的解析,tiny-springDefaultXMLBeanDefinitionReader也是這么實現(xiàn)的。

    /**
     * 對xml配置文件解析器的抽象。
     * 這是一個策略接口,XMLBeanDefinitionReader通過
     * XMLBeanDefinitionParser來做具體的解析。
     */
    public interface XMLBeanDefinitionParser {
        /**
         * 讀取<bean>標(biāo)簽的定義生成BeanDefinition,再通過
         * BeanDefinitionRegistry注冊進(jìn)BeanFactory。
         * @param document 代表xml配置文件的Document對象
         * @param classLoader 加載<bean>標(biāo)簽對應(yīng)JavaBean的類加載器
         * @param registry 用來注冊BeanDefinition的注冊器
         */
        void registerBeanDefinitions(@NotNull Document document,
                                     @NotNull ClassLoader classLoader,
                                     @NotNull BeanDefinitionRegistry registry);
    }

XMLBeanDefinitionReader將真正的解析行為代理給了XMLBeanDefinitionParser。NOTE:說是策略模式可以,說是代理模式也可以。具體如何解析,只是一個對應(yīng)的算法,從這個層面說是策略模式,ok;XMLBeanDefinitionReader本身不進(jìn)行xml文件的解析,而是將這個行為委托給了XMLBeanDefinitionParser,這么說是代理也沒啥毛病吧。設(shè)計模式吧,大多都是語意上的區(qū)別,理解就好,犯不著鉆牛角尖,Spring中有很多地方用到了這種模式。

XML配置文件的解析

??以上都理解了之后,下面就進(jìn)入DefaultXMLBeanDefinitionParser執(zhí)行真正的配置文件解析了。按照Spring xml配置文件的格式,首先獲取最頂層標(biāo)簽<beans>(這里其實是什么標(biāo)簽都可以),<bean>標(biāo)簽是<beans>的子標(biāo)簽,因此我們逐個遍歷<beans>的子標(biāo)簽找到其中的<bean>標(biāo)簽,因此重心便轉(zhuǎn)到了解析<bean>標(biāo)簽上。

    @Override
    public void registerBeanDefinitions(Document document, ClassLoader classLoader, BeanDefinitionRegistry registry) {
        // 獲取頂層元素(也就是<beans>標(biāo)簽)
        Element root = document.getDocumentElement();
        // 獲取<beans>下的子標(biāo)簽列表
        NodeList nodes = root.getChildNodes();
        // 統(tǒng)計<bean>標(biāo)簽的數(shù)量
        int numberOfBeans = 0;
        // 遍歷子標(biāo)簽列表
        for (int i = 0; i < nodes.getLength(); ++i) {
            Node node = nodes.item(i);
            // 找到<bean>標(biāo)簽
            if (node instanceof Element &&
                    BEAN_ELEMENT.equals(node.getNodeName())) {
                // 每一個<bean>標(biāo)簽就對應(yīng)一個BeanDefinition
                numberOfBeans++;
                // 加載其配置信息
                loadBeanDefinition((Element) node, classLoader, registry);
            }
        }
        System.out.println("一共找到" + numberOfBeans + "個<bean>標(biāo)簽");
    }

每個<bean>標(biāo)簽對應(yīng)著一個BeanDefinition,因此在解析的過程中我們創(chuàng)建了一個BeanDefinition實例來保存解析的結(jié)果。tiny-spring并不支持bean name aliasinner bean,也不支持BeanFactory的層級結(jié)構(gòu),因此<bean>標(biāo)簽必須指定id屬性和class屬性,解析出來的BeanDefinition就直接交給BeanDefinitionRegistry注冊了。

    /**
     * 解析并注冊<bean>標(biāo)簽
     */
    private void loadBeanDefinition(Element element, ClassLoader classLoader, BeanDefinitionRegistry registry) {
        // tiny spring不支持inner bean,也不支持bean的別名,
        // 因此獲取到的id就是bean的名稱,也是關(guān)聯(lián)對應(yīng)BeanDefinition的key
        String beanName = element.getAttribute(ID_ATTRIBUTE);
        if (!StringUtils.hasLength(beanName)) {
            throw new BeansException("每個<bean>標(biāo)簽都必須明確指定id屬性");
        }
        // 解析出對應(yīng)的BeanDefinition
        BeanDefinition beanDefinition = parseBeanDefinition(beanName, element, classLoader);
        // 檢驗一下是否有效
        beanDefinition.validate();
        // 并注冊進(jìn)BeanFactory
        registry.registerBeanDefinition(beanName, beanDefinition);
        System.out.println("已解析出[" + beanName + "]對應(yīng)的bean定義[" + beanDefinition + "]");
    }

解析的過程是非常直白的,查看<bean>標(biāo)簽有沒有定義lazy-init、singletoninit-method等屬性,有的話提取出來存儲進(jìn)BeanDefinition。

    /**
     * 解析<bean>標(biāo)簽
     */
    private BeanDefinition parseBeanDefinition(String beanName, Element element, ClassLoader classLoader) {
        // tiny spring也沒有支持BeanFactory的層次結(jié)構(gòu),
        // 因此每個bean也需要明確指明其所屬的類
        String beanClassName = element.getAttribute(CLASS_ATTRIBUTE);
        if (!StringUtils.hasLength(beanClassName)) {
            throw new BeansException("每個<bean>標(biāo)簽都必須明確指定class屬性");
        }
        try {
            // 加載這個類
            Class<?> beanClass = Class.forName(beanClassName, true, classLoader);
            // 獲取所有<property>標(biāo)簽的內(nèi)容
            MutablePropertyValues propertyValues = parseAllPropertyElements(beanName, element);
            // 獲取所有<constructor-arg>標(biāo)簽的內(nèi)容
            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, 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 + "]對應(yīng)的類", e);
        }
    }

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

    /**
     * 解析帶有屬性值的標(biāo)簽,提取值
     */
    private Object parsePropertySubElement(String beanName, Element element) {
        // <property>標(biāo)簽下有<value>/<list>/<ref>三種標(biāo)簽標(biāo)識了屬性值
        // <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>標(biāo)簽必須通過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標(biāo)簽
            return null;
        }
        throw new BeansException("[" + beanName + "] - 發(fā)現(xiàn)一個<property>標(biāo)簽下未知的子標(biāo)簽<" + element.getTagName() + ">");
    }

對于<ref>標(biāo)簽,我們用RuntimeBeanReference來標(biāo)識它是一個對其它bean的引用,后續(xù)通過RuntimeBeanReference中保存的bean name,使用BeanFactorygetBean(...)就能獲取到對應(yīng)的bean并設(shè)置進(jìn)去;對于<list>標(biāo)簽,我們同樣用一個標(biāo)記類ManagedList來標(biāo)識它,至于<value>,在xml中只是普通的字符串,直接提取出來保存即可,后續(xù)根據(jù)屬性的實際類型來進(jìn)行轉(zhuǎn)換,當(dāng)然,這是后面的故事了。

總結(jié)

??至此我們完成了配置文件的抽象和讀取過程,接下來就是BeanFactory的戲份了。下一篇將會詳細(xì)介紹BeanFactory如何利用這些配置信息來幫我們管理對象,碼字不易,感覺有幫助的話,點個star吧~~

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • rljs by sennchi Timeline of History Part One The Cognitiv...
    sennchi閱讀 7,854評論 0 10
  • **2014真題Directions:Read the following text. Choose the be...
    又是夜半驚坐起閱讀 11,092評論 0 23
  • Chapter 1 - we are introduced to the narrator, a pilot, a...
    久然丶閱讀 3,954評論 1 8
  • 這世界最愛我的人,可能他不是我這一生中最愛的人,但他卻永遠(yuǎn)是這世界上最愛我的人。 還記得我2011年的冬天,那...
    鄧冰是我女神閱讀 475評論 2 0
  • 我很清楚自己的問題 我生氣的 不是別人毫無顧忌的指摘 可能是 難以改變 可能是 費(fèi)勁全力 也跳脫不出怪圈
    蘋果_ee07閱讀 129評論 0 0

友情鏈接更多精彩內(nèi)容