spring web容器加载始末

// 缓慢更新ing

这是很早以前就想搞明白的一个问题,但回回看,回回晕。最开始连从哪一个类进都不知道,后来虽然晓得是从org.springframework.web.context.ContextLoaderListener这个监听器开始的,但点进去后,立马就陷入了方法调用的汪洋大海…… ……

为何看的会头晕呢,或者换种问法,什么状况下脑子会不够用?
一,线索与线索之间的关联度过低;二,线索太多,致使遗忘,很难串成一条线。
其实对于spring来说,也不存在什么过高深的代码,命名严谨,封装规范,看上去简洁易懂。因此根本上讲,仍是由于没找到一条清晰的链路进行跟踪。

直到某天,我瞄了一眼idea的调用栈。
惊了!
从入口到断点处,通过了哪些类、哪一个方法都明明白白,另外一个窗口还能够看有哪些线程。

屌诶

另外我碰巧打在一个BeanPostProcessor接口的实现类上,这个调用栈又恰好走过了容器加载的过程。
而,这不就是我想要的,清晰的链路吗

:-)





记录的意义

9102年了,spring源码解读类的书都出到…… em…… ……
都不知道出到第几版了。

那还写博客干吗?
于我的研究终有意义,但记录好像略显矫情。因此最后,应该仍是印证了那句话:
我的博客是写给本身看的,于他人,更像是展示一种成长的痕迹,而不是技术的原理。





主流程

web环境下,容器加载主流程基本被org.springframework.context.support.AbstractApplicationContext#refresh()方法给归纳了。可是在着重研究它以前,仍是要简单讲下进入这个方法以前的代码。

首先,只要是java web项目, 第一都是tomcat启动,而后servlet容器启动,加载并读取web.xml,建立ServletContext,启动listener,启动filter,启动servlet
为了让spring在这个流程中启动,咱们都会在web.xml里配置spring的一个监听器(上下文加载器监听器)。而后经过这个监听器,调用spring的初始化方法。之因此使用监听器,是为了在filter与servlet启动以前把容器加载完毕,由于这俩有可能用到容器中的内容。
<listener>
        <description>Spring容器加载监听器</description>
        <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
    </listener>
复制代码

由于监听器的缘故,ContextLoaderListener#contextInitialized()方法会被自动调用,在contextInitialized()方法内,又会调用org.springframework.web.context.ContextLoader#initWebApplicationContext()方法。

java

initWebApplicationContext()
初始化web应用上下文,首先要建立一个上下文对象。建立时,优先从web.xml里寻找,有没有名为“contextClass”参数,有的话根据名字获取Class对象,而后以反射的方式将上下文对象new出来,最后new出来的必定是ConfigurableWebApplicationContext类型。若是web.xml里不存在“contextClass”,那么默认使用org.springframework.web.context.support.XmlWebApplicationContext类来建立上下文。
node

protected WebApplicationContext createWebApplicationContext(ServletContext sc) {
    Class<?> contextClass = determineContextClass(sc);// 这里选择上下文类的Class对象
    // 此处省略……
    return (ConfigurableWebApplicationContext) BeanUtils.instantiateClass(contextClass);
}
复制代码

root上下文建立完后,还会从web.xml里查询是否配置了parent上下文,有的话就加载并指定,可是这个操做,我工做这几年也没见过哪一个项目配置过,就暂不理会了。下一步:

web

configureAndRefreshWebApplicationContext()
配置并刷新上下文,这个方法内,比较重要的逻辑是从web.xml里读取spring各项配置文件的路径,具体什么文件因项目而异,我我的的习惯是定义application-main.xml,而后在main.xml里,import数据库,缓存,队列等的xml。spring

<context-param><!---->
    <param-name>contextConfigLocation</param-name>
    <param-value>classpath*:config/applicationContext-main.xml</param-value>
</context-param>
复制代码

读取完配置文件路径后,spring会再从web.xml中初始化一些参数,以及新建一个ConfigurableEnvironment对象
初始化的参数这几年项目中也没见过,暂不详述。主要是ConfigurableEnvironment,这个类的对象接下来出现的频率还比较高,经过这个接口能够获取到不少spring须要的配置参数,而后记录一下类之间的包含关系。mongodb

AbstractApplicationContext==包含==>ConfigurableEnvironment==包含==>MutablePropertySources
复制代码

再接下来初始化一些实现了org.springframework.context.ApplicationContextInitializer接口的类,详情在那些实现了Spring接口的类,都是怎么被加载的文章中有记录。

最后终于到org.springframework.context.support.AbstractApplicationContext#refresh()。
从ContextLoaderListener#contextInitialized()方法到AbstractApplicationContext#refresh()方法,中间没有特别深奥的东西,就是A方法调B方法,B方法调C方法,等等等等。
但,起名严谨直观,看一眼就知道类和方法的做用,这是写代码要学习的地方。

由于直接贴代码看的不是很清晰,因此用表格列一下refresh()中的几个方法:数据库

正常流程 官方注释
prepareRefresh() // Prepare this context for refreshing.         ↓
obtainFreshBeanFactory() // Tell the subclass to refresh the internal bean factory.         ↓
prepareBeanFactory(beanFactory) // Prepare the bean factory for use in this context.         ↓
postProcessBeanFactory(beanFactory) // Allows post-processing of the bean factory in context subclasses.         ↓
invokeBeanFactoryPostProcessors(beanFactory) // Invoke factory processors registered as beans in the context.         ↓
registerBeanPostProcessors(beanFactory) // Register bean processors that intercept bean creation.         ↓
initMessageSource() // Initialize message source for this context.         ↓
initApplicationEventMulticaster() // Initialize event multicaster for this context.         ↓
onRefresh() // Initialize other special beans in specific context subclasses.         ↓
registerListeners() // Check for listener beans and register them.         ↓
finishBeanFactoryInitialization(beanFactory) // Instantiate all remaining (non-lazy-init) singletons.         ↓
finishRefresh() // Last step: publish corresponding event.         ↓
抛异常执行的方法
destroyBeans() // Destroy already created singletons to avoid dangling resources.         ↓
cancelRefresh(ex) // Reset 'active' flag.         ↓
finally块的方法
resetCommonCaches() // Reset common introspection caches in Spring's core, since we       // might not ever need metadata for singleton beans anymore...         ↓

能够说是很长了

:-(




prepareRefresh()

这个方法没什么好说的,初始化一些参数,并校验。由于这这篇文章是从ContextLoaderListener#contextInitialized()方法一路看下来的,因此初始化参数这段代码,以前就出现过了。但下面这个语句仍是值得留心,虽然如今也不知道这有什么用……
// Allow for the collection of early ApplicationEvents,
// to be published once the multicaster is available...
this.earlyApplicationEvents = new LinkedHashSet<ApplicationEvent>();
复制代码

ok,这个方法结束。

缓存

obtainFreshBeanFactory()

加载xml并读取bean,一个长的要死的流程的开始……

1.refreshBeanFactory()

首先获取新的BeanFactory。
给谁获取?org.springframework.web.context.support.**XmlWebApplicationContext**对象。XmlWebApplicationContext的父类包含一个DefaultListableBeanFactory的私有成员变量。
/** Bean factory for this context */
private DefaultListableBeanFactory beanFactory;
复制代码

在获取以前会先检查是否已经存在BeanFactory,检查的依据,就是看对象是否是null。有意思的是,这个方法如何作到线程同步。
tomcat

1.1锁

在类内new一个Object类型的私有成员变量,做为一把锁。要注意的是,这把锁 不是静态的。所以,仅当多个线程想要调用 这个XmlWebApplicationContext对象的hasBeanFactory()方法时,才会等待。
/** Synchronization monitor for the internal BeanFactory */
private final Object beanFactoryMonitor = new Object();

protected final boolean hasBeanFactory() {
    synchronized (this.beanFactoryMonitor) {
        return (this.beanFactory != null);
    }
}
复制代码

若是,当前真的已经存在了一个BeanFactory,那么会销毁以前建立出来的所有bean,以及这个已经存在了的BeanFactory。因为spring是靠Map、List、Set来持有bean的,因此销毁建立的bean,就是把集合清空,而后销毁BeanFactory,就是将引用置为null

判断完之后,开始真正的建立。默认new一个org.springframework.beans.factory.support.DefaultListableBeanFactory类。这段代码研究不深,略过。bash

protected DefaultListableBeanFactory createBeanFactory() {
    return new DefaultListableBeanFactory(getInternalParentBeanFactory());
}
复制代码

DefaultListableBeanFactory类须要重点关注,由于这个对象是容器的核心,比方说要根据名字或类型从容器拿对象,最后靠的就是它的方法。好比:app

org.springframework.beans.factory.support.AbstractBeanFactory#doGetBean();
org.springframework.beans.factory.support.AbstractBeanFactory#containsBean();
复制代码

而后进入又一个重头戏:

2.loadBeanDefinitions(beanFactory)

从spring的配置文件里加载定义好的bean。所以具体实如今这个位置org.springframework.web.context.support. XmlWebApplicationContext#loadBeanDefinitions()

关于如何加载的,本身也能够猜一下。先确认xml的路径,而后用dom4j之类的东西读取xml,读取完之后,按必定的规则,靠反射建立对象。
spring也基本是这个逻辑,但中间的过程却很复杂,因此下面要搞懂的,就是他为何这么复杂。

2.1 找到xml的所在位置

解析xml的工具,再怎么强悍,也要先知道xml到底保存在哪里才行。咱们配置的spring xml路径都是相对的(以下),关键是怎么根据这个相对路径找到绝对路径。
classpath*:config/applicationContext-main.xml
复制代码

咱们顺着loadBeanDefinitions()方法一路往下走,在org.springframework.beans.factory.support.AbstractBeanDefinitionReader#loadBeanDefinitions()方法内有这么一行代码,这就是获取绝对路径的关键。

Resource[] resources = ((ResourcePatternResolver) resourceLoader).getResources(location);
复制代码

这方法再往下走也很深,debug让人头晕。能确认的是,这一整套逻辑和CLassLoader相关。起效的方法以下:sun.misc.URLClassPath#findResources

public Enumeration<URL> findResources(final String var1, final boolean var2) {
    return new Enumeration<URL>() {
        private int index = 0;
        private int[] cache = URLClassPath.this.getLookupCache(var1);
        private URL url = null;

        private boolean next() {
            if (this.url != null) {
                return true;
            } else {
                do {
                    URLClassPath.Loader var1x;
                    if ((var1x = URLClassPath.this.getNextLoader(this.cache, this.index++)) == null) {
                        return false;
                    }
                    // 双眼盯准这一句
                    this.url = var1x.findResource(var1, var2);
                } while(this.url == null);

                return true;
            }
        }

        public boolean hasMoreElements() {
            return this.next();
        }

        public URL nextElement() {
            if (!this.next()) {
                throw new NoSuchElementException();
            } else {
                URL var1x = this.url;
                this.url = null;
                return var1x;
            }
        }
    };
}
复制代码

最后在这个URLClassPath对象下找到了文件目录

文件绝对路径以URLResource的方式包裹,并向上转型为Resource接口。

2.2 loadBeanDefinitions

即使得到了绝对路径,离解析xml并提取BeanDefinition的代码,还差得远。
以上那一连串获取xml文件绝对路径的代码,都只是XmlBeanDefinitionReader类中的一部分。为了避免让思惟陷入混沌或是跑偏,继续用表格的方式罗列一下过程

类名 简单描述
org.springframework.web.context.support.XmlWebApplicationContext 准备XmlBeanDefinitionReader对象,提供xml文件的相对路径
org.springframework.beans.factory.xml.XmlBeanDefinitionReader 根据相对路径获取xml的绝对路径,读取并解析成org.w3c.dom.Document对象,准备BeanDefinitionDocumentReader对象
org.springframework.beans.factory.xml.BeanDefinitionDocumentReader 提取root节点,为解析作一些判断并包含递归解析之类的操做。建立BeanDefinitionParserDelegate对象
org.springframework.beans.factory.xml.BeanDefinitionParserDelegate 里面都是实打实的,从document、element解析bean以及属性的方法

but,最后返回的不是类对象、不是BeanDefinition,是org.springframework.beans.factory.config.BeanDefinitionHolder。

其实,我是能理解不返回类对象,只返回BeanDefinition。由于spring做为一个庞大的框架,在初始化类对象前,一定还有一些其它的操做,若是直接返回一个类对象,那么解析这一环的操做会变得更加复杂(自己解析就已经通过了不少方法了)。
经验告诉咱们,超复杂代码的维护成本是极其昂贵的。因此在解析这一环,作的就是 纯解析而不是“解释”。不过我没搞明白的是,干吗还要用BeanDefinitionHolder把BeanDefinition包装一层再返回?

虽然返回的是BeanDefinitionHolder,但解析成BeanDefinition的过程仍是实打实存在的,因此先收回来,看看怎么解析BeanDefinition的。可是解析的代码过长,阅读源码也不可能说一行行的解释过去,所以这里只能记录一些我以为有趣,有借鉴意义的代码。

2.2.1 递归解析

要先从这个方法开始,前面有些代码我可能就省略了。

org.springframework.beans.factory.xml.DefaultBeanDefinitionDocumentReader#doRegisterBeanDefinitions
复制代码

第一,判断这个document的root节点是否是<beans>,判断依据是提取节点的namespaceUri。

public boolean isDefaultNamespace(Node node) {
    return isDefaultNamespace(getNamespaceURI(node));
}

public boolean isDefaultNamespace(String namespaceUri) {
    return (!StringUtils.hasLength(namespaceUri) || BEANS_NAMESPACE_URI.equals(namespaceUri));
}
复制代码

就看这个代码,是否是也挺简单的?因此原则上任何一个问题,被拆分红数个小模块以后,都是简单的。
而后,提取beans节点的profile属性,若是profile属性为空,直接解析。若是不为空,那么判断profile是否被激活,未激活则放弃解析。(篇幅问题只放一点代码)

String profileSpec = root.getAttribute(PROFILE_ATTRIBUTE);
if (StringUtils.hasText(profileSpec)) {
    String[] specifiedProfiles = StringUtils.tokenizeToStringArray(
			profileSpec, BeanDefinitionParserDelegate.MULTI_VALUE_ATTRIBUTE_DELIMITERS);
    if (!getReaderContext().getEnvironment().acceptsProfiles(specifiedProfiles)) {
	    if (logger.isInfoEnabled()) {
	        logger.info("Skipped XML bean definition file due to specified profiles [" + profileSpec +
					"] not matching: " + getReaderContext().getResource());
		}
		return;
	}
}
复制代码

那么判断激活的逻辑呢?

代码是死的,数据是活的,因此一定是先在某个地方配置了激活的profile的名字,而后才能以此为依据来判断beans节点的profile属性是否激活。spring有个参数名为spring.active.profiles,此参数的值,便表明着激活的profile名。
这个变量,默认从java.lang.System类中获取。想必你也发现了,这里有个加载前后的注意点。必须先读取并加载spring.active.profiles的值,而后才能开始判断beans节点中的profile属性是否激活。

所以操做顺序是这样的,一,读取spring.active.profiles;二,加载spring.active.profiles;三,根据spring.active.profiles去判断是否激活
容器启动的代码已经很靠前了,要怎么更靠前一点,去读取参数呢?
ApplicationContextInitializer接口,咱们能够自定义一个ApplicationContextInitializer接口,在这个类内读取参数,详情点击查看另外一篇博客。

关于加载的过程就一图以蔽之了。

好,扯回来,再贴两段代码,这就是递归解析所在。

protected void parseBeanDefinitions(Element root, BeanDefinitionParserDelegate delegate) {
    if (delegate.isDefaultNamespace(root)) {
    	NodeList nl = root.getChildNodes();
    	for (int i = 0; i < nl.getLength(); i++) {
    	    Node node = nl.item(i);
            if (node instanceof Element) {
                Element ele = (Element) node;
                if (delegate.isDefaultNamespace(ele)) {
                    parseDefaultElement(ele, delegate);
                }
                else {
            	    delegate.parseCustomElement(ele);
                }
            }
    	}
    }
    else {
    	delegate.parseCustomElement(root);
    }
}
    
private void parseDefaultElement(Element ele, BeanDefinitionParserDelegate delegate) {
    if (delegate.nodeNameEquals(ele, IMPORT_ELEMENT)) {
    	importBeanDefinitionResource(ele);
    }
    else if (delegate.nodeNameEquals(ele, ALIAS_ELEMENT)) {
    	processAliasRegistration(ele);
    }
    else if (delegate.nodeNameEquals(ele, BEAN_ELEMENT)) {
    	processBeanDefinition(ele, delegate);
    }
    else if (delegate.nodeNameEquals(ele, NESTED_BEANS_ELEMENT)) {
    	// recurse
    	doRegisterBeanDefinitions(ele);
    }
}
复制代码

首先解释一下customElement与defaultElement的区别。

<bean id="threadPool" class="org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor" lazy-init="false">
    <!-- 线程池维护线程的最少数量 -->
    <property name="corePoolSize" value="5"/>
    <!-- 容许的空闲时间 -->
    <property name="keepAliveSeconds" value="200"/>
    <!-- 线程池维护线程的最大数量 -->
    <property name="maxPoolSize" value="20"/>
    <!-- 缓存队列 -->
    <property name="queueCapacity" value="20"/>
    <!-- 对拒绝task的处理策略 -->
    <property name="rejectedExecutionHandler">
        <bean class="java.util.concurrent.ThreadPoolExecutor$CallerRunsPolicy"/>
    </property>
</bean>

<mongo:db-factory id="mongoFactory" client-uri="mongodb://root:root@127.0.0.1:27017/test"/>
<mongo:mapping-converter id="mongoConverter" db-factory-ref="mongoFactory"/>
<mongo:template id="mongoTemplate" db-factory-ref="mongoFactory" converter-ref="mongoConverter"/>
<mongo:gridFsTemplate id="gridFsTemplate" bucket="allot_image" db-factory-ref="mongoFactory" converter-ref="mongoConverter"/>
复制代码

最简单的解释,<beans>、<bean>、<import>、<alias>这四种节点就是defaultElement,其它全部节点,都属于customElement。好比<mongo:>、<context:>等等等等。
由于<beans>节点是能够嵌套的,因此一旦在<beans>节点内发现了<beans>节点,就会递归调用
org.springframework.beans.factory.xml.DefaultBeanDefinitionDocumentReader#doRegisterBeanDefinitions
继续判断是否须要根据profile来加载,子节点属于defaultElement仍是customElement……bla……bla……bla……

2.2.2 ParseState堆栈

以上文贴的,threadPool bean举例。这是defaultElement中的beanElement,下一步终于能够开始实打实的解析了。
第一步,提取bean节点相关属性,id,alias,lazyInit,singleton,scope,abstract,parent等等等等,而后建立一个BeanDefinition对象
第二步,提取嵌套在bean节点内的节点,好比最多见的<property>、<meta>、<look-up>、<constructor-arg>等等。另外,由于节点直接能够互相嵌套,因此像这样解析一种节点就是一个方法,而后在方法内组合调用,天然而然造成递归。
模块化,提升的不只仅是可读性,还有效率!

parseMetaElements(ele, bd);
parseLookupOverrideSubElements(ele, bd.getMethodOverrides());
parseReplacedMethodSubElements(ele, bd.getMethodOverrides());

parseConstructorArgElements(ele, bd);
parsePropertyElements(ele, bd);
parseQualifierElements(ele, bd);
复制代码

看过spring文档再来看这部分代码,如见故人的感受会很强烈,咱们写文档也该这样,与代码逻辑高度同步,言简意赅。
这些简单的,按规则解析属性的代码就不看了,重点关注一下ParseState。

this.parseState.push(new QualifierEntry(typeName));

this.parseState.pop();
复制代码

一个普普统统的类,把堆栈的方法给包了一层,而后额外提供了一个复制当前堆栈值的方法。除了封装,要达到一样的效果,还可使用继承,就是直接继承Stack类,各有利弊。总之,就把ParseState理解成一个堆栈就行了。
而后,再解析每一种节点类型前,都会往这个堆栈内push此节点的类型与名称,如图。

起初也没搞懂,设计这个堆栈是干吗的。后来反应过来了,解析节点以及属性时,普遍应用了递归。递归这个东西出问题时是很那排查的,debug都看的晕。所以搞一个堆栈,解析前push,解析后pop,出异常了,就把当前堆栈信息提如今异常中。 基于先进后出的特性,立马就能看出是在解析哪一个节点时出的错。

最后的最后,终于见证了一个简单的bean,成为了BeanDefinition,又被封装成BeanDefinitionHolder,并保存到map中:
org.springframework.beans.factory.support.DefaultListableBeanFactory#beanDefinitionMap

2.2.3 customElement

to be continued…… ……
相关文章
相关标签/搜索