手写Spring-第五章-解放双手!自动化配置!

前言

上次我们完成了Bean之间依赖的注入。但在最后测试的时候,吃到了苦头。Bean定义的构建太麻烦了>_<!而且现在的Bean定义还只有class和属性两个内容,如果以后再继续扩充,想必构建起来会更加麻烦。我们迫切需要一种机制,来解放双手,让Spring来帮我们构建这些麻烦的玩意儿。

那么,终于到配置文件出场的时候了。现在的项目,终究会有配置文件。它不仅解决了对可变信息硬编码的问题,还让我们可以通过配置就完成很多设置和注册、创建等工作。配置文件就相当于我们的需求列表,填写它,剩下的让框架来做。所以这次的目标,就是实现对配置文件的读取、解析,自动化注册以及创建bean。

工程结构

├─src
│  ├─main
│  │  ├─java
│  │  │  └─com
│  │  │      └─akitsuki
│  │  │          └─springframework
│  │  │              ├─beans
│  │  │              │  ├─exception
│  │  │              │  │      BeanException.java
│  │  │              │  │  
│  │  │              │  └─factory
│  │  │              │      │  BeanFactory.java
│  │  │              │      │  
│  │  │              │      ├─config
│  │  │              │      │      BeanDefinition.java
│  │  │              │      │      BeanReference.java
│  │  │              │      │      DefaultSingletonBeanRegistry.java
│  │  │              │      │      PropertyValue.java
│  │  │              │      │      PropertyValues.java
│  │  │              │      │      SingletonBeanRegistry.java
│  │  │              │      │  
│  │  │              │      ├─support
│  │  │              │      │      AbstractAutowireCapableBeanFactory.java
│  │  │              │      │      AbstractBeanDefinitionReader.java
│  │  │              │      │      AbstractBeanFactory.java
│  │  │              │      │      BeanDefinitionReader.java
│  │  │              │      │      BeanDefinitionRegistry.java
│  │  │              │      │      CglibSubclassingInstantiationStrategy.java
│  │  │              │      │      DefaultListableBeanFactory.java
│  │  │              │      │      InstantiationStrategy.java
│  │  │              │      │      SimpleInstantiationStrategy.java
│  │  │              │      │  
│  │  │              │      └─xml
│  │  │              │              XmlBeanDefinitionReader.java
│  │  │              │      
│  │  │              ├─core
│  │  │              │  └─io
│  │  │              │          ClasspathResource.java
│  │  │              │          DefaultResourceLoader.java
│  │  │              │          FileSystemResource.java
│  │  │              │          Resource.java
│  │  │              │          ResourceLoader.java
│  │  │              │          UrlResource.java
│  │  │              │  
│  │  │              └─util
│  │  │                      ClassUtils.java
│  │  │              
│  │  └─resources
│  └─test
│      ├─java
│      │  └─com
│      │      └─akitsuki
│      │          └─springframework
│      │              └─test
│      │                  │  ApiTest.java
│      │                  │  
│      │                  └─bean
│      │                          UserDao.java
│      │                          UserService.java
│      │                  
│      └─resources
│              config.yml
│              spring.xml

许久没怎么变化的工程结构图,再次迎来暴涨!庆贺吧,受苦的时刻来临力!

一切配置,皆为资源

既然我们要读取配置,那么我们的配置,总要有个来源。这个配置可以是某个项目中的配置文件,也可以是文件系统中的某个文件,或者也可以来自网络,来自云端。但不管如何,它们都是资源。所以,我们需要一个接口,来提供获取这些资源的功能。

package com.akitsuki.springframework.core.io;

import java.io.IOException;
import java.io.InputStream;

/**
 * 资源加载接口,用于加载Spring的配置
 *
 * @author ziling.wang@hand-china.com
 * @date 2022/11/9 11:12
 */
public interface Resource {

    /**
     * 获取资源的inputStream
     *
     * @return
     * @throws IOException
     */
    InputStream getInputStream() throws IOException;
}

我们采用流的方式来读取资源,所以这个接口提供了获取输入流的方法。这次demo,我们准备开发3个不同来源的资源实现:classpath、文件系统、url。这三个类分别实现Resource接口,提供自己的输入流获取方法。

首先是classpath

package com.akitsuki.springframework.core.io;

import cn.hutool.core.lang.Assert;
import com.akitsuki.springframework.util.ClassUtils;
import lombok.Getter;
import lombok.Setter;

import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;

/**
 * 获取Classpath下的input stream,实现Resource接口
 *
 * @author ziling.wang@hand-china.com
 * @date 2022/11/9 13:35
 */
@Getter
@Setter
public class ClasspathResource implements Resource {

    private String path;

    private ClassLoader classLoader;

    public ClasspathResource(String path) {
        this(path, null);
    }

    public ClasspathResource(String path, ClassLoader classLoader) {
        Assert.notNull(path);
        this.path = path;
        this.classLoader = null == classLoader ? ClassUtils.getDefaultClassLoader() : classLoader;
    }

    @Override
    public InputStream getInputStream() throws IOException {

        InputStream is = classLoader.getResourceAsStream(path.substring("classpath:".length()));
        if (null == is) {
            throw new FileNotFoundException(path + "文件未找到");
        }
        return is;
    }
}

这里的要点是ClassLoader类,而且需要注意的是,我们传输进来的路径一般是 classpath:spring.xml这样的类型。但是 classLoader.getResourceAsStream()方法无法识别 classpath:前缀,所以我们需要手动将这个前缀去掉。

然后是文件系统

package com.akitsuki.springframework.core.io;

import lombok.Getter;
import lombok.Setter;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;

/**
 * 从文件系统获取input stream,实现Resource接口
 *
 * @author ziling.wang@hand-china.com
 * @date 2022/11/9 13:49
 */
@Getter
@Setter
public class FileSystemResource implements Resource {

    private String path;

    private File file;

    public FileSystemResource(String path) {
        this.path = path;
        this.file = new File(path);
    }

    public FileSystemResource(File file) {
        this.file = file;
        this.path = file.getPath();
    }

    @Override
    public InputStream getInputStream() throws IOException {
        return new FileInputStream(file);
    }
}

这个就相对简单一些,从File中获取输入流

最后是url

package com.akitsuki.springframework.core.io;

import cn.hutool.core.lang.Assert;

import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLConnection;

/**
 * 从url中获取input stream,实现Resource接口
 *
 * @author ziling.wang@hand-china.com
 * @date 2022/11/9 13:53
 */
public class UrlResource implements Resource {

    private final URL url;

    public UrlResource(URL url) {
        Assert.notNull(url);
        this.url = url;
    }

    @Override
    public InputStream getInputStream() throws IOException {
        URLConnection urlConnection = url.openConnection();
        try {
            return urlConnection.getInputStream();
        } catch (IOException e) {
            if (urlConnection instanceof HttpURLConnection) {
                ((HttpURLConnection) urlConnection).disconnect();
            }
            throw e;
        }
    }
}

这块也比较简单,主要是开启连接,然后返回输入流即可。

到这里,三种资源的输入流获取类,我们都完成了。那么有了资源,我们还需要一个资源加载器。它的作用是根据传入的路径,自动返回相应的资源。

package com.akitsuki.springframework.core.io;

/**
 * 资源加载器
 *
 * @author ziling.wang@hand-china.com
 * @date 2022/11/9 13:59
 */
public interface ResourceLoader {

    String CLASSPATH_URL_PREFIX = "classpath:";

    /**
     * 根据传入的参数,获取对应的资源
     *
     * @param location location
     * @return resource
     */
    Resource getResource(String location);
}

接下来是这个接口的默认实现类

package com.akitsuki.springframework.core.io;

import cn.hutool.core.lang.Assert;

import java.net.MalformedURLException;
import java.net.URL;

/**
 * 资源加载器默认实现
 *
 * @author ziling.wang@hand-china.com
 * @date 2022/11/9 14:02
 */
public class DefaultResourceLoader implements ResourceLoader {
    @Override
    public Resource getResource(String location) {
        Assert.notNull(location);
        if (location.startsWith(CLASSPATH_URL_PREFIX)) {
            return new ClasspathResource(location);
        } else {
            try {
                URL url = new URL(location);
                return new UrlResource(url);
            } catch (MalformedURLException e) {
                return new FileSystemResource(location);
            }
        }
    }
}

可以看到,先是判断有没有 classpath:前缀,如果有,就返回 ClasspathResource。如果没有,则尝试使用 UrlResource。如果有错误,则返回 FileSystemResource

读取配置,自动注册

前面写了那么多,其实都是为了铺垫。我们可不能忘了初心。我们是为了完成自动化配置bean定义。那么既然要配置bean定义,自然也要有一个bean定义的读取接口。

package com.akitsuki.springframework.beans.factory.support;

import com.akitsuki.springframework.beans.exception.BeanException;
import com.akitsuki.springframework.core.io.Resource;
import com.akitsuki.springframework.core.io.ResourceLoader;

/**
 * bean定义读取接口
 *
 * @author ziling.wang@hand-china.com
 * @date 2022/11/9 14:09
 */
public interface BeanDefinitionReader {

    /**
     * 获取bean定义的registry
     *
     * @return registry
     */
    BeanDefinitionRegistry getRegistry();

    /**
     * 获取resource loader
     *
     * @return resource loader
     */
    ResourceLoader getResourceLoader();

    /**
     * 读取bean定义(通过resource)
     *
     * @param resource res
     * @throws BeanException e
     */
    void loadBeanDefinitions(Resource resource) throws BeanException;

    /**
     * 读取bean定义(通过多个resource)
     *
     * @param resources res
     * @throws BeanException e
     */
    void loadBeanDefinitions(Resource... resources) throws BeanException;

    /**
     * 读取bean定义(通过路径)
     *
     * @param location location
     * @throws BeanException e
     */
    void loadBeanDefinitions(String location) throws BeanException;
}

看起来很多,但大部分都是方法的重载。实际上就是三个功能:

  1. 获取bean定义的注册类,以便进行bean定义的注册
  2. 获取资源加载类,准备加载资源
  3. 通过资源,来读取bean定义

有了接口,我们下面需要一个抽象类来实现这个接口。经过前面的那些练习,想必已经习惯于这种方式了。抽象类实现公共的方法,再由具体的类实现独有的方法。

package com.akitsuki.springframework.beans.factory.support;

import com.akitsuki.springframework.core.io.DefaultResourceLoader;
import com.akitsuki.springframework.core.io.ResourceLoader;

/**
 * bean定义读取接口的抽象类,实现了两个工具方法
 *
 * @author ziling.wang@hand-china.com
 * @date 2022/11/9 14:13
 */
public abstract class AbstractBeanDefinitionReader implements BeanDefinitionReader {

    private final BeanDefinitionRegistry registry;

    private final ResourceLoader resourceLoader;

    protected AbstractBeanDefinitionReader(BeanDefinitionRegistry registry) {
        this(registry, new DefaultResourceLoader());
    }

    protected AbstractBeanDefinitionReader(BeanDefinitionRegistry registry, ResourceLoader resourceLoader) {
        this.registry = registry;
        this.resourceLoader = resourceLoader;
    }

    @Override
    public BeanDefinitionRegistry getRegistry() {
        return registry;
    }

    @Override
    public ResourceLoader getResourceLoader() {
        return resourceLoader;
    }
}

可以看到,这里的抽象类果然实现的都是一些很公共的方法。获取bean定义注册类、获取资源加载器。当然也很好理解,这两个内容,不与具体的资源有关。所以自然可以放在抽象类中实现。而 loadBeanDefinitions方法,则会根据资源的不同,而有不同的实现,所以就需要子类来进行完善了。那么接下来,我们就来写一个基于xml配置文件的子类,实现从xml中读取配置来加载bean定义。

package com.akitsuki.springframework.beans.factory.xml;

import cn.hutool.core.util.StrUtil;
import cn.hutool.core.util.XmlUtil;
import com.akitsuki.springframework.beans.exception.BeanException;
import com.akitsuki.springframework.beans.factory.config.BeanDefinition;
import com.akitsuki.springframework.beans.factory.config.BeanReference;
import com.akitsuki.springframework.beans.factory.config.PropertyValue;
import com.akitsuki.springframework.beans.factory.support.AbstractBeanDefinitionReader;
import com.akitsuki.springframework.beans.factory.support.BeanDefinitionRegistry;
import com.akitsuki.springframework.core.io.Resource;
import com.akitsuki.springframework.core.io.ResourceLoader;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

import java.io.IOException;
import java.io.InputStream;

/**
 * xml方式读取bean定义
 *
 * @author ziling.wang@hand-china.com
 * @date 2022/11/9 14:18
 */
public class XmlBeanDefinitionReader extends AbstractBeanDefinitionReader {


    public XmlBeanDefinitionReader(BeanDefinitionRegistry registry) {
        super(registry);
    }

    public XmlBeanDefinitionReader(BeanDefinitionRegistry registry, ResourceLoader resourceLoader) {
        super(registry, resourceLoader);
    }

    @Override
    public void loadBeanDefinitions(Resource resource) throws BeanException {
        try (InputStream inputStream = resource.getInputStream()) {
            doLoadBeanDefinitions(inputStream);
        } catch (IOException | ClassNotFoundException e) {
            throw new BeanException("读取bean定义时出错:", e);
        }
    }

    @Override
    public void loadBeanDefinitions(Resource... resources) throws BeanException {
        for (Resource r : resources) {
            loadBeanDefinitions(r);
        }
    }

    @Override
    public void loadBeanDefinitions(String location) throws BeanException {
        ResourceLoader resourceLoader = getResourceLoader();
        Resource resource = resourceLoader.getResource(location);
        loadBeanDefinitions(resource);
    }

    /**
     * 真正通过xml读取bean定义的方法实现
     *
     * @param inputStream xml配置文件输入流
     * @throws BeanException          e
     * @throws ClassNotFoundException e
     */
    private void doLoadBeanDefinitions(InputStream inputStream) throws BeanException, ClassNotFoundException {
        Document doc = XmlUtil.readXML(inputStream);
        Element root = doc.getDocumentElement();
        NodeList childNodes = root.getChildNodes();

        for (int i = 0; i < childNodes.getLength(); i++) {
            //如果不是bean,则跳过
            if (!isBean(childNodes.item(i))) {
                continue;
            }
            // 解析标签
            Element bean = (Element) childNodes.item(i);
            String id = bean.getAttribute("id");
            String name = bean.getAttribute("name");
            String className = bean.getAttribute("class");
            // 获取 Class,方便获取类中的名称
            Class<?> clazz = Class.forName(className);
            // 优先级 id > name
            String beanName = StrUtil.isNotEmpty(id) ? id : name;
            if (StrUtil.isEmpty(beanName)) {
                beanName = StrUtil.lowerFirst(clazz.getSimpleName());
            }
            // 定义Bean
            BeanDefinition beanDefinition = new BeanDefinition(clazz);
            // 读取属性并填充
            buildProperty(bean, beanDefinition);
            if (getRegistry().containsBeanDefinition(beanName)) {
                throw new BeanException("Duplicate beanName[" + beanName + "] is not allowed");
            }
            // 注册 BeanDefinition
            getRegistry().registerBeanDefinition(beanName, beanDefinition);
        }
    }

    /**
     * 填充beanDefinition的属性
     *
     * @param bean           配置文件中的bean信息
     * @param beanDefinition 等待填充属性的bean定义
     */
    private void buildProperty(Element bean, BeanDefinition beanDefinition) {
        // 读取属性并填充
        for (int j = 0; j < bean.getChildNodes().getLength(); j++) {
            //如果不是属性,则跳过
            if (!isProperty(bean.getChildNodes().item(j))) {
                continue;
            }
            // 解析标签:property
            Element property = (Element) bean.getChildNodes().item(j);
            String attrName = property.getAttribute("name");
            String attrValue = property.getAttribute("value");
            String attrRef = property.getAttribute("ref");
            // 获取属性值:引入对象、值对象
            Object value = StrUtil.isNotEmpty(attrRef) ? new BeanReference(attrRef) : attrValue;
            // 创建属性信息
            PropertyValue propertyValue = new PropertyValue(attrName, value);
            beanDefinition.getPropertyValues().addPropertyValue(propertyValue);
        }
    }

    /**
     * 判断一个节点是不是bean
     *
     * @param node 待判断节点
     * @return result
     */
    private boolean isBean(Node node) {
        if (!(node instanceof Element)) {
            return false;
        }
        return "bean".equals(node.getNodeName());
    }

    /**
     * 判断一个节点是不是bean的属性
     *
     * @param node 待判断节点
     * @return result
     */
    private boolean isProperty(Node node) {
        if (!(node instanceof Element)) {
            return false;
        }
        return "property".equals(node.getNodeName());
    }
}

好长好长。我们来逐一分析。

首先是 loadBeanDefinitions三个方法的重载实现。可以看到实际上都会归到 loadBeanDefinitions(Resource resource)这个方法中,再由这个方法来继续调用最终的处理方法 doLoadBeanDefinitions

接着我们看最后的两个工具方法,是用来判断一个节点是否为bean/属性的。判断内容也很好理解。

最后我们终于要看处理方法 doLoadBeanDefinitions了。上面的一部分都是操作xml文件的,作用就是从xml中把配置读取出来。可以看到通过配置的class,利用反射拿到了真正的class,再通过这个class来创建一个BeanDefinition的实例。接着,为其填充属性。属性填充完毕后,将其注册到容器中。这个过程,确实就是我们之前手动完成的步骤。而这里调用的 buildProperty方法,内容也与上面大体相似,只不过最后是创建PropertyValues,设置进BeanDefinition而已。

让我访问!坚持访问!来测试吧!

这次demo,第一次出现了配置文件,是一个长足的进步。既然要读取配置文件,我们得先有配置文件。这些配置文件,我们放在test目录下的resource文件夹下。

spring.xml

<?xml version="1.0" encoding="utf-8" ?>
<beans>
    <bean id="userDao" class="com.akitsuki.springframework.test.bean.UserDao"/>

    <bean id="userService" class="com.akitsuki.springframework.test.bean.UserService">
        <property name="dummyString" value="dummy"/>
        <property name="dummyInt" value="114514"/>
        <property name="userDao" ref="userDao"/>
    </bean>
</beans>

有了这个配置文件,整个项目Spring的感觉立刻就起来了。

接下来,测试类出场

package com.akitsuki.springframework.test;

import com.akitsuki.springframework.beans.exception.BeanException;
import com.akitsuki.springframework.beans.factory.support.BeanDefinitionReader;
import com.akitsuki.springframework.beans.factory.support.CglibSubclassingInstantiationStrategy;
import com.akitsuki.springframework.beans.factory.support.DefaultListableBeanFactory;
import com.akitsuki.springframework.beans.factory.xml.XmlBeanDefinitionReader;
import com.akitsuki.springframework.core.io.DefaultResourceLoader;
import com.akitsuki.springframework.core.io.Resource;
import com.akitsuki.springframework.core.io.ResourceLoader;
import com.akitsuki.springframework.test.bean.UserService;
import org.junit.Before;
import org.junit.Test;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;

/**
 * @author ziling.wang@hand-china.com
 * @date 2022/11/9 16:03
 */
public class ApiTest {

    private ResourceLoader resourceLoader;

    @Before
    public void init() {
        resourceLoader = new DefaultResourceLoader();
    }

    @Test
    public void testXml() throws InstantiationException, IllegalAccessException, BeanException {
        //初始化BeanFactory
        DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory(CglibSubclassingInstantiationStrategy.class);
        //读取配置文件,注册bean
        BeanDefinitionReader reader = new XmlBeanDefinitionReader(beanFactory);
        reader.loadBeanDefinitions("classpath:spring.xml");
        //获取Bean,测试
        UserService userService = beanFactory.getBean("userService", UserService.class);
        userService.queryUserInfo(1L);
        userService.queryUserInfo(3L);
        userService.queryUserInfo(5L);
        userService.queryUserInfo(114514L);
    }
}

测试结果

dummyString:dummy
dummyInt:114514
用户名:akitsuki
dummyString:dummy
dummyInt:114514
用户名:kugimiya
dummyString:dummy
dummyInt:114514
用户名:momonogi
dummyString:dummy
dummyInt:114514
用户未找到>_<

Process finished with exit code 0

可以看到,我们终于摆脱了Bean定义的手动创建,转变成了自动化操作。那么这一章,我们也就到此,告一段落了。

相关源码可以参考我的gitee:https://gitee.com/akitsuki-kouzou/mini-spring,这里对应的代码是mini-spring-05

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值