xmlBeanFactory源码解析

一.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标签

 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值