实现 Spring Beans
   一些代码   0 评论   877 浏览

实现 Spring Beans

   一些代码   0 评论   877 浏览

关于

本系列是对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如何利用这些配置信息来帮我们管理对象。

本文由 RawChen 发表, 最后编辑时间为:2021-11-13 13:17
如果你觉得我的文章不错,不妨鼓励我继续写作。

发表评论
选择表情
Top