Spring如何通过XMLBeanFactory加载一个Bean?
我们深入分析以下功能的代码实现:
BeanFactory bf = new XmlBeanFactory(new ClassPathResource("applicationContext.xml"));
通过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();
}
对于不同来源的资源文件都有相应的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),而这句代码则是整个资源加载的切入点,我们来看看这个方法的时序图:
总结上述处理过程如下:
- 封装资源文件,进入XmlBeanDefinitionReader后首先对参数Resource使用EncodedResource类进行封装
- 获取输入流。从Resource中获取对应的InputStream并构造InputSource
- 通过构造的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<EncodedResource> 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中获取其中的inputStreamInputStream 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);}
}
上述代码做了三件事:
- 获取对XML文件的验证模式
- 加载XML文件,并得到对应的Document
- 根据返回的Document注册Bean信息
这三个步骤支撑着整个Spring容器部分的实现,尤其第三步对配置文件的解析,逻辑非常复杂。