SpringFramework源码解析——Bean在Spring中是如何被加载的?
在阅读本文前请先确保你了解什么是
BeanDefinition
。
1. 背景
以XML配置文件配置的Bean为例,搞明白Bean在Spring中是如何被加载的可以使我们更了解Spring的XML配置文件中有哪些配置信息,以及各个配置信息是如何生效的。Spring的XML配置文件提供了很多标签和属性,我们不仅可以使用bean
标签配置Bean,使用alias
标签配置别名,还可以使用import
标签导入资源,使用component-scan
标签开启组件扫描等。
根据二八定律,我们往往只会用到20%的配置,现在让我们去了解剩余的80%。
2. 如果让你设计,你会如何设计?
首先,我们需要设计一个类来存储已经被加载的BeanDefinition
,我们起名为BeanDefinitionRegistry
。
- 其持有一个Bean名称与
BeanDefinition
的映射。 - 并提供一个注册
BeanDefinition
的方法。
其次,我们需要设计一个类来从XML配置文件中加载BeanDefinition
,我们起名为BeanDefinitionReader
。
- 其持有一个
BeanDefinitionRegistry
的实例,用于注册BeanDefinition
。 - 并提供一个加载
BeanDefinition
的方法。
3. Spring是如何设计的?
同我们上面的设计类似,Spring在此基础上进行接口和类的抽象以及各个功能的拆分,所以派生了大量的类,看着比我们上面的设计复杂不少,实际上殊途同归。
Spring为什么设计的这么复杂,按照我们的设计两个类足矣。如果按照我们的设计会有以下几个问题:
- 一方面,所有的处理逻辑全部集中在
BeanDefinitionReader
中,很难扩展和维护,违背了单一职责原则(SRP)。- 另一方面,我们的设计并没有进行抽象,如果后续添加支持解析
Properties
配置文件,我们无从下手,抽离接口是为了服务于依赖倒置原则(DIP)。- 更宽泛地说,遵循面向对象设计的原则,才能写出更方便维护与扩展的代码。
3.1. BeanDefinition是如何存储的?
首先我们来看BeanDefinition
的存储,为此Spring抽象出了一个BeanDefinitionRegistry
接口,并提供了注册BeanDefinition
的方法,由DefaultListableBeanFactory
进行实现,类图如下:
3.2. BeanDefinition是如何被加载的?
有了存储BeanDefinition
的类,还需要能够从XML或其他类型的配置文件中加载BeanDefinition
,为此Spring抽象出了一个BeanDefinitionReader
接口,用于从资源里加载BeanDefinition
,由XMLBeanDefinitionReader
等类进行实现,类图如下:
Spring提供了加载
BeanDefinition
的通用抽象类实现AbstractBeanDefinitionReader
,并提供了抽象方法loadBeanDefinitions(Resource)
。如此以来,无论是从XML配置,Groovy配置,还是从Properties配置中加载BeanDefinition
都只需要实现该抽象方法即可,这是一种模板方法模式。
下面我们来看下XMLBeanDefinitionReader
的loadBeanDefinitions(String)
方法是如何工作的?
3.2.1. 如何通过路径获取XML配置的资源?
获取资源无非就是通过一个路径找到一个资源。Spring将资源声明为Resource
类,并获取资源的工作则交给ResourceLoader
类,其会根据传入路径的类型决定返回具体的Resource
类的子类(详见下文实战),类图如下:
对应源码所在位置:
DefaultResourceLoader#getResource
。PathMatchingResourcePatternResolver#getResources
。
3.2.2. 拿到XML配置的资源后,如何将其加载为XML文档?
在Java中操作XML需要先将其转为XML文档,然后才能进行读取等操作。在Spring中,拿到Resource
资源后,DocumentLoader
会将Resource
资源的输入流转为Document
。
对应源码所在位置:
DefaultDocumentLoader#loadDocument
。
3.2.3. 拿到XML文档后,如何解析文档中的各种标签?
拿到Document
文档后,会通过BeanDefinitionDocumentReader
解析文档中的标签。
- 如果是默认命名空间的标签:
- 如
import
,alias
,beans
标签,会通过DefaultBeanDefinitionDocumentReader#parseDefaultElement
方法处理 - 如
bean
标签,会委派给BeanDefinitionParserDelegate
的parseBeanDefinitionElement
方法处理。
- 如
- 如果是自定义命名空间的标签,如
component-scan
,aspectj-autoproxy
等标签,会委派给BeanDefinitionParserDelegate
的parseCustomElement
方法处理。
对应源码所在位置:
DefaultBeanDefinitionDocumentReader#parseDefaultElement
。BeanDefinitionParserDelegate#parseBeanDefinitionElement(Element)
。BeanDefinitionParserDelegate#parseCustomElement(Element)
。
3.2.4. 拿到XML文档中的bean标签后,如何将其解析为BeanDefinition?
拿到XML文档中的bean标签后,会委派给BeanDefinitionParserDelegate
的parseBeanDefinitionElement
方法处理,并返回一个持有BeanDefinition
,bean的名称和bean的别名等信息的BeanDefinitionHolder
。
对应源码所在位置:
BeanDefinitionParserDelegate#parseBeanDefinitionElement(Element, BeanDefinition)
。
3.2.5. 拿到BeanDefinition后,如何将其注册到BeanDefinitionRegistry中?
拿到BeanDefinition
后,通过BeanDefinitionReaderUtils#registerBeanDefinition
方法,将BeanDefinition
注册到BeanDefinitionRegistry
中去。
对应源码所在位置:
BeanDefinitionReaderUtils#registerBeanDefinition
。
至此,通过XML配置的bean加载到Spring容器中的流程全部完成,BeanDefinition
准备就绪等待容器后续处理。
4. 实战
4.1. 使用不同类型的路径来获取资源,会返回什么类型的资源?
我们使用DefaultResourceLoader
来获取ResourceLoaderTest.class
这个资源,不过我们通过传入不同类型的路径去获取,并观察返回资源的类型,如下代码:
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.io.DefaultResourceLoader;
import org.springframework.core.io.Resource;
import org.springframework.core.io.ResourceLoader;
@Slf4j
public class ResourceLoaderTest {
public static void main(String[] args) {
testResourceLoader();
}
public static void testResourceLoader() {
getResource(getPathOfClass(ResourceLoaderTest.class));
getResource("classpath:" + getPathOfClass(ResourceLoaderTest.class));
getResource(ResourceLoaderTest.class.getResource("/" + getPathOfClass(ResourceLoaderTest.class)).getPath());
}
public static void getResource(String location) {
final ResourceLoader resourceLoader = new DefaultResourceLoader();
final Resource resource = resourceLoader.getResource(location);
printResourceInfo(location, resource);
}
private static void printResourceInfo(String location, Resource resource) {
log.info("location: {}, resource type: {}, resource: {}", location, resource.getClass(), resource);
}
private static String getPathOfClass(Class<?> clazz) {
return clazz.getPackage().getName().replace(".", "/") + "/" + clazz.getSimpleName() + ".class";
}
}
运行结果如下图,我们可以看出即便是同一个资源文件,通过传入不同类型的路径,也会返回不同类型的资源。不同类型的资源有不同的适用场景。
location: com/remeio/upsnippet/spring/beandefinition/ResourceLoaderTest.class, resource type: class org.springframework.core.io.DefaultResourceLoader$ClassPathContextResource, resource: class path resource [com/remeio/upsnippet/spring/beandefinition/ResourceLoaderTest.class]
location: classpath:com/remeio/upsnippet/spring/beandefinition/ResourceLoaderTest.class, resource type: class org.springframework.core.io.ClassPathResource, resource: class path resource [com/remeio/upsnippet/spring/beandefinition/ResourceLoaderTest.class]
location: /E:/project/remeio/upsnippet/upsnippet-spring/target/classes/com/remeio/upsnippet/spring/beandefinition/ResourceLoaderTest.class, resource type: class org.springframework.core.io.DefaultResourceLoader$ClassPathContextResource, resource: class path resource [E:/project/remeio/upsnippet/upsnippet-spring/target/classes/com/remeio/upsnippet/spring/beandefinition/ResourceLoaderTest.class]
对应源码所在位置:
DefaultResourceLoader#getResource
。
4.2. 使用debug的方式跟踪XML配置文件是如何被加载的?
以下面的XML配置文件为示例,通过debug的方式剖析Spring根据这个XML配置文件做了哪些处理,以便深入了解Spring中XML配置文件中标签和属性的内部处理流程,也可以通过源码来反推Spring的XML配置文件中有哪些标签和属性。
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd">
<!-- 配置一个Bean -->
<bean id="testBean" class="com.remeio.upsnippet.spring.beandefinition.TestBean">
<property name="name" value="foo"/>
<property name="age" value="1"/>
</bean>
<!-- 为testBean配置一个别名 -->
<alias name="testBean" alias="aliaOfTestBean"/>
<!-- 引入另一个XML配置文件 -->
<import resource="BeanDefinitionLoadTest-inner.xml"/>
<!-- 启用注解配置 -->
<context:annotation-config />
</beans>
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<!-- 配置一个Bean -->
<bean id="innerTestBean" class="com.remeio.upsnippet.spring.beandefinition.TestBean">
<property name="name" value="bar"/>
<property name="age" value="1"/>
</bean>
</beans>
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
import org.springframework.beans.factory.xml.XmlBeanDefinitionReader;
@Slf4j
public class BeanDefinitionLoadTest {
public static void main(String[] args) {
testXmlBeanDefinitionReader();
}
public static void testXmlBeanDefinitionReader() {
final DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory();
final XmlBeanDefinitionReader xmlBeanDefinitionReader = new XmlBeanDefinitionReader(beanFactory);
xmlBeanDefinitionReader.loadBeanDefinitions(getResourcePathOfClass(BeanDefinitionLoadTest.class));
Object testBean = beanFactory.getBean("testBean");
Object testBeanWithAlia = beanFactory.getBean("aliaOfTestBean");
Object innerTestBean = beanFactory.getBean("innerTestBean");
log.info("testBean: {}, testBeanWithAlia: {}, innerTestBean: {}", testBean, testBeanWithAlia, innerTestBean);
}
private static String getResourcePathOfClass(Class<?> clazz) {
return clazz.getPackage().getName().replace(".", "/") + "/" + clazz.getSimpleName() + ".xml";
}
}
从XML配置文件的处理顺序,断点依次打到以下方法处:
- 定位资源:
PathMatchingResourcePatternResolver#getResources
。 - 处理
testBean
:BeanDefinitionParserDelegate#parseBeanDefinitionElement(Element, BeanDefinition)
。 - 处理
alias
标签:DefaultBeanDefinitionDocumentReader#processAliasRegistration
。 - 处理
import
标签:DefaultBeanDefinitionDocumentReader#importBeanDefinitionResource
。 - 处理
innerTestBean
:同testBean
。 - 处理
context:annotation-config
:BeanDefinitionParserDelegate#parseCustomElement(Element)
。
运行结果如下:
testBean: TestBean(name=foo, age=1), testBeanWithAlia: TestBean(name=foo, age=1), innerTestBean: TestBean(name=bar, age=1)