1 容器的基础 XmlBeanFactory
我们来看一个Spring的示例代码:
public class BeanFactoryTest {
public void testSimpleLoad() {
BeanFactory bf = new XmlBeanFactory(new ClassPathResource("beanFactoryTest.xml"));
MyTestBean bean = (MyTestBean)bf.getBean("myTestBean");
assertEquals("testStr", bean.getTestStr());
}
}
接下来,我们深入分析一下以下功能的代码实现:
BeanFactory bf = new XmlBeanFactory(new ClassPathResource("beanFactoryTest.xml"));
以下是XmlBeanFactory的初始化时序图:
时序图从BeanFactoryTest
测试类开始,首先调用ClassPathResource
的构造函数来构造Resource
资源文件的实例对象,这样后续的资源处理就可以用Resource
提供的各种服务来操作了。
1.1 配置文件封装
Spring对其内部用到的资源实现了自己的抽象结构:Resource
接口封装底层资源。
public interface Resource extends InputStreamSource {
boolean exists();
default boolean isReadable() {
return exists();
}
default boolean isOpen() {
return false;
}
default boolean isFile() {
return false;
}
URL getURL() throws IOException;
URI getURI() throws IOException;
File getFile() throws IOException;
default ReadableByteChannel readableChannel() throws IOException {
return Channels.newChannel(getInputStream());
}
long contentLength() throws IOException;
long lastModified() throws IOException;
Resource createRelative(String relativePath) throws IOException;
@Nullable
String getFilename();
String getDescription();
}
Resource
接口定义了3个判断当前资源的方法:存在性(exists)、可读性(isReadable)、是否处于打开状态(isOpen)。Resource
接口提供了不同资源到URL、URI、File类型的转换,以及获取lastModified属性、文件名的方法。- 为了方便操作,
Resource
接口还提供了基于当前资源创建一个相对资源的方法:createRelative()
。 - 在错误处理中需要详细地打印出错的资源文件,因而
Resource
还提供了getDescription()
方法用来在错误处理中打印信息。 - 对于不同来源的资源文件都有相应的Resource实现,文件(
FileSystemResource
)、Classpath资源(ClassPathResource
)、URL资源(UrlResource
)、InputStream资源(InputStreamResource
)、Byte数组(ByteArrayResource
)等。
当通过Resource
相关类完成了对配置文件进行封装后,配置文件的读取工作就全权交给XmlBeanDefinitionReader
来处理。在XmlBeanFactory
的初始化过程中,使用的是Resource
实例作为构造方法参数的办法,代码如下:
private final XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(this);
public XmlBeanFactory(Resource resource) throws BeansException {
// 调用构造方法
this(resource, null);
}
public XmlBeanFactory(Resource resource, BeanFactory parentBeanFactory) throws BeansException {
super(parentBeanFactory);
// 使用XmlBeanDefinitionReader进行资源加载
this.reader.loadBeanDefinitions(resource);
}
}
在使用XmlBeanDefinitionReader解析Resource资源之前,有一个调用父类构造方法的过程super(parentBeanFactory)
,跟踪代码到父类。
public AbstractAutowireCapableBeanFactory() {
super();
ignoreDependencyInterface(BeanNameAware.class);
ignoreDependencyInterface(BeanFactoryAware.class);
ignoreDependencyInterface(BeanClassLoaderAware.class);
if (IN_NATIVE_IMAGE) {
this.instantiationStrategy = new SimpleInstantiationStrategy();
}
else {
this.instantiationStrategy = new CglibSubclassingInstantiationStrategy();
}
ignoreDependencyInterface
方法的作用是忽略给定接口setter
方法的自动装配(byName、byType...)。详情请见https://blog.csdn.net/qq_36951116/article/details/99587519 。另外有个类似的方法是ignoreDependencyType
,它的作用是忽略给定类型的自动装配。
1.2 加载Bean
XmlBeanFactory的构造方法中,this.reader.loadBeanDefinition(resource)
是整个资源加载的切入点。这个方法的处理过程如下:
- 封装资源文件。当进入
XmlBeanDefinitionReader
后首先对参数Resource
使用EncodeResource
类进行封装。 - 获取输入流。从
Resource
中获取对应的InputStream
并构造InputSource
。 - 通过构造的
InputSource
实例和Resource
实例继续调用函数doLoadBeanDefinition
。
2 获取XML的验证模式
在1.2节最后提到的loadBeanDefinition
一共做了三件事。
- 获取对XML文件的验证模式。
- 加载XML文件,并得到对应的
Document
。 - 根据返回的
Document
注册Bean
信息。
我们首先来看第一件事:获取XML的验证模式。XML文件的验证模式保证了XML文件的正确性,而比较常用的验证模式有两种:DTD和XSD。
DTD是一种保证XML文档格式正确的有效方法,可以通过比较XML文档和DTD文件来看文档是否符合规范,元素和标签使用是否正确,使用DTD验证模式的XML文件必然会包含<!DOCTYPE>这一行。
XSD本身是一种XML文档,XSD文档设计者可以通过XML Schema指定XML文档所允许的结构和内容,并据此检查文档是否是有效的。
2.1 验证模式的获取
Spring通过XmlBeanDefinitionReader
中的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;
}
2.2 获取Document
经过了验证模式准备的步骤就可以进行Document
加载了,同样XmlBeanFactoryReader
类对于文档读取没有亲力亲为,而是委托给了DocumentLoader
去执行。这里DocumentLoader
是一个接口,真正的执行类是DefaultDocumentLoader
。
// DefaultDocumentLoader.java
public Document loadDocument(InputSource inputSource, EntityResolver entityResolver,
ErrorHandler errorHandler, int validationMode, boolean namespaceAware) throws Exception {
DocumentBuilderFactory factory = createDocumentBuilderFactory(validationMode, namespaceAware);
if (logger.isTraceEnabled()) {
logger.trace("Using JAXP provider [" + factory.getClass().getName() + "]");
}
DocumentBuilder builder = createDocumentBuilder(factory, entityResolver, errorHandler);
return builder.parse(inputSource);
}
与大多数通过SAX解析XML文档的套路差不多,Spring先创建DocumentBuilderFactory
,再通过DocumentBuilderFactory
创建DocumentBuilder
,进而解析inputSource
来返回Document
对象。loadDocument
方法涉及一个参数EntityResolver
,它的作用是项目本身可以提供一个寻找DTD声明的方法,比如我们将DTD文件放在项目某处,在实现时直接将此文档读取并返回给SAX即可,这样就避免了通过网络来寻找相应的声明。
3 解析及注册BeanDefinitions
把XML文件转化为Document对象后,程序就会被引入这个方法:
public int registerBeanDefinitions(Document doc, Resource resource) throws BeanDefinitionStoreException {
// 使用DefaultBeanDefinitionDocumentReader实例化BeanDefinitionDocumentReader
BeanDefinitionDocumentReader documentReader = createBeanDefinitionDocumentReader();
int countBefore = getRegistry().getBeanDefinitionCount();
// 加载及注册Bean
documentReader.registerBeanDefinitions(doc, createReaderContext(resource));
return getRegistry().getBeanDefinitionCount() - countBefore;
}
createBeanDefinitionDocumentReader()方法会返回一个DefaultBeanDefinitionDocumentReader对象,用于加载及注册Bean。进入DefaultBeanDefinitionDocumentReader后,发现这个方法先提取了root,然后将root作为参数继续BeanDefnition的注册。
public void registerBeanDefinitions(Document doc, XmlReaderContext readerContext) {
this.readerContext = readerContext;
Element root = doc.getDocumentElement();
doRegisterBeanDefinitions(root);
}
doRegisterBeanDefinitions
是真正开始解析XML的地方,它的作用是从给定的root
参数开始,注册每个BeanDefinition
。其代码如下:
protected void doRegisterBeanDefinitions(Element root) {
// 专门处理解析
BeanDefinitionParserDelegate parent = this.delegate;
this.delegate = createDelegate(getReaderContext(), root, parent);
if (this.delegate.isDefaultNamespace(root)) {
// PROFILE_ATTRIBUTE = "profile"
String profileSpec = root.getAttribute(PROFILE_ATTRIBUTE);
if (StringUtils.hasText(profileSpec)) {
// 处理profile属性
String[] specifiedProfiles = StringUtils.tokenizeToStringArray(
profileSpec, BeanDefinitionParserDelegate.MULTI_VALUE_ATTRIBUTE_DELIMITERS);
if (!getReaderContext().getEnvironment().acceptsProfiles(specifiedProfiles)) {
return;
}
}
}
// 解析前处理,留给子类实现
preProcessXml(root);
parseBeanDefinitions(root, this.delegate);
// 解析后处理,留给子类实现
postProcessXml(root);
this.delegate = parent;
}
首先对profile
属性进行处理,然后preProcessXml
和postProcessXml
方法的实现都是空的,这里使用的是模板方法,留给子类实现。最后我们来看parseBeanDefinitions()
方法。
protected void parseBeanDefinitions(Element root, BeanDefinitionParserDelegate delegate) {
// 对beans的处理
if (delegate.isDefaultNamespace(root)) {
NodeList nl = root.getChildNodes();
for (int i = 0; i < nl.getLength(); i++) {
Node node = nl.item(i);
if (node instanceof Element) {
Element ele = (Element) node;
if (delegate.isDefaultNamespace(ele)) {
// 如果是默认命名空间
parseDefaultElement(ele, delegate);
}
else {
// 如果是自定义命名空间
delegate.parseCustomElement(ele);
}
}
}
}
else {
delegate.parseCustomElement(root);
}
}
在Spring的XML配置里有两大类Bean声明,一个是默认的,如
<bean id="test" class="test.TestBean"/>
另一类是自定义的,如:
<tx:annotation-driven/>
而两种方式的读取及解析差别是非常大的,如果采用Spring默认配置,Spring知道该怎么做,如果是自定义的,就需要用户实现一些接口及配置。对于根节点或者子节点,该方法会使用node.getNamespaceURI()
获取命名空间并与Spring中固定的命名空间http://www.springframework/org/schema/beans 进行比对,如果一致则认为是默认,使用parseDefaultElement
解析,否则就认为是自定义,使用delegate.parseCustomElement
解析。