2.1 容器基本用法
下面将由一个简单的实例来开始 spring 容器的学习:
public class BeanFactoryTest {
//省略了配置文件和 TestBean
@Test
public void testSimpleLoad() {
BeanFactory bf = new XmlBeanFactory(new ClasspathResource("beanFactoryTest.xml");
TestBean bean = (TestBean) bf.getBean("testBean");
}
}
这个例子很简单,直接使用 BeanFactory 作为容器对于 Spring 的使用来说并不多见,大多数时候都是使用 ApplicationContext,这里只是用于测试帮助更好的了解 Spring 的内部原理。
2.2 功能分析
这段测试代码主要完成三个功能:
- 读取配置文件 beanFactoryTest.xml;
- 根据 beanFactoryTest.xml 中的配置找到对应类的配置,并实例化;
- 调用实例化后的实例。
2.5 容器的基础 XmlBeanFactory
接下来我们将要分析以下功能代码的实现:
BeanFactory bf = new XmlBeanFactory(new ClasspathResource("beanFactoryTest.xml");
我们将通过 XmlBeanFactory 的初始化时序图来分析上面代码的执行逻辑:
- 在 BeanFactoryTest 中首先调用 ClassPathResource 的构造函数来构造Resource 资源文件对象;
- 使用 Resource 对象构造 XmlBeanFactory 对象;
- 在构造过程中通过 Resource 进行读取和配置文件。
2.5.1 配置文件封装
之前已经知道 spring 通过 Resource 对文件进行读取,在这里使用的是
Resource 的实现类 ClassPathResource。这一节主要了解封装配置文件的类 Resource。
使用 Resource 封装文件的原因
在 Java 中可以将不同来源的资源抽象成 URL,但是 URL 没有默认定义相对 Classpath 或 ServletContext 等资源的 handler,虽然可以注册自己的 URLStream-Handler 来解析特定的 URL 前缀,然而这需要了解 URL 的实现机制,而且 URL 也没有提供一些基本的方法,比如检查当前资源是否在存在、是否可读等方法。所以 Spring 对其内部使用到的资源实现了自己的抽象机构:Resource 接口 封装底层资源。
public interface InputStreamSource {
// 返回任何实现了 InputStream 接口的类
InputStream getInputStream() throws IOException;
}
public interface Resource extends InputStreamSource {
boolean exists(); // 存在性
boolean isReadable(); //可读性
boolean isOpen(); // 是否处于打开状态
URL getURL() throws IOException; //封装 URL 类型
URI getURI() throws IOException; // 封装 URI 类型
File getFile() throws IOException; // 封装 File 类型
long contentLength() throws IOException;
long lastModified() throws IOException;
Resource createRelative(String relativePath) throws IOException;
String getFilename(); // 获得不带路径信息的文件名
String getDescription();
}
Resource 接口抽象了 Spring 内部使用到的所有底层资源:File 、URI、URL 等。有了 Resource 接口就可以对所有资源文件进行统一处理,至于实现其实很简单。以 getInputStream 为例,ClassPathResource 中的实现方式是通过 class 或者 classLoader 提供的底层方法实现的。
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);
}
...
return is;
}
使用 Resource 构造 XmlBeanFactory
Resource 封装对象之后将交由 XmlBeanFactory 进行处理,这里分析使用 Resource 作为实例的构造函数。时序图中提到的 XmlBeanFactory 加载数据就是在方法 reader.loadBeanDefinitions(resource) 中完成的。
public XmlBeanFactory(Resource resource) throws BeansException {
this(resource, null);
}
public XmlBeanFactory(Resource resource, BeanFactory parentBeanFactory) throws BeansException {
// 调用父类的构造方法
super(parentBeanFactory);
// 资源加载的真正实现的地方
this.reader.loadBeanDefinitions(resource);
}
2.5.2 加载Bean
之前只是在了解 Resource 类,这里开始分析 XmlBeanFactory 的加载过程。在上一节知道 XmlBeanFactory 通过 XmlBeanDefinitionReader 类型的 reader属性调用 loadBeanDefinitions(resource) 方法,这句代码是整个资源加载的切入点,资源加载的真正开始的地方。我们将通过 loadBeanDefinitions(resource) 的时序图来梳理整个处理过程。
- 封装资源文件。当进入 XmlBeanDefinitionReader 后首先对参数 Resource 使用 EncodedResource 类进行封装;
- 获取输入流。从 Resource 中获取对应的 InputStream 并构造 InputSource;
- 通过构造的 InputSource 实例和 Resource 实例继续调用 doLoadBeanDifinitions。
EncodedResource 类
首先观察在上一节调用的方法 loadBeanDefinitions(resource),发现这个方法只是将 Resource 封装成 EncodedResource,接着就调用另一个重载方法 loadBeanDefinitions(new EncodedResource(resource))。那么 EncodedResource 是用来干什么的呢?
public int loadBeanDefinitions(Resource resource) {
// 将 Resource 封装成 EncodedResource,并调用其重载方法
return loadBeanDefinitions(new EncodedResource(resource));
}
EncodedResource 顾名思义可以知道这个类主要是用于对资源文件的编码进行处理。其主要逻辑体现在 getReader() 方法中,当设置了编码属性的 Spring 会使用相应的编码作为输入流的编码:
public Reader getReader() {
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());
}
loadBeanDefinitions(EncodedResource resource) 方法
这个方法内部是真正的数据准备阶段,主要是构造对应编码和流的 InputSource 并调用 doLoadBeanDefinitions(inputSource, encodedResource.getResource()) 方法,直到这里仍主要是对流进行处理,在下一个方法将对 XML 文件进行处理。
public int loadBeanDefinitions(EncodedResource encodedResource) {
// 日志和判断...
// 记录已经加载的资源...
try {
// 从 encodedResource 获取已经封装好的 Resource 对象
// 再次从 Resource 中获取期中的 InputStream
InputStream inputStream = encodedResource.getResource().getInputStream();
try {
// 这个类并不是来自 Spring,它的全路径是 org.xml.sax.InputSource
InputSource inputSource = new InputSource(inputStream);
if (encodedResource.getEncoding() != null) {
// EncodedSource 的主要作用在这里,和携带 InputSource
inputSource.setEncoding(encodedResource.getEncoding());
}
// 这里真正进入逻辑核心部分
return doLoadBeanDefinitions(inputSource, encodedResource.getResource());
}
}
// 异常处理和其他一些逻辑...
}
doLoadBeanDefinitions(InputSource ..., Resource ...) 方法
从方法名(do...)就知道对 XML 文件的真正处理在这里,获取验证模式并将 XML 文件封装成 Document 类,最后通过 registerBeanDefinitions(doc, resource) 方法注册 Bean 信息。接下来将针对下面方法的三个步骤进行讲解。
protected int doLoadBeanDefinitions(InputSource inputSource, Resource resource) {
// 获取对 XML 文件的验证模式
int validationMode = getValidationModeForResource(resource);
// 加载 XML 文件,并得到对应的 Document 信息
Document doc = this.documentLoader.loadDocument(inputSource,
getEntityResolver(), this.errorHandler,
validationMode, isNamespaceAware());
// 根据返回的 Document 注册 Bean 信息
return registerBeanDefinitions(doc, resource);
}
2.6 XML 的验证模式
为什么需要 XML 的验证模式呢?因为 XML 通过验证模式保证 XML 文件的正确性,而比较常用的验证模式有两种 DTD 和 XSD。对 DTD 和 XSD 的相关知识不在这里进行展开。通过获取 XML 的验证模式采取不同的策略对 XML 进行封装和注册。
2.7 获取 Document
XmlBeanDefinitionReader 并没有将对 Document 的加载逻辑封装在内部,而是使用 DocumentLoader 的实现类完成对 XML 的加载,这里使用的实现类是 DefaultDocumentLoader 。
public Document loadDocument(InputSource inputSource, EntityResolver entityResolver,
ErrorHandler errorHandler, int validationMode, boolean namespaceAware) {
// 创建 DocumentBuilderFactory
DocumentBuilderFactory factory = createDocumentBuilderFactory(validationMode, namespaceAware);
// 通过 DocumentBuilderFactory 创建 DocumentBuilder
DocumentBuilder builder = createDocumentBuilder(factory, entityResolver, errorHandler);
// 通过 DocumentBuilder 完成对 Document 的加载
return builder.parse(inputSource);
}
EntityResolver
这里讲解一下 EntityResolver 的用法。Spring 解析一个 XML 文件首先会读取该 XML 文档上的声明,然后根据声明去寻找相应的 DTD 定义,以便对文档进行验证。但默认的寻找规则是通过网络下载,这过程可能会比较长而且有局限性(网络中断或不可用)。
EntityResolver 就用于解决这一问题的,它的作用就是项目本身可以提供一个寻找 DTD 声明的方法,由程序实现寻找 DTD 声明的过程。
2.8 解析及注册 BeanDefinitions
之前主要还是对流的处理或者文件的封装,这里将开始真正的对 XML 进行解析。XmlBeanDefinitionReader 将对 Document 的加载委托给了 DefaultBeanDefinitionDocumentReader 类,应用了面向对象中单一职责的原则。
public int registerBeanDefinitions(Document doc, Resource resource) throws BeanDefinitionStoreException {
//使用 createBeanDefinitionDocumentReader 创建 BeanDefinitionDocumentReader
BeanDefinitionDocumentReader documentReader = createBeanDefinitionDocumentReader();
//设置环境变量
documentReader.setEnvironment(getEnvironment());
//记录注册前已加载数量
int countBefore = getRegistry().getBeanDefinitionCount();
//加载及注册 bean
documentReader.registerBeanDefinitions(doc, createReaderContext(resource));
//记录本次加载的 bean 个数
return getRegistry().getBeanDefinitionCount() - countBefore;
}
registerBeanDefinitions 方法
public void registerBeanDefinitions(Document doc, XmlReaderContext readerContext) {
this.readerContext = readerContext;
Element root = doc.getDocumentElement();
doRegisterBeanDefinitions(root);
}
4
4
4
4
4
4
4
4
4
4