序
// 缓慢更新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()方法。
initWebApplicationContext()
初始化web应用上下文,首先要创建一个上下文对象。创建时,优先从web.xml里寻找,有没有名为“contextClass”参数,有的话根据名字获取Class对象,然后以反射的方式将上下文对象new出来,最后new出来的一定是ConfigurableWebApplicationContext类型。如果web.xml里不存在“contextClass”,那么默认使用org.springframework.web.context.support.XmlWebApplicationContext类来创建上下文。
protected WebApplicationContext createWebApplicationContext(ServletContext sc) {
Class<?> contextClass = determineContextClass(sc);// 这里选择上下文类的Class对象
// 此处省略……
return (ConfigurableWebApplicationContext) BeanUtils.instantiateClass(contextClass);
}
root上下文创建完后,还会从web.xml里查询是否配置了parent上下文,有的话就加载并指定,但是这个操作,我工作这几年也没见过哪个项目配置过,就暂不理会了。下一步:
configureAndRefreshWebApplicationContext()
配置并刷新上下文,这个方法内,比较重要的逻辑是从web.xml里读取spring各项配置文件的路径,具体什么文件因项目而异,我个人的习惯是定义application-main.xml,然后在main.xml里,import数据库,缓存,队列等的xml。
<context-param><!---->
<param-name>contextConfigLocation</param-name>
<param-value>classpath*:config/applicationContext-main.xml</param-value>
</context-param>
读取完配置文件路径后,spring会再从web.xml中初始化一些参数,以及新建一个ConfigurableEnvironment对象。
初始化的参数这几年项目中也没见过,暂不详述。主要是ConfigurableEnvironment,这个类的对象接下来出现的频率还比较高,通过这个接口可以获取到很多spring需要的配置参数,然后记录一下类之间的包含关系。
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。有意思的是,这个方法如何做到线程同步。
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类。这段代码研究不深,略过。
protected DefaultListableBeanFactory createBeanFactory() {
return new DefaultListableBeanFactory(getInternalParentBeanFactory());
}
DefaultListableBeanFactory类需要重点关注,因为这个对象是容器的核心,比方说要根据名字或类型从容器拿对象,最后靠的就是它的方法。比如:
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对象下找到了文件目录
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此节点的类型与名称,如图。
最后的最后,终于见证了一个简单的bean,成为了BeanDefinition,又被封装成BeanDefinitionHolder,并保存到map中:
org.springframework.beans.factory.support.DefaultListableBeanFactory#beanDefinitionMap
2.2.3 customElement
to be continued…… ……