在前面我们介绍过Spring将资源抽象为Resource,并且提供了不同的实现处理不同类型的资源,比如File、Http、Ftp、classpath等等,本篇我们讲述Spring容器的创建与资源的加载,对于Spring容器,我们说XmlBeanFactory或者ApplicationContext都可以是Spring容器,都可以从其中获取到Bean,但是其核心类为DefaultListableBeanFactory,创建IOC容器的过程也就是创建DefaultListableBeanFactory实例的过程,本篇将会讲解创建容器的过程和资源文件加载的过程。在这之前,我们先介绍Java读取资源文件的几种方式。最简单读取资源的方式估计就是使用java.io.File和java.io包提供的InputStream/OutputStream等API了,示例代码如下所示:
File file = new File("");
InputStream inputStream = new FileInputStream(file);
除了Java.io提供的API之外,我们还可以通过ClassLoader加载资源文件,用于加载classpath下的资源文件。ClassLoader提供了三个方法用于加载资源文件,代码如下所示:
// 返回一个URL标识
ClassLoader classLoader = FileDemo.class.getClassLoader();
URL url = classLoader.getResource("");
// 获取一个输入流
InputStream inputStream = classLoader.getResourceAsStream("");
// 获取一个URL列表
Enumeration<URL> enumeration = classLoader.getResources("");
在了解Java的文件资源加载机制之后,我们开始介绍Spring的资源加载机制,在前面我们说过在Spring中资源是被抽象为Resource,下面我们介绍Spring是如何将资源抽象为Resource的,也就是如何加载资源的。Spring也提供了容器的创建方式,比如XmlBeanFactory,ApplicationContext等,我们需要在构造方法中传入一个资源文件路径,在创建的容器中加载资源文件,如下示例是Spring常用的方式,其中XmlBeanFactory已经不建议使用。
XmlBeanFactory xmlBeanFactory= new XmlBeanFactory(new ClassPathResource("spring.xml"));
ApplicationContext classpathContext = new ClassPathXmlApplicationContext("spring.xml");
ApplicationContext fileSystemContext = new FileSystemXmlApplicationContext("spring.xml");
资源的加载是在其容器完成的,这里我们以ClassPathXmlApplicationContext为例,介绍Spring文件的加载过程。在ClassPathXmlApplicationContext中,根据参数的不同最终会调用以下两个构造方法中的一个,其构造如下:
public ClassPathXmlApplicationContext(String[] configLocations, boolean refresh, ApplicationContext parent)throws BeansException {
super(parent);
setConfigLocations(configLocations);
if (refresh) {
refresh();
}
}
public ClassPathXmlApplicationContext(String[] paths, Class<?> clazz, ApplicationContext parent) throws BeansException {
super(parent);
......
this.configResources = new Resource[paths.length];
for (int i = 0; i < paths.length; i++) {
this.configResources[i] = new ClassPathResource(paths[i], clazz);
}
refresh();
}
上面的代码主要逻辑就是设置资源路径或者封装资源为Resource,然后调用refresh()方法,资源的加载以及容器的创建、Bean的解析与注册等都是在该方法中完成,下面我们首先讲解的是容器的创建和资源的加载流程,后续再讲解Bean的解析与注册流程。下面我们查看refresh()方法:
public void refresh() throws BeansException, IllegalStateException {
synchronized (this.startupShutdownMonitor) {
// 准备刷新容器.
prepareRefresh();
// 告诉子类刷新内部的Bean工厂,其实就是一个创建容器的过程.也是我们主要讲解的
ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();
.....//其他逻辑后续讲解
}
上面的代码中,通过obtainFreshBeanFactory()创建一个IOC容器,下面我们主要讲解容器的创建过程,其方法如下:
protected ConfigurableListableBeanFactory obtainFreshBeanFactory() {
refreshBeanFactory();
ConfigurableListableBeanFactory beanFactory = getBeanFactory();
return beanFactory;
}
上面调用的两个方法都是调用子类的方法,其有两个直接子类分别为GenericApplicationContext和AbstractRefreshableApplicationContext,其类层次结构图如下:
上面的方法会根据不同的实现调用不同的子类,比如如果是ClassPathXmlApplicationContext或者FileSystemXmlApplicationContext会调用AbstractRefreshableApplicationContext的方法,其他的实现比如AnnotationConfigApplicationContext会调用GenericApplicationContext中的方法,而容器的创建就是在这两个子类中创建,在子类中有一个DefaultListableBeanFactory的实例,该实例是Spring IOC的核心,几乎很多IOC操作都是在该实例中进行,也可以称其为Spring容器。如下为两个子类的所有的实现:
对于GenericApplicationContext来说DefaultListableBeanFactory实例实在其构造方法中创建的,代码如下就创建了一个IOC容器。
public GenericApplicationContext() {
this.beanFactory = new DefaultListableBeanFactory();
}
对于AbstractRefreshableApplicationContext则是在refreshBeanFactory()方法中创建的,代码如下:
protected final void refreshBeanFactory() throws BeansException {
//如果已经存在,销毁Bean,销毁容器
if (hasBeanFactory()) {
destroyBeans();
closeBeanFactory();
}
try {
//创建工厂,创建IOC容器
DefaultListableBeanFactory beanFactory = createBeanFactory();
beanFactory.setSerializationId(getId());
customizeBeanFactory(beanFactory);
//加载Bean定义,即加载处理资源
loadBeanDefinitions(beanFactory);
synchronized (this.beanFactoryMonitor) {
this.beanFactory = beanFactory;
}
}
......//catch代码
}
在上面的方法中,通过createBeanFactory()创建了IOC容器,即DefaultListableBeanFactory,我们查看createBeanFactory()方法源码,这里涉及到父子容器的问题,这里不做讲解,只需要知道在这里创建了一个IOC容器即可,后续我们再介绍Spring的IOC容器。
protected DefaultListableBeanFactory createBeanFactory() {
return new DefaultListableBeanFactory(getInternalParentBeanFactory());
}
既然IOC容器已经创建完毕,下面就是加载资源了,加载资源是在也是在refreshBeanFactory()完成的,调用的是loadBeanDefinitions(beanFactory)方法,该方法是在其子类AbstractXmlApplicationContext中,并且是通过XmlBeanDefinitionReader 实例加载资源,下面我们继续查看代码:
protected void loadBeanDefinitions(DefaultListableBeanFactory beanFactory) throws BeansException, IOException {
// 创建一个XmlBeanDefinitionReader实例.
XmlBeanDefinitionReader beanDefinitionReader = new XmlBeanDefinitionReader(beanFactory);
//为XmlBeanDefinitionReader配置Environment实例,ResourceLoader实例
//和EntityResolver实例,这里的配置是为了解析资源文件所用这里不做介绍
beanDefinitionReader.setEnvironment(this.getEnvironment());
beanDefinitionReader.setResourceLoader(this);
beanDefinitionReader.setEntityResolver(new ResourceEntityResolver(this));
//初始化XmlBeanDefinitionReader实例
initBeanDefinitionReader(beanDefinitionReader);
//加载资源
loadBeanDefinitions(beanDefinitionReader);
}
在创建XmlBeanDefinitionReader 实例之后,为XmlBeanDefinitionReader 实例设置一些实例并且初始化之后调用loadBeanDefinitions(beanDefinitionReader)加载资源。代码如下:
protected void loadBeanDefinitions(XmlBeanDefinitionReader reader) throws BeansException, IOException {
Resource[] configResources = getConfigResources();
if (configResources != null) {
reader.loadBeanDefinitions(configResources);
}
String[] configLocations = getConfigLocations();
if (configLocations != null) {
reader.loadBeanDefinitions(configLocations);
}
}
上面的代码中加载了两种形式的资源,一种是直接加载Resource实例,第二种则是加载字符串路径形式的资源文件,其核心实现在类AbstractBeanDefinitionReader中。其中以字符串路径形式的资源会被封装为Resource实例,其代码如下:
public int loadBeanDefinitions(String location, Set<Resource> actualResources) throws BeanDefinitionStoreException {
//获取ResourceLoader实例
ResourceLoader resourceLoader = getResourceLoader();
if (resourceLoader instanceof ResourcePatternResolver) {
// 处理Resource实例类型的资源.
Resource[] resources = ((ResourcePatternResolver) resourceLoader).getResources(location);
//调用loadBeanDefinitions(Resource resource)加载资源
int loadCount = loadBeanDefinitions(resources);
......
return loadCount;
}......
else {
//如果是字符串类型的资源,加载资源封装为Resource
Resource resource = resourceLoader.getResource(location);
//调用loadBeanDefinitions(Resource resource)加载资源
int loadCount = loadBeanDefinitions(resource);
......
return loadCount;
}
}
如上代码所示,最终资源的加载调用的是loadBeanDefinitions(Resource resource)方法,该方法在AbstractBeanDefinitionReader子类中,不同的资源采用不用的Reader类,如下为AbstractBeanDefinitionReader的实现的层次结构图。
因为我们一般使用XML作为配置文件,因此,我们可以查看XmlBeanDefinitionReader的方法如下,最终就是将资源封装为InputSource,然后调用doLoadBeanDefinitions加载资源文件。
public int loadBeanDefinitions(Resource resource) throws BeanDefinitionStoreException {
return loadBeanDefinitions(new EncodedResource(resource));
}
public int loadBeanDefinitions(EncodedResource encodedResource) throws BeanDefinitionStoreException {
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());
}
......
}
doLoadBeanDefinitions方法的源码,代码如下:
protected int doLoadBeanDefinitions(InputSource inputSource, Resource resource)
throws BeanDefinitionStoreException {
try {
//加载资源解析为Document实例
Document doc = doLoadDocument(inputSource, resource);
//解析并且注册Bean
return registerBeanDefinitions(doc, resource);
}
......//catch代码
}
protected Document doLoadDocument(InputSource inputSource, Resource resource) throws Exception {
return this.documentLoader.loadDocument(inputSource, getEntityResolver(), this.errorHandler, getValidationModeForResource(resource), isNamespaceAware());
}
上面的代码分为两部分,一是加载资源解析XML为Document实例,二是注册Bean,这里我们主要讲解如何封装为Document实例的,也就是doLoadDocument方法。其实这个过程就是将配置的Xml解析为Document的过程,这里我们需要了解的就是XMl的验证模式,也就是doLoadDocument方法中调用的getValidationModeForResource方法,其方法如下:
protected int getValidationModeForResource(Resource resource) {
int validationModeToUse = getValidationMode();
//如果验证模式不是自动验证,返回验证模式
if (validationModeToUse != VALIDATION_AUTO) {
return validationModeToUse;
}
//根据资源检测验证模式
int detectedMode = detectValidationMode(resource);
if (detectedMode != VALIDATION_AUTO) {
return detectedMode;
}
//返回验证模式
return VALIDATION_XSD;
}
验证模最终是通过XmlValidationModeDetector的方法:detectValidationMode(InputStream inputStream)检测的,它读取资源文件,判断是否包含DOCTYPE字符串,如果包含则为DTD验证模式,否则为XSD验证模式
public int detectValidationMode(InputStream inputStream) throws IOException {
BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
.....
boolean isDtdValidated = false;
String content;
while ((content = reader.readLine()) != null) {
//读取字符串,并且过滤文件中的注释
content = consumeCommentTokens(content);
if (this.inComment || !StringUtils.hasText(content)) {
continue;
}
//如果包含DOCTYPE,是DTD验证模式
if (hasDoctype(content)) {
isDtdValidated = true;
break;
}
if (hasOpeningTag(content)) {
break;
}
//返回验证模式
return (isDtdValidated ? VALIDATION_DTD : VALIDATION_XSD);
}
......
}
对于方法getEntityResolver(),是获取一个XML解析器,也就是org.xml.sax.EntityResolver实例。官方是这么解释EntityResolver,EntityResolver 的作用就是项目本身就可以提供一个如何寻找DTD 的声明方法, 即:由程序来实现寻找DTD声明的过程,比如我们将DTD放在项目的某处在实现时直接将此文档读取并返回个SAX即可,这样就避免了通过网络来寻找DTD的声明,其接口定义如下:
public abstract InputSource resolveEntity (String publicId, String systemId) throws SAXException, IOException;
通过传入一个publicId,和systemId返回一个InputSource实例,如下为Spring的两种配置XMl的模式,如果为xsd模式,publicId为null,systemId为http://www.springframework.org/schema/beans/spring-beans-2.5.xsd,如果采用DTD模式,publicId为-//SPRING//DTD BEAN//EN,systemId为http://www.springframework.org/dtd/spring-beans.dtd,不同的模式采用了不用的EntityResolver ,如下为Spring提供的EntityResolver 实现:
其中BeansDtdResolver和PluggableSchemaResolver是被封装在另外两个使用的,因此我们的getEntityResolver()会创建的是ResourceEntityResolver或者DelegatingEntityResolver实例,在ResourceEntityResolver或者DelegatingEntityResolver类中创建了BeansDtdResolver和PluggableSchemaResolve实例,代码如下所示:
protected EntityResolver getEntityResolver() {
if (this.entityResolver == null) {
// 获取一个EntityResolver实例
ResourceLoader resourceLoader = getResourceLoader();
if (resourceLoader != null) {
this.entityResolver = new ResourceEntityResolver(resourceLoader);
}else {
this.entityResolver = new DelegatingEntityResolver(getBeanClassLoader());
}
}
return this.entityResolver;
}
下面我们以DelegatingEntityResolver为例介绍BeansDtdResolver和PluggableSchemaResolver的创建,如下为其构造方法,分别创建了两个实例
public DelegatingEntityResolver(ClassLoader classLoader) {
this.dtdResolver = new BeansDtdResolver();
this.schemaResolver = new PluggableSchemaResolver(classLoader);
}
public InputSource resolveEntity(String publicId, String systemId) throws SAXException, IOException {
if (systemId != null) {
//如果systemId以dtd结尾,使用BeansDtdResolver
if (systemId.endsWith(DTD_SUFFIX)) {
return this.dtdResolver.resolveEntity(publicId, systemId);
} else if (systemId.endsWith(XSD_SUFFIX)) {
//如果systemId以xsd结尾,使用PluggableSchemaResolver
return this.schemaResolver.resolveEntity(publicId, systemId);
}
}
return null;
}
至于如何将XML资源文件解析为Document实例,这里不再介绍,这已经属于Java解析XMl的范畴了,可以自行了解。综上所述:本篇主要介绍了Spring IOC容器的创建也就是DefaultListableBeanFactory的创建,以及XML配置文件的加载,XML加载之后后续的操作就是注册Bean的操作也就是前面展示代码中的registerBeanDefinitions(doc, resource)的操作,这部分代码我们在后续的博文中讲解,这里不做讲解。最后附上Spring创建容器的时序图: