加载,解析资源并注册Bean对象

日志

上一节我们实现了往 Bean 对象注入属性,但所有的操作都需要用户手动填写,这显然是不合适的,因此这一节我们来优化

本章节主要有以下改动:

  • 资源加载接口的定义和实现
  • 解析 XML 处理 Bean 注册

问题

通过单元测试进行手动操作 Bean 对象的定义、注册和属性填充,以及最终获取对象调用方法。但这里会有一个问题,就是如果实际使用这个 Spring 框架,是不太可能让用户通过手动方式创建的,而是最好能通过配置文件的方式简化创建过程。需要完成如下操作:

image-20230207130632924

  • 如图中我们需要把步骤:2、3、4整合到Spring框架中,通过 Spring 配置文件的方式将 Bean 对象实例化。
  • 接下来我们就需要在现有的 Spring 框架中,添加能解决 Spring 配置的读取、解析、注册Bean的操作。

XmlReader 的作用,就是为 Spring 自动配置 Bean 对象的注册、属性设置、Bean 对象实例化。整个过程用户无感,最终用户所需要做的操作就是调用所需的 Bean 对象就行!

设计

依照本章节的需求背景,我们需要在现有的 Spring 框架雏形中添加一个资源解析器,也就是能读取classpath、本地文件和云文件的配置内容。这些配置内容就是像使用 Spring 时配置的 Spring.xml 一样,里面会包括 Bean 对象的描述和属性信息。在读取配置文件信息后,接下来就是对配置文件中的 Bean 描述信息解析后进行注册操作,把 Bean 对象注册到 Spring 容器中。整体设计结构如下图:

image-20230207131936379

  • 资源加载器属于相对独立的一部分,它位于 Spring 框架核心包下的 IO 实现内容,主要用于处理 Class、本地、云环境的文件信息
  • 当资源可以加载后,接下来就是解析和注册 Bean 到 Spring 的所有操作,这一部分的实现需要和 DefaultListableBeanFactory 核心类结合起来,因为所有的解析后的注册动作,都会把 Bean 定义信息放到这个类里
  • 实现需要实现接口的层次关系,包括需要定义出 Bean 定义的读取接口 BeanDeinitionReader 以及做好对应的实现类,在实现类中完成对 Bean 对象的解析和注册

Spring Bean 容器资源加载和使用类关系

手写Spring-资源加载和读取

  • 本章节为了能把 Bean 的定义、注册和初始化交给 Spring.xml 配置化处理,那么就需要实现两大块内容,分别是:资源加载器、xml资源处理类,实现过程主要以对接口 ResourceResourceLoader 的实现,而另外 BeanDefinitionReader 接口则是对资源的具体使用,将配置信息注册到 Spring 容器中去。
  • 在 Resource 的资源加载器的实现中包括了 ClassPath、系统文件、云配置文件,这三部分与 Spring 源码中的设计和实现保持一致,最终在 DefaultResourceLoader 中做具体的调用。
  • 接口:BeanDefinitionReader、抽象类:AbstractBeanDefinitionReader、实现类:XmlBeanDefinitionReader,这三部分内容主要是合理清晰的处理了资源读取后的注册 Bean 容器操作。接口管定义,抽象类处理非接口功能外的注册Bean组件填充,最终实现类即可只关心具体的业务实现

另外本章节还参考 Spring 源码,做了相应接口的集成和实现的关系,虽然这些接口目前还并没有太大的作用,但随着框架的逐步完善,它们也会发挥作用。

image-20230207220856393

  • BeanFactory,已经存在的 Bean 工厂接口,用于获取 Bean 对象,这次新增加了按照类型获取 Bean 的方法<T> T getBean(String name, Class<T> requiredType)
  • ListableBeanFactory,是一个扩展 Bean 工厂接口的接口,新增加了 getBeansOfTypegetBeanDefinitionNames() 方法,在 Spring 源码中还有其他扩展方法。
  • HierarchicalBeanFactory,在 Spring 源码中它提供了可以获取父类 BeanFactory 方法,属于是一种扩展工厂的层次子接口。Sub-interface implemented by bean factories that can be part of a hierarchy.
  • AutowireCapableBeanFactory,是一个自动化处理Bean工厂配置的接口,目前案例工程中还没有做相应的实现,后续逐步完善。
  • ConfigurableBeanFactory,可获取 BeanPostProcessor 后置处理器、BeanClassLoader 类加载器等的一个配置化接口
  • ConfigurableListableBeanFactory,提供分析和修改Bean以及预先实例化的操作接口,不过目前只有一个 getBeanDefinition 方法。

工程

资源加载接口的定义与实现

public interface Resource {

InputStream getInputStream() throws IOException;

}
  • 在 Spring 框架下创建 core.io 核心包,在这个包中主要用于处理资源加载流。
  • 定义 Resource 接口,提供获取 InputStream 流的方法,接下来再分别实现三种不同的流文件操作:classPath、FileSystem、URL

ClassPath:

FileSystem

URL

包装资源加载器

按照资源加载的不同方式,资源加载器可以把这些方式集中到统一的类服务下进行处理,外部用户只需要传递资源地址即可,简化使用。

定义统一接口

public interface ResourceLoader {

// 使用路径前缀强制指定从 Spring 容器中获取 Resource 实现类
// 当前的前缀是指定使用 ClassPathResource
String CLASSPATH_URL_PREFIX = "classpath:";

/**
* 外部调度资源加载器统一接口
* @param location 外部资源地址
* @return Resource 接口实例
*/
Resource getResource(String location);

}
  • 定义获取资源接口,里面传递 location 地址即可。

默认资源加载器实现类:(按照 Spring 内定顺序指定不同的 Resource 加载资源)

public class DefaultResourceLoader implements ResourceLoader {

@Override
public Resource getResource(String location) {
Assert.notNull(location, "Location must not be null -- 资源路径不能为空");
if (location.startsWith(CLASSPATH_URL_PREFIX)) {
// ClassPath 方式加载资源: 获取 location: 后的内容,并加载资源
return new ClassPathResource(location.substring(CLASSPATH_URL_PREFIX.length()));
} else {
try {
// URL 方式加载资源
URL url = new URL(location);
return new UrlResource(url);
} catch (MalformedURLException e) {
// File 加载资源
return new FileSystemResource(location);
}
}
}

}
  • 默认资源加载器,用于用户、Spring IoC 传参只有注册器,没有加载器时的默认添加,具体逻辑在 AbstractBeanDefinitionReader

这里需要说明一下 Spring 的 Resource 策略

背景知识: ApplicationContext的创建方式

学过原生Spring的同学都知道,当我们想开发一个Spring项目,也就是创建ApplicationContext容器、让Spring启动生效时,我们:

  1. 首先会写一个xml文件,其中定义若干个元素;
  2. 然后在java代码中用如下的几种写法,去加载其中的bean元素,生成对应的实例并注入ApplicationContext容器内:
    1. new ClassPathXmlApplicationContext(“xxx.xml”)
    2. new FileSystemXmlApplicationContext(“xxx.xml”)
    3. WebApplicationContextUtils.getWebApplicationContext(servletContext)
  3. 在SpringMVC、SpringBoot中,程序员没有直接这样创建applicationContext对象,是因为框架已经使用这些方法封装好了创建方式,在启动时会自动加载。但内部也都存在对应的ApplicationContext容器,会有类似的调用或创建语法。

获取指定的Resource实现类

Spring 通过使用路径前缀的方式来强制指定从 Spring 容器获取 Resource 实现类,在接口 ResourceLoader 有所体现

例如前缀**"classpath:“是指定使用ClassPathResource;前缀"file:”**则指定使用UrlResource访问本地系统资源等

Bean 定义读取接口

定义 BeanDefinitionReader ,其作用是读取 Spring 配置文件中的内容,将其转换为 IoC 容器内部的数据结构:BeanDefinition。

BeanDefinitionReader

我们之前实现的 BeanDefinitionRegistry 接口一次只能注册一个 BeanDefinition,而且只能通过自己构造 BeanDefinition 数据结构才能注册

本次我们升级,使用 BeanDefinitionReader 集成解决问题,它可以使用一个 BeanDefinitionRegistry 构造,然后通过 loadBeanDefinitions() 等方法读取 Resources,把 Resources 通过流的方式转化成多个 BeanDefinition 并注册到 BeanDefinitionRegistry 中。其中转化成流的好处不用多说,二进制节省内存以及加快传输效率

具体代码如下:

  • 这是一个 Simple interface for bean definition readers. 其实里面无非定义了几个方法,包括:getRegistry()、getResourceLoader(),以及三个加载Bean定义的方法:分别为单个资源加载、多个资源加载、指定资源路径加载。流程为 先指定资源路径加载,然后调用具体的资源加载
  • 这里需要注意 getRegistry()、getResourceLoader(),都是用于提供给后面三个方法的工具,加载和注册,这两个方法的实现会包装到抽象类中,以免污染具体的接口实现方法

Bean 定义抽象类实现

该类是实现了 BeanDefinitionReader 接口的抽象类,提供常见属性:注册 bean 信息的工厂、资源加载器。具体定义如下

public abstract class AbstractBeanDefinitionReader implements BeanDefinitionReader {

/**
* 注册 Bean 定义的 Bean 工厂
*/
private final BeanDefinitionRegistry registry;

/**
* 资源加载器
*/
private final ResourceLoader resourceLoader;

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

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

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

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

}
  • 抽象类把 BeanDefinitionReader 接口的前两个方法全部实现完了,并提供了构造函数,让外部的调用使用方,把Bean定义注入类,传递进来。
  • 这样在接口 BeanDefinitionReader 的具体实现类中,就可以把解析后的 XML 文件中的 Bean 信息,注册到 Spring 容器去了。以前我们是通过单元测试使用,调用 BeanDefinitionRegistry 完成 Bean的注册,现在可以放到 XMl 中操作了

解析 XML 处理 Bean 注册

宏观层面上,配置 xml 配置文件后执行 BeanDefinitionReader 会先调用 XmlBeanDefinitionReader。底层是通过 loadBeanDefinitions() 系列方法将配置加载成流对象,其可以节省内存以及提高对象传输效率。之后通过 doLoadBeanDefinitions() 读取 XML 配置信息,拆分成 <bean>、<property>、<raf>、<id>、<value> 等标签读取其内容。

具体代码:

XmlBeanDefinitionReader 类最核心的内容就是对 XML 文件的解析,把我们本来在代码中的操作放到了通过解析 XML 自动注册的方式。

  • loadBeanDefinitions 方法,处理资源加载,这里新增加了一个内部方法:doLoadBeanDefinitions,它主要负责解析 xml
  • 在 doLoadBeanDefinitions 方法中,主要是对xml的读取 XmlUtil.readXML(inputStream) 和元素 Element 解析。在解析的过程中通过循环操作,以此获取 Bean 配置以及配置中的 id、name、class、value、ref 信息。
  • 最终把读取出来的配置信息,创建成 BeanDefinition 以及 PropertyValue,最终把完整的 Bean 定义内容注册到 Bean 容器:getRegistry().registerBeanDefinition(beanName, beanDefinition)

一个 XML 文件的大致信息如下:

<?xml version="1.0" encoding="UTF-8" ?>
<beans>

<bean id="userDao" class="com.bantanger.springframework.test.bean.UserDao"/>

<bean id="userService" class="com.bantanger.springframework.test.bean.UserService">
<property name="uId" value="10001"/>
<property name="userDao" ref="userDao"/>
</bean>

</beans>

通过这些操作将所有标签的值读取下来:getAttribute() 获取方法

// 解析标签:bean 并获取其 id、name、className
Element bean = (Element) childNodes.item(i);
String id = bean.getAttribute("id"); // userDao、userService
String name = bean.getAttribute("name"); // null、null
String className = bean.getAttribute("class"); // com.bantanger.springframework.test.bean.UserDao、com.bantanger.springframework.test.bean.UserService

// 解析标签: property 并获取其 name、value、ref
Element property = (Element) bean.getChildNodes().item(j);
String attrName = property.getAttribute("name"); // uId、userDao
String attrValue = property.getAttribute("value"); // 10001
String attrRef = property.getAttribute("ref"); // userDao

读取下来的 className 通过反射机制:forName() 获取具体类信息 clazz,这是 注册 bean 信息的关键,BeanDefinition(clazz)

XML中可以通过id、name两种方式指定 beanName 但 Spring 规定优先级:id > name。所以默认以 id 作为 beanName,只有 id 不存在、才使用 name 指定

<property>标签的值读取之后,作为属性信息注入到 bean 定义中

// 创造属性信息并注入到 bean 定义
PropertyValue propertyValue = new PropertyValue(attrName, value);
beanDefinition.getPropertyValues().addPropertyValue(propertyValue);

最后注册 BeanDefinition

getRegister().registerBeanDefinition(beanName, beanDefinition);

可以看到,我们将配置信息 在单元测试类手写 使用 XML 文件配置通过 Spring 自动读取。更能体现 Spring 中 DI 依赖注入思想。并且使用 getRegister() 而不是 register,防止污染具体的实现接口。整个设计十分优雅

测试类

@Test
public void test_xml() {
// 1.初始化 BeanFactory
DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory();

// 2. 读取配置文件&注册Bean
XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(beanFactory);
reader.loadBeanDefinitions("classpath:spring.xml");

// 3. 获取Bean对象调用方法
UserService userService = beanFactory.getBean("userService", UserService.class);
String result = userService.queryUserInfo();
System.out.println("测试结果:" + result);
}

总结

回顾整篇文章,可以略探 Spring 框架的精髓,以配置文件为入口解析和注册 Bean 信息,最终再通过 Bean 工厂获取 Bean 以及做出相应调用操作