<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... | ↓ |
// Allow for the collection of early ApplicationEvents,
// to be published once the multicaster is available...
this.earlyApplicationEvents = new LinkedHashSet<ApplicationEvent>();
复制代码
ok,这个方法结束。
缓存
/** Bean factory for this context */
private DefaultListableBeanFactory beanFactory;
复制代码
在获取以前会先检查是否已经存在BeanFactory,检查的依据,就是看对象是否是null。有意思的是,这个方法如何作到线程同步。
tomcat
/** 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();
复制代码
而后进入又一个重头戏:
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对象下找到了文件目录
类名 | 简单描述 | |
---|---|---|
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以及属性的方法 | ↓ |
要先从这个方法开始,前面有些代码我可能就省略了。
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……
以上文贴的,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此节点的类型与名称,如图。