一.xmlBeanFactory结构
先来看看怎么用
我们可以使用xmlBeanFactory把spring使用起来,代码如下:
ClassPathResource classPathResource = new ClassPathResource("spring2.xml");
XmlBeanFactory xmlBeanFactory = new XmlBeanFactory(classPathResource);
model2 model2 = (com.luban.以xml的方式调试spring.model.model2) xmlBeanFactory.getBean("dao");
System.out.println(model2.getStr1());
xmlBeanFactory的结构:
看上去是不是头皮发麻?只能说习惯就好,spring就是这么变态。
可以看到XmlBeanFactory继承了DefaultListableBeanFactory,而DefaultListableBeanFactory一路向上到SimpleAliasRegistry。
总之,我们可以这样认为:xmlBeanFactory在它的父类的基础上多了一个功能,那就是读取xml里面的内容,并且加入到ioc容器中。
xmlBeanFactory的构造方法:
xmlBeanFactory内部维护了应该reader:
看名字也能够猜测是,这个东西是用来读取xml的,但其实spring里面存在很多中间商,比如这个reader,乍一看上去这个reader是用来读取xml文件里面beanDef,事实上他只是一个中间商,他把这个读取的工作又外包了出去,后面会说到。这里我们只需要知道,这个reader只需要调用一下,能够向xmlBeanFactory里的ioc容器塞入beanDef。
这里要注意到,这个变量是在XmlBeanFactory一实例化出来就有了,这里把this传入,相当于这个reader持有了外部的一个引用(这个外部引用自然就是xmlBeanFactory),后面可以直接操作这个this,回调xmlBeanFactory里面的ioc容器的注册方法。
private final XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(this);
xmlBeanFactory的构造方法:
可以看到,直接就调用reader的loadBeanDefinitions
public XmlBeanFactory(Resource resource, BeanFactory parentBeanFactory) throws BeansException {
// 初始化父类,如果无需设置父类ioc容器,这么传一个null
super(parentBeanFactory);
// 这里直接调用到子类的方法,
// 需要和FileSystemXmlApplicationContext做一下区别,
this.reader.loadBeanDefinitions(resource);
}
二.Resource和EncodedResource
reosurce是spring对资源的底层做的一个抽象,万物皆可抽象,一切属于资源的东西,都能抽象成一个resource。比如文件系统的某个文件,比如网络资源等等。
在这个例子中,我们使用的是ClassPathResource,看一下ClassPathResource的层次结构:
可以看到,所有的文件资源,最后都会实现Resource和InputStreamSource,这两个接口
看下InputStreamResource,只有一个抽象方法,就是返回一个文件输出流
这里的含义为:对于一切能够返回输入流的资源,实现这个接口,重写这个方法,返回一个关于该文件的输出流
Reosurce接口定义了资源的一些操作,比如文件是否存在,是否打开,是否可读,是否是一个文件,拿到文件的定位符等。
这里其实也是一个单一职责问题,像Resource接口,里面其实都是一个读的操作,不是写,就是不会对文件本身产生什么影响。而获取一个文件的输入流,是直接拿到这个文件的本体,所以这里也是spring需要把这两个东西区分开来原因之一。
我们来看看ClassPathReousrce是怎么实现getInputStream
也是非常简单,调用的Class底层的方法,或者是classLoader的的底层方法
public InputStream getInputStream() throws IOException {
InputStream is;
if (this.clazz != null) {// 先使用class的底层方法,把一个路径转成一个输入流
is = this.clazz.getResourceAsStream(this.path);
}
// 如果加载器不为空,调用底层方法,
// 这个加载器在本类实例化的时候,会吧这个classLoad设置进去,所以这里必定不为空
else if (this.classLoader != null) {
is = this.classLoader.getResourceAsStream(this.path);
}
else {// 如果两个都没有的话,那么就使用默认的系统加载器,AppClassLoader
is = ClassLoader.getSystemResourceAsStream(this.path);
}
// 加载不到,说明该路径对应的文件不存在,直接报错
if (is == null) {
throw new FileNotFoundException(getDescription() + " cannot be opened because it does not exist");
}
// 返回对应的文件输入流
return is;
/**
* 1.使用Class类来加载文件输入流
* 2.使用类加载器来加载文件输入流
* 3.使用jvm内置的类加载器来加载文件输入流
* 4.返回加载出来的输入流【输入流为空,抛异常】
*/
}
EncodedResource:
这个类内部维护了1个编码记录
getReader,对Resource里面的inputStream进行了包装,弄成一个InputStreamReader,使用对应的编码格式进行了重新编码。
三.使用XmlBeanDefinitionReader读取xml文件
我们回到xmlBeanFactory的构造方法,看这一行,跟进去:
调用了另外一个重载方法,把resource重新包装了一下,这里其实就是为reosurce重新设置了一个编码。
进入该方法,会先把对应的编码好的resource加入到一个本地线程中,表示当前有xml文件正在处理。
把xml的输出流包装成一个InputResource,因为SAX解析xml需要转成一个inputResource。然后进入下一层方法。
最后完成xml解析和注册之后,在finally里面把本地线程的内容清空,表示当前没有对应的xml在处理。
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.getResource());
}
// 从本地线程获取到对应的EncodedResource集合,在实例化本类的时候,会有一个name设置进去
// XML bean definition resources currently being loaded
Set<EncodedResource> currentResources = this.resourcesCurrentlyBeingLoaded.get();
// 第一次进入,这个为null
if (currentResources == null) {
currentResources = new HashSet<>(4);
this.resourcesCurrentlyBeingLoaded.set(currentResources);
}
// 把要处理的Resource添加到本地线程的set集合中
if (!currentResources.add(encodedResource)) {
throw new BeanDefinitionStoreException(
"Detected cyclic loading of " + encodedResource + " - check your import definitions!");
}
try {
//将资源文件转为InputStream的IO流
InputStream inputStream = encodedResource.getResource().getInputStream();
try {
//从InputStream中得到XML的解析源,InputSource专门用来做xml解析的一个类
InputSource inputSource = new InputSource(inputStream);
if (encodedResource.getEncoding() != null) {
inputSource.setEncoding(encodedResource.getEncoding());
}
//这里是具体的读取过程
return doLoadBeanDefinitions(inputSource, encodedResource.getResource());
}
finally {
//关闭从Resource中得到的IO流
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();
}
}
/**
* 1.处理之前,会把传入的EncodedResource放入本地线程,是一个set集合
* 2.把EncodedResource转成一个InputSource【就是把输入流和编码,从EncodedResource拿出来,再set到InputSource】
* 3.调用doLoadBeanDefinitions(转换完成的InputSource,raw Resource)
* 4.处理完毕,本地线程把对应的集合remove掉 */
}
调用了doLoadDocment来对,inputSoruce进行转换,直接转成一个doc对象。
跟进去doLoadDocument看看,这里的的load是这个DefaultDocumentLoader:
/**
* 参数 : inputSource:xm文件的InputSource resource:xm文件的Resource
* 1.校验,得到xml的解析规则【tdt/xsd】
* 2.创建一个文档解析工厂,通过工厂得到一个文档解析器【设置了2个基本内容:1.错误Handler。2.EntityResolver】
* 3.返回解析出来的doc对象
*
* getValidationModeForResource得到xml是以dtd或者xsd解析。如果是xsd,那么会set进去一个attribute
* 然后getEntityResolver会根据dtd或者xsd来判断,在回调阶段,会拿到getValidationModeForResource设置进去的attrobute【如果是xsd解析的话】
* 如果是dtd结尾,则使用默认的spring-beans.dtd来解析
* 如果是xsd结尾,则使用对应的网站作为key来解析
* 简单来说,就是getValidationModeForResource给你设值,getEntityResolver拿出这个值做判断,取出最终的dtd或者xsd文件给你解析
*/
protected Document doLoadDocument(InputSource inputSource, Resource resource) throws Exception {
return this.documentLoader.loadDocument(
inputSource,// xml的inputSource
getEntityResolver(), // 得到一个xsd和dtd的本地寻找器,xsd和dtd不可能每次都从网络下载
this.errorHandler,// 错误处理器
getValidationModeForResource(resource),// 这里对xml文件进行模式校验,主要是得出xsd校验还是dtd校验
isNamespaceAware()
);
}
dtd和xsd的区别:
DTD即文档类型定义,是一种XML约束模式语言,是XML文件的验证机制,属于XML文件组成的一部分。
DTD 是一种保证XML文档格式正确的有效方法,可以通过比较XML文档和DTD文件来看文档是否符合规范,元素和标签使用是否正确。
一个 DTD文档包含:元素的定义规则,元素间关系的定义规则,元素可使用的属性,可使用的实体或符号规则。
DTD和XSD相比:DTD 是使用非 XML 语法编写的。 DTD 不可扩展,不支持命名空间,只提供非常有限的数据类型 .
XML Schema语言也就是XSD。XML Schema描述了XML文档的结构。 可以用一个指定的XML Schema来验证某个XML文档,以检查该XML文档是否符合其要求。 文档设计者可以通过XML Schema指定一个XML文档所允许的结构和内容,并可据此检查一个XML文档是否是有效的。 XML Schema本身是一个XML文档,它符合XML语法结构。可以用通用的XML解析器解析它。 一个XML Schema会定义:文档中出现的元素、文档中出现的属性、子元素、子元素的数量、子元素的顺序、 元素是否为空、元素和属性的数据类型、元素或属性的默认 和固定值。
XSD是DTD替代者的原因,一是据将来的条件可扩展,二是比DTD丰富和有用,三是用XML书写, 四是支持数据类型,五是支持命名空间。
综上,xsd比dtd拥有更好的扩展性,支持更多的数据类型,可用xml语法规则进行解析等
getValidationModeForResource(resource)
这个方法是用来确定xml文件是使用xsd来校验的还是使用dtd来校验的。xmlBeanDefinitionReader本身是可以设定按什么模式来解析xml文件的,比如xsd或者dtd。
瞅一瞅源码:
可以看到,如果是设定了自动查找,则进行自动查找,否则,就按照用户指定的模式进行校验。
如果设定了自动查找,但是xml里面没匹配到对应的校验模式,那么按照xsd来校验。
跟入detectValidationMode看看
可以看到,这个工作,XmlBeanDefinitionReader本身不会去验证,交给了一个this.validationModeDetector的属性去操作
看一下这个this.validationModeDetector是什么东西:
可以看到,是一个XmlValidationModeDetetor实例
xml验证模式解析
可以看到,这里spring采用的其实没什么高深的方式,就是直接一行一行读取xml文件,然后把读取出来的内容进行判断。
首先是判断是不是是不是注释,如果是注释直接跳过。如果加了DOCTYPE,马上返回,加了DOCTYPE就表示是以dtd来校验。如果上面连个条件都不命中,然后又读取到了一个正常的标签,说明是xsd格式,直接返回。
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,马上break
if (hasDoctype(content)) {
isDtdValidated = true;
break;
}
// 如果上面的第二个 if (hasDoctype(content)) 不中,然后这个中了,
// 说明这个xml文件铁定没有以DOCTYPE开头了,因为DOCTYPE必定在最前面,
// 最前面都没有,然后就到了一个正常的xml标签了,那么并且不是DOCTYPE,也就意味着该xml没有引入一个dtd文件
// 那么我们就可以用xsd来解析之
// hasOpeningTag => 判断一个标签是不是一个正常的标签【1.以<开头、2.<后面是字母】
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();
}
/**1.得到输入流,isDtdValidated=false
* 2.一行一行读取xml文件
* 如果是注释,跳过
* 如果检测到以DOCTYPE开头,直接断开循环, isDtdValidated=true
* 如果检测到以 "<开头,并且<后面是一个字母",断开循环
* 3.isDtdValidated => false xsd
* isDtdValidated => true dtd
*/
}
这里看一下spring是如何判断一个标签是注释的:
通过一个变量来记录,上一次是读取到一个注释的末尾还是一个注释的开端。在consume里面会不断把对应的字符串进行裁剪,如果是一个注释,那么最终这个字符串会被裁剪成一个空字符串。
/**
* the DOCTYPE declaration or the root element of the document.
* 处理line,如果是注释的话,返回空字符串,如果不是,返回原字符串
* 否则返回null
*/
@Nullable
private String consumeCommentTokens(String line) {
if (!line.contains(START_COMMENT) && !line.contains(END_COMMENT)) {
return line;
}
// 如果到这里,可能加了<!--或者-->
String currLine = line;
// 进行判断,是不是真的加了,如果加了,那么这里会返回截取后的结果。
// eg:<!--123--> => 这里会返回 123 -->
// eg: 123 --> => 这里会返回 "" 空字符串
while ((currLine = consume(currLine)) != null) {
// 当一个注释全部解析完毕,会命中这里,返回一个currLine,应该是空字符串
if (!this.inComment && !currLine.trim().startsWith(START_COMMENT)) {
return currLine;
}
}
return null;
}
/**
* 根据this.inComment来解析,如果this.inComment为fasle,表示之前已经处理过一个注释的开头,现在需要处理结尾
* 如果this.inComment是一个true,表示之前已经处理过一个注释的开头了,现在需要处理结尾
*/
@Nullable
private String consume(String line) {
int index = (this.inComment ? endComment(line) : startComment(line));
return (index == -1 ? null : line.substring(index));
}
/**
* Try to consume the {@link #START_COMMENT} token.
* @see #commentToken(String, String, boolean)
* 期望该line是一个含有 <!--
* 并且需要把本类的inComment改成true,表示当前正处于一个注释的开头,还没到结尾
*/
private int startComment(String line) {
return commentToken(line, START_COMMENT, true);
}
/**
* 同上,期望该line是一个含有 -->
* 并且需要把本类的inComment改成false,表示当前正处于一个注释的结尾
* @param line
* @return
*/
private int endComment(String line) {
return commentToken(line, END_COMMENT, false);
}
/**
* inCommentIfPresent => true表示是一个注释的开始
* inCommentIfPresent => false表示是一个注释的结束
* 匹配line里面是否有token,如果有,吧this.inComment设置上对应的值 => inCommentIfPresent
* 返回对应的结束位置,如果没有匹配上,则返回-1
*/
private int commentToken(String line, String token, boolean inCommentIfPresent) {
int index = line.indexOf(token);
// 如果index大于-1,说明能够匹配到 token,也说明是一个符合预期的字符串
if (index > - 1) {
this.inComment = inCommentIfPresent;// 符合预期,就把 当前字符串是注释的开始或者结束给设置上
}
// 返回匹配到的诶之,eg:<!-- --> token="<!--",那么返回值为0+4
return (index == -1 ? index : index + token.length());
}
这样子就能够得到对应的xml文件的验证模式了。
getEntityResolver()
上面得到对应的xml的验证模式之后,在这里就需要提供对应的xsd或者dtd文件,spring的做法是从本类路径下面去加载的。
对应的文件存放在spring-beans模块下面。
下面我们就来看看,spring是怎么去寻找的。
可以看到,这里的ResourceEntityResolver和DelegatinEntityResolver,其实他们是父子关系,ResourceEntityResolver继承了DelegatingEntotyResolver。
ResourceEntityResolver:
可以看到,这里直接调用到了父类的resolveEntity方法。
@Override
@Nullable
public InputSource resolveEntity(String publicId, @Nullable String systemId) throws SAXException, IOException {
/** 回调父类的resolveEntity,得到一个xsd或者tdt的InputSource
* 以systemId来判断,如果是xsd结尾的,取对应的网站对应的内容
* 如果是tdt结尾的,默认取到当前这个类的类路径下面的/spring-beas.tdt
*/
InputSource source = super.resolveEntity(publicId, systemId);
// 如果读取不到,但是systemId又有,这里做特殊处理
if (source == null && systemId != null) {
String resourcePath = null;
try {
String decodedSystemId = URLDecoder.decode(systemId, "UTF-8");
String givenUrl = new URL(decodedSystemId).toString();
String systemRootUrl = new File("").toURI().toURL().toString();
// Try relative to resource base if currently in system root.
if (givenUrl.startsWith(systemRootUrl)) {
resourcePath = givenUrl.substring(systemRootUrl.length());
}
}
catch (Exception ex) {
// Typically a MalformedURLException or AccessControlException.
if (logger.isDebugEnabled()) {
logger.debug("Could not resolve XML entity [" + systemId + "] against system root URL", ex);
}
// No URL (or no resolvable URL) -> try relative to resource base.
resourcePath = systemId;
}
if (resourcePath != null) {
if (logger.isTraceEnabled()) {
logger.trace("Trying to locate XML entity [" + systemId + "] as resource [" + resourcePath + "]");
}
Resource resource = this.resourceLoader.getResource(resourcePath);
source = new InputSource(resource.getInputStream());
source.setPublicId(publicId);
source.setSystemId(systemId);
if (logger.isDebugEnabled()) {
logger.debug("Found XML entity [" + systemId + "]: " + resource);
}
}
}
return source;
}
DelegatingEntityResolver:
这里面会根据systemId的后缀,来寻找对应的验证文件。
我们来看看这两个Resolver是什么:
原来在构造方法执行完毕后,就会把这两个初始化出来
先来看看dtdResolver
直接跑去读取了类路径下面的spring-beans.dtd,这个文件在上面已经截图展示过了,这里不在细说
public InputSource resolveEntity(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 + "]");
}
// 如果是dtd结尾的
if (systemId != null && systemId.endsWith(DTD_EXTENSION)) {
int lastPathSeparator = systemId.lastIndexOf("/");
int dtdNameStart = systemId.indexOf(DTD_NAME, lastPathSeparator);
if (dtdNameStart != -1) {
// 这里拼接了最终的路径 spring-beans.dtd
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.isDebugEnabled()) {
logger.debug("Found beans DTD [" + systemId + "] in classpath: " + dtdFile);
}
return source;
}
catch (IOException ex) {
if (logger.isDebugEnabled()) {
logger.debug("Could not resolve beans DTD [" + systemId + "]: not found in classpath", ex);
}
}
}
}
// Use the default behavior -> download from website or wherever.
return null;
}
PluggableSchemaResolver
这个也很简单,直接通过一个systemId拿到对应的文件位置,然后转成一个Resource返回
public InputSource resolveEntity(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) {
// 这里getSchemaMappings()会把spring里面所有资源文件都加载到一个map里面,key:systemId val:真正对应的路径
String resourceLocation = getSchemaMappings().get(systemId);
if (resourceLocation != null) {
// 取到了对应的位置,直接搞成一个resource
Resource resource = new ClassPathResource(resourceLocation, this.classLoader);
try {
// 得到InputSource
InputSource source = new InputSource(resource.getInputStream());
source.setPublicId(publicId);
source.setSystemId(systemId);
if (logger.isDebugEnabled()) {
logger.debug("Found XML schema [" + systemId + "] in classpath: " + resourceLocation);
}
return source;
}
catch (FileNotFoundException ex) {
if (logger.isDebugEnabled()) {
logger.debug("Couldn't find XML schema [" + systemId + "]: " + resource, ex);
}
}
}
}
return null;
/** 1.根据SystemId拿到对应的网站的位置
* 2.直接使用ClassPathResource加载,得到一个InputSource
* 3.返回一个InputSource */
}
getSchemaMappings
懒加载所有的资源文件,以systemId为key,真实文件路径为val
private Map<String, String> getSchemaMappings() {
Map<String, String> schemaMappings = this.schemaMappings;
/** 如果这个map为空,把所有的xsd加载进来,以所在的网站为key,对应的位置为val。
* 【spring的xml文档解析的dtd或者xsd文件,存放在spring-bean这个工程下面】eg:
* 0 = {ConcurrentHashMap$MapEntry@1379} "http://www.springframework.org/schema/context/spring-context-3.2.xsd" ->
* "org/springframework/context/config/spring-context.xsd"
*
1 = {ConcurrentHashMap$MapEntry@1380} "http://www.springframework.org/schema/cache/spring-cache-4.3.xsd" ->
"org/springframework/cache/config/spring-cache.xsd"
*/
if (schemaMappings == null) {
synchronized (this) {
schemaMappings = this.schemaMappings;
if (schemaMappings == null) {
if (logger.isDebugEnabled()) {
logger.debug("Loading schema mappings from [" + this.schemaMappingsLocation + "]");
}
try {
Properties mappings =
PropertiesLoaderUtils.loadAllProperties(this.schemaMappingsLocation, this.classLoader);
if (logger.isDebugEnabled()) {
logger.debug("Loaded schema mappings: " + mappings);
}
Map<String, String> mappingsToUse = new ConcurrentHashMap<>(mappings.size());
CollectionUtils.mergePropertiesIntoMap(mappings, mappingsToUse);
schemaMappings = mappingsToUse;
this.schemaMappings = schemaMappings;
}
catch (IOException ex) {
throw new IllegalStateException(
"Unable to load schema mappings from location [" + this.schemaMappingsLocation + "]", ex);
}
}
}
}
return schemaMappings;
/** 获取一个concurrentHashMap
* 如果为空,加载当前路径下面的/META-INF/spring.schemas
* 如果不为空,直接返回map
*/
}
四.使用DefaultBeanDefinitionDocumentReader来读取doc对象
上面说了那么多,现在我们继续回归正题。
接下来我们进入registryBeanDefinitions看看
在这里可以看到,xmlBeanDefinitionReader不会自己亲自去读取doc,而是交给了一个叫BeanDefinitionDocumentReaer去做处理,其实xmlBeanDefinitionReader只是一个中间商,把输入流转成一个doc,是交给了DefaultDocumentLoader。而现在,这个doc的读取,交给了BeanDefinitionDocumentReader。
createBeanDefinitionDocumentReader:
这里的这个this.documentReaderClass实际上就是DefaultBeanDefinitionDocumentReader.class
//创建BeanDefinitionDocumentReader对象,解析Document对象 => DefaultBeanDefinitionDocumentReader
protected BeanDefinitionDocumentReader createBeanDefinitionDocumentReader() {
// BeanUtils.instantiateClass => 使用反射把一个Class对象实例化成对应的对象
// DefaultBeanDefinitionDocumentReader impliments BeanDefinitionDocumentReader
// 实例化之后,使用Object的cast强转。
return BeanDefinitionDocumentReader.class.cast(BeanUtils.instantiateClass(this.documentReaderClass));
}
继续回归正题:
可以看到,调用到了DefaultBeanDefinitionDocumentReader的registerBeanDefinitions
这里跟进去,最终会到这里:
这里进来会先处理profile,会先按照,或者;来把profile分割开来,传入acceptsProfiles循环处理。这里主要是验证,当前的profile是否需要跳过,如果不符合,直接跳过。
/**
* Register each bean definition within the given root {@code <beans/>} element.
*/
protected void doRegisterBeanDefinitions(Element root) {
// Any nested <beans> elements will cause recursion in this method. In
// order to propagate and preserve <beans> default-* attributes correctly,
// keep track of the current (parent) delegate, which may be null. Create
// the new (child) delegate with a reference to the parent for fallback purposes,
// then ultimately reset this.delegate back to its original (parent) reference.
// this behavior emulates a stack of delegates without actually necessitating one.
// 具体的解析过程由BeanDefinitionParserDelegate实现,
// BeanDefinitionParserDelegate中定义了Spring Bean定义XML文件的各种元素
// 把本类里面的BeanDefinitionParserDelegate作为新的BeanDefinitionParserDelegate的父类,这里的parent是null
BeanDefinitionParserDelegate parent = this.delegate;
this.delegate = createDelegate(getReaderContext(), root, parent);
// 默认的nameSpace => http://www.springframework.org/schema/beans
// 下面是处理profile,
if (this.delegate.isDefaultNamespace(root)) {
String profileSpec = root.getAttribute(PROFILE_ATTRIBUTE);
if (StringUtils.hasText(profileSpec)) {
String[] specifiedProfiles = StringUtils.tokenizeToStringArray(
profileSpec, BeanDefinitionParserDelegate.MULTI_VALUE_ATTRIBUTE_DELIMITERS);
// 看一下当前标签里面的profile文件是不是合法的,如果命中这里,说明当前标签的profile是一个不需要解析的环境
if (!getReaderContext().getEnvironment().acceptsProfiles(specifiedProfiles)) {
if (logger.isInfoEnabled()) {
logger.info("Skipped XML bean definition file due to specified profiles [" + profileSpec +
"] not matching: " + getReaderContext().getResource());
}
return;
}
}
}
//在解析Bean定义之前,进行自定义的解析,增强解析过程的可扩展性
preProcessXml(root);
//从Document的根元素开始进行Bean定义的Document对象
parseBeanDefinitions(root, this.delegate);
//在解析Bean定义之后,进行自定义的解析,增加解析过程的可扩展性
postProcessXml(root);
this.delegate = parent;
/** 1.为BeanDefinitionParserDelegate设置父类,主要是处理beans
* 2.处理profile,根据当前beans的prifile属性,再配合加载进来的profile环境,对比一下当前的beans是否需要加载
* 3.执行BeanDef解析
*/
}
先看看profile的处理,可以看到,这里如果发现是!开头的话,去掉!,调用isProfileActive看看,没有命中,则说明这个profile需要解析。比如!abc,表示的含义是,如果当前环境不是abc的话,该profile需要解析。这里可以看到,如果是命中了某个profile文件,那么会直接返回,剩下的不理会。
public boolean acceptsProfiles(String... profiles) {
Assert.notEmpty(profiles, "Must specify at least one profile");
for (String profile : profiles) {
// 如果当前的profile不为空,并且以!开头,去掉!放入验证
if (StringUtils.hasLength(profile) && profile.charAt(0) == '!') {
// 去掉 "!" 放入检测,如果没命中
// !xxx,表示的是如果运行环境不是xxx的,都可以,所以这里如果发现profile没有命中两个map,一个set集合,那么则说明
// 此处语义符合。
if (!isProfileActive(profile.substring(1))) {
return true;
}
}
// 是否是活跃的profile,主要是检测有没有命中两个map,一个set集合
else if (isProfileActive(profile)) {
return true;
}
}
return false;
}
看下isProfileActive
这里的validateProfile是验证profile文件是不是为空,或者是以!开头的
这里面有一个获取活跃的profile的方法,doGetActiveProfiles,这个方法会拿出我们配置进去的profile文件的name
取出来是一个集合。
protected boolean isProfileActive(String profile) {
// 校验profile文件,如果是 【!开头/为空】 报错
validateProfile(profile);
// 拿出活跃的profile文件,默认的key是 => ACTIVE_PROFILES_PROPERTY_NAME => spring.profiles.active
Set<String> currentActiveProfiles = doGetActiveProfiles();
// 当前传入的profile是活跃的,并且是默认的
return (currentActiveProfiles.contains(profile) ||
(currentActiveProfiles.isEmpty() && doGetDefaultProfiles().contains(profile)));
}
doGetActiveProfile
里面会固定从spring.profile.active这个key去获取对应的活跃profile
protected Set<String> doGetActiveProfiles() {
synchronized (this.activeProfiles) {
if (this.activeProfiles.isEmpty()) {
// ACTIVE_PROFILES_PROPERTY_NAME => spring.profiles.active
// 从环境里面拿出这个key对应的property数据
String profiles = getProperty(ACTIVE_PROFILES_PROPERTY_NAME);
if (StringUtils.hasText(profiles)) {
setActiveProfiles(StringUtils.commaDelimitedListToStringArray(
StringUtils.trimAllWhitespace(profiles)));
}
}
return this.activeProfiles;
}
}
继续回归正题:
protected void doRegisterBeanDefinitions(Element root) {
// 具体的解析过程由BeanDefinitionParserDelegate实现,
// BeanDefinitionParserDelegate中定义了Spring Bean定义XML文件的各种元素
// 把本类里面的BeanDefinitionParserDelegate作为新的BeanDefinitionParserDelegate的父类,这里的parent是null
BeanDefinitionParserDelegate parent = this.delegate;
this.delegate = createDelegate(getReaderContext(), root, parent);
// 默认的nameSpace => http://www.springframework.org/schema/beans
// 下面是处理profile,
if (this.delegate.isDefaultNamespace(root)) {
String profileSpec = root.getAttribute(PROFILE_ATTRIBUTE);
if (StringUtils.hasText(profileSpec)) {
String[] specifiedProfiles = StringUtils.tokenizeToStringArray(
profileSpec, BeanDefinitionParserDelegate.MULTI_VALUE_ATTRIBUTE_DELIMITERS);
// 看一下当前标签里面的profile文件是不是合法的,如果命中这里,说明当前标签的profile是一个不需要解析的环境
if (!getReaderContext().getEnvironment().acceptsProfiles(specifiedProfiles)) {
if (logger.isInfoEnabled()) {
logger.info("Skipped XML bean definition file due to specified profiles [" + profileSpec +
"] not matching: " + getReaderContext().getResource());
}
return;
}
}
}
//在解析Bean定义之前,进行自定义的解析,增强解析过程的可扩展性
preProcessXml(root);
//从Document的根元素开始进行Bean定义的Document对象
parseBeanDefinitions(root, this.delegate);
//在解析Bean定义之后,进行自定义的解析,增加解析过程的可扩展性
postProcessXml(root);
this.delegate = parent;
/** 1.为BeanDefinitionParserDelegate设置父类,主要是处理beans
* 2.处理profile,根据当前beans的prifile属性,再配合加载进来的profile环境,对比一下当前的beans是否需要加载
* 3.执行BeanDef解析
*/
}
这里要2个地方要注意:
对于这种beans嵌套beans的标签,spring内部的delegate会组建成一颗树。
eg:
<beans>
<beans>
<beans></beans>
</beans>
<bean></bean>
</beans>
解析上面的栗子的时候,会进到第三个beans,相当于现在有了3层嵌套的delegate,然后当最后的beans解析完毕之后,会回到上一层,这个时候,树就削减了一层,对应这个代码:
相当于当前的delegate指针指向了它的父亲,其实也就是递归回溯的时候,做了一个改变。
解析玩profile之后,如果确定该profile需要解析,则进入下一步。这里有2个方法:preProcessXml和postProcessXml
这两个在DefaultBeanDefinitionDocumentReader并没有实现,留给子类做扩展。
parseBeanDefinitions:
主要是根据命名空间来确定,是使用spring的规则来解析,还是使用用户自定义的规则来解析
//使用Spring的Bean规则从Document的根元素开始进行Bean定义的Document对象
protected void parseBeanDefinitions(Element root, BeanDefinitionParserDelegate delegate) {
//Bean定义的Document对象使用了Spring默认的XML命名空间
if (delegate.isDefaultNamespace(root)) {
//获取Bean定义的Document对象根元素的所有子节点,最外面的beans
NodeList nl = root.getChildNodes();
for (int i = 0; i < nl.getLength(); i++) {
Node node = nl.item(i);
//获得Document节点是XML元素节点
if (node instanceof Element) {
Element ele = (Element) node;
//Bean定义的Document的元素节点使用的是Spring默认的XML命名空间
if (delegate.isDefaultNamespace(ele)) {
//使用Spring的Bean规则解析元素节点
parseDefaultElement(ele, delegate);
}
else {
//没有使用Spring默认的XML命名空间,则使用用户自定义的解析规则解析元素节点
delegate.parseCustomElement(ele);
}
}
}
}
else {
//Document的根节点没有使用Spring默认的命名空间,则使用用户自定义的
//解析规则解析Document根节点
delegate.parseCustomElement(root);
}
}
我们把重点放在parseDefaultElement
xml文件里面一共有四种标签,这里对应四种标签进行进一步的解析
//使用Spring的Bean规则解析Document元素节点
private void parseDefaultElement(Element ele, BeanDefinitionParserDelegate delegate) {
//如果元素节点是<Import>导入元素,进行导入解析
if (delegate.nodeNameEquals(ele, IMPORT_ELEMENT)) {
importBeanDefinitionResource(ele);
}
//如果元素节点是<Alias>别名元素,进行别名解析
else if (delegate.nodeNameEquals(ele, ALIAS_ELEMENT)) {
processAliasRegistration(ele);
}
//元素节点既不是导入元素,也不是别名元素,即普通的<Bean>元素,
//按照Spring的Bean规则解析元素
else if (delegate.nodeNameEquals(ele, BEAN_ELEMENT)) {
processBeanDefinition(ele, delegate);
}
/** 如果是beans中又包含beans,那么会搞成一个树状。
* eg:
* <beans>A
* <beans>
* <beans></beans>D
* </beans>B
* <beans></beans>C
* </beans>
* A是根结点,B和C是A下面的节点,D是B下面的节点
* A
* B C
* D
*/
else if (delegate.nodeNameEquals(ele, NESTED_BEANS_ELEMENT)) {
// recurse
doRegisterBeanDefinitions(ele);
}
}
本次的源码解析到这里就结束了,下一届我们会继续上面的代码,讲解怎么解析bean标签