Spring IOC容器的初始化过程(1)

时间:2022-03-21 19:41:01

概述

IOC 容器的初始化过程主要分为3个步骤:

  1. Resource的定位:这个指的是利用各种方式来找到BeanDefinition的位置,BeanDefinition是对Bean定义的一个抽象,Resource是对BeanDefinition的所处位置的抽象(想想你使用spring的时候写的xml文件)。
  2. BeanDefinition的载入和解析:通过第一步,我们能够找到Resource的位置,并获得它的实例,在这一步,我们会从Resource中读取到BeanDefinition。
  3. 向IOC容器注册这些BeanDefinition:将第二步所获得的BeanDefinition注册到IOC容器中,这样我们就能够通过IOC容器获取定义的bean的实例对象了。

本篇博客,我们来分别分析Resource的定位过程

Resource的定位

首先我们来看一看Resource这个接口,Resource定义的方法如下:

  1. exists():返回类型为boolean,用于判断对应的资源是否真的存在。
  2. isReadable():返回类型为boolean,用于判断对应资源的内容是否可读。需要注意的是当其结果为true的时候,其内容未必真的可读,但如果返回false,则其内容必定不可读。
  3. isOpen():返回值为boolean,用于判断当前资源是否代表一个已打开的输入流,如果结果为true,则表示当前资源的输入流不可多次读取(可以理解为这个资源不在你的硬盘上面,是一个你一关机就没了的资源),而且在读取以后需要对它进行关闭,以防止内存泄露。该方法主要针对于InputStreamResource,实现类中只有它的返回结果为true,其他都为false。
  4. getURL():返回值URL,返回当前资源对应的URL。如果当前资源不能解析为一个URL则会抛出异常。如ByteArrayResource就不能解析为一个URL。
  5. getFile():返回值File,返回当前资源对应的File。如果当前资源不能以绝对路径解析为一个File则会抛出异常。
  6. getInputStream():返回值InputStream,获取当前资源代表的输入流。

总之,Resource其实就是对一个只读资源(文件)的一种抽象,Resource又分为很多种,常见的有:

  1. ClassPathResource:这个资源代表你的项目中写的资源,处于你的类路径下面,我们常常把一些配置文件放在项目里面,如果要读取那些文件,我们就需要ClassPathResource。当然,文件路径写相对路径
  2. FileSystemResource:这个资源代表你的计算机上的资源,从名称来看,这个资源的定位范围为整个文件系统。使用的时候文件路径写绝对路径。
  3. UrlResource:这个资源的范围更广泛一些,我们可以通过传入URL地址来获取到其他计算机资源文件。
  4. 其他:ByteArrayResource,ServletContextResource,InputStreamResource等,不再一一描述。

    现在我们明白了Recourse是什么一回事情,单单获取一个资源也很简单,以ClassPathResource为例,我们只需要以下代码即可获得:

Resource resource = new ClassPathResource("beans.xml");

然而为了保证IOC容器的封装性和可扩展性,这样简单粗暴的获取Resource显然是不明智的,那么spring是如何做到优雅的获取Resource的呢?我们来看一看一个抽象类AbstractRefreshableApplicationContext。

从名称上来看,这个类首先是一个ApplicationContext,我们知道ApplicationContext是实现了BeanFactory接口的类,说了这么多实际上我只想表明AbstractRefreshableApplicationContext是一个IOC容器。其次这个IOC容器是Refreshable(可刷新的)的类,那么可刷新是什么意思呢?意思就是说,如果我想改变这个IOC容器的BeanDifinition,但是我又不想重新创建一个IOC容器,此时这个IOC容器必然有个功能叫做可刷新。

做了这么多铺垫以后,我们终于可以进入正题了,废话不多说,直接上代码:

//定位Resource的核心方法
public int loadBeanDefinitions(String location, Set actualResources) throws BeanDefinitionStoreException{
ResourceLoader resourceLoader = getResourceLoader();
if(resourceLoader == null){
throw new BeanDefinitionStoreException(
"哎呀呀,资源加载器找不着了");
}
/*
上面代码的作用是获得一个资源加载器ResourceLoader,有了资源加载器,我们就可以通过我们传入的location(XML的文件路径)来获取一个Resource
*/

if(resourceLoader instanceof ResourcePatternResolver){
/*
ResourcePatternResolver,从名字上可以看出带有pattern的功能,比如我传入的路径是一个正则表达式,那么这条路径就有可能对应多个Resource,resourceLoader是可以用户自己定义的。
*/

try{
Resource[] resources = ((ResourcePatternResolver)resourceLoader).getResources(location);
int loadCount =loadBeanDefinitions(resources);
/*
注意,此处的loadBeanDefinitions方法并不是递归调用,而是调用了它的一个重载方法,这里的resourceLoader
的作用是将location所表示的文件封装成一个个的Resource,本身不负责解析,而此处loadBeanDefinitions方
法会负责解析这些Resource,并返回成功解析的资源个数,这个loadBeanDifinition方法会在后面提到。
*/

if (actualResources != null) {
for (Resource resource : resources) {
actualResources.add(resource);
}
}
/*
上面的代码的作用是保存我们解析到的资源
*/

if (logger.isDebugEnabled()) {
logger.debug("Loaded " + loadCount + " bean definitions from location pattern [" + location + "]");
}
return loadCount;
} catch (IOException ex) {
throw new BeanDefinitionStoreException(
"Could not resolve bean definition resource pattern [" + location + "]", ex);
}
} else {
// 这里的resourceLoader就比较简单了,它只能解析具体的一条路径
Resource resource = resourceLoader.getResource(location);
int loadCount = loadBeanDefinitions(resource);
if (actualResources != null) {
actualResources.add(resource);
}
if (logger.isDebugEnabled()) {
logger.debug("Loaded " + loadCount + " bean definitions from location [" + location + "]");
}
return loadCount;
}
}
}
}

从上面的代码我们可以看出,Resource的定位实际上在下面所述的代码中获取

Resource resource = resourceLoader.getResource(location);

在继续说下去之前,我们有必要理一理ResourceLoader和AbstractRefreshableApplicationContext之间的关系
Spring IOC容器的初始化过程(1)

我们可以看到AbstractRefreshableApplicationContext实际上是实现了ResourceLoader这个接口的,但是,问题也来了,我在上面的方法了为什么还要在get一个ResourceLoader,然后再调用ResourceLoader的getResource方法呢,直接用自己的难道不行吗?其实这个问题很好解答,这其实是一个装饰者模式。AbstractRefreshableApplicationContext拥有一个成员变量ResourceLoader,通常它默认是DefaultResourceLoader(图里有),AbstractRefreshableApplicationContext可以通过包装它的一些方法,来完成它不能完成的一些事情,像DefaultResourceLoader的子类FileSystemResourceLoader也可以被包装,这样resourceLoader.getResource就变得多种多样了。

现在我们来看看DefaultResourceLoader的getResource方法,一且就真相大白了。

public Resource getResource(String location){
Assert.notNull(location, "Location must not be null");
if(location.startsWith(CLASSPATH_URL_PREFIX)){
return new ClassPathResource(
location.subString(CLASSPATH_URL_PREFIX.length()),
getClassLoader());
/*
带CLASSPATH_URL_PREFIX前缀的路径会被理解成相对路径,这里干的事情是去掉这个前缀,然后使用该路径来创建一个ClassPathResource,并返回
*/

}else{
try{
URL url = new URL(location);
return new UrlResource(url);
/*
这里尝试创建一个UrlResource,如果成功了,那么这个路径其实就是一条url路径,如果失败了,使用catch语句中的代码来补救
*/

}catch(MalformedURLException ex){
/*
这个getResourceByPath可以被子类重写,通过子类,我们就形成了多样化的Resource的生成模式
*/

return getResourceByPath(location);
}
}
}