Spring如何通过XMLBeanFactory加载一个Bean?

我们深入分析以下功能的代码实现:

BeanFactory bf = new XmlBeanFactory(new ClassPathResource("applicationContext.xml"));

通过XmlBeanFactory初始化时序图,我们来看一看上面代码的执行逻辑。
Spring如何通过XMLBeanFactory加载一个Bean?_第1张图片

根据时序图,可以清晰的看到:

  1. 首先调用ClassPathResource构造函数来构造Resource资源文件的实例对象
  2. 使用Resource进行XmlBeanFactory的初始化

Resource资源是如何封装的呢

配置文件封装

Spring的配置文件读取是通过ClassPathResource进行封装的,如new ClassPathResource(“applicationContext.xml”),那么ClassPathResource完成了什么功能呢?

Spring对其内部使用到的资源实现了自己的抽象结构:Resource接口封装底层资源

public interface InputStreamSource {
	InputStream getInputStream() throws IOException;
}
public interface Resource extends InputStreamSource {
    boolean exists();
    default boolean isReadable() {
		return true;
	}
	default boolean isOpen() {
		return false;
	}
	URL getURL() throws IOException;
	URI getURI() throws IOException;
	File getFile() throws IOException;
	long lastModified() throws IOException;
	Resource createRelative(String relativePath) throws IOException;
	String getFilename();
	String getDescription();
}

Spring如何通过XMLBeanFactory加载一个Bean?_第2张图片
对于不同来源的资源文件都有相应的Resource实现:

  • 文件(FileSystemResource)
  • ClassPath资源(ClassPathResource)
  • URL资源(UrlResource)
  • InputStream资源(InputStreamResource)
  • Byte数组(ByteArrayResource)

在日常的开发工作中,资源文件的加载也是常用的,可以直接使用Spring提供的类,比如在希望加载文件时可以使用以下代码:

Resource resource = new ClassPathResource("beanFactoryTest.xml");
InputStream inputStream = resource.getInputStream();

有了Resource接口便可以对所有资源文件进行统一处理。至于实现,其实是非常简单地,以getInputStream为例,ClassPathResource中的实现方式便是通过class或者classLoader提供的底层方法进行调用,而对于FileSystemResource,实现更简单,直接使用FileInputStream对文件进行实例化。

ClassPathResource.java:

@Override
public InputStream getInputStream() throws IOException {
	InputStream is;
	if (this.clazz != null) {
		is = this.clazz.getResourceAsStream(this.path);
	}
	else if (this.classLoader != null) {
		is = this.classLoader.getResourceAsStream(this.path);
	}
	else {
		is = ClassLoader.getSystemResourceAsStream(this.path);
	}
	if (is == null) {
		throw new FileNotFoundException(getDescription() + " cannot be opened because it does not exist");
	}
	return is;
}

FileSystemResource.java:

@Override
public InputStream getInputStream() throws IOException {
	try {
		return Files.newInputStream(this.file.toPath());
	}
	catch (NoSuchFileException ex) {
		throw new FileNotFoundException(ex.getMessage());
	}
}

当通过Resource相关类完成了对配置文件进行封装后,配置文件的读取工作就全权交给XmlBeanDefinitionReader来处理了

XmlBeanFactory的初始化有若干办法,Spring中提供了很多的构造函数,在这里分析的是使用Resource实例作为构造函数参数的办法,代码如下:

public class XmlBeanFactory extends DefaultListableBeanFactory {

	private final XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(this);

	
	public XmlBeanFactory(Resource resource) throws BeansException {
		this(resource, null);
	}
    //parentBeanFactory为父类BeanFactory用于factory合并,可以为空
	public XmlBeanFactory(Resource resource, BeanFactory parentBeanFactory) throws BeansException {
		super(parentBeanFactory);
		this.reader.loadBeanDefinitions(resource);
	}

}

以上代码this.reader.loadBeanDefinitions(resource)才是资源加载的真正实现

我们可以看到时序图中提到的XmlBeanDefinitionReader加载数据就是在这里完成的,在这之前super(parentBeanFactory),调用父类构造函数初始化的过程,查看代码:

/**
 * Create a new AbstractAutowireCapableBeanFactory.
 */
public AbstractAutowireCapableBeanFactory() {
	super();
	ignoreDependencyInterface(BeanNameAware.class);
	ignoreDependencyInterface(BeanFactoryAware.class);
	ignoreDependencyInterface(BeanClassLoaderAware.class);
}

/**
 * Create a new AbstractAutowireCapableBeanFactory with the given parent.
 * @param parentBeanFactory parent bean factory, or {@code null} if none
 */
public AbstractAutowireCapableBeanFactory(@Nullable BeanFactory parentBeanFactory) {
	this();
	setParentBeanFactory(parentBeanFactory);
}

ignoreDependencyInterface方法的主要功能是忽略给定接口的自动装配功能,这样做的目的是什么?会产生怎样的效果呢?

举例来说,当A中有属性B,那么当Spring在获取A的Bean的时候如果其属性B还没有初始化,那么Spring会自动初始化B,这也是Spring中提供的一个重要特性。但是,某些情况下B不会被初始化,其中的一种情况就是B实现了BeanNameAware接口。

Spring是这样介绍的:自动装配时忽略给定的依赖接口,典型应用是通过其他方式解析Application上下文注册依赖,类似于BeanFactory通过BeanFactoryAware进行注入或者ApplicationContext通过ApplicationContextAware进行注入。

加载Bean

XmlBeanFactory构造函数中调用了XmlBeanDefinitionReader类型的reader属性提供的方法this.reader.loadBeanDefinitions(resource),而这句代码则是整个资源加载的切入点,我们来看看这个方法的时序图:
Spring如何通过XMLBeanFactory加载一个Bean?_第3张图片

总结上述处理过程如下:

  1. 封装资源文件,进入XmlBeanDefinitionReader后首先对参数Resource使用EncodedResource类进行封装
  2. 获取输入流。从Resource中获取对应的InputStream并构造InputSource
  3. 通过构造的InputSource实例和Resource实例继续调用函数doLoadBeanDefinitions
@Override
public int loadBeanDefinitions(Resource resource) throws BeanDefinitionStoreException {
	return loadBeanDefinitions(new EncodedResource(resource));
}

EncodedResource的作用主要用于对资源文件的编码进行处理。其中的主要逻辑体现在getReader()方法中,当设置了编码属性的时候,Spring会使用相应的编码作为输入流的编码

public Reader getReader() throws IOException {
	if (this.charset != null) {
		return new InputStreamReader(this.resource.getInputStream(), this.charset);
	}
	else if (this.encoding != null) {
		return new InputStreamReader(this.resource.getInputStream(), this.encoding);
	}
	else {
		return new InputStreamReader(this.resource.getInputStream());
	}
}

上面代码构造了一个有编码(encoding)的InputStreamReader。当构造好encodedResource对象后,再次转入了可复用方法loadBeanDefinitions(new EncodedResource(resource))这个方法内部才是真正的数据准备阶段,也就是时序图描述的逻辑:

public int loadBeanDefinitions(EncodedResource encodedResource) throws BeanDefinitionStoreException {
	Assert.notNull(encodedResource, "EncodedResource must not be null");
	if (logger.isInfoEnabled()) {
		logger.info("Loading XML bean definitions from " + encodedResource);
	}
    //通过属性来记录已经加载的资源
	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 {
	    //从encodedResource中获取已经封装好的Resource对象并再次从Resource中获取其中的inputStream
		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();
		}
	}
}

核心逻辑部分:

protected int doLoadBeanDefinitions(InputSource inputSource, Resource resource)
			throws BeanDefinitionStoreException {
	try {
		Document doc = doLoadDocument(inputSource, resource);
		//核心部分
		return registerBeanDefinitions(doc, resource);
	}
	catch (BeanDefinitionStoreException ex) {
		throw ex;
	}
	catch (SAXParseException ex) {
		throw new XmlBeanDefinitionStoreException(resource.getDescription(),
				"Line " + ex.getLineNumber() + " in XML document from " + resource + " is invalid", ex);
	}
	catch (SAXException ex) {
		throw new XmlBeanDefinitionStoreException(resource.getDescription(),
				"XML document from " + resource + " is invalid", ex);
	}
	catch (ParserConfigurationException ex) {
		throw new BeanDefinitionStoreException(resource.getDescription(),
				"Parser configuration exception parsing XML from " + resource, ex);
	}
	catch (IOException ex) {
		throw new BeanDefinitionStoreException(resource.getDescription(),
				"IOException parsing XML document from " + resource, ex);
	}
	catch (Throwable ex) {
		throw new BeanDefinitionStoreException(resource.getDescription(),
				"Unexpected exception parsing XML document from " + resource, ex);
	}
}

上述代码做了三件事:

  1. 获取对XML文件的验证模式
  2. 加载XML文件,并得到对应的Document
  3. 根据返回的Document注册Bean信息

这三个步骤支撑着整个Spring容器部分的实现,尤其第三步对配置文件的解析,逻辑非常复杂

你可能感兴趣的:(spring,java,servlet)