Spring源码解析(1)
SpringBean工厂的体系结构
从BeanFactory说起
其中,BeanFactory是整个bean工厂的顶级接口,里面定义了容器最基本的约束和规范,提供了管理Bean的基本功能。例如这里有根据名称获取Bean:Object getBean(String name),根据类型获取Bean:<T> T getBean(Class<T> requiredType),判断是否是单例Bean:boolean isSingleton(String name),判断是否是多例Bean:boolean isPrototype(String name),根据名称获取Bean的类型:Class<?> getType(String name)等。由于开始先关心整个Bean工厂的体系结构,对于某个工厂的细节先不过的的考虑。
HierarchicalBeanFactory提供了子父容器的管理。
什么是子父容器?例如:在SpringMVC中,@Controller是子容器,@Service和@Repository是父容器,子容器的Bean注入的话只能够注入父容器的Bean,反之父容器不能够注入子容器的Bean,这就解释了为什么Controller层要进行Service层进行注入,而不能Service层注入被Controller注入。
父子容器关系:父容器不知道子容器的存在,而子容器可以访问父容器中的所有bean。这意味着:
- 子容器中的bean(如@Controller)可以直接依赖并注入父容器中的bean(如@Service, @Repository)。
- 父容器中的bean不能直接引用或注入子容器中的bean。
AutowireCapableBeanFactory:可以让bean具备依赖注入的能力,其中的void autowireBean(Object existing) throws BeansException起着重要作用。这个方法接收一个已经存在的对象实例作为参数,并根据Spring容器中现有的bean定义,自动为该对象注入其依赖项。这意味着,即使这个对象不是通过Spring容器直接管理的(例如,它可能是手动使用new关键字创建的),也可以通过调用这个方法来让Spring为其注入所需的依赖。
ListableBeanFactory:可以根据名称判断bean的类型,以及判断是否包含一些bean的定义信息,获取bean的一些数量等。
ConfigurableBeanFactory:其中最主要的是设置bean的作用域
void registerScope(String scopeName, Scope scope);
和后置处理bean,意思是允许在bean初始化前后对其进行操作。这对于AOP,事务管理常见非常有用,因为可以允许不修改代码的情况下增强或改变bean的行为
void addBeanPostProcessor(BeanPostProcessor beanPostProcessor);
DefaultListableBeanFactory:一个集成不同Bean工厂的一个复合体
XmlBeanFactory:是基于XML的配置信息,完成bean对象的创建,如何创建的呢?
创建 Bean 的过程
-
加载 XML 配置文件:首先需要提供一个包含 bean 定义的 XML 文件路径。它描述了应用程序中各个 bean 的配置信息,包括它们的依赖关系、作用域等。
-
使用 XmlBeanDefinitionReader 读取配置:
XmlBeanFactory
内部会使用XmlBeanDefinitionReader
来解析提供的 XML 文件。XmlBeanDefinitionReader
负责从 XML 文件中读取 bean 定义,并将这些定义注册到BeanFactory
中。
-
注册 Bean 定义:
XmlBeanDefinitionReader
解析 XML 文件后,会将每个<bean>
元素转换成对应的BeanDefinition
对象,并将其注册到BeanFactory
中。这意味着所有的 bean 都被准备好,但尚未实例化。 -
根据需要实例化 Bean:当调用
getBean()
方法请求某个特定的 bean 时,XmlBeanFactory
会检查该 bean 是否已经被实例化。如果没有,则根据其BeanDefinition
进行实例化,同时处理依赖注入、设置属性以及执行任何必要的初始化方法。
XmlBeanFactory加载配置文件的底层原理
public class TestUser {
public static void main(String[] args) {
BeanFactory beanFactory = new XmlBeanFactory(new ClassPathResource("applicationContext.xml"));
User user = (User) beanFactory.getBean("user");
System.out.println(user);
}
}
首先,先抛出一个疑问,Spring是如何读取XML的配置文件呢?从ClassPathResource可知,ClassPathResource实现了Resource的接口,并且使用类加载器加载资源。
Resource接口是干什么的呢?从它的源码中可知,它继承了InputStreamSource接口。
总的图形如下。
InputStreamSource里面有一个方法,getInputStream,它的返回值是一个输入流,ClassPathResource也重写了getInputStream这个方法。
@Override
public InputStream getInputStream() throws IOException {
InputStream is;
if (this.clazz != null) {
is = this.clazz.getResourceAsStream(this.path);
}
else if (this.classLoader != null) {
is = this.classLoader.getResourceAsStream(this.path);
}
else {
is = ClassLoader.getSystemResourceAsStream(this.path);
}
if (is == null) {
throw new FileNotFoundException(getDescription() + " cannot be opened because it does not exist");
}
return is;
}
可以通过类加载器获取输入流,也可以通过字节码对象调用getResourceAsSream获取输入流。因此可以对Resouce这样理解,Resource接口用于读取资源,既可以读取普通的文件 xml porperties配置文件 txt文件,网络中的资源等
读取资源的方式也是不一样的:
- FileSystemResource 基于文件的绝对路径进行资源的读取操作
- ClassPathResource 基于文件的类路径进行资源的读取操作
- ByteArrayResource 基于二级制流进行资源的读取
- ......
到了这里,可以明白它是通过类加载器去进行读取资源的。
配置文件读取是一个IO操作,如果每次使用时都需要进行读取的话效率较低,因此我们想让他只读取一次,读取之后把它以Java对象的方式存放在Java虚拟机中。具体来说,通过Resouce的getInputStream的方法以流的方式读到虚拟机中,以BeanDedinition对象方式来保存的。
现在知道是如何读取xml文件了,那么它是如何进行解析的呢?通过SAX进行解析,这个后面再说。
还要其他的问题获取到配置文件的信息之后,spring是如何将这些信息封装成Java对象的?根据BeanDefinition对象,又是如何创建Bean对象的?Bean对象的生命周期是什么?这些问题,都会在后文进行解答。第二篇文章已解答。
如何封装成Java对象?
前面提到,Resource接口的实现类ClassPathResource对配置信息读取,获取输入流,为了减少IO操作,需要将这些配置信息以BeanDefinition对象的方式进行存储,那么,它是如何将这些信息转化为对象呢?谁又去做这件事,是BeanDefinitionReader去做的。BeanDefinitionReader有这样一句话,Simple interface for bean definition readers,我们想想,为什么要设计成接口,因为对于spring配置文件有不同的配置形式,可以是xml,可以是纯注解的形式进行配置。
到这里,我们的关注点到了,Spring底层是如何通过XmlBeanDefinitionReader将xml信息封装成BeanDefinition对象的? XmlBeanDefinitionReader有一个loadBeanDefinitions方法,它是对配置的输入流进行解析。对吗?点开这个类,发现在package org.xml.sax;到下面的return doLoadBeanDefinitions,它是真正去干活的。那么这个loadBeanDefinitions到底是干什么的呢?从这个方法的前面的可以看出,它是用来检验循环依赖的。
如果循环检查,当两个或更多的配置文件相互引用时,会导致无限递归地尝试加载相同的资源,最终耗尽系统资源或导致程序崩溃。
this.resourcesCurrentlyBeingLoaded.get();
获取一个ThreadLocal<Set<EncodedResource>>
变量,该变量保存了当前线程中正在加载的所有资源。- 如果当前没有正在加载的资源,则初始化一个新的
HashSet
来存储这些资源。 - 使用
currentResources.add(encodedResource)
尝试将新资源添加到集合中。 - 如果返回
false
,意味着该资源已经存在于集合中,表明发生了循环依赖加载,此时会抛出BeanDefinitionStoreException
异常,并提示用户检查他们的导入定义。
doLoadBeanDefinitions 方法
doLoadBeanDefinitions
方法的主要任务是从给定的 InputSource
和 Resource
中读取 XML 内容,并将其转换为一个 Document
对象。然后,它会解析该文档来提取所有的 bean 定义,并将它们注册到 BeanFactory
中。
主要步骤:
- 将输入源转换为 Document 对象:
- 使用
DocumentLoader
将 XML 输入流转换成Document
对象。这是通过调用DocumentBuilderFactory
来创建DocumentBuilder
实例,并使用它来解析 XML 数据。
- 使用
- 解析 Document 对象:
- 一旦有了
Document
对象,下一步就是遍历这个文档,查找所有<bean>
标签以及其它可能包含 bean 定义的标签(如<import>
标签)。
- 一旦有了
- 注册 Bean 定义:
- 对于每一个找到的 bean 定义,都会生成一个
BeanDefinition
对象,并通过registerBeanDefinition
方法将其注册到BeanFactory
中。
- 对于每一个找到的 bean 定义,都会生成一个
registerBeanDefinition 方法的作用
-
将
BeanDefinition
对象添加到BeanFactory
中:这意味着将每个 bean 的定义信息存储在一个合适的数据结构中,以便后续可以根据名称或其他标识符检索和实例化这些 bean。 -
处理覆盖逻辑:如果存在同名的 bean 定义,根据配置的不同,可能会选择覆盖旧的定义或抛出异常。
为什么要先转化为 Document 对象?
- 便于解析和处理:转换为
Document
对象后,可以方便地使用标准的 DOM API 来遍历和查询 XML 文档。这使得提取特定元素(如<bean>
标签)变得简单直接。 - 支持复杂的 XML 结构:XML 配置文件可能包含嵌套的元素、属性和其他复杂结构。将整个文档加载为
Document
对象后,可以更容易地处理这些复杂性,确保所有必要的信息都被正确解析。 - 提高效率:一次性将整个文档加载为
Document
对象,可以在内存中对文档进行多次遍历和查询,而不需要反复从磁盘读取数据或重新解析 XML。
ParseBeanDefinitions是如何进行解析的
其中的parseDefaultElement是解析基本的标签,例如bean标签,import标签alias标签,delegate是解析自定义标签,自定义标签的格式是<标签头:标签名>。对于bean标签来说,交给parseDefaultElement来进行解析,之后交给processBeanDefinition,先把标签解析为BeanDefinitionHold对象,这里面包含beanName,aliases别名,有多个,因此把他作为数组,得到对象后调用BeanDefinitionReaderUtils工具类进行bean信息的注册,注册时是保存到map集合中,key是beanid,value是beanDefinition注册完成后进行事件的发送。那么,解析成BeanDedinitionHold对象时,它是如何封装的呢?请看下一篇。
总的来说:
- 资源加载:首先,使用
ClassPathResource
或其他实现Resource
接口的类来加载 XML 配置文件。这一步将 XML 文件转换为一个Resource
对象 - 创建
XmlBeanFactory
实例:创建XmlBeanFactory
实例时传入上面创建的Resource
对象。实际上,XmlBeanFactory
内部会使用XmlBeanDefinitionReader
来读取和解析 XML 文件 - 资源包装与循环依赖检查:在
XmlBeanDefinitionReader
中,当掉用loadBeanDefinitions(Resource resource)
方法时,首先会对资源进行包装成EncodedResource
,这是为了支持编码设置等功能。然后,它会检查当前是否已经在加载相同的资源,以防止循环依赖问题。 -
真正加载 Bean 定义:
调用doLoadBeanDefinitions(InputSource inputSource, Resource resource)
方法来进行实际的加载工作。这个方法首先将 XML 文件内容转换为 W3C DOMDocument
对象。 -
解析 Document 并注册 Bean 定义:
使用registerBeanDefinitions(Document doc, Resource resource)
方法处理Document
对象。这个方法内部会调用BeanDefinitionDocumentReader
的registerBeanDefinitions
方法。BeanDefinitionDocumentReader
会遍历文档中的<bean>
标签和其他相关的标签(如<import>
,<alias>
),并将每个找到的 bean 定义转化为BeanDefinition
对象。 -
具体解析 Bean 定义:
parseBeanDefinitions(Element root, BeanDefinitionParserDelegate delegate)
方法负责遍历 XML 文档的根元素及其子元素。对于每个<bean>
元素,调用processBeanDefinition(Element ele, BeanDefinitionParserDelegate delegate)
方法进一步解析属性,并生成BeanDefinitionHolder
对象。最终通过registerBeanDefinition
方法将BeanDefinition
注册到BeanFactory
中。
在之前提到的XmlBeanFactory这个工厂,现在Spring已经不提倡使用它了,取代它的是DefaultListableBeanFactory。它们之间的异同点
相同:
- Bean管理:两者都是用于管理 Spring 容器中的 bean 实例的核心组件。
- Bean定义的抽象:两者都依赖于
BeanDefinition
来描述 bean 的元数据。 - 生命周期回调:两者都支持对 bean 生命周期的管理,包括初始化方法(如
init-method
或@PostConstruct
注解)和销毁方法(如destroy-method
或@PreDestroy
注解)。 -
资源共享:两者都可以与 Spring 的资源加载机制(如
ResourceLoader
和Resource
接口)结合使用,允许从不同的源(如文件系统、类路径等)加载配置信息。
不同:
- 限制性:主要用于基于 XML 的配置,并且不支持很多现代 Spring 特性,例如注解配置、国际化消息资源等。缺乏对事件发布机制的支持,这使得它不适合需要事件驱动的应用场景。
- 生命周期管理:仅提供基础的 bean 生命周期管理功能,缺少对更多高级特性的支持,比如对
@PostConstruct
和@PreDestroy
注解的支持有限。 - 扩展性:不如
DefaultListableBeanFactory
灵活,难以扩展以支持更多的功能。 - 更强大的功能集:作为
ConfigurableListableBeanFactory
接口的一个实现,提供了比XmlBeanFactory
更丰富的功能集合。支持多种类型的配置源,包括 XML 文件、注解、Java 配置类等。提供了对@PostConstruct
和@PreDestroy
注解的支持,以及对@Autowired
和其他依赖注入注解的支持。 - 事件发布机制:内置了对事件监听器的支持,允许应用程序通过事件驱动的方式进行组件间的通信。
- 灵活的扩展点:可以通过添加
BeanPostProcessor
、自定义作用域、属性编辑器等方式轻松扩展其功能。 - 集成与兼容性:更好地与其他 Spring 模块(如 Spring MVC, Spring AOP 等)集成,提供全面的上下文环境支持。