手把手教你实现spring-beans (一)

系列文章

手把手教你实现spring-beans (一)
手把手教你实现spring-beans (二)
手把手教你实现spring-context (TODO)
手把手教你实现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 setName(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, ClassLoader classLoader, 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, 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如何利用这些配置信息来帮我们管理对象,码字不易,感觉有帮助的话,点个star吧~~

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,839评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,543评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,116评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,371评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,384评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,111评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,416评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,053评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,558评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,007评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,117评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,756评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,324评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,315评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,539评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,578评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,877评论 2 345

推荐阅读更多精彩内容

  • rljs by sennchi Timeline of History Part One The Cognitiv...
    sennchi阅读 7,294评论 0 10
  • **2014真题Directions:Read the following text. Choose the be...
    又是夜半惊坐起阅读 9,399评论 0 23
  • Chapter 1 - we are introduced to the narrator, a pilot, a...
    久然丶阅读 3,188评论 1 8
  • 这世界最爱我的人,可能他不是我这一生中最爱的人,但他却永远是这世界上最爱我的人。 还记得我2011年的冬天,那...
    邓冰是我女神阅读 408评论 2 0
  • 我很清楚自己的问题 我生气的 不是别人毫无顾忌的指摘 可能是 难以改变 可能是 费劲全力 也跳脱不出怪圈
    苹果_ee07阅读 113评论 0 0