手写Spring-第五章-解放双手!自动化配置!
admin
2024-02-13 04:27:21
0

前言

上次我们完成了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;}@Overridepublic 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();}@Overridepublic 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;}@Overridepublic 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 {@Overridepublic 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;}@Overridepublic BeanDefinitionRegistry getRegistry() {return registry;}@Overridepublic 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);}@Overridepublic void loadBeanDefinitions(Resource resource) throws BeanException {try (InputStream inputStream = resource.getInputStream()) {doLoadBeanDefinitions(inputStream);} catch (IOException | ClassNotFoundException e) {throw new BeanException("读取bean定义时出错:", e);}}@Overridepublic void loadBeanDefinitions(Resource... resources) throws BeanException {for (Resource r : resources) {loadBeanDefinitions(r);}}@Overridepublic 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 > nameString beanName = StrUtil.isNotEmpty(id) ? id : name;if (StrUtil.isEmpty(beanName)) {beanName = StrUtil.lowerFirst(clazz.getSimpleName());}// 定义BeanBeanDefinition beanDefinition = new BeanDefinition(clazz);// 读取属性并填充buildProperty(bean, beanDefinition);if (getRegistry().containsBeanDefinition(beanName)) {throw new BeanException("Duplicate beanName[" + beanName + "] is not allowed");}// 注册 BeanDefinitiongetRegistry().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;}// 解析标签:propertyElement 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




有了这个配置文件,整个项目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;@Beforepublic void init() {resourceLoader = new DefaultResourceLoader();}@Testpublic void testXml() throws InstantiationException, IllegalAccessException, BeanException {//初始化BeanFactoryDefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory(CglibSubclassingInstantiationStrategy.class);//读取配置文件,注册beanBeanDefinitionReader 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
用户未找到>_

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

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

相关内容

热门资讯

为什么有十万个为什么的书呢? 为什么有十万个为什么的书呢?是为了对孩子们进行科普教育,交给他们一些日常生活中可能会碰到的现象后面的...
大班故事《上课》谁知道啊? 大班故事《上课》谁知道啊?大班故事《上课》的内容如下:今天是开学第一天,我高高兴兴地去上学,小鸟唧唧...
有什么搞笑的笑话没,给女朋友讲... 有什么搞笑的笑话没,给女朋友讲的,让她开心就好一冬日晨,特冷。姐妹三个人去提款机领钱,正好遇见运钞车...
各位网友们,跪求凉夕挨打的故事... 各位网友们,跪求凉夕挨打的故事,编一个?那天傍晚,凉夕被一个太监叫入一间空房。时光飞逝,三个月的时间...
幂想练习多长时间能看到效果 幂想练习多长时间能看到效果幂想练习多长时间能看到效果半个小时以上吧。一般在清晨进行冥想练习,效果会比...
浅怎么读 浅怎么读浅(拼音:qiǎn、jiān)是汉语通用一级汉字(常用字)。此字始见于战国金文,形声字,古字...
第一个进入太空地球的生物为什么... 第一个进入太空地球的生物为什么是一条狗?因为当时选取了它用作实验对象,就把它带去太空了。还有可能当时...
如何获得能量? 如何获得能量?人的能量是客观存在的,能量越高,内在力量就越强大,也越容易吸引高频的人事物来到身边。提...
文过饰非的意思以及造句有哪些 文过饰非的意思以及造句有哪些文过饰非 [ wén guò shì fēi ] 基本释义 详细释义 [...
找一些主角很冷血而又孤独的小说 找一些主角很冷血而又孤独的小说我看了很多冷血的小说可是很多到了后期,副角都变多了这我很讨厌,MM有没...
哪些破案片好看 哪些破案片好看《给工藤新一的一封信》 最新(真人版)的连载的。《侦探Q学院》有电影版也有电视剧版《法...
仙剑中的纯音乐、所有的、仙剑一... 仙剑中的纯音乐、所有的、仙剑一和三拜托了~~仙剑1:1. 大地之母 2. 妖域 3. 桃花岛 ...
为什么《活着》作者余华生活中是... 为什么《活着》作者余华生活中是一个很有趣的人,却能写出如此悲惨的作品?这是由于他曾经体验过这样的生活...
求魔兽1.20所有单位语音和翻... 求魔兽1.20所有单位语音和翻译war3的语音只有旧版本的,求新的语音(就是守望者,破法者等新加入的...
晚上总是睡不着该怎么办? 晚上总是睡不着该怎么办?对于晚上老是睡不着有以下几个方法:1.睡前泡泡脚、喝一杯温牛奶、看一些书籍、...
斩!赤红之瞳漫画3个多月没跟新... 斩!赤红之瞳漫画3个多月没跟新了,是不是不更了?哪有三个月,两个月吧。TV版的早就结局了,估计漫画也...
在说明书中,符号“/”“\”“... 在说明书中,符号“/”“\”“~”和乘号英语要怎么个读法?单词是什么?谢谢!Packing: PVC...
嫁女九字对联 嫁女九字对联嫁女九字对联九个字 嫁女对联联谊攀亲,求门当户对  择郎嫁女,要志同道合  花烛光中连开...
牛顿的资料主要点,字数少点 牛顿的资料主要点,字数少点只要和初二物理有关的牛顿第一定律牛顿第二定律
红尘如狱,众生皆苦是什么意思 ... 红尘如狱,众生皆苦是什么意思 红尘,指的是人间红尘,指的是人间。 人间就像监狱, 众生都在苦水里挣...