最近在学习Spring源码相关部分,第一篇文章就从Spring源码开始吧。
容器的初始化
- BeanFactory的初始化
基本代码实现
BeanFactory beanFactory = new XmlBeanFactory(new ClassPathResource("org/springframework/example/bean-factory-demo.xml"));
注:XmlBeanFactory已经被标注了@Deprecated;
不过这里只是分析思想和实现,不影响;也为了和下面的ApplicationContext的实现联系起来。
- ApplicationContext的初始化
基本代码实现
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("org/springframework/example/application-context-demo.xml");
1. XML配置文件的读取
1.1 XmlBeanFactory配置文件的读取
new XmlBeanFactory(new ClassPathResource("org/springframework/example/bean-factory-demo.xml"));
1.1.1 ClassPathResource到Resource
public ClassPathResource(String path, @Nullable ClassLoader classLoader) {
Assert.notNull(path, "Path must not be null");
String pathToUse = StringUtils.cleanPath(path);
if (pathToUse.startsWith("/")) {
pathToUse = pathToUse.substring(1);
}
this.path = pathToUse;
this.classLoader = (classLoader != null ? classLoader : ClassUtils.getDefaultClassLoader());
}
XmlBeanFactory中使用ClassPathResource来读取指定的配置文件。
在Java中将各种资源抽象成的URL,并通过注册不同的handler(URLStreamHandler)来处理不同来源的资源的读取逻辑,但是并没有提供基本的方法(如:检查资源是否存在、检查资源是否可读等)。
所以Spring对其内部使用到的资源实现了一个抽象结构:Resource接口(用于封装底层资源)
public interface InputStreamSource {
InputStream getInputStream() throws IOException;
}
public interface Resource extends InputStreamSource {
boolean exists(); // 存在性
default boolean isReadable() {return this.exists();} // 可读性
default boolean isOpen() {return false;} // 是否打开
default boolean isFile() {return false;} // 是否是文件
URL getURL() throws IOException; // 提供了不同资源到URL URI File 类型的转换
URI getURI() throws IOException;
File getFile() throws IOException;
default ReadableByteChannel readableChannel() throws IOException {return Channels.newChannel(this.getInputStream());
long contentLength() throws IOException;
long lastModified() throws IOException;
Resource createRelative(String var1) throws IOException;
@Nullable
String getFilename();
String getDescription();
}
在ClassPathResource中实现了getInputStream()方法,使用传入的class或者classLoader来调用jdk的方法获取InputStream:
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(this.getDescription() + " cannot be opened because it does not exist");
} else {
return is;
}
}
在将配置文件封装为Resource之后,开始进行XmlBeanFactory的初始化过程:
XmlBeanFactory.java
public XmlBeanFactory(Resource resource) throws BeansException {
this(resource, (BeanFactory)null);
}
public XmlBeanFactory(Resource resource, BeanFactory parentBeanFactory) throws BeansException {
super(parentBeanFactory);
// 资源加载的真正实现,整个资源加载的切入点
this.reader.loadBeanDefinitions(resource);
}
这里有个调用父类构造方法的初始化过程:
public AbstractAutowireCapableBeanFactory() {
super();
ignoreDependencyInterface(BeanNameAware.class);
ignoreDependencyInterface(BeanFactoryAware.class);
ignoreDependencyInterface(BeanClassLoaderAware.class);
}
这里有个ignoreDependencyInterface()方法,主要功能是:自动装配时忽略该接口的实现类中的和接口本身setter方法入参类型相同的依赖。
正常情况下:当A的bean中有属性B,当Spring在获取A的bean的时候,如果B还没有初始化,那么Spring会自动初始化B。但是有些情况下,B不会被初始化。
典型例子为:通过其他方式解析ApplicationContext,如通过BeanFactoryAware注入BeanFactory或者通过ApplicationContextAware注入ApplicationContext。
所以ignoreDependencyInterface()方法可以使得ApplicationContextAware和BeanFactoryAware中的ApplicationContext或BeanFactory依赖在自动装配时被忽略,而统一由框架设置依赖(在ApplicationContextAwareProcessor中设置)。
详解可见
1.1.2 XmlBeanFactory加载BeanDefinitions
在使用Resource对配置文件完成封装之后,配置文件的读取工作就交给了 XmlBeanDefinitionReader来进行处理,直接使用XmlBeanDefinitionReader调用loadBeanDefinitions(方法):
this.reader.loadBeanDefinitions(resource);
1.首先对Resource使用了EncodedResource进行封装,构造一个encodedResource对象:
XmlBeanDefinitionReader.java
public int loadBeanDefinitions(Resource resource) throws BeanDefinitionStoreException {
// 对Resource,使用EncodedResource进行封装(主要是考虑到Resource可能存在编码要求)
return loadBeanDefinitions(new EncodedResource(resource));
}
EncodedResource作用:对资源文件的编码进行处理。主要逻辑体现在:
public Reader getReader() throws IOException {
if (this.charset != null) {
return new InputStreamReader(this.resource.getInputStream(), this.charset);
} else {
return this.encoding != null ? new InputStreamReader(this.resource.getInputStream(), this.encoding) : new InputStreamReader(this.resource.getInputStream());
}
}
2.在构造好encodedResource对象之后,转入了可复用方法 loadBeanDefinitions(EncodedResource encodedResource)进行处理:
2. 加载BeanDefinitions
2.1 XML配置文件解析的准备阶段
XmlBeanDefinitionReader.java
public int loadBeanDefinitions(Resource resource) throws BeanDefinitionStoreException {
// 对Resource,使用EncodedResource进行封装(主要是考虑到Resource可能存在编码要求)
return loadBeanDefinitions(new EncodedResource(resource));
}
public int loadBeanDefinitions(EncodedResource encodedResource) throws BeanDefinitionStoreException {
Assert.notNull(encodedResource, "EncodedResource must not be null");
if (logger.isTraceEnabled()) {
logger.trace("Loading XML bean definitions from " + encodedResource);
}
// 通过一个set来记录已经加载过的资源
Set<EncodedResource> currentResources = this.resourcesCurrentlyBeingLoaded.get();
// 添加失败,说明已经被加载过了
if (!currentResources.add(encodedResource)) {
throw new BeanDefinitionStoreException(
"Detected cyclic loading of " + encodedResource + " - check your import definitions!");
}
// 从encodedResource中获取已经封装的Resource对象,再从Resource中获取对应的 InputStream
try (InputStream inputStream = encodedResource.getResource().getInputStream()) {
// 构造 InputSource,InputSource类并不是Spring的类
InputSource inputSource = new InputSource(inputStream);
if (encodedResource.getEncoding() != null) {
// 如果前面设置了编码,就设置InputSource的编码
inputSource.setEncoding(encodedResource.getEncoding());
}
// 进入核心逻辑
return doLoadBeanDefinitions(inputSource, encodedResource.getResource());
}
catch (IOException ex) {
throw new BeanDefinitionStoreException(
"IOException parsing XML document from " + encodedResource.getResource(), ex);
}
finally {
currentResources.remove(encodedResource);
if (currentResources.isEmpty()) {
this.resourcesCurrentlyBeingLoaded.remove();
}
}
}
主要流程:
- 验证这个Resource有没有被加载过,如果加载过就抛出异常;
- 获取输入流inputStream,并使用inputStream通过SAX读取XML文件的方式来准备inputSource;如果Resource中设置了编码,就对inputSource也设置编码;
- 通过构造的inputSource和Resource继续调用
doLoadBeanDefinitions(inputSource, encodedResource.getResource());
protected int doLoadBeanDefinitions(InputSource inputSource, Resource resource)
throws BeanDefinitionStoreException {
try {
// 通过inputSource和resource 得到对应的 Document
Document doc = doLoadDocument(inputSource, resource);
// 注册及解析 BeanDefinitions
int count = registerBeanDefinitions(doc, resource);
if (logger.isDebugEnabled()) {
logger.debug("Loaded " + count + " bean definitions from " + resource);
}
return count;
}
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);
}
}
首先是获取Document:
protected Document doLoadDocument(InputSource inputSource, Resource resource) throws Exception {
return this.documentLoader.loadDocument(inputSource, getEntityResolver(), this.errorHandler,
getValidationModeForResource(resource), isNamespaceAware());
}
这里有个 getValidationModeForResource(resource),用于获取XML的验证模式。 2.1.1 获取XML的验证模式
XML文件的验证模式保证了XML文件的正确性,比较常用的验证模式有两种:DTD和XSD。
2.1.1.1 DTD
DTD(Document Type Definition)即文档类型定义,是一种XML约束模式语言,是XML文件的验证机制,属于XML文件组成的一部分。
DTD是一种保证XML文档格式正确的有效方法,可以通过比较XML文档和DTD文件来判断文档是否符合规范,元素和标签的使用是否正确。
一个DTD文档包含:
- 元素的定义规则;
- 元素间关系的定义规则;
- 元素可使用的属性;
- 可使用的实体或符号规则;
要使用DTD验证模式的时候需要在XML文件的头部声明,例如:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN" "https://www.springframework.org/dtd/spring-beans.dtd">
<beans>
......
</beans>
spring-beans-2.0.dtd的部分内容如下:
<!ELEMENT beans (
description?,
(import | alias | bean)*
)>
<!ATTLIST beans default-lazy-init (true | false) "false">
<!ATTLIST beans default-merge (true | false) "false">
<!ATTLIST beans default-autowire (no | byName | byType | constructor | autodetect) "no">
<!ATTLIST beans default-init-method CDATA #IMPLIED>
<!ATTLIST beans default-destroy-method CDATA #IMPLIED>
......
2.1.1.2 XSD
XML Schema语言就是XSD(XML Schemas Definition),XML Schema描述了XML文档的结构;可以用一个指定的XML Schema来验证某个XML文档,以检查该XML文档是否符合其要求。
文档设计者可以通过XML Schema指定XML文档所允许的结构和内容,并可根据此检查XML文档是否有效;XML Schema本身也是XML文档,也符合XML语法结构,可以用通用的XML解析器解析它。
使用:
- 声明名称空间: xmlns=“http://www.springframework.org/schema/beans”
- 指定该名称空间所对应的XML Schema文档的存储位置,通过schemaLocation属性来指定名称空间对应的XML Schema文档的存储位置:
第一部分是名称空间的URI,第二部分是这个名称空间所标识的XML Schema具体文件的URL;xsi:schemaLocation="http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd"
例如:
<?xml version="1.0" encoding="UTF-8"?>
<!-- 声明名称空间 -->
<!-- 指定名称空间所对应的XML Schemas文档的存储位置 schemaLocation = URI + URL -->
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd">
......
</beans>
spring-beans.xsd的部分内容如下:
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<xsd:schema xmlns="http://www.springframework.org/schema/beans"
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
targetNamespace="http://www.springframework.org/schema/beans">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace"/>
<xsd:annotation>
<xsd:documentation><![CDATA[
Spring XML Beans Schema, version 4.3
Authors: Juergen Hoeller, Rob Harrop, Mark Fisher, Chris Beams
......
]]></xsd:documentation>
</xsd:annotation>
<!-- base types -->
<xsd:complexType name="identifiedType" abstract="true">
<xsd:annotation>
<xsd:documentation><![CDATA[
The unique identifier for a bean. The scope of the identifier
is the enclosing bean factory.
]]></xsd:documentation>
</xsd:annotation>
<xsd:attribute name="id" type="xsd:string">
<xsd:annotation>
<xsd:documentation><![CDATA[
The unique identifier for a bean. A bean id may not be used more than once
within the same <beans> element.
]]></xsd:documentation>
</xsd:annotation>
</xsd:attribute>
</xsd:complexType>
......
</xsd:schema>
2.1.1.3 验证模式的读取
接着上面
protected int getValidationModeForResource(Resource resource) {
int validationModeToUse = getValidationMode();
// 如果手动指定了验证模式则使用指定的
if (validationModeToUse != VALIDATION_AUTO) {
return validationModeToUse;
}
// 未指定则使用自动检测
int detectedMode = detectValidationMode(resource);
if (detectedMode != VALIDATION_AUTO) {
return detectedMode;
}
// Hmm, we didn't get a clear indication... Let's assume XSD,
// since apparently no DTD declaration has been found up until
// detection stopped (before finding the document's root tag).
return VALIDATION_XSD;
如果是设定了验证模式就使用设定的验证模式(可以通过调用XmlBeanDefinitionReader的setValidationMode()方法进行设定),如果没有设定就使用自动检测的方式:
protected int detectValidationMode(Resource resource) {
if (resource.isOpen()) {
throw new BeanDefinitionStoreException(
"Passed-in Resource [" + resource + "] contains an open stream: " +
"cannot determine validation mode automatically. Either pass in a Resource " +
"that is able to create fresh streams, or explicitly specify the validationMode " +
"on your XmlBeanDefinitionReader instance.");
}
InputStream inputStream;
try {
inputStream = resource.getInputStream();
}
catch (IOException ex) {
throw new BeanDefinitionStoreException(
"Unable to determine validation mode for [" + resource + "]: cannot open InputStream. " +
"Did you attempt to load directly from a SAX InputSource without specifying the " +
"validationMode on your XmlBeanDefinitionReader instance?", ex);
}
try {
// 将自动检测验证模式的工作委托给了,专门处理类XmlValidationModeDetector
return this.validationModeDetector.detectValidationMode(inputStream);
}
catch (IOException ex) {
throw new BeanDefinitionStoreException("Unable to determine validation mode for [" +
resource + "]: an error occurred whilst reading from the InputStream.", ex);
}
}
又将自动检测的工作委托给了专门的处理类XmlValidationModeDetector:
public int detectValidationMode(InputStream inputStream) throws IOException {
// Peek into the file to look for DOCTYPE.
BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
try {
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;
}
// 并且判断 < 的下一个字符是 小写字母:Character.isLetter(content.charAt(openTagIndex + 1)),则直接退出
// 因为 <?xml 或者 <!DOCTYPE 这种定义语言的 < 后面都是符号;而<beans> <bean>后面才是字母;
// 所以,如果读取到了 < 后面的小写字母,直接退出,肯定没有指定验证模式
if (hasOpeningTag(content)) {
// End of meaningful data...
break;
}
}
return (isDtdValidated ? VALIDATION_DTD : VALIDATION_XSD);
}
catch (CharConversionException ex) {
// Choked on some character encoding...
// Leave the decision up to the caller.
return VALIDATION_AUTO;
}
finally {
reader.close();
}
}
Spring用来检测验证模式的方法:判断是否包含了DOCTYPE,如果包含了就是DTD,如果没有就是XSD。
2.1.2 获取Document
通过了检测验证模式的步骤之后,就可以进行Document的加载了;同样,Document加载的工作委托给了DocumentLoader接口,并由实现类 DefaultDocumentLoader执行:
DocumentLoader.java
Document loadDocument(InputSource inputSource, EntityResolver entityResolver,
ErrorHandler errorHandler, int validationMode, boolean namespaceAware) throws Exception;
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);
// 解析 inputSource 来返回 Document对象
return builder.parse(inputSource);
}
首先创建了DocumentBuilderFactory,再通过DocumentBuilderFactory创建了DocumentBuilder,进而解析inputSource 来返回 Document对象。
2.1.2.1 EntityResolver
在上面的 loadDocument() 方法中有一个参数EntityResolver。官网解释EntityResolver的作用为:如果SAX应用程序需要实现自定义处理外部实体,就必须实现此接口并使用setEntityResolver方法向SAX驱动器注册一个实例。
也就是说,对于解析一个XML,SAX首先读取该XML文档上的声明,根据声明去寻找相应的DTD定义(或者XSD),来对文档做一个验证。
默认的寻找规则为通过网络(即声明上的DTD的URI地址)来下载相应的DTD声明并进行验证。但是可能由于网络中断或者不可用等原因,找不到相应的DTD声明,这里就会报错导致不可用。
所以EntityResolver的作用就是:
项目本身可以提供一个如何寻找DTD声明的方法,由程序来实现寻找DTD声明的过程(比如将DTD文件放到项目中的某处,在实现时直接将这个文档读取出来返回给SAX即可),这样就可以避免通过网络来寻找。
EntityResolver的接口方法声明:
public interface EntityResolver {
InputSource resolveEntity(String var1, String var2) throws SAXException, IOException;
}
接收的两个参数分别为publicId和systemId,并返回一个InputSource对象。
示例:
- 如果在解析XSD的配置文件:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd">
......
</beans>
- publicId: null
- systemId: https://www.springframework.org/schema/beans/spring-beans.xsd
- 如果在解析DTD的配置文件:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN" "https://www.springframework.org/dtd/spring-beans.dtd">
<beans>
......
</beans>
- publicId: -//SPRING//DTD BEAN 2.0//EN
- systemId: https://www.springframework.org/dtd/spring-beans.dtd
由于通过URI进行网络下载会造成延迟用户体验也不好,所以一般做法是将验证文件直接放在工程里面,这样的话就需要将URL转换称为自己工程里面对应的地址文件。
之前使用的getEntityResolver() 对EntityResolver的获取中,Spring使用了DelegatingEntityResolver类作为 EntityResolver的实现类,所以在DelegatingEntityResolver类中的resolveEntity方法为:
public InputSource resolveEntity(@Nullable String publicId, @Nullable String systemId)
throws SAXException, IOException {
// 不同的验证模式,spring使用不同的解析器解析;
if (systemId != null) {
// 如果是dtd
if (systemId.endsWith(DTD_SUFFIX)) {
return this.dtdResolver.resolveEntity(publicId, systemId);
}
// 如果是xsd
else if (systemId.endsWith(XSD_SUFFIX)) {
// 调用META-INF/Spring.schemas进行解析
return this.schemaResolver.resolveEntity(publicId, systemId);
}
}
// Fall back to the parser's default behavior.
return null;
}
可以看到,对于不同的验证模式,Spring使用了不同的解析器解析。
- 加载DTD类型的时候,使用的是BeansDtdResolver的resolveEntity()方法,直接截取systemId最后的xx.dtd,然后去当前路径下寻找:
public InputSource resolveEntity(@Nullable String publicId, @Nullable String systemId) throws IOException {
if (logger.isTraceEnabled()) {
logger.trace("Trying to resolve XML entity with public ID [" + publicId +
"] and system ID [" + systemId + "]");
}
if (systemId != null && systemId.endsWith(DTD_EXTENSION)) {
int lastPathSeparator = systemId.lastIndexOf('/');
int dtdNameStart = systemId.indexOf(DTD_NAME, lastPathSeparator);
if (dtdNameStart != -1) {
String dtdFile = DTD_NAME + DTD_EXTENSION;
if (logger.isTraceEnabled()) {
logger.trace("Trying to locate [" + dtdFile + "] in Spring jar on classpath");
}
try {
Resource resource = new ClassPathResource(dtdFile, getClass());
InputSource source = new InputSource(resource.getInputStream());
source.setPublicId(publicId);
source.setSystemId(systemId);
if (logger.isTraceEnabled()) {
logger.trace("Found beans DTD [" + systemId + "] in classpath: " + dtdFile);
}
return source;
}
catch (FileNotFoundException ex) {
if (logger.isDebugEnabled()) {
logger.debug("Could not resolve beans DTD [" + systemId + "]: not found in classpath", ex);
}
}
}
}
// Fall back to the parser's default behavior.
return null;
}
- 加载XSD类型的时候,使用的是PluggableSchemaResolver的resolveEntity()方法,默认到META-INF/Spring.schemas文件中找到systemId所对应的XSD文件并加载:
public InputSource resolveEntity(@Nullable String publicId, @Nullable String systemId) throws IOException {
if (logger.isTraceEnabled()) {
logger.trace("Trying to resolve XML entity with public id [" + publicId +
"] and system id [" + systemId + "]");
}
if (systemId != null) {
// 因为在 spring.schemas中配置了 网络和本地的映射,所以可以通过 URL获取到 systemId在项目中的路径
String resourceLocation = getSchemaMappings().get(systemId);
if (resourceLocation == null && systemId.startsWith("https:")) {
// Retrieve canonical http schema mapping even for https declaration
resourceLocation = getSchemaMappings().get("http:" + systemId.substring(6));
}
if (resourceLocation != null) {
Resource resource = new ClassPathResource(resourceLocation, this.classLoader);
try {
InputSource source = new InputSource(resource.getInputStream());
source.setPublicId(publicId);
source.setSystemId(systemId);
if (logger.isTraceEnabled()) {
logger.trace("Found XML schema [" + systemId + "] in classpath: " + resourceLocation);
}
return source;
}
catch (FileNotFoundException ex) {
if (logger.isDebugEnabled()) {
logger.debug("Could not find XML schema [" + systemId + "]: " + resource, ex);
}
}
}
}
// Fall back to the parser's default behavior.
return null;
}
在这里以前,一直是对XML文件加载解析的 准备阶段,从下面就真正开始进行解析了。
2.2 XML配置文件解析的正式阶段
2.2.1 解析BeanDefinitions
当把配置文件转换为Document之后,接下来就可以进行解析BeanDefinitions了,继续上面的代码:
XmlBeanDefinitionReader.java
int count = registerBeanDefinitions(doc, resource);
public int registerBeanDefinitions(Document doc, Resource resource) throws BeanDefinitionStoreException {
// 使用 DefaultBeanDefinitionDocumentReader 实例化BeanDefinitionDocumentReader
BeanDefinitionDocumentReader documentReader = createBeanDefinitionDocumentReader();
// 记录统计前 BeanDefinition 的加载个数
int countBefore = getRegistry().getBeanDefinitionCount();
// 加载及注册 beanDefinition
documentReader.registerBeanDefinitions(doc, createReaderContext(resource));
// 返回本次加载的 BeanDefinition 个数
return getRegistry().getBeanDefinitionCount() - countBefore;
}
在这里使用 createBeanDefinitionDocumentReader() 方法实例化出了一个 BeanDefinitionDocumentReader 接口的实现类 DefaultBeanDefinitionDocumentReader 的对象,再通过调用这个对象的 registerBeanDefinitions() 方法注册BeanDefinitions。
DefaultBeanDefinitionDocumentReader.java
public void registerBeanDefinitions(Document doc, XmlReaderContext readerContext) {
this.readerContext = readerContext;
doRegisterBeanDefinitions(doc.getDocumentElement());
}
protected void doRegisterBeanDefinitions(Element root) {
// 专门处理解析
// BeanDefinitionParserDelegate 用于解析 xml bean 的状态委托类
BeanDefinitionParserDelegate parent = this.delegate;
this.delegate = createDelegate(getReaderContext(), root, parent);
// 在这里如果是默认的命名空间,即:http://www.springframework.org/schema/beans
if (this.delegate.isDefaultNamespace(root)) {
// 处理profile属性(可以配置多套不同的开发环境,便于切换)
// 获取bean节点是否定义了profile属性,
String profileSpec = root.getAttribute(PROFILE_ATTRIBUTE);
if (StringUtils.hasText(profileSpec)) {
// profile可以同时指定多个
String[] specifiedProfiles = StringUtils.tokenizeToStringArray(
profileSpec, BeanDefinitionParserDelegate.MULTI_VALUE_ATTRIBUTE_DELIMITERS);
// We cannot use Profiles.of(...) since profile expressions are not supported
// in XML config. See SPR-12458 for details.
// 判断每一个profile是否都符合环境变量中所定义的,如果不符合则不会浪费性能去解析
if (!getReaderContext().getEnvironment().acceptsProfiles(specifiedProfiles)) {
if (logger.isDebugEnabled()) {
logger.debug("Skipped XML bean definition file due to specified profiles [" + profileSpec +
"] not matching: " + getReaderContext().getResource());
}
return;
}
}
}
// 解析前处理,留给子类实现(空方法,面向继承设计的,如果继承自DefaultBeanDefinitionDocumentReader的类,
// 需要在Bean解析前后做一些出来的话,就可以重写这两个方法)
preProcessXml(root);
// 正式解析 BeanDefinition
parseBeanDefinitions(root, this.delegate);
// 解析后处理,留给子类实现(空方法,面向继承设计的,如果继承自DefaultBeanDefinitionDocumentReader的类,
// 需要在Bean解析前后做一些出来的话,就可以重写这两个方法)
postProcessXml(root);
this.delegate = parent;
}
这里最开始,判断到是默认的命名空间的话,就先对profile进行处理。
2.2.1.1 profile属性的使用
Spring官方文档上的示例代码:
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:jdbc="http://www.springframework.org/schema/jdbc"
xmlns:jee="http://www.springframework.org/schema/jee"
xsi:schemaLocation="...">
<!-- other bean definitions -->
<beans profile="development">
<jdbc:embedded-database id="dataSource">
<jdbc:script location="classpath:com/bank/config/sql/schema.sql"/>
<jdbc:script location="classpath:com/bank/config/sql/test-data.sql"/>
</jdbc:embedded-database>
</beans>
<beans profile="production">
<jee:jndi-lookup id="dataSource" jndi-name="java:comp/env/jdbc/datasource"/>
</beans>
</beans>
通过这个特性,同时在配置文件中部署不同的配置来适用于不同的开发环境生产环境等,可以方便的进行切换、部署环境;profile的配置一定要在xml文档的最下边,否则会有异常。
- 激活profile(Spring提供了多种激活profile的方式):
- ENV方式:
ConfigurableEnvironment.setActiveProfiles("development");
- JVM参数方式:
set JAVA_OPTS="-Dspring.profiles.active=development"
- web.xml方式:
<context-param> <param-name>spring.profiles.active</param-name> <param-value>development</param-value> </context-param>
- 注解方式(junit单元测试):
@ActiveProfiles({"development","production"})
了解了profile的使用之后,回到代码;首先程序会获取beans节点是否定义了profile属性,如果定义了就需要到环境变量中去寻找,并解析每个profile是否都符合环境变量中定义的,如果不符合就直接返回,不再进行解析:
if (!getReaderContext().getEnvironment().acceptsProfiles(specifiedProfiles)) {
if (logger.isDebugEnabled()) {
logger.debug("Skipped XML bean definition file due to specified profiles [" + profileSpec +
"] not matching: " + getReaderContext().getResource());
}
return;
}
2.2.1.2 正式解析BeanDefinitions
preProcessXml(root);
// 正式解析 BeanDefinition
parseBeanDefinitions(root, this.delegate);
postProcessXml(root);
这里有两个方法:preProcessXml(root) 和 postProcessXml(root),跟进代码去看到,都是空的。这里可能会有疑惑,空方法为什么还要写在这里呢?
在这里是使用了模板方法模式,这两个方法是为子类而设计的,如果继承自 DefaultBeanDefinitionDocumentReader 的子类需要在Bean解析前后做一些处理的话,只需要重写这两个方法即可。
在处理了profile之后,就可以进行XML配置文件的读取了;跟进代码:
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 node = nl.item(i);
// 对bean的处理
if (node instanceof Element) {
Element ele = (Element) node;
/**
*/
// 对默认标签的bean的处理
if (delegate.isDefaultNamespace(ele)) {
// 对bean的处理
parseDefaultElement(ele, delegate);
}
// 对自定义标签的bean的处理
else {
delegate.parseCustomElement(ele);
}
}
}
}
else {
delegate.parseCustomElement(root);
}
}
在这里通过遍历,对每个BeanDefinition进行解析和注册;对于root节点或者子节点来说,如果是默认命名空间的话,就使用对默认标签的解析:parseDefaultElement;不是的话则使用对自定义标签的解析:parseCustomElement。
因为在Spring中,XML配置里面有两大类型的Bean声明:
- 默认类型Bean:
<bean id="test" class="myTestBean"/>
- 自定义类型Bean
<tx: annotation-driven/>
这两种不同声明方式的Bean的解析差别很大,所以需要不同的解析方法。
3. 整体流程思维导图
最后附上一个整体流程的思维导图:Spring容器初始化体系,本篇文章对应其中的 容器初始化到正式解析BeanDefinitions部分。