本文中,所有spring framework源码,均采用5.0.x版本
在《Spring IoC概念分析》一文中,我们对Spring IoC的前置概念和整体流程有了一个初步的了解,在本文中,我们会解读源码,力图将spring IoC的逻辑扁平化,可视化。
1. Spring IoC容器的启动
applicationContext是Spring的核心,Context我们通常解释为上下文环境,我想用“容器”来表述它更容易理解一些,ApplicationContext则是“应用的容器”了;在Web应用中,我们会用到WebApplicationContext,WebApplicationContext继承自ApplicationContext;
以企业级java项目最常用的web项目为例;我们知道,在web项目中,Spring启动是在web.xml配置监听器,如下所示:
1 | <!-- 配置Spring上下文监听器 --> |
可以看到,ContextLoaderListener类是spring IoC容器启动的核心,也是整个Spring框架的启动入口:
ContextLoaderListener类实现了Tomcat容器的ServletContextListener接口,所以它与普通的Servlet监听是一样的。同样是重写到两个方法:
- contextInitialized()方法在web容器初始化时执行
- contextDestroyed()方法在容器销毁时执行。
WEB容器启动时会触发初始化事件,ContextLoaderListener监听到这个事件,其contextInitialized()方法会被调用,在这个方法中Spring会初始化一个root上下文,即WebApplicationContext。
WebApplicationContext是一个接口,其实际默认实现类是XmlWebApplicationContext。(重点关注红色箭头指示的两条继承/实现关系)
这个就是Spring IOC的容器,其对应bean定义的配置信息由web.xml中的context-param来指定:
1 | <!-- 配置Spring配置文件路径 --> |
在ContextLoaderListener类中,只是实现了ServletContextListener提供的到两个方法,Spring启动主要的逻辑在父类ContextLoader的方法initWebApplicationContext实现。
1 | public class ContextLoaderListener extends ContextLoader implements ServletContextListener { |
ContextLoaderListener的作用就是启动web容器时自动装配ApplicationContext的配置信息。更细化一点讲,Spring的启动过程其实就是Spring IOC容器的启动过程。
1.1 ContextLoader剖析
上文说过,Spring启动主要的逻辑在父类ContextLoader的方法initWebApplicationContext实现。所以我们有必要重点看下ContextLoader类:
这里面,最核心的方法是initWebApplicationContext(),它被ContextLoaderListene的contextInitialized()调用,负责spring容器的初始化。
注意,该图代码非5.0.x版本,但核心逻辑变化不大,可以参考。
1.2 Context的生成门面
在这个方法中,入参ServletContext是由web容器监听器(ContextLoaderListener)提供。
1 | /** |
归纳一下:
- 首先判断servlectContext中是否已经存在根上下文,如果存在,则抛出异常;
- 否则通过createWebApplicationContext方法创建新的根上下文。
- 然后通过loadParentContext()方法为其设置父上下文。
- 再通过configureAndRefreshWebApplicationContext为根上下文构建bean工厂和bean对象。
- 最后把上下文存入servletContext,并且存入currentContextPerThread。
- 至此初始化过程完毕,接下来可以获取WebApplicationContext,进而用getBean(“bean name”)得到bean。
1.3 创建Context
我们刚刚看到,initWebApplicationContext方法主要调用createWebApplicationContext方法来创建上下文:
1 | /** |
归纳一下:
- createWebApplicationContext方法使用determineContextClass方法,从web.xml配置的contextClass参数中,获取要创建的context的具体实现类,如果没有指定,则默认使用XmlWebApplicationContext为其实现类。
- 限制根上下文的实现类必须是ConfigurableWebApplicationContext的子类。
- 使用BeanUtils.instantiateClass工具方法,根据类名创建类实例。
1.4 确定Context实现类
1 | /** |
String contextClassName = servletContext.getInitParameter(CONTEXT_CLASS_PARAM);方法获取的,就是web.xml中配置指定的IoC容器类型:
1 | <context-param> |
截止到这里为止,WebApplicationContext的实例就已经创建出来了。然后逻辑往下走,到了configureAndRefreshWebApplicationContext(cwac, servletContext)方法,该方法为根上下文构建bean工厂和bean对象。
1.5 配置和刷新Context
1 | protected void configureAndRefreshWebApplicationContext(ConfigurableWebApplicationContext wac, ServletContext sc) { |
String configLocationParam = sc.getInitParameter(CONFIG_LOCATION_PARAM);方法获取的,就是web.xml中配置指定的IoC容器配置文件:
1 | <!-- 配置Spring配置文件路径 --> |
我们可以看到,容器的初始化逻辑,都在wac.refresh();中。
1.6 阶段性流程图
2 refresh()核心方法
我们之前说过,WebApplicationContext方法的默认实现是XmlWebApplicationContext,而XmlWebApplicationContext的refresh()方法,继承自它的父类AbstractApplicationContext。
1 | @Override |
3 加载xml文件的beanDefinition
refresh()方法调用了obtainFreshBeanFactory()方法,在这个方法中,会发起IoC容器对配置文件的读取,并将其加载为beanDefinition,不过在了解beanDefinition加载过程之前,我们有需要了解一些前置知识点。
3.1 Resource资源文件框架
详见文章本博客《Spring Resource资源文件体系》;
3.2 加载前的准备工作
1 | /** |
refreshBeanFactory()方法的实现在AbstractRefreshableApplicationContext类中:
1 | /** |
3.3 设置和获取Reader工具类
前文说过,XmlWebApplicationContext类是默认的Context类,所以默认情况下,loadBeanDefinitions(DefaultListableBeanFactory)调用的是XmlWebApplicationContext类中实现的loadBeanDefinitions(DefaultListableBeanFactory);
1 | @Override |
loadBeanDefinitions(beanDefinitionReader)方法调用的是loadBeanDefinitions(XmlBeanDefinitionReader)方法:
1 | /** |
3.4 通配/完整路径解析策略
核心逻辑又进入了reader.loadBeanDefinitions(configLocation)方法,这个方法,XmlBeanDefinitionReader自己没有实现,是继承自AbstractBeanDefinitionReader的方法:
1 | /** |
这里的两个分支,即resourceLoader支持解析location为通配符形式的和resourceLoader支持解析location为完整路径形式的,二者分别调用了:
- resourceLoader支持解析location为通配符形式的
- PathMatchingResourcePatternResolver#getResources(String locationPattern)
- AbstractBeanDefinitionReader#loadBeanDefinitions(Resource… resources)方法
- resourceLoader支持解析location为完整路径形式的
- DefaultResourceLoader#getResources(String locationPattern)
- XmlBeanDefinitionReader#loadBeanDefinitions(Resource resource)
而AbstractBeanDefinitionReader#loadBeanDefinitions(Resource… resources)方法的逻辑很简单:
1 | @Override |
所以最后殊途同归,最后都是落在了XmlBeanDefinitionReader#loadBeanDefinitions(Resource resource)方法上。
3.5 编码设置和循环依赖检测
XmlBeanDefinitionReader的loadBeanDefinitions(Resource resource)重写方法,主要就是加载xml文件配置的beanDefinition:
1 | /** |
可以看到,resource在这里被封装为了EncodedResource,我们继续往下看。
1 | /** |
我们看到,核心逻辑,又进入了doLoadBeanDefinitions方法:
1 | /** |
OK,两个核心方法,doLoadDocument和registerBeanDefinitions方法,前者负责解析xml文件,后者负责注册BeanDefinitions,我们分别来分析。先看doLoadDocument()方法
3.6 解析XML文件
此方法在XmlBeanDefinitionReader类中
1 | /** |
3.6.1 参数1
上文中的getEntityResolver() 方法返回 XmlBeanDefinitionReader 类的 entityResolver 属性。
entityResolver 属性在 loadBeanDefinitions(DefaultListableBeanFactory beanFactory) 方法中被赋值。
beanDefinitionReader.setEntityResolver(new ResourceEntityResolver(this));
和resourceLoader一样,this拿的是XmlWebApplicationContext实例,我们再来看下ResourceEntityResolver的构造方法
1 | /** |
调用了父类构造,再跟进一层
1 | /** |
中间的this.errorHandler参数可忽略。
3.6.2 参数2
ok,然后是getValidationModeForResource(resource)入参。
1 | /** |
getValidationModeForResource的核心方法是detectValidationMode(),我们继续:
1 | /** |
所以来看validationModeDetector调用的detectValidationMode方法
1 | /** |
3.6.3 解析的核心逻辑
讲完核心的两个入参后进入正主,loadDocument方法。
1 | return this.documentLoader.loadDocument(inputSource, getEntityResolver(), this.errorHandler, |
documentLoader属性的默认实现是DefaultDocumentLoader;
1 | /** |
最后一步,DocumentBuilder的默认实现是DocumentBuilderImpl,这个是jdk里面的xml解析器了,不再赘述。
至此,我们拿到了Document对象
3.7 准备注册BeanDefinitions
两个核心方法,看完了doLoadDocument方法,我们再来看registerBeanDefinitions方法,后者负责注册BeanDefinitions,我们分别来分析。
1 | /** |
核心方法documentReader.registerBeanDefinitions实现在DefaultBeanDefinitionDocumentReader类中:
1 | /** |
再看DefaultBeanDefinitionDocumentReader类的doRegisterBeanDefinitions方法。
1 | /** |
好,这里两个核心方法,我们一个一个来
3.8 生成代理和传递基础配置
先看DefaultBeanDefinitionDocumentReader类的createDelegate方法。
1 | protected BeanDefinitionParserDelegate createDelegate( |
3.9 拆解根层级,并根据子标签命名空间做不同处理
我们再来看另一个核心方法:DefaultBeanDefinitionDocumentReader类的parseBeanDefinitions方法。
1 | /** |
3.10 解析beans命名空间的配置
前文我们说到,如果该标签属于beans的名称空间,则进入parseDefaultElement(ele, delegate)方法,
1 | private void parseDefaultElement(Element ele, BeanDefinitionParserDelegate delegate) { |
上述几个标签中,我们主要来看<bean>
标签的解析方法。
1 | /** |
3.11 解析并生成BeanDefinition实例
进入BeanDefinitionParserDelegate类的parseBeanDefinitionElement方法
1 | /** |
在上文中,核心方法是这句话:
AbstractBeanDefinition beanDefinition = parseBeanDefinitionElement(ele, beanName, containingBean);
1 | /** |
两个重要的核心方法
1 | //创建BeanDefinition并设置两属性,核心方法 |
我们依次来看:
3.11.1 创建实例
1 | /** |
调用BeanDefinitionReaderUtils的静态方法createBeanDefinition():
1 | /** |
3.11.2 标签的属性注入
看完了create,我们再来看parseBeanDefinitionAttributes(ele, beanName, containingBean, bd);方法,该方法会将bean标签的属性值注入到BeanDefinition实例中,也就是给BeanDefinition赋值:
1 | /** |
这样,bean标签上的属性也就解析完成了,对其属性的描述不管设置了还是没有设置的,都有相应的值对应到bean definition中。
3.11.3 bean标签下的property注入
1 | <bean id="student" class="com.sgcc.bean.Student"> |
如上述配置,我们知道property的存在,parsePropertyElements()负责解析Property的属性设置到beanDefinition中,此方法在BeanDefinitionParserDelegate类中实现
1 | /** |
3.12 注册BeanDefinition到工厂中
好,回到processBeanDefinition方法。
1 | /** |
BeanDefinitionHolder bdHolder = delegate.parseBeanDefinitionElement(ele);这一句我们已经在3.11章节中详细介绍了,接下来我们视角继续往下,看下BeanDefinitionReaderUtils.registerBeanDefinition(bdHolder, getReaderContext().getRegistry());
1 | /** |
主流程由此进入了registry.registerBeanDefinition中,其中registry实例是DefaultListableBeanFactory的实例。
1 | @Override |
再看别名的注册:
1 | @Override |