Spring(三):Dom解析与注册流程分析

前面已经大体理解了加载Bean的那个过程

  • 进行编码
  • 使用ThreadLocal来避免循环加载配置文件
  • 获取Dom信息并注册

获取Dom信息并注册

在doLoadDocument方法里面就是获取Dom信息

	protected Document doLoadDocument(InputSource inputSource, Resource resource) throws Exception {
		return this.documentLoader.loadDocument(inputSource, getEntityResolver(), this.errorHandler,
				getValidationModeForResource(resource), isNamespaceAware());
	}

可以看到这个方法,使用了documentLoader去实际加载指定的文档

这里面还调用了一个方法getValidationModeForResource

这个方法的作用是获取对XML的验证模式!

获取XML的验证模式

XML的验证模式保证了XML文件的正确性,比较常用的验证模式主要有两种,DTD和XSD

DTO和XSD验证模式的区别

DTO,称为文档类型定义,是一种XML约束模式语言,同时也是XML的验证机制,验证的机制是通过比较XML文档和DTD文件来看文档是否符合规范

XSD,本身就是XML语言(Xml Scheme Definition),使用一个指定的XMl Scheme来验证某个XML文档,来检查该XML文档是否符合其要求,即XML Scheme可以限制XML文档所允许的结构和内容,并根据此检查XML文档是否有效

而验证模式的定义使用,是放在配置文件的头描述里面的

比如使用XSD

在这里插入图片描述
使用DTD格式的
在这里插入图片描述

验证模式的获取

获取验证模式,其实就是XmlBeanDefinitionReader调用了getValidationModeForResource方法

下面就来看看这个方法的底层

getValidationModeForResource
	protected int getValidationModeForResource(Resource resource) {
        //获取手动设置的验证模式(XmlBeanDefinitionReader可以设置validationMode)
        //也就是从代码层面上进行设置
		int validationModeToUse = getValidationMode();
        //如果验证模式不是未指定的,就返回获取的验证模式
		if (validationModeToUse != VALIDATION_AUTO) {
			return validationModeToUse;
		}
        //如果获取的验证模式是未指定,则需要自动的检测(检测配置文件)
		int detectedMode = detectValidationMode(resource);
        //如果自动检测后得到的验证模式不是未指定的,返回
		if (detectedMode != VALIDATION_AUTO) {
			return detectedMode;
		}
        //如果配置文件上依然没有指定验证模式
        //默认使用XSD模式(注释上写的原因是,因为找不到DTD所以使用XSD)
		// 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来获取validationMode,即从代码上设置validationMode,并且优先级最高
  • 通过配置文件去获取validationMode,优先级最低
  • 如果两个都没有设置,默认使用XSD验证模式
配置文件去获取validationMode(detectValidationMode方法)

对应的方法就是detectValidationMode方法

	protected int detectValidationMode(Resource resource) {
        //判断配置文件抽象成的resoure是不是open状态的
        //如果处于open状态,则不能从该配置文件上读取验证模式
        //因为open状态,表示已经被打开了
        //被打开了就只能被读取一次然后马上关闭,因为可能会造成资源泄露
		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.");
		}
		
        //获取文件的io流
		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);
		}

        //具体的获取验证模式的功能又是交由validationModeDetector去执行
		try {
			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);
		}
	}

可见XmlBeanDefinitionReader判断了resource是否可用之后,调用validationModeDector对象去获取校验模式

ValidationModeDector的detectValidationMode方法

在这个方法里面才是真正地读取配置文件去获取校验模式的细节!!!!

	public int detectValidationMode(InputStream inputStream) throws IOException {
		// Peek into the file to look for DOCTYPE.
        // 通过读取配置文件去寻找DOCTYPE,DOCTYPE就是配置文件中指定DTD校验模式的前缀
        // 所以这也体现了一句话,约定大于配置大于编码
        // 从这也可以估计到,这里是通过寻找DTD,找不到DTD就使用XSD。。。
		try (BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream))) {
			boolean isDtdValidated = false;
			String content;
			while ((content = reader.readLine()) != null) {
                //这一步其实判断当前行是不是注释
				content = consumeCommentTokens(content);
                //如果这一行为注释,此时content为null,则读取下一行,这一行不处理
                // inComment代表当前解析位置处于注释里面
				if (this.inComment || !StringUtils.hasText(content)) {
					continue;
				}
                //判断当前解析位置是否有DOCTYPE 如果有DOCTYPE就是DTD校验模式
				if (hasDoctype(content)) {
					isDtdValidated = true;
					break;
				}
				if (hasOpeningTag(content)) {
					// End of meaningful data...
					break;
				}
			}
            // 如果有DOCTYPE,为DTD校验格式,如果没有,则是XSD校验格式
			return (isDtdValidated ? VALIDATION_DTD : VALIDATION_XSD);
		}
		catch (CharConversionException ex) {
			// Choked on some character encoding...
			// Leave the decision up to the caller.
            
            //对于一些不支持的编码,就将主动权返回给调用者去处理。。。。
			return VALIDATION_AUTO;
		}
	}

在这里插入图片描述
从源码上可以看出,通过配置文件去获取校验模式,除了不支持编码问题,通过逐行解析文本,如果碰到了DOCTYPE字样,就代表是DTD模式,如果没有碰到,就代表是XSD模式

获取校验模式后,接下来就返回上一层的XmlBeanDefinitionReader去解析Document了

解析Document

在这里插入图片描述
也就是使用documentLoader方法去执行loadDocument方法去获取Document对象,可以看到,documentLoader其实是一个接口,而且只有一个实现类DefaultDocumentLoader,并且里面只有一个方法就是loadDocument

在这里插入图片描述
所以,我们直接进入DefaultLoadDocument

DefaultLoadDocument

下面就看看那个实现的loadDocument方法

	@Override
	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);
	}

其实这个loadDocument方法是使用标准的JAXP配置的XML解析器去简单地加载XML文档的,与使用SAX解析XML套路大致都一样

EntityResolver对象

返回上一层可以看到,这个对象是调用getEntityResolver方法去获得的

在这里插入图片描述
具体的源码如下

	protected EntityResolver getEntityResolver() {
		if (this.entityResolver == null) {
			// Determine default EntityResolver to use.
			ResourceLoader resourceLoader = getResourceLoader();
			if (resourceLoader != null) {
				this.entityResolver = new ResourceEntityResolver(resourceLoader);
			}
			else {
				this.entityResolver = new DelegatingEntityResolver(getBeanClassLoader());
			}
		}
		return this.entityResolver;
	}

这个EntityResolver是干什么的

EntityResolver的作用是提供一个如何寻找DTD、XSD声明的方法,也就是由程序来实现寻找校验模式的声明

下面来看一下这个接口

在这里插入图片描述
在这里插入图片描述
这个接口提供了唯一一个方法,这个方法就是定义去哪里寻找校验模式的

解析及注册BeanDefinitions

经过DefaultLoadDocument进行解析后,现在已经为XML配置文件生成了一棵DOM树了,即Document实体,此时就回到了XmlBeanDefinitionReader的doLoadBeanDefinitions方法
在这里插入图片描述
下一步就是进行注册BeanDefinitions了,也就是执行registerBeanDefinitions方法

registerBeanDefinitions方法

这个方法就是用来提取以及注册Bean的

	public int registerBeanDefinitions(Document doc, Resource resource) throws BeanDefinitionStoreException {
        //使用DefaultBeanDefinitionsReader来实例化BeanDefinitionDocumentReader接口
		BeanDefinitionDocumentReader documentReader = createBeanDefinitionDocumentReader();
        //记录BeanDefinitions的加载个数(即之前加载的个数)
		int countBefore = getRegistry().getBeanDefinitionCount();
        //加载和注册配置文件的Bean
		documentReader.registerBeanDefinitions(doc, createReaderContext(resource));
        //记录本次加载配置文件的Bean的个数并返回
		return getRegistry().getBeanDefinitionCount() - countBefore;
	}

在这个方法中,注册Bean交给了BeanDefinitionDocumentReader去做,而XmlBeanDefinitionReader只关心配置文件进行加载和转化的工作,很好地应用了面向对象的单一职责的原则,将逻辑处理都委托给单一的类进行处理,也就是交由BeanDefinitionDocumentReader去做

下面就来看看BeanDefinitionDocumentReader是怎么注册Bean的

BeanDefinitionDocumentReader进行注册Bean

源码如下

	@Override
	public void registerBeanDefinitions(Document doc, XmlReaderContext readerContext) {
		this.readerContext = readerContext;
        //注册的细节还在doRegisterBeanDefinitions里面
        //可以看到给的参数为Dom里面的DocumentElement,也就是最上层标签,Dom树的根节点!
		doRegisterBeanDefinitions(doc.getDocumentElement());
	}
doRegisterBeanDefinitions方法

源码如下

下面就是真正的注册Bean的过程了!

/**
	 * Register each bean definition within the given root {@code <beans/>} element.
	 */
	@SuppressWarnings("deprecation")  // for Environment.acceptsProfiles(String...)
	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.
        
        //装饰者模式,BeanDefinitionDocumentReader装饰了BeanDefinitionParserDelegate
        //实际上进行XML解析的是BeanDefinitionParserDelegate去做的(单一职责)
        //而BeanDefinitions主要做的是处理profiles属性
		BeanDefinitionParserDelegate parent = this.delegate;
		this.delegate = createDelegate(getReaderContext(), root, parent);
		
        //判断给定的节点是否是默认的命名空间
        //命名空间是XML里的说法,命名空间可以理解为导入资源
        //只有引入了命名空间才可以使用对应的标签
        //默认命名空间为bean标签,alias标签和import标签
        //这里是加载Bean,必须使用到Bean标签,所以一定要有引入默认的命名空间
        //因为如果是这个命名空间,证明使用bean标签进行加载!!
        //Spring才知道要怎样进行解析
		if (this.delegate.isDefaultNamespace(root)) {
            //处理profile属性(也就是对应环境,dev环境还是produce环境)
			String profileSpec = root.getAttribute(PROFILE_ATTRIBUTE);
			if (StringUtils.hasText(profileSpec)) {
				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.
				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;
				}
			}
		}
		
        //下面就是专门去解析XML了
        //这个是个空方法,代表处理XML前要做的事情(留给了子类去实现,面向继承而设计的思想,模板方法!)
		preProcessXml(root);
        //开始处理XML
		parseBeanDefinitions(root, this.delegate);
        //这个也是个空方法,代表解析完XML后要做的事情(留给了子类去实现,面向继承而设计的思想,模板方法!)
        //如果后面要对开始处理XML做特殊处理的时候,可以让子类去实现这两个方法,子类正常调用该方法即可
		postProcessXml(root);
		this.delegate = parent;
	}

从代码上可以看到,有以下重要几点

  • BeanDefinitionDocumentReader采用了模板方法、面向继承去设计,对XML进行解析的前后处理交由子类去实现

  • BeanDefinitionDocumentReader装饰了BeanDefinitionParserDelegate,本身自己主要负责Profiles属性的处理,而真正的解析XML是要靠BeanDefinitionParserDelegate做的,很好地体现了单一职责原则

profile属性的应用

估计很少人在Bean上很少用这个属性,但是在SpringBoot的配置文件肯定很多人使用spring.profiles = dev去对应设置配置文件对应的环境,其实在bean标签上也是可以设置profiles属性的,表明Bean在什么环境下才会被激活

下面是来自官方文档的示例

在这里插入图片描述
而且这里要注意的是,profiles是可以指定多个的,下面就看看如何处理profiles的

		if (this.delegate.isDefaultNamespace(root)) {
            //注意,这里处理的是根节点的profiles,相当于是整个容器的profile
            //获取bena标签内profile属性对应的value
			String profileSpec = root.getAttribute(PROFILE_ATTRIBUTE);
            //判断profile属性有没有对应的value
			if (StringUtils.hasText(profileSpec)) {
                //这里是解析value的,因为profile可以指定多个,使用逗号分开
                //这里的MULTI_VALUE_ATTRIBUTE_DELIMITERS代表分界符的意思,为常量逗号
                //所以,这里的specifiedProfiles字符串数组存储了所有的环境
				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.
                //下面是判断解析出来的profiles是不是都符合环境变量中定义的,
                //如果不符合定义就不会浪费性能(在启动Spring就要指定启动的环境)
				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;
				}
			}
		}

下面来看看怎么进行匹配环境的

	@Override
	@Deprecated
	public boolean acceptsProfiles(String... profiles) {
		Assert.notEmpty(profiles, "Must specify at least one profile");
        //遍历所有的profiles
		for (String profile : profiles) {
            //如果某个profile开头是!,为只要不是这个环境就进行加载
			if (StringUtils.hasLength(profile) && profile.charAt(0) == '!') {
                //去除掉!
                //判断当前环境是否不对应这个环境
                //如果不对应环境,返回true代表可以进行下面的加载!
				if (!isProfileActive(profile.substring(1))) {
					return true;
				}
			}
            //如果不是以!开头,就是代表这个环境才尽心加载
            //判断当前环境是否对应这个环境,对应的就返回True,可以进行下一步加载
			else if (isProfileActive(profile)) {
				return true;
			}
		}
		return false;
	}

再看看如何判断环境是否对应的吗,下面是源码

	protected boolean isProfileActive(String profile) {
        //这一步只是校验传进来的profile而已
        //具体校验有两种情况
        //一种是,开头还是!(因为传进来时已经去掉感叹号了,所以不应该存在感叹号)
        //另外一种是为空,为空还判断个P
		validateProfile(profile);
        //取出现在激活的环境,使用的是一个Set集合存储的
        //具体来说还是一个LinkedHashSet
		Set<String> currentActiveProfiles = doGetActiveProfiles();
        //判断当前激活环境包含传进来的profile,如果包含,代表环境对应
        //如果当前环境为空,那就参考代表以默认环境启动
        //所以判断默认环境是否包含传进来的profile,如果包含代表环境对应
		return (currentActiveProfiles.contains(profile) ||
				(currentActiveProfiles.isEmpty() && doGetDefaultProfiles().contains(profile)));
	}

校验传进来的profile的源码!!!
在这里插入图片描述
总结以下,这个解析profile分为以下几个步骤

  • 取传进来的root节点的profiles属性

  • 解析profiles属性,因为profiles属性是可以指定多个的,并且用逗号划分,所以根据逗号将多个profile属性放进数组里面

  • 判断当前激活的环境是否包含profile,使用遍历(激活环境使用LinkedHashSet来存储的)

    • 先校验传进来的profile是否符合贵方

      • 不能为空
      • 不能有!(感叹号传进来时已经去掉了)
    • 如果激活的环境为空,代表没有配置,使用默认的环境,以默认环境去判断是否包含profile

    • 激活环境不为空,就以激活环境为准

  • 如果profile没有一个对应的,那就直接return了,不会进行下一步的注册Bean了

解析并注册BeanDefinition

处理完Profiles后,并且有profile对应激活环境,那么现在就要开始解析和注册BeanDefinition了

前面提到过,注册BeanDefinition是交给beanDefinitionParseDelegate去做的

	protected void parseBeanDefinitions(Element root, BeanDefinitionParserDelegate delegate) {
        //同样去判断是否引入了默认的命名空间
        //要支持Bean标签
		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;
                    //同样去判断注册的Bean是不是也为默认的命名空间
                    //如果开启了默认的命名空间
                    //parseDefaultElement来进行注册
					if (delegate.isDefaultNamespace(ele)) {
						parseDefaultElement(ele, delegate);
					}
                    //如果不是默认的命名空间
                    //parseCustomElement进行注册
					else {
						delegate.parseCustomElement(ele);
					}
				}
			}
		}
        //如果Root,不是默认的命名空间
        //同理去使用parseCutomElement进行注册
		else {
			delegate.parseCustomElement(root);
		}
	}

可以看到,针对命名空间,Spring提供了两种注册方式

这是因为Spring的XML配置里面有两大类Bean声明,一个是默认的就是Bean标签,另外一种就是开启注解支持在代码上进行Bean声明,因为两种的解析差异是比较大的,所以要分开去注册

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值