手把手教你实现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、定义配置文件,描述需求
    
    
    
        
            
                240000.00
            
            
                BYD
            
        
        
        
            
                Saber
            
            
                
            
        
        
    
    
    // 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的功能子集:每一个标签都定义了一个容器管理的对象,同时支持setter注入和构造函数注入两种方式,分别由标签代表,bean之间的引用由标签代表,注入的值可以是简单类型,由标签代表,也可以是复合类型,比如数组,由标签代表(map/set在tiny-spring中就不做支持了)。其他的诸如自动装配、懒加载、生命周期回调等属性也同样支持。

XML配置文件到BeanDefinition的转换

  在读取配置文件之前,思考一下提取出来的信息如何保存?我们说过,每一个标签都定义了一个容器管理的对象,自然就引出了BeanDefinition,可以说一个标签就对应着一个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;
        
        // 省略若干
        ......
    }

而对于标签,它们也有着各自的子标签和属性,因此分别由MutablePropertyValuesConstructorArgumentValues两个类来表示。很简单的两个类,各位同学自行查看一下third-stage的代码即可,这里就不贴了。

BeanDefinition的注册

  概念上我们知道了一个标签等于一个BeanDefinition实例,那么tiny-spring怎么实现从XML配置文件到BeanDefinition实例的转换呢?这就引出了XMLBeanDefinitionReader接口,它只有一个方法,从Resource中提取出BeanDefinition(s)

    /**
     * 对xml配置文件读取器的抽象。
     * 读取器最主要的目的是读取一个个标签,
     * 解析出其中的信息,生成对应的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-spring的DefaultXMLBeanDefinitionReader也是这么实现的。

    /**
     * 对xml配置文件解析器的抽象。
     * 这是一个策略接口,XMLBeanDefinitionReader通过
     * XMLBeanDefinitionParser来做具体的解析。
     */
    public interface XMLBeanDefinitionParser {
        /**
         * 读取标签的定义生成BeanDefinition,再通过
         * BeanDefinitionRegistry注册进BeanFactory。
         * @param document 代表xml配置文件的Document对象
         * @param classLoader 加载标签对应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配置文件的格式,首先获取最顶层标签(这里其实是什么标签都可以),标签是的子标签,因此我们逐个遍历的子标签找到其中的标签,因此重心便转到了解析标签上。

    @Override
    public void registerBeanDefinitions(Document document, ClassLoader classLoader, BeanDefinitionRegistry registry) {
        // 获取顶层元素(也就是标签)
        Element root = document.getDocumentElement();
        // 获取下的子标签列表
        NodeList nodes = root.getChildNodes();
        // 统计标签的数量
        int numberOfBeans = 0;
        // 遍历子标签列表
        for (int i = 0; i < nodes.getLength(); ++i) {
            Node node = nodes.item(i);
            // 找到标签
            if (node instanceof Element &&
                    BEAN_ELEMENT.equals(node.getNodeName())) {
                // 每一个标签就对应一个BeanDefinition
                numberOfBeans++;
                // 加载其配置信息
                loadBeanDefinition((Element) node, classLoader, registry);
            }
        }
        System.out.println("一共找到" + numberOfBeans + "个标签");
    }

每个标签对应着一个BeanDefinition,因此在解析的过程中我们创建了一个BeanDefinition实例来保存解析的结果。tiny-spring并不支持bean name aliasinner bean,也不支持BeanFactory的层级结构,因此标签必须指定id属性和class属性,解析出来的BeanDefinition就直接交给BeanDefinitionRegistry注册了。

    /**
     * 解析并注册标签
     */
    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("每个标签都必须明确指定id属性");
        }
        // 解析出对应的BeanDefinition
        BeanDefinition beanDefinition = parseBeanDefinition(beanName, element, classLoader);
        // 检验一下是否有效
        beanDefinition.validate();
        // 并注册进BeanFactory
        registry.registerBeanDefinition(beanName, beanDefinition);
        System.out.println("已解析出[" + beanName + "]对应的bean定义[" + beanDefinition + "]");
    }

解析的过程是非常直白的,查看标签有没有定义lazy-initsingletoninit-method等属性,有的话提取出来存储进BeanDefinition

    /**
     * 解析标签
     */
    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("每个标签都必须明确指定class属性");
        }
        try {
            // 加载这个类
            Class beanClass = Class.forName(beanClassName, true, classLoader);
            // 获取所有标签的内容
            MutablePropertyValues propertyValues = parseAllPropertyElements(beanName, element);
            // 获取所有标签的内容
            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);
        }
    }

因为是的子标签而不是属性,因此需要单独处理。标签的解析过程基本是一致的,这里就以来作为说明。首先也是要遍历出下的所有标签,转换成对应的PropertyValue,然后存入MutablePropertyValues,最后归入BeanDefinition。在tiny-spring中,下可能存在中的一个来表示要注入属性的值,具体是怎么处理的呢?

    /**
     * 解析带有属性值的标签,提取值
     */
    private Object parsePropertySubElement(String beanName, Element element) {
        // 标签下有//三种标签标识了属性值
        // //inner bean这些这里就不做支持了
        if (element.getTagName().equals(REF_ELEMENT)) {
            // 如果是,它指向另一个bean的定义
            String beanRef = element.getAttribute(BEAN_REF_ATTRIBUTE);
            if (!StringUtils.hasLength(beanRef)) {
                throw new BeansException("[" + beanName + "] - 标签必须通过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 + "] - 发现一个标签下未知的子标签<" + element.getTagName() + ">");
    }

对于标签,我们用RuntimeBeanReference来标识它是一个对其它bean的引用,后续通过RuntimeBeanReference中保存的bean name,使用BeanFactorygetBean(...)就能获取到对应的bean并设置进去;对于标签,我们同样用一个标记类ManagedList来标识它,至于,在xml中只是普通的字符串,直接提取出来保存即可,后续根据属性的实际类型来进行转换,当然,这是后面的故事了。

总结

  至此我们完成了配置文件的抽象和读取过程,接下来就是BeanFactory的戏份了。下一篇将会详细介绍BeanFactory如何利用这些配置信息来帮我们管理对象,码字不易,感觉有帮助的话,点个star吧~~

你可能感兴趣的:(手把手教你实现spring-beans (一))