先来一张我认识的spring架构,可能有不合理的地方,希望在后续的源码跟踪中,能将目前的疑问逐一探索清楚。
我相信大部分的人每天都在业务层疲于奔命,在工作的前几年其实我也一样,日复一日的CRUD完成那些无聊的业务流程。近两年好了一些基本都集中编码都集中在spring之上的一些框架封装工作,因此对spring的一些拓展机制也多了一些了解,不过从来没有系统性的研究过这些底层结构,有时候实践起来也并不是那么一帆风顺,只能不停的百度,不停的copy。
在读源码开始之前,看了很多B站的spring源码解析视频,UP主提示的一些阅读技巧很是受益,spring也是人写的,只要是人就一定有自己的编码思路,因此后续我的步骤将遵循以下规则:
- 先大而化小,分而治之,每天只突破一个流程。
- 找一些文章看,理清脉络,理解作者的思路。
- 跟踪代码,跟代码的大流程,看作者的注释,不注重细节,先找到感觉。
- 精读代码,看懂每一行代码的用意。
好了,准备开始今天的任务,其实读spring的流程还没整理清楚,我将在下一篇文章结合网上各种建议和自己的思考定出这个流程然后逐步击破。今天先定一个小目标,spring最初都是通过xml做配置管理各种bean的依赖的,因此今天的目的就是项目里面这一大堆xml是如何被spring加载的。
先说思路:
第一步,xml是如何读取成对象的
第二步,读出来的对象将用来做什么
在第一章demo工程中,我已贴出spring加载xml的代码
//1、创建一个Spring的IOC容器对象 ApplicationContext context = new ClassPathXmlApplicationContext("spring-config.xml");
其实spring还有几种方式可以读取xml,可以参考这篇博客
spring读取xml配置文件的6种方法_ZixiangLi的博客-CSDN博客_spring 读取xml
这里我跟踪ClassPathXmlApplicationContext找到最顶层的接口ResourceLoader
这个接口是spring加载资源的一个最顶层接口,ApplicationContext也是从这个接口的拓展接口ResourcePatternResolver继续拓展出去的。
ResourceLoader默认是仅返回一个单独的Resource对象,有一个默认实现是DefaultResourceLoader:
Resource getResource(String location);
而ResourcePatternResolver支持返回一个Resource对象数组,也就是所有的上下文即ApplicationContext和它的子类均支持返回一个对象数组。
Resource[] getResources(String locationPattern) throws IOException;
视线回到ClassPathXmlApplicationContext这个我用的类上,跟踪这个类发现中间有很多层继承关系,这当中关系和思想短时间很难理清,因此先忽略中间找到最顶层的一个抽象类AbstractApplicationContext这个类继承自DefaultResourceLoader并实现ConfigurableApplicationContext(这个类派生于ApplicationContext,大多数的ApplicationContext都要实现此接口。它在ApplicationContext的基础上增加了一系列配置应用上下文的功能。配置应用上下文和控制应用上下文生命周期的方法在此接口中被封装起来,以免客户端程序直接使用),DefaultResourceLoader注释中有说明
也就是说ClassPathXmlApplicationContext拥有上下文和资源操作的能力,并且中间多次继承包含xml、refresh等等字眼的父类猜想就是xml操作和刷新作用这个在后面涉及到再一探究竟。
回到demo测试代码:
ApplicationContext context = new ClassPathXmlApplicationContext("spring-config.xml");
一个xml文件传进这个方法,怎么就变成了上下文?
入口代码,三个参数
第一个参数标志这个类可以同时读取多个配置文件,第二个刷新(不知道啥用),第三个parent也不知道是啥,继续跟踪
从第一行开始 ,第三个参数,原来是上下文。一路往上跟,发现这个parent最终到AbstractApplicationContext才最终被使用,看注释意思是parent不为空,就做环境的融合。
第二行,设置地址
有意思的是这个resolvePath方法,一直往上跟发现
原来这里会把 路径上的通配符给替换了。注意这个地方依然还是路径(spring-config.xml),好像没这么玩过有通配符的路径。
设置了父环境(有就融合),设置了文件的路径(有通配符就替换),应该要放大招了吧,还跟上下文没关呢,猜想是不是要解析xml然后组装对象关系了,接着往下跟:关键的refresh方法,这个方法来自于ConfigurableApplicationContext,跟踪进去,这个地方不贴源码不行了:
public void refresh() throws BeansException, IllegalStateException {
synchronized (this.startupShutdownMonitor) {
// 首先创建应用启动类,并打个标志开始(简单跟了一下,返回了tag、name、id啥的,猜想可能是要记录一些关键信息之类的作用,这个后面可以验证)
StartupStep contextRefresh = this.applicationStartup.start("spring.context.refresh");
// Prepare this context for refreshing.
// 准备啥?
// 1.记录时间、设置激活停止开关
// 2.initPropertySources();默认空实现留给子类,目的是初始化上下文环境中的任何占位符属性源
// 3.getEnvironment().validateRequiredProperties();验证标记为所需的所有属性都是可解析的
// 4.设置一些早期的监听器以及初始化早期的事件
prepareRefresh();
// Tell the subclass to refresh the internal bean factory.
// 告诉子类刷新,beanFactory 就构造出来了,做了些啥操作
// 两种实现
// AbstractRefreshableApplicationContext
// 1.refreshBeanFactory 关闭当前的bean factory,为上下文下一阶段初始化一个刷新工
// 2.getBeanFactory 返回上一步初始化的工厂
// GenericApplicationContext
// 1.默认已经初始化了一个bean factory,依靠外部调用来注册bean
// 2.返回这个默认的bean factory
ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();
// Prepare the bean factory for use in this context.
// 为这个bean factory设置各种属性,表达式解析、资源、国际化、默认的bean等等
prepareBeanFactory(beanFactory);
try {
// Allows post-processing of the bean factory in context subclasses.
// 标准初始化后允许对这个beanFactory进行修改,有3种实现,很多增强器就在这里加入
postProcessBeanFactory(beanFactory);
//bean的后处理开始
StartupStep beanPostProcess = this.applicationStartup.start("spring.context.beans.post-process");
// Invoke factory processors registered as beans in the context.
// 实例化并调用所有注册的增强器,必须在单例实例化之前执行
invokeBeanFactoryPostProcessors(beanFactory);
// Register bean processors that intercept bean creation.
// 实例化并调用所有注册的增强器,必须在bean实例化之前执行
registerBeanPostProcessors(beanFactory);
beanPostProcess.end();
// Initialize message source for this context.
// 初始化消息处理器,即国际化
initMessageSource();
// Initialize event multicaster for this context.
// 初始化事件多播器(观察者模式)
initApplicationEventMulticaster();
// Initialize other special beans in specific context subclasses.
// 这里留了一个模板方法,可以重写特定的上下问刷新工作,有4中实现,做的都是主题资源初始化工作之类的
onRefresh();
// Check for listener beans and register them.
// 把监听器注册到刚才初始化的多播器中
registerListeners();
// Instantiate all remaining (non-lazy-init) singletons.
// 初始化所有剩余的单例bean
finishBeanFactoryInitialization(beanFactory);
// Last step: publish corresponding event.
// 结束刷新,发布刷新事件
finishRefresh();
}
catch (BeansException ex) {
if (logger.isWarnEnabled()) {
logger.warn("Exception encountered during context initialization - " +
"cancelling refresh attempt: " + ex);
}
// Destroy already created singletons to avoid dangling resources.
destroyBeans();
// Reset 'active' flag.
cancelRefresh(ex);
// Propagate exception to caller.
throw ex;
}
finally {
// Reset common introspection caches in Spring's core, since we
// might not ever need metadata for singleton beans anymore...
resetCommonCaches();
contextRefresh.end();
}
}
}
跟完这个大流程以后,发现仍然有些细节没梳理清楚,首先xml中的配置是如何解析的,bean是如何装配的。这里列成2个问题,再次回到源码找答案。
1.xml解析
2.bean的装配
在这个流程中,前两步都是在准备,因此上述两步一定在refresh方法中,因此再次深入refresh
super(parent);
setConfigLocations(configLocations);
if (refresh) {
refresh();
}
在跟踪到obtainFreshBeanFactory()方法中的refreshBeanFactory()发现玄机
跟踪进去发现这个方法
继续跟踪发现加载的接口在BeanDefinitionReader中,这个接口有3个实现,
最终跟踪进去到这里,发现刚才的第一个问题xml是如何解析的已有答案。这当中的逻辑经过了很多次的包装,非常绕。如果不站在作者的角度,要理解或者想通非常困难,所以还是有一个大概的流程处理印象即可,不再继续深究。
第二个问题,bean的装配
回到刚才的refresh方法,继续去找bean相关的代码,发现在finishBeanFactoryInitialization中
preInstantiateSingletons方法找到玄机
大名鼎鼎的多级缓存出现,很复杂后面找一个章节专门讲这个。
由于只有beanName,那肯定还有个一个反射创建对象的过程,继续跟踪createBean方法
然后就是来回的包装,最终在BeanUtils中找到了这句关键的反射代码
对象有了 还需要属性。在doCreateBean中发现populateBean(装饰Bean)
思路其实很清晰,只是spring为了拓展做了很多的封装,因此跟踪代码给人的感觉就很绕。我觉得唯一可行的方案也只有根据正常的思路尽量去理解这个流程,内部的逻辑一般按正常来说,看了记不住,记住了也不见得有用。在理解大流程的前提下,尽可能的去理解并吸收作者包装的思想化为己用就足够了。至此,spring从xml的解析到bean的创建管理就到这里。
下一章就来研究一下上面那个大名鼎鼎的bean缓存。