spring源码解析 1 —— xml配置文件的读取

首先,看源码这东西呀,要有耐心。

这里假设大家都用过spring这个框架了,没用过的,或者才涉足的,最好不要马上就去看源码,没啥意义。

1、配置文件读取

这里来个经典的例子,《spring源码深度解析》中的例子。

 public static void main(String[] args){
     XmlBeanFactory beanFactory = new XmlBeanFactory(new ClassPathResource("applicationContext.xml"));
     XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(beanFactory);
}



    
        
        
        
    

我们就从配置文件的读取开始往下看。

直接进入XmlBeanFactory的构造方法中,该类持有一个用于xml读取的XmlBeanDefinitionReader,重点在与loadBeanDefinitions这个方法。

public class XmlBeanFactory extends DefaultListableBeanFactory {

    private final XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(this);

    public XmlBeanFactory(Resource resource) throws BeansException {
        this(resource, null);
    }

    public XmlBeanFactory(Resource resource, BeanFactory parentBeanFactory) throws BeansException {
        super(parentBeanFactory);
        this.reader.loadBeanDefinitions(resource);
    }
}

点击进入loadBeanDefinitions方法中,这里首先对配置文件的编码做了一下,在其调用getReader()方法时会根据设置的编码进行解码操作

@Override
public int loadBeanDefinitions(Resource resource) throws BeanDefinitionStoreException {
    return loadBeanDefinitions(new EncodedResource(resource));
}

/**
* 通过 charset 还是通过encoding其实底层都是一样的
* 底层会通过encoding这个字符串去获取到一个Charset对象
* @return
* @throws IOException
*/
public Reader getReader() throws IOException {
    if (this.charset != null) {
        return new InputStreamReader(this.resource.getInputStream(), this.charset);
    }
    else if (this.encoding != null) {
        return new InputStreamReader(this.resource.getInputStream(), this.encoding);
    }
    else {
        return new InputStreamReader(this.resource.getInputStream());
    }
}

进入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 currentResources = this.resourcesCurrentlyBeingLoaded.get();
    //若添加不成功,则抛异常--检测到encodingResource的循环加载-检查您的导入定义! ——> set返回false的也就因为重复添加,不清楚的可以去看下set的源码
    if (!currentResources.add(encodedResource)) {
        throw new BeanDefinitionStoreException(
            "Detected cyclic loading of " + encodedResource + " - check your import definitions!");
    }

    try (InputStream inputStream = encodedResource.getResource().getInputStream()) {
        // 将资源封装为InputSource供SAX解析XML
        // SAX解析器将使用InputSource对象来确定如何读取XML输入。
        // 如果有可用的字符流,则解析器将直接读取该流,而不考虑该流中发现的任何文本编码声明。
        // 如果没有字符流,但是有字节流,则解析器将使用InputSource中指定的编码使用该字节流,否则(如果未指定编码)则使用算法自动检测字符编码,例如XML规范中的一个。
        // 如果字符流或字节流均不可用,则解析器将尝试打开与系统标识符标识的资源的URI连接。
        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 {
        //从当前正在加载的集合中移除encodedResource
        currentResources.remove(encodedResource);
        if (currentResources.isEmpty()) {
            this.resourcesCurrentlyBeingLoaded.remove();
        }
    }
}

点击进入doLoadBeanDefinitions(inputSource, encodedResource.getResource())方法,屏蔽掉那些异常捕捉的,这里的代码其实很清晰,先解析XML,获取Document对象,在根据Document对象获取BeanDefinition对象,并注册。注册这个动作嘛,简单理解就是给放到一个map里面去。

protected int doLoadBeanDefinitions(InputSource inputSource, Resource resource)
    throws BeanDefinitionStoreException {

    try {
        //加载并解析XML,获取到一个Document对象,使用的是SAX,相比于DOM,SAX是一种速度更快,更有效的方法。它逐行扫描文档,一边扫描一边解析。
        //而DOM解析,是将整份xml文档读取到内存中,形成一个树状结构。若xml文件过大,可能内存溢出
        Document doc = doLoadDocument(inputSource, resource);
        //注册BeanDefinition:
        int count = registerBeanDefinitions(doc, resource);
        if (logger.isDebugEnabled()) {
            logger.debug("Loaded " + count + " bean definitions from " + resource);
        }
        return count;
    }
    catch (BeanDefinitionStoreException ex) {
        throw ex;
    }
    ...
}

进入doLoadDocument(inputSource, resource)方法,这里可能需要一些xml文档的知识。这里先补充下xml相关的基础知识。

//document加载器:负责XML文件的读取
private DocumentLoader documentLoader = new DefaultDocumentLoader();

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

2、 XML介绍

XML文件的约束,来判断你写的这个XML文档有没有按照我规定的格式去写。毕竟,XML叫可扩展标记语言,里面的标签是可以自定义的,所以,需要约束,防止别人乱写,解析报错。即规定XML中的标签,标签的层级关系,标签的位置,标签的属性等

XML有两种约束模式:DTD和XSD。

2.1、DTD约束

我们来看一份DTD约束的XML文件,这是一份被我简化掉的mybatis框架的mapper的xml映射文件

这里只介绍一下DTD约束的引入,对于具体的DTD约束的写法不做介绍。使用DTD约束需要在xml文件的头部输入以下信息

故,可通过“DOCTYPE ” 这个关键字来判断一份xml是否是DTD约束,若没有“DOCTYPE ” ,则为XSD约束。

mybatis映射文件:




    
        select count(1) from tb_student
    

dtd约束文件:



        
        
        

        
            

2.1、XSD约束

以下是一份XSD约束的XML文件,XSD比DTD复杂很多,但也意味着它更强大,介绍几个概念就好。

xmlns="http://www.springframework.org/schema/beans" : 为这个XML文档设置了一个命名空间,这个名字随便起,保证唯一就行。

spring的XML配置文件



    
        
        
        
    

XSD约束文件:





    
    
        
            
                
            
        
    

    
    
        
            
                
                    
                
            
        
    

    
        
            
                
            
        
    

    
        
            
                
            
        
        
            
                
            
        
    

    
        
            
        
        
            
                
                    
                    
                
            
        
    

    
        
            
        
    

    
        
            
                
            
        
        
            
                
            
        
        
            
                
            
        
        
            
                
            
        
    


3、继续配置文件的读取

有了上面对XML的了解,我们就可以继续往下看源码了。

//document加载器:负责XML文件的读取
private DocumentLoader documentLoader = new DefaultDocumentLoader();

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

这里我们先看loadDocument方法的第二个入参方法getEntityResolver()。该方法的作用其实就是在项目中去查找约束文件。

/**
* Return the EntityResolver to use, building a default resolver
* if none specified.
* EntityResolver :实体解析器,用来获取XML的约束
* 说人话就是,XML的约束,一般都是个url,例如http://www.springframework.org/schema/beans/spring-beans.xsd
* 这时候如果通过http调用去获取这个约束的话,由于网络原因,会比较耗时,甚至可能网络异常。所以,spring将这些约束都放在项目中了 
* 通过这个 实体解析器 去获取,这样就不需要通过http调用了
*/
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;
}

我们接着看DelegatingEntityResolver这个类。其构造器中会创建两个处理器,分别是针对DTD和XSD的本地约束文件获取的。

入参的这个ClassLoader,其实就是为了加载本地文件的,不清楚的可以阅读我的另一篇文章《关于java中文件路径》

public DelegatingEntityResolver(@Nullable ClassLoader classLoader) {
    //针对DTD约束的处理器
    this.dtdResolver = new BeansDtdResolver();
    //针对XSD约束的处理器
    this.schemaResolver = new PluggableSchemaResolver(classLoader);
}

那具体是怎么个获取法呢,接着往下看。我们先看BeansDtdResolver。结合spring源码中dtd文件的存放位置,就比较清楚了

/**
     *
     *< ?xml version="1.0" encoding="UTF-8"?>
     *< !DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN 2.0//EN" "http://www.springframework.org/dtd/spring-beans-2.0.dtd">
     *      就上面这个例子 -> publicId = -//SPRING//DTD BEAN 2.0//EN
     *                    systemId = http://www.springframework.org/dtd/spring-beans-2.0.dtd
     * @param publicId
     * @param systemId
     * @return
     * @throws IOException
     */
@Override
@Nullable
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) {
            //spring-beans.dtd
            String dtdFile = DTD_NAME + DTD_EXTENSION;
            if (logger.isTraceEnabled()) {
                logger.trace("Trying to locate [" + dtdFile + "] in Spring jar on classpath");
            }
            try {
                //getClass, 从当前类所在的包(包含当前包)及其子包里去找名为 "spring-beans.dtd"的文件
                //在spring这里就是去 org/springframework/beans/factory/xml 路径下去找 spring-beans.dtd
                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;
}
spring源码解析 1 —— xml配置文件的读取_第1张图片
image-20210303220350598.png

再来看下xsd的PluggableSchemaResolver。可以直接看getSchemaMappings()方法。说白了就是根据XML文档中的publicId跟systemId去META-INF/spring.schemas文件中,找到对应的地址映射。在根据这个地址,找到xsd文件。地址其实在上图的dtd文件下面那些。

    /**
     * < ?xml version="1.0" encoding="UTF-8"?>
     * 
     * 
     *  就上面这个例子 -> publicId = http://www.springframework.org/schema/beans
     *                  systemId = http://www.springframework.org/schema/beans/spring-beans.xsd
     * Load the specified schema mappings lazily.
     */
    private Map getSchemaMappings() {
        Map schemaMappings = this.schemaMappings;
        if (schemaMappings == null) {
            synchronized (this) {
                schemaMappings = this.schemaMappings;
                if (schemaMappings == null) {
                    if (logger.isTraceEnabled()) {
                        logger.trace("Loading schema mappings from [" + this.schemaMappingsLocation + "]");
                    }
                    try {
                        // 用类加载器获取资源 META-INF/spring.schemas
                        // spring.schemas文件的内容:xml的systemId与本地xsd的路径映射
        // http\://www.springframework.org/schema/beans/spring-beans-2.0.xsd = org/springframework/beans/factory/xml/spring-beans.xsd
                        Properties mappings =
                                PropertiesLoaderUtils.loadAllProperties(this.schemaMappingsLocation, this.classLoader);
                        if (logger.isTraceEnabled()) {
                            logger.trace("Loaded schema mappings: " + mappings);
                        }
                        schemaMappings = new ConcurrentHashMap<>(mappings.size());
                        CollectionUtils.mergePropertiesIntoMap(mappings, schemaMappings);
                        this.schemaMappings = schemaMappings;
                    }
                    catch (IOException ex) {
                        throw new IllegalStateException(
                                "Unable to load schema mappings from location [" + this.schemaMappingsLocation + "]", ex);
                    }
                }
            }
        }
        return schemaMappings;
    }
spring源码解析 1 —— xml配置文件的读取_第2张图片
image-20210303220603914.png

有了找到本地约束文件的方法后,我们就要决定到底是找dtd文件还是xsd文件了。让我们来看doLoadDocument方法的第3个入参getValidationModeForResource(resource)。还记得我们上面说的,使用什么约束文件,其实在XML文档的头部就声明好了。就是判断是否存在这个信息。spring其实就判断的是DOCTYPE 这个关键字。让我来看下具体方法。

这个方法的作用就是读取配置文件

/**
     * xml的验证模式相关:xml文件,有两种约束形式,DTD跟XSD
     * xml的约束即规定xml中的标签,标签的层级关系,标签的位置,标签的属性等
     * DTD约束 例如:  class下面有至少1个元素
     *               学生标签下可有(可无)名字,年龄,介绍三个元素且有序
     *               对名字进行说明,PCDATA表示可解析的
     *              
     *              
     *              使用的时候,在xml文件的开头,加入 < !DOCTYPE 文档根节点 SYSTEM "dtd文件路径">,这里<跟!间我加了个空格,实际是没有的
     * XSD约束 例如:
     *                  
     *                      
     *                          
     *                          
     *                          
     *                          
     *                          
     *                       
     *                      
     *                  
     *              
     * @param resource
     * @return
     */
protected int getValidationModeForResource(Resource resource) {
    int validationModeToUse = getValidationMode();
    //如果手动指定了xml文件的验证模式则使用指定的验证模式
    if (validationModeToUse != VALIDATION_AUTO) {
        return validationModeToUse;
    }
    //如果没有指定验证模式,则使用自动检测 --> 其实就是判断文件中是否含有"DOCTYPE",有就是DTD,没有就XSD
    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).
    //(⊙o⊙)… 这也行。。。
    return VALIDATION_XSD;
}

进去detectValidationMode(resource)方法中会发现,真正确定验证模式的代码在this.validationModeDetector.detectValidationMode(inputStream)中。其中hasDoctype(content)中就是去判断是否包含DOCTYPE字样。

/**
     * Detect the validation mode for the XML document in the supplied {@link InputStream}.
     * Note that the supplied {@link InputStream} is closed by this method before returning.
     * 简单来说就是判断这份xml文件中,是否包含 DOCTYPE ,是的话就是DTD,不然就是XSD
     * 因为DTD约束的xml文件的格式如下:
     * < ?xml version="1.0" encoding="UTF-8"?>
     * < !DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN 2.0//EN" "https://www.springframework.org/dtd/spring-beans-2.0.dtd">
     * @param inputStream the InputStream to parse
     * @throws IOException in case of I/O failure
     * @see #VALIDATION_DTD
     * @see #VALIDATION_XSD
     */
public int detectValidationMode(InputStream inputStream) throws IOException {
    // Peek into the file to look for DOCTYPE.
    try (BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream))) {
        boolean isDtdValidated = false;
        String content;
        while ((content = reader.readLine()) != null) {
            content = consumeCommentTokens(content);
            if (this.inComment || !StringUtils.hasText(content)) {
                continue;
            }
            if (hasDoctype(content)) {
                isDtdValidated = true;
                break;
            }
            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;
    }
}

private boolean hasDoctype(String content) {
    return content.contains(DOCTYPE);
}

这些准备工作都准好后,就要开始解析XML文件了。

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

protected DocumentBuilderFactory createDocumentBuilderFactory(int validationMode, boolean namespaceAware)
    throws ParserConfigurationException {

    //获取JAXP文档解析器工厂
    DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
    factory.setNamespaceAware(namespaceAware);

    if (validationMode != XmlValidationModeDetector.VALIDATION_NONE) {
        factory.setValidating(true);
        if (validationMode == XmlValidationModeDetector.VALIDATION_XSD) {
            // Enforce namespace aware for XSD...
            //默认情况下,JAXP验证的不是XSD类型的文件,而是DTD类型的文件
            //而在JAXP(JavaAPI for XML Processing),要开启XSD验证的话:
            //1、设置namespaceAware=true; 默认情况下:false
     //2、设置解析器的验证语言即 将http://java.sun.com/xml/jaxp/properties/schemaLanguage这个key的值,设置为http://www.w3.org/2001/XMLSchema
            factory.setNamespaceAware(true);
            try {
                factory.setAttribute(SCHEMA_LANGUAGE_ATTRIBUTE, XSD_SCHEMA_LANGUAGE);
            }
            catch (IllegalArgumentException ex) {
                ParserConfigurationException pcex = new ParserConfigurationException(
                    "Unable to validate using XSD: Your JAXP provider [" + factory +
                    "] does not support XML Schema. Are you running on Java 1.4 with Apache Crimson? " +
                    "Upgrade to Apache Xerces (or Java 1.5) for full XSD support.");
                pcex.initCause(ex);
                throw pcex;
            }
        }
    }

    return factory;
}

/**
     * @param factory 用来创建文档解析器
     * @param entityResolver 用来本地寻找DTD或XSD约束文件
     * @param errorHandler 用来处理异常
     * @return 文档解析器
     * @throws ParserConfigurationException
     */
protected DocumentBuilder createDocumentBuilder(DocumentBuilderFactory factory,
                                                @Nullable EntityResolver entityResolver, @Nullable ErrorHandler errorHandler)
    throws ParserConfigurationException {

    DocumentBuilder docBuilder = factory.newDocumentBuilder();
    if (entityResolver != null) {
        docBuilder.setEntityResolver(entityResolver);
    }
    if (errorHandler != null) {
        docBuilder.setErrorHandler(errorHandler);
    }
    return docBuilder;
}

到这里,读取XML文件的代码就结束了。至此,我们已经获得了XML文档的内容对象——Document对象了。后面要做的工作就是读取Document对象的内容,创建BeanDefinition对象,并注册。这部分我们下次再讲。

感谢阅读。

你可能感兴趣的:(spring源码解析 1 —— xml配置文件的读取)