前言
上次我们完成了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;
}
看起来很多,但大部分都是方法的重载。实际上就是三个功能:
- 获取bean定义的注册类,以便进行bean定义的注册
- 获取资源加载类,准备加载资源
- 通过资源,来读取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