曹工说Spring Boot源码(7)-- Spring解析xml文件,到底从中得到了什么(上)


写在前面的话

相关背景及资源:

工程代码地址 思维导图地址

工程结构图:

概要

大家看到这个标题,不知道心里有答案了没?大家再想想,xml文件里都有什么呢?

这么一想,spring的xml文件里,内容真的很多,估计很多元素你也没配置过,尤其是这两年新出来的程序员,估计都在吐槽了,现在不都是注解了吗,谁还用xml?但其实,不管是xml,还是注解,都是配置信息,只是不同的表现形式而已,看过我前面几讲的同学,应该知道,我们用json、properties文件写过bean的配置信息。

所以,具体形式不重要,xml和注解只是最常用的两种表达方式罢了,我们这次就以xml为例来讲解。

xml中,其实还是很有条理的,各种元素,都按照namespace分得明明白白的,我列了个表格如下:

namespace element
util constant、property-path、list、set、map、properties
context property-placeholder、property-override、annotation-config、component-scan、load-time-weaver、spring-configured、mbean-export、mbean-server
beans import、bean、alias
task annotation-driven、scheduler、scheduled-tasks、executor
cache advice、annotation-driven
aop config、scoped-proxy、aspectj-autoproxy

大家看到了吗,spring其实对xml的支持才是最全面的,注解有的,xml基本都有。作为一个工作了6年的码农,我发现好多元素我都没配置过,更别说熟悉其内在原理了。但是呢,我们还是不能忘记了今天的标题,这么多元素,难道没有什么共性吗?spring解析这些元素,到底都是怎么实现的呢,且不说这些元素怎么生效,读了东西总需要地方存起来吧,那,是怎么存放的呢?

我们会挑选一些元素来讲解。我们本讲,先讲解spring采用的xml解析方式;再从util这个namespace开始,挑了constant这个元素进行深入讲解。

spring中所采用的xml解析方式

上一讲,我们讲了,spring是怎么解析xml元素的,我今天想办法从spring源码里,把它用来解析xml的主干代码提取了一下,基本就是下面这样的,比如针对如下xml文件,我们打算遍历一遍:

test-xml-read.xml:
<?xml version="1.0" encoding="UTF-8"?>

	African Coffee Table
	80
	120

	

那么,spring里的代码骨架,大概如下:

package org.springframework.bootstrap.sample;

import lombok.extern.slf4j.Slf4j;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.*;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;

@Slf4j
public class XmlSimpleUse {

    public static void main(String[] args) {
        //读取xml文件
        URL url = Thread.currentThread().getContextClassLoader()
                .getResource("test-xml-read.xml");
        InputStream inputStream = url.openStream();
        //将流转变为InputSource,在后续xml解析使用
        InputSource inputSource = new InputSource(inputStream);
        DocumentBuilderFactory factory = createDocumentBuilderFactory();

        DocumentBuilder docBuilder = factory.newDocumentBuilder();
        // 可选,设置实体解析器,其实就是:你可以自定义去哪里加载xsd/dtd文件
        docBuilder.setEntityResolver(new EntityResolver() {
            @Override
            public InputSource resolveEntity(String publicId, String systemId) throws SAXException, IOException {
                return null;
            }
        });
        // 设置回调处理器,当解析出现错误时,(比如xsd里指定了不能出现a元素,然后xml里出现了a元素)
        docBuilder.setErrorHandler(null);
        //解析xml文件,获取到Document,代表了整个文件
        Document document = docBuilder.parse(inputSource);
        // 获取根元素
        Element root = document.getDocumentElement();
        log.info("root is {}",root);
        
        //获取根元素下的每个child元素
        NodeList nodeList = root.getChildNodes();
        for (int i = 0; i < nodeList.getLength(); i++) {
            Node node = nodeList.item(i);
            if (node instanceof Element) {
                Element ele = (Element) node;
                log.info("ele:{}",ele);
            }
        }
    }
    
    protected static DocumentBuilderFactory createDocumentBuilderFactory() {

        DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
        factory.setValidating(true);
        // Enforce namespace aware for XSD...
        factory.setNamespaceAware(true);

        return factory;
    }
}

输出如下:

21:38:19.638 [main] INFO  o.s.bootstrap.sample.XmlSimpleUse - root is [f:table: null]
21:38:19.653 [main] INFO  o.s.bootstrap.sample.XmlSimpleUse - ele:[f:name: null]
21:38:19.653 [main] INFO  o.s.bootstrap.sample.XmlSimpleUse - ele:[f:width: null]
21:38:19.653 [main] INFO  o.s.bootstrap.sample.XmlSimpleUse - ele:[f:length: null]
21:38:19.654 [main] INFO  o.s.bootstrap.sample.XmlSimpleUse - ele:[t:abc: null]

大家可以看上面的demo代码,没有依赖任何spring的类,基本还原了spring解析xml时的大体过程,在spring中多出来的细节部分,主要有两处:

自定义entityResolver

docBuilder.setEntityResolver,这个部分,我们上面是默认实现。

大家看我们前面的xml,有一定了解的同学可能知道,前面定义了两个namespace,语法一般是下面这样的:

xmlns:namespace-prefix="namespaceURI"

所以,我们这边的两个namespace,前缀分别是f、t,内容分别是:

http://www.w3school.com.cn/furniture、http://www.w3school.com.cn/t

但是,我们一般xml文件是有格式要求的,比如spring里,比如这个命名空间下,可以定义什么元素,这都是定死了的:

那,这个约束是在哪里呢?在namespaceURI 对应的dtd/xsd等文件中。

像上面截图这样,就是:

这一句,定义一个命名空间
xmlns:context="http://www.springframework.org/schema/context" 
    
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
// 下面这个,你要当成key/value来理解,key就是:http://www.springframework.org/schema/context,
    //value,就是对应的xsd文件
				http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd"

有了上面的基础知识,再来说那个接口:

public interface EntityResolver {

	// 一般传入的systemId即为后边这样的:http://www.springframework.org/schema/context/spring-context.xsd
    public abstract InputSource resolveEntity (String publicId,
                                               String systemId)
        throws SAXException, IOException;

}

这个接口呢,就是让我们自定义一个方法,来解析外部xml实体,一般传入的参数如下:

即,publicId为null,systemId为xsd的uri,这个uri一般是可以通过网络获取的,比如:

http://www.springframework.org/schema/context/spring-context.xsd

但是,spring是自定义了自己的entityResolver,实现类为:org.springframework.beans.factory.xml.ResourceEntityResolver

这个类,会在本地寻找对应的xsd文件,主要逻辑就是去查找classpath下的META-INF/spring.schemas,我们可以看看spring-beans包内的该文件:

spring为什么要自定义EntityResolver呢,spring为啥要在本地找呢,原因是:

如果不自定义,jdk的dom解析类,就会直接使用http://www.springframework.org/schema/context/spring-context.xsd这个东西,去作为URL,建立socket网络连接来获取。而部分环境,比如生产环境,基本是外网隔离的,你这时候是没办法去下载这个xsd文件的,岂不是就没法校验xml文件的语法、格式了吗?

所以,spring要将这个外部的xsd引用,转为在classpath下的查找。

自定义元素解析逻辑

这部分,大家再看下之前的骨架代码:

Document document = docBuilder.parse(inputSource);
        Element root = document.getDocumentElement();
        log.info("root is {}",root);
        NodeList nodeList = root.getChildNodes();
        for (int i = 0; i < nodeList.getLength(); i++) {
            Node node = nodeList.item(i);
            //遍历每个元素,我们这里只是简单输出
            if (node instanceof Element) {
                Element ele = (Element) node;
                log.info("ele:{}",ele);

            }
        }

骨架代码里,遍历每个元素,在spring里,遍历到每个ele时,要去判断对应的namespace,如果是默认的,交给xxx处理;如果不是默认的,要根据namespace找到对应的namespacehandler,具体大家可以看看上一节:

我们在前面说了,本讲只先挑一个元素来讲解,即https://gitee.com/ckl111/spring-boot-first-version-learn/tree/master/all-demo-in-spring-learning/spring-xml-demo/src/main/java/org/springframework/utilnamespace中的TestConstant.java

xml的解析demo在:

https://gitee.com/ckl111/spring-boot-first-version-learn/tree/master/all-demo-in-spring-learning/spring-xml-demo/src/test/java/org/springframework/bootstrap/sample

我发现,一个东西,自己看懂可能还行,相对容易点,但是要把这个东西写出来,却是一个大工程。。。看似简单的元素解析,你要把它讲清楚,还真的要点篇幅,哈哈,所以,这也是为什么本篇比较长的原因。

总的来说,再次回答标题,spring到底得到了什么,得到了beanDefinition,本篇里,只得到了一个beanDefinition,还是工厂类型的;后面,我们会看到其他多种多样的元素解析方式。

ok,就到这里,如果大家觉得有帮助,记得点赞。

相关