4742 2017-12-03 2024-08-01

在注解大肆横行的年代,Xml配置方式显得尤为臃肿。但鉴于市面上仍有不少老项目,且本篇很多年前就写了(2017),还是发下(手动狗头)。

原理都是相通的,在Spring Boot和注解还没有大规模推广开来前,其实是Xml的天下!

一、XML的验证

XML文件有两种验证模式,分别为DTD和XSD,它们保证了XML文件的正确性。

1、DTD

DTD(Document Type Definition)即文档类型定义,是一种XML约束模式语言,是XML文件的验证机制,属于XML文件组成的一部分。DTD是一种保证XML文档格式正确的有效方法,可以通过比较XML文档和DTD文件来判断文档是否符合规范,元素和标签使用是否正确。

一个DTD文档包含:元素的定义规则,元素间关系的定义规则,元素可使用的属性,可使用的实体或符号规则。要使用DTD验证模式,需要在XML文件的头部声明,以下是在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">
<beans>
....
</beans>

而以Spring为例,具体的Spring-beans-2.0.dtd部分如下:

<!ELEMENT beans (
    description?,
    (import | alias | bean)*
)>
<!ATTLIST beans default-lazy-init (true | false) "false">
<!ATTLIST beans default-merge (true | false) "false">
<!ATTLIST beans default-autowire (no | byName | byType | constructor | autodetect) "no">
<!ATTLIST beans default-dependency-check (none | objects | simple | all) "none">
<!ATTLIST beans default-init-method CDATA #IMPLIED>
<!ATTLIST beans default-destroy-method CDATA #IMPLIED>
....

2、XSD

XML Schema语言就是XSD(XML Schemas Definition)。XML Schema描述了XML文档的结构。可以用一个指定的XML Schema来验证某个XML文档,以检查该XML文档是否符合其要求。

文档设计者可以通过XML Schema指定一个XML文档所允许的结构和内容,并可据此检查一个XML文档是否有效。XML Schema本身是一个XML文档,它符合XML语法结构,可以用通用的XML解析器解析它。

在使用XML Schema文档对XML实例文档进行检验,除了要声明名称空间外,还必须指定该名称空间所对应的XML Schema文档的存储位置。通过schemaLocation属性来指定名称空间所对应的XML Schema文档的存储位置,它包含两个部分,一部分是名称空间的URI,另一部分就是该名称空间所标识的XML Schema文件位置或URI地址,如下

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
            http://www.springframework.org/schema/beans/spring-beans.xsd>
....
</beans>            

Spring-beans-3.0.xsd部分代码如下

<?xml version="1.0" encoding="UTF-8" standalone="no"?>

<xsd:schema xmlns="http://www.springframework.org/schema/beans"
        xmlns:xsd="http://www.w3.org/2001/XMLSchema"
        targetNamespace="http://www.springframework.org/schema/beans">

    <xsd:import namespace="http://www.w3.org/XML/1998/namespace"/>
        <!-- base types -->
    <xsd:complexType name="identifiedType" abstract="true">
        <xsd:annotation>
            <xsd:documentation><![CDATA[
    The unique identifier for a bean. The scope of the identifier
    is the enclosing bean factory.
            ]]></xsd:documentation>
        </xsd:annotation>
        <xsd:attribute name="id" type="xsd:ID">
            <xsd:annotation>
                <xsd:documentation><![CDATA[
    The unique identifier for a bean.
                ]]></xsd:documentation>
            </xsd:annotation>
        </xsd:attribute>
    </xsd:complexType>
    ....
</xsd:schema>

3、XmlValidationModeDetector

Spring有关XML的验证集中在类 XmlValidationModeDetector,其代码如下

/**
 * Detects whether an XML stream is using DTD- or XSD-based validation.
 */
public class XmlValidationModeDetector {
    /**
     * Indicates that the validation should be disabled.
     */
    public static final int VALIDATION_NONE = 0;

    /**
     * Indicates that the validation mode should be auto-guessed, since we cannot find
     * a clear indication (probably choked on some special characters, or the like).
     */
    public static final int VALIDATION_AUTO = 1;

    /**
     * Indicates that DTD validation should be used (we found a "DOCTYPE" declaration).
     */
    public static final int VALIDATION_DTD = 2;

    /**
     * Indicates that XSD validation should be used (found no "DOCTYPE" declaration).
     */
    public static final int VALIDATION_XSD = 3;

    /**
     * The token in a XML document that declares the DTD to use for validation
     * and thus that DTD validation is being used.
     */
    private static final String DOCTYPE = "DOCTYPE";

    /**
     * The token that indicates the start of an XML comment.
     */
    private static final String START_COMMENT = "<!--";

    /**
     * The token that indicates the end of an XML comment.
     */
    private static final String END_COMMENT = "-->";

    /**
     * Indicates whether or not the current parse position is inside an XML comment.
     */
    private boolean inComment;

    /**
     * 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.
     * @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.
        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
                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;
        }
        finally {
            reader.close();
        }
    }


    /**
     * Does the content contain the the DTD DOCTYPE declaration?
     */
    private boolean hasDoctype(String content) {
        return content.contains(DOCTYPE);
    }

    /**
     * Does the supplied content contain an XML opening tag. If the parse state is currently
     * in an XML comment then this method always returns false. It is expected that all comment
     * tokens will have consumed for the supplied content before passing the remainder to this method.
     */
    private boolean hasOpeningTag(String content) {
        if (this.inComment) {
            return false;
        }
        int openTagIndex = content.indexOf('<');
        return (openTagIndex > -1 && (content.length() > openTagIndex + 1) &&
                Character.isLetter(content.charAt(openTagIndex + 1)));
    }

    /**
     * Consumes all the leading comment data in the given String and returns the remaining content, which
     * may be empty since the supplied content might be all comment data. For our purposes it is only important
     * to strip leading comment content on a line since the first piece of non comment content will be either
     * the DOCTYPE declaration or the root element of the document.
     */
    private String consumeCommentTokens(String line) {
        if (!line.contains(START_COMMENT) && !line.contains(END_COMMENT)) {
            return line;
        }
        while ((line = consume(line)) != null) {
            if (!this.inComment && !line.trim().startsWith(START_COMMENT)) {
                return line;
            }
        }
        return line;
    }

    /**
     * Consume the next comment token, update the "inComment" flag
     * and return the remaining content.
     */
    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)
     */
    private int startComment(String line) {
        return commentToken(line, START_COMMENT, true);
    }

    private int endComment(String line) {
        return commentToken(line, END_COMMENT, false);
    }

    /**
     * Try to consume the supplied token against the supplied content and update the
     * in comment parse state to the supplied value. Returns the index into the content
     * which is after the token or -1 if the token is not found.
     */
    private int commentToken(String line, String token, boolean inCommentIfPresent) {
        int index = line.indexOf(token);
        if (index > - 1) {
            this.inComment = inCommentIfPresent;
        }
        return (index == -1 ? index : index + token.length());
    }
}

只要我们理解了XSD与DTD的使用方法,理解上面的代码应该不会太难,Spring用来检测验证模式的办法就是判断是否包含DOCTYPE,如果包含就是DTD,否则就是XSD。

二、获取Document

1、DefaultDocumentLoader

经过验证模式准备步骤就可以进行Document加载了,类 DefaultDocumentLoader提供了转化支持,代码如下

/**
 * Spring's default {@link DocumentLoader} implementation.
 *
 * <p>Simply loads {@link Document documents} using the standard JAXP-configured
 * XML parser. If you want to change the {@link DocumentBuilder} that is used to
 * load documents, then one strategy is to define a corresponding Java system property
 * when starting your JVM. For example, to use the Oracle {@link DocumentBuilder},
 * you might start your application like as follows:
 *
 * <pre code="class">java -Djavax.xml.parsers.DocumentBuilderFactory=oracle.xml.jaxp.JXDocumentBuilderFactory MyMainClass</pre>
 *
 */
public class DefaultDocumentLoader implements DocumentLoader {
    /**
     * JAXP attribute used to configure the schema language for validation.
     */
    private static final String SCHEMA_LANGUAGE_ATTRIBUTE = "http://java.sun.com/xml/jaxp/properties/schemaLanguage";

    /**
     * JAXP attribute value indicating the XSD schema language.
     */
    private static final String XSD_SCHEMA_LANGUAGE = "http://www.w3.org/2001/XMLSchema";

    private static final Log logger = LogFactory.getLog(DefaultDocumentLoader.class);

    /**
     * Load the {@link Document} at the supplied {@link InputSource} using the standard JAXP-configured
     * XML parser.
     */
    public Document loadDocument(InputSource inputSource, EntityResolver entityResolver,
            ErrorHandler errorHandler, int validationMode, boolean namespaceAware) throws Exception {
        // 根据验证参数,创建Document工厂
        DocumentBuilderFactory factory = createDocumentBuilderFactory(validationMode, namespaceAware);
        if (logger.isDebugEnabled()) {
            logger.debug("Using JAXP provider [" + factory.getClass().getName() + "]");
        }
        // 创建Document建造者
        DocumentBuilder builder = createDocumentBuilder(factory, entityResolver, errorHandler);
        // 转换为Document
        return builder.parse(inputSource);
    }

    /**
     * Create the {@link DocumentBuilderFactory} instance.
     * @param validationMode the type of validation: {@link XmlValidationModeDetector#VALIDATION_DTD DTD}
     * or {@link XmlValidationModeDetector#VALIDATION_XSD XSD})
     * @param namespaceAware whether the returned factory is to provide support for XML namespaces
     * @return the JAXP DocumentBuilderFactory
     * @throws ParserConfigurationException if we failed to build a proper DocumentBuilderFactory
     */
    protected DocumentBuilderFactory createDocumentBuilderFactory(int validationMode, boolean namespaceAware)
            throws ParserConfigurationException {

        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...
                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;
    }

    /**
     * Create a JAXP DocumentBuilder that this bean definition reader
     * will use for parsing XML documents. Can be overridden in subclasses,
     * adding further initialization of the builder.
     * @param factory the JAXP DocumentBuilderFactory that the DocumentBuilder
     * should be created with
     * @param entityResolver the SAX EntityResolver to use
     * @param errorHandler the SAX ErrorHandler to use
     * @return the JAXP DocumentBuilder
     * @throws ParserConfigurationException if thrown by JAXP methods
     */
    protected DocumentBuilder createDocumentBuilder(
            DocumentBuilderFactory factory, EntityResolver entityResolver, ErrorHandler errorHandler)
            throws ParserConfigurationException {

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

对于这部分代码,其实并没有太多可描述的,因为通过SAX解析XML文档的套路大致都差不多,Spring在这里并没有什么特殊的地方,同样首先创建DocumentBuilderFactory,再通过DocumentBuilderFactory创建DocumentBuilder,进而解析inputSource来返回Document对象。

补充:解析XMl文件一般有四种方法,分别为DOM、SAX、JDOM、DOM4J。大部分情况下,SAX的解析速度最快(基于事件驱动)

2、EntityResolver

这里有必要提及一下EntityResolver,对于参数entityResolver,传入是通过getEntityResolver()函数获取的返回值,如下代码

/**
 * Return the EntityResolver to use, building a default resolver
 * if none specified.
 */
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;
}

在loadDocument方法中涉及一个参数EntityResolver,何为EntityResolver?官网这样解释的:如果SAX应用程序需要实现自定义外部实体,则必须实现此接口并使用setEntityResolver方法向SAX驱动注册一个实例。也就是说,对于解析一个XML,SAX首先读取该XML文档上的生命,根据声明去寻找对应的DTD定义,以便对文档进行一个验证。默认的寻找规则,即通过网络(实际上就是声明的DTD的URI地址)来下载相应的DTD声明,并进行验证。下载的过程是一个漫长的过程,而且当网络中断或不可用时,这里会报错,就是因为相应的DTD声明没有别找到的原因。

EntityResolver的作用是项目本身就可以提供一个如何寻找DTD声明的方法,即由程序来实现寻找DTD声明的过程,比如我们将DTD文件放到项目中的某处,在实现时直接将此文档读取并返回给SAX即可。这样就避免了通过网络来寻找相应的声明。

首先看EntityResolver的接口方法声明

public abstract InputSource resolveEntity (String publicId,
                                           String systemId)
    throws SAXException, IOException;

这里,它接受两个参数publicId和systemId,并返回一个inputSource对象。这里我们以特定配置文件来进行讲解。

1、XSD配置文件

如果我们在解析验证模式为XSD的配置文件,代码如下

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
            http://www.springframework.org/schema/beans/spring-beans.xsd
....
</beans>

读取到以下两个参数:

  • publicId:null
  • systemIdhttp://www.springframework.org/schema/beans/spring-beans.xsd

2、DTD配置文件

如果我们在解析验证模式为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">
<beans>
....
</beans>

读取到以下两个参数:

  • publicId:-//Spring//DTD BEAN 2.0//EN
  • systemIdhttp://www.springframework.org/dtd/Spring-beans-2.0.dtd

之前提到过,验证文件默认的加载方式是通过URL进行网络下载获取,这样会造成延迟,用户体验也不好,一般的做法是将验证文件放置在自己的工程里,那么怎么做才能将这个URL转换为自己工程里对应的地址文件呢?我们以加载DTD文件为例来看看Spring中是如何实现的。

3、转换URL

根据之前Spring通过getEntityResolver()对EntityResolver的获取,我们知道,Spring中使用 DelegatingEntityResolver类作为EntityResolver的实现类(entityResolver默认为null),resolverEntity方法代码如下

public DelegatingEntityResolver(ClassLoader classLoader) {
    this.dtdResolver = new BeansDtdResolver();
    this.schemaResolver = new PluggableSchemaResolver(classLoader);
}

public InputSource resolveEntity(String publicId, String systemId) throws SAXException, IOException {
    if (systemId != null) {
        if (systemId.endsWith(DTD_SUFFIX)) {
            //如果是dtd
            return this.dtdResolver.resolveEntity(publicId, systemId);
        }
        else if (systemId.endsWith(XSD_SUFFIX)) {
            //通过调用META-INF/Spring.schemas解析
            return this.schemaResolver.resolveEntity(publicId, systemId);
        }
    }
    return null;
}

我们可以看到,对不同验证模式,Spring使用了不同的解析器解析。这里简单描述一下原理,比如加载DTD类型的BeansDtdResolver的resolveEntity是直接截取systemId最后的xx.dtd然后去当前路径下寻找,而加载XSD类型的PluggableSchemaResolver类的resolveEntity是默认到META-INF/Spring.schemas文件中找到systemid所对应的XSD文件并加载,如下代码

public class BeansDtdResolver implements EntityResolver {

    private static final String DTD_EXTENSION = ".dtd";

    private static final String[] DTD_NAMES = {"spring-beans-2.0", "spring-beans"};

    private static final Log logger = LogFactory.getLog(BeansDtdResolver.class);

    public InputSource resolveEntity(String publicId, 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("/");
            for (String DTD_NAME : DTD_NAMES) {
                int dtdNameStart = systemId.indexOf(DTD_NAME);
                if (dtdNameStart > lastPathSeparator) {
                    String dtdFile = systemId.substring(dtdNameStart);
                    if (logger.isTraceEnabled()) {
                        logger.trace("Trying to locate [" + dtdFile + "] in Spring jar");
                    }
                    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 class path", ex);
                        }
                    }

                }
            }
        }

        // Use the default behavior -> download from website or wherever.
        return null;
    }

    @Override
    public String toString() {
        return "EntityResolver for DTDs " + Arrays.toString(DTD_NAMES);
    }

}

三、解析XML

1、解析root

当把文件转换为Document后,接下里提取及注册bean就是我们的重头戏。继续上面的分析,当程序已经拥有XML文档文件的Document实例对象时,就会被引入下面这个方法

/**
 * Register the bean definitions contained in the given DOM document.
 * Called by {@code loadBeanDefinitions}.
 * <p>Creates a new instance of the parser class and invokes
 * {@code registerBeanDefinitions} on it.
 * @param doc the DOM document
 * @param resource the resource descriptor (for context information)
 * @return the number of bean definitions found
 * @throws BeanDefinitionStoreException in case of parsing errors
 * @see #loadBeanDefinitions
 * @see #setDocumentReaderClass
 * @see BeanDefinitionDocumentReader#registerBeanDefinitions
 */
public int registerBeanDefinitions(Document doc, Resource resource) throws BeanDefinitionStoreException {
    // 使用DefaultBeanDefinitionDocumentReader实例化BeanDefinitionDocumentReader
    BeanDefinitionDocumentReader documentReader = createBeanDefinitionDocumentReader();
    // 将环境变量设置其中
    documentReader.setEnvironment(getEnvironment());
    // 在实例化BeanDefinitionReader时候会将BeanDefinitionRegistry传入,默认使用继承自DefaultListableBeanFactory的子类
    // 记录统计前BeanDefinition的加载个数
    int countBefore = getRegistry().getBeanDefinitionCount();
    // 加载及注册bean
    documentReader.registerBeanDefinitions(doc, createReaderContext(resource));
    // 记录本次加载的BeanDefinition个数
    return getRegistry().getBeanDefinitionCount() - countBefore;
}

public static final String PROFILE_ATTRIBUTE = "profile";

private Class<?> documentReaderClass = DefaultBeanDefinitionDocumentReader.class;

/**
 * Create the {@link BeanDefinitionDocumentReader} to use for actually
 * reading bean definitions from an XML document.
 * <p>The default implementation instantiates the specified "documentReaderClass".
 * @see #setDocumentReaderClass
 */
protected BeanDefinitionDocumentReader createBeanDefinitionDocumentReader() {
    return BeanDefinitionDocumentReader.class.cast(BeanUtils.instantiateClass(this.documentReaderClass));
}

其中的参数doc是通过上一节loadDocument加载转换出来的。在这个方法中很好地应用了面向对象中单一职责的原则,将逻辑处理委托给单一的类进行处理,而这个逻辑处理类就是 BeanDefinitionDocumentReader。BeanDefinitionDocumentReader是一个接口,而实例化的工作是在createBeanDefinitionDocumentReader()中完成的,而通过此方法,BeanDefinitionDocumentReader真正的类型其实已经是 DefaultBeanDefinitionDocumentReader了,进入DefaultBeanDefinitionDocumentReader后,发现这个方法的重要目的就是提取root,以便于再次将root作为参数继续BeanDefinition的注册,代码如下

/**
 * This implementation parses bean definitions according to the "spring-beans" XSD
 * (or DTD, historically).
 * <p>Opens a DOM Document; then initializes the default settings
 * specified at the {@code <beans/>} level; then parses the contained bean definitions.
 */
public void registerBeanDefinitions(Document doc, XmlReaderContext readerContext) {
    this.readerContext = readerContext;
    logger.debug("Loading bean definitions");
    Element root = doc.getDocumentElement();
    doRegisterBeanDefinitions(root);
}

经过艰难险阻,磕磕绊绊,我们终于到了核心逻辑的底部doRegisterBeanDefinitions(root),至少我们在这个方法中看到了希望。

如果说以前一直是XML加载解析的准备阶段,那么doRegisterBeanDefinitions(root)算是真正地开始进行解析了,我们期待的核心部分真正开始了。

/**
 * Register each bean definition within the given root {@code <beans/>} element.
 */
protected void doRegisterBeanDefinitions(Element root) {
    // 处理profile属性
    String profileSpec = root.getAttribute(PROFILE_ATTRIBUTE);
    if (StringUtils.hasText(profileSpec)) {
        String[] specifiedProfiles = StringUtils.tokenizeToStringArray(
                profileSpec, BeanDefinitionParserDelegate.MULTI_VALUE_ATTRIBUTE_DELIMITERS);
        if (!getEnvironment().acceptsProfiles(specifiedProfiles)) {
            return;
        }
    }

    // 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 parent = this.delegate;
    this.delegate = createDelegate(this.readerContext, root, parent);

    // 解析前处理,留给子类实现
    preProcessXml(root);
    parseBeanDefinitions(root, this.delegate);
    // 解析后处理,留个子类实现
    postProcessXml(root);
    this.delegate = parent;
}

通过上面的代码我们看到了处理流程,首先是对profile的处理,然后开始进行解析,可是当我跟进preProcessXml(root)或者postProcessXml(root)方法时发现代码是空的,既然是空的写着还有什么用?就像面向对象设计方法学中常说的一句话,一个类要么是面向继承设计的,要么就用final修饰。在DefaultBeanDefinitionDocumentReader中并没有用final修饰,所以他是面向继承而设计的,让子类重写,反映的是模板方法模式

2、profile属性

我们注意到在注册bean的最开始是对 PROFILE_ATTRIBUTE属性的解析,可能对于我们来说,profile属性并不是很常用,我们先来了解一下这个属性。

分析profile前我们先了解下profile的用法,官方实例代码片段如下:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
            http://www.springframework.org/schema/beans/spring-beans.xsd">
    <bean profile="dev">
    ....
    </bean>
    <bean profile="production">
    ....
    </bean>

集成到web环境中时,在web.xml中加入以下代码

<context-param>
    <param-name>spring.profiles.active</param-name>
    <param-value>dev</param-value>
</context-param>    

有了这个特性我们就可以同时在配置文件中部署两套配置来适用于生产环境和开发环境,这样可以方便的进行开发、部署环境,最常用的就是更换不同的数据库。

了解了profile的使用再来分析代码会清晰得多,首先程序会获取beans节点是否定义了profile属性,如果定义了则会需要到环境变量中去寻找,所以这里首先断言environment不可能为空,因为profile是可以同时指定多个的,需要程序对其拆分,并解析每个profile是都符合环境变量中所定义的,不定义则不会浪费性能去解析。

3、namespace

处理了profile后就可以进行XML的读取了,跟踪代码进入parseBeanDefinitions(root, this.delegate)方法,如下

/**
 * Parse the elements at the root level in the document:
 * "import", "alias", "bean".
 * @param root the DOM root element of the document
 */
protected void parseBeanDefinitions(Element root, BeanDefinitionParserDelegate delegate) {
    // 对bens的处理
    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;
                if (delegate.isDefaultNamespace(ele)) {
                    // 对默认bean的处理
                    parseDefaultElement(ele, delegate);
                }
                else {
                    // 对自定义bean的处理
                    delegate.parseCustomElement(ele);
                }
            }
        }
    }
    else {
        delegate.parseCustomElement(root);
    }
}

上面的代码看起来逻辑还是蛮清晰的,因为在Spring的XML配置里面有两大类Bean声明,一个是默认的,如

<bean id="test" class="text.TestBean"/>

另一类就是自定义的,如:

<tx:annotation-driven/>

而两种方式的读取及解析差别是非常大的,如果采用Spring默认的配置,Spring当让知道该怎么做,但是如果是自定义的,那么需要用户实现一些接口及配置了。对于根节点或者子节点如果是默认命名空间的话则采用parseDefaultElement方法进行解析,否则使用delegate.parseCustomElement(ele)方法对自定义命名空间进行解析。

而判断是否默认命名空间还是自定义命名空间的办法其实是使用node.getNamespaceURI()获取命名空间,并于Spring中固有的命名空间http://www.springframework.org/schema/beans进行比对。如果一致则认为是默认,否则就认为是自定义。

下节我们将详细分析对于XML中namespace、bean的解析。

总访问次数: 192次, 一般般帅 创建于 2017-12-03, 最后更新于 2024-08-01

进大厂! 欢迎关注微信公众号,第一时间掌握最新动态!