我该如何学习spring源码以及解析bean定义的注册

前言
本文属于spring源码解析的系列文章之一,文章主要是介绍如何学习spring的源码,希望能够最大限度的帮助到有需要的人。文章总体难度不大,但比较繁重,学习时一定要耐住性子坚持下去。
获取源码
源码的获取有多种途径
GitHub
spring-framework
spring-wiki
可以从GitHub上获取源代码,然后自行编译
maven
使用过maven的都知道可以通过maven下载相关的源代码和相关文档,简单方便。
这里推荐通过maven的方式构建一个web项目。通过对实际项目的运行过程中进行调试来学习更好。
如何开始学习
前置条件
如果想要开始学习spring的源码,首先要求本身对spring框架的使用基本了解。明白spring中的一些特性如ioc等。了解spring中各个模块的作用。
确定目标
首先我们要知道spring框架本身经过多年的发展现在已经是一个庞大的家族。可能其中一个功能的实现依赖于多个模块多个类的相互配合,这样会导致我们在阅读代码时难度极大。多个类之间进行跳跃很容易让我们晕头转向。
所以在阅读spring的源代码的时候不能像在JDK代码时一行一行的去理解代码,需要把有限的精力更多的分配给重要的地方。而且我们也没有必要这样去阅读。
在阅读spring某一功能的代码时应当从一个上帝视角来总览全局。只需要知道某一个功能的实现流程即可,而且幸运的是spring的代码规范较好,大多数方法基本都能见名知意,这样也省去了我们很多的麻烦。
利用好工具
阅读代码最好在idea或者eclipse中进行,这类IDE提供的很多功能很有帮助。
在阅读时配合spring文档更好(如果是自行编译源码直接看注释更好)。
笔记和复习
这个过程及其重要,我以前也看过一些spring的源码,但是好几次都是感觉比较吃力在看过一些后就放弃了。而由于没有做笔记和没有复习的原因很快就忘了。下次想看的时候还要重新看一遍,非常的浪费时间。
下面以IOC为例说明下我是怎么看的,供参考。
IOC
入口:ApplicationContext
在研究源码时首先要找到一个入口,这个入口怎么选择可以自己定,当一定要和你需要看的模块有关联。
比如在IOC中,首先我们想到创建容器是在什么过程?
在程序启动的时候就创建了,而且在启动过程中大多数的bean实例就被注入了。
那问题来了,在启动的时候是从那个类开始的呢?熟悉spring的应该都知道我们平时在做单元测试时如果要获取bean实例,一个是通过注解,另外我们还可以通过构建一个ApplicationContext来获取:
ApplicationContext applicationContext = new ClassPathXmlApplicationContext(“classpath*:application.xml”);
XxxService xxxService = applicationContext.getBean(“xxxService”);
复制代码在实例化ApplicationContext后既可以获取bean,那么实例化的这个过程就相当于启动的过程了,所以我们可以将ApplicationContext当成我们的入口。
ApplicationContext是什么
首先我们要明白的事我们平时一直说的IOC容器在Spring中实际上指的就是ApplicationContext。
如果有看过我之前手写Spring系列文章的同学肯定知道在当时文章中充当ioc容器的是BeanFactory,每当有bean需要注入时都是由BeanFactory保存,取bean实例时也是从BeanFactory中获取。
那为什么现在要说ApplicationContext才是IOC容器呢?
因为在spring中BeanFactory实际上是被隐藏了的。ApplicationContext是对BeanFactory的一个封装,也提供了获取bean实例等功能。因为BeanFactory本身的能力实在太强,如果可以让我们随便使用可能会对spring功能的运行造成破坏。于是就封装了一个提供查询ioc容器内容的ApplicationContext供我们使用。
如果项目中需要用到ApplicationContext,可以直接使用spring提供的注解获取:
@Autowired
private ApplicationContext applicationContext;
复制代码如何使用ApplicationContext
如果我们要使用ApplicationContext可以通过new该类的一个实例即可,定义好相应的xml文件。然后通过下面的代码即可:
@Test
public void testClassPathXmlApplicationContext() {
//1.准备配置文件,从当前类加载路径中获取配置文件
//2.初始化容器
ApplicationContext applicationContext = new ClassPathXmlApplicationContext(“classpath*:application.xml”);
//2、从容器中获取Bean
HelloApi helloApi = applicationContext.getBean(“hello”, HelloApi.class);
//3、执行业务逻辑
helloApi.sayHello();
}
复制代码ApplicationContext的体系
了解一个类,首先可以来看看它的继承关系来了解其先天的提供哪些功能。然后在看其本身又实现了哪些功能。

上图中继承关系从左至右简要介绍其功能。

ApplicationEventPublisher:提供发布监听事件的功能,接收一个监听事件实体作为参数。需要了解的可以通过这篇文章:事件监听
ResourcePatternResolver:用于解析一些传入的文件路径(比如ant风格的路径),然后将文件加载为resource。
HierarchicalBeanFactory:提供父子容器关系,保证子容器能访问父容器,父容器无法访问子容器。
ListableBeanFactory:继承自BeanFactory,提供访问IOC容器的方法。
EnvironmentCapable:获取环境变量相关的内容。
MessageSource:提供国际化的message的解析

配置文件的加载
Spring中每一个功能都是很大的一个工程,所以在阅读时也要分为多个模块来理解。要理解IOC容器,我们首先需要了解spring是如何加载配置文件的。
纵览大局
idea或者eclipse提供了一个很好的功能就是能在调试模式下看到整个流程的调用链。利用这个功能我们可以直接观察到某一功能实现的整体流程,也方便在阅读代码时在不同类切换。
以加载配置文件为例,这里给出整个调用链。

上图中下面的红框是我们写的代码,即就是我们应该开始的地方。下面的红框就是加载配置文件结束的地方。中间既是整体流程的实现过程。在阅读配置文件加载的源码时我们只需要关心这一部分的内容即可。
需要知道的是这里展示出来的仅仅只是跟这个过程密切相关的一些方法。实际上在这个过程中还有需要的方法被执行,只不过执行完毕后方法栈弹出所以不显示在这里。不过大多数方法都是在为这个流程做准备,所以基本上我们也不用太在意这部分内容
refresh()
前面的关于ClassPathXmlApplicationContext的构造函数部分没有啥好说的,在构造函数中调用了一个方法AbstractApplicationContext#refresh。该方法非常重要,在创建IOC容器的过程中该方法基本上是全程参与。主要功能为用于加载配置或这用于刷新已经加载完成的容器配置。通过该方法可以在运行过程中动态的加入配置文件等:
ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext();
ctx.setConfigLocation(“application-temp.xml”);
ctx.refresh();
复制代码AbstractApplicationContext#refresh
public void refresh() throws BeansException, IllegalStateException {
synchronized (this.startupShutdownMonitor) {
prepareRefresh();

		ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();
		// more statement ...
	}
}

复制代码这里将于当前功能不相关的部分删除掉了,可以看到进入方法后就会进入一个同步代码块。这是为了防止在同一时间有多个线程开始创建IOC容器造成重复实例化。
prepareRefresh();方法主要用于设置一些日志相关的信息,比如容器启动时间用于计算启动容器整体用时,以及设置一些变量用来标识当前容器已经被激活,后续不会再进行创建。
obtainFreshBeanFactory();方法用于获取一个BeanFactory,在这一过程中便会加载配置文件和解析用于生成一个BeanFactory。
refreshBeanFactory
refreshBeanFactory方法有obtainFreshBeanFactory方法调用
protected final void refreshBeanFactory() throws BeansException {
if (hasBeanFactory()) {
destroyBeans();
closeBeanFactory();
}
try {
DefaultListableBeanFactory beanFactory = createBeanFactory();
beanFactory.setSerializationId(getId());
customizeBeanFactory(beanFactory);
loadBeanDefinitions(beanFactory);
synchronized (this.beanFactoryMonitor) {
this.beanFactory = beanFactory;
}
}catch (IOException ex) {
throw new ApplicationContextException("I/O error parsing bean definition source for " + getDisplayName(), ex);
}
}
复制代码该方法首先判断是否已经实例化好BeanFactory,如果已经实例化完成则将已经实例化好的BeanFactory销毁。
然后通过new关键字创建一个BeanFactory的实现类实例,设置好相关信息。customizeBeanFactory(beanFactory)方法用于设置是否运行当beanName重复是修改bean的名称(allowBeanDefinitionOverriding)和是否运行循环引用(allowCircularReferences)。
loadBeanDefinitions(beanFactory)方法既是开始加载bean定义的方法。当BeanFactory在加载完所有配置信息后创建,然后将创建好的BeanFactory赋值给当前context下的BeanFactory。
loadBeanDefinitions
protected void loadBeanDefinitions(DefaultListableBeanFactory beanFactory) throws BeansException, IOException {
XmlBeanDefinitionReader beanDefinitionReader = new XmlBeanDefinitionReader(beanFactory);

	beanDefinitionReader.setEnvironment(this.getEnvironment());
	beanDefinitionReader.setResourceLoader(this);
	beanDefinitionReader.setEntityResolver(new ResourceEntityResolver(this));

	initBeanDefinitionReader(beanDefinitionReader);
	loadBeanDefinitions(beanDefinitionReader);
}

复制代码loadBeanDefinitions见名知意其就是用于加载bean定义的方法,在AbstractXmlApplicationContext中定义了一系列该方法的重载方法。上面的方法主要便是引入XmlBeanDefinitionReader。XmlBeanDefinitionReader是一个用于读取xml文件中bean定义的类,其提供了一些诸如BeanFactory和BeanDefinitionRegistery类的属性以供使用。但其实真正的读取操作并没该类完成,其也是作为一个代理存在。
在spring中如果是完成一些类似操作的类的命名都是有迹可循的,比如这里读取xml文件就是以reader结尾,类似的读取注解中bean定义也有如AnnotatedBeanDefinitionReader。如果需要向类中注入一些Spring中的bean,一般是以Aware结尾如BeanFactoryAware等。所以在阅读spring源码时如果遇到这样的类很多时候我们可以直接根据其命名了解其大概的实现方式。
public int loadBeanDefinitions(String location, Set actualResources) throws BeanDefinitionStoreException {
ResourceLoader resourceLoader = getResourceLoader();
if (resourceLoader == null) {
throw new BeanDefinitionStoreException(
“Cannot import bean definitions from location [” + location + “]: no ResourceLoader available”);
}

	if (resourceLoader instanceof ResourcePatternResolver) {
		try {
			Resource[] resources = ((ResourcePatternResolver) resourceLoader).getResources(location);
			int loadCount = loadBeanDefinitions(resources);
			if (actualResources != null) {
				for (Resource resource : resources) {
					actualResources.add(resource);
				}
			}
            //logging
			return loadCount;
		}catch (IOException ex) {
			throw new BeanDefinitionStoreException(
					"Could not resolve bean definition resource pattern [" + location + "]", ex);
		}
	}else {
		Resource resource = resourceLoader.getResource(location);
		int loadCount = loadBeanDefinitions(resource);
		if (actualResources != null) {
			actualResources.add(resource);
		}
        //logging
		return loadCount;
	}
}

复制代码上面代码是loadBeanDefinitions的一个实现类,该方法的主要注意点在于三个地方。
一个是方法中抛出的两个异常,前一个异常时因为ResourceLoader定义的问题,一般来说不需要我们关注。后一个就是配置文件出错了,可能是因为文件本身xml格式出错或者是由于循环引用等原因,具体的原因也会通过日志打印。我们需要对这些异常信息有印象,也不用刻意去记,遇到了能快速定位问题即可。
另一个就是代码中的一个if(){}else{}语句块,判断语句快中都是用于解析配置文件,不同之处在于if中支持解析匹配风格的location,比如classpath*:spring.xml这种,该功能的实现由ResourcePatternResolver提供,ResourcePatternResolver对ResourceLoader的功能进行了增强,支持解析ant风格等模式的location。而else中仅仅只能解析指定的某一文件如spring.xml这种。实际上在ApplicationContext中实现了ResourcePatternResolver,如果也按照spring.xml配置,也是按照ResourceLoader提供的解析方式解析。
最后一处就是Resource类,Resource是spring为了便于加载文件而特意设计的接口。其提供了大量对传入的location操作方法,支持对不同风格的location(比如文件系统或者ClassPath)。其本身还有许多不同的实现类,本质上是对File,URL,ClassPath等不同方式获取location的一个整合,功能十分强大。即使我们的项目不依赖spring,如果涉及到Resource方面的操作也可以使用Spring中的Resource。
public int loadBeanDefinitions(EncodedResource encodedResource) throws BeanDefinitionStoreException {
//log and assert
Set currentResources = this.resourcesCurrentlyBeingLoaded.get();
if (currentResources == null) {
currentResources = new HashSet(4);
this.resourcesCurrentlyBeingLoaded.set(currentResources);
}
if (!currentResources.add(encodedResource)) {
throw new BeanDefinitionStoreException(
“Detected cyclic loading of " + encodedResource + " - check your import definitions!”);
}
try {
InputStream inputStream = encodedResource.getResource().getInputStream();
try {
InputSource inputSource = new InputSource(inputStream);
if (encodedResource.getEncoding() != null) {
inputSource.setEncoding(encodedResource.getEncoding());
}
return doLoadBeanDefinitions(inputSource, encodedResource.getResource());
}
finally {
inputStream.close();
}
}catch (IOException ex) {
throw new BeanDefinitionStoreException(
"IOException parsing XML document from " + encodedResource.getResource(), ex);
}finally {
currentResources.remove(encodedResource);
if (currentResources.isEmpty()) {
this.resourcesCurrentlyBeingLoaded.remove();
}
}
}
复制代码该方法依旧是loadBeanDefinitions的重载方法。
方法传入一个EncodedResource,该类可以通过制定的字符集对Resource进行编码,利于统一字符编码格式。
然后try语句块上面的代码也是比较重要的,主要功能便是判断是否有配置文件存在循环引用的问题。
循环应用问题出现在比如我加载一个配置文件application.xml,但是在该文件内部又通过import标签引用了自身。在解析到import时会加载import指定的文件。这样就造成了一个死循环,如果不解决程序就会永远启动不起来。
解决的方法也很简单,通过一个ThreadLocal记录下当前正在加载的配置文件名称(包括路径),每一次在加载新的配置文件时从ThreadLocal中取出放入到set集合中,通过set自动去重的特性判断是否循环加载了。当一个文件加载完成后,就从ThreadLocal中去掉(finally)。这里是判断xml文件时否重复加载,而在spring中判断bean是否循环引用是虽然实现上有点差别,但基本思想也是这样的。
doLoadBeanDefinitions(InputSource, Resource)
到了这一步基本上才算是真正开始解析了。该方法虽然代码行数较多,但是大多都是异常处理,异常代码已经省略。我们需要关注的就是try中的两句代码。
protected int doLoadBeanDefinitions(InputSource inputSource, Resource resource)
throws BeanDefinitionStoreException {
try {
Document doc = doLoadDocument(inputSource, resource);
return registerBeanDefinitions(doc, resource);
}catch (Exception ex) {
//多个catch语句块
}
}
复制代码Document doc = doLoadDocument(inputSource, resource)就是读取配置文件并将其内容解析为一个Document的过程。解析xml一般来说并不需要我们特别的去掌握,稍微有个了解即可,spring这里使用的解析方式为Sax解析,有兴趣的可以直接搜索相关文章,这里不进行介绍。下面的registerBeanDefinitions才是我们需要关注的地方。
registerBeanDefinitions(Document, Resource)
public int registerBeanDefinitions(Document doc, Resource resource) throws BeanDefinitionStoreException {
BeanDefinitionDocumentReader documentReader = createBeanDefinitionDocumentReader();
documentReader.setEnvironment(getEnvironment());
int countBefore = getRegistry().getBeanDefinitionCount();
documentReader.registerBeanDefinitions(doc, createReaderContext(resource));
return getRegistry().getBeanDefinitionCount() - countBefore;
}
复制代码在进入该方法后首先创建了一个BeanDefinitionDocumentReader的实例,这和之前的用于读取xml的reader类一样,只不过该类是用于从xml文件中读取BeanDefinition。
Environment
在上面的代码中给Reader设置了Environment,这里谈一下关于Environment。
Environment是对spring程序中涉及到环境有关的一个描述集合,主要分为profile和properties。
profile是一组bean定义的集合,通过profile可以指定不同的配置文件用以在不同的环境中,如测试环境,生产环境的配置分开。在部署时只需要配置好当前所处环境值即可按不同分类加载不同的配置。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值