第四章 Spring之假如让你来写IOC容器——XML配置文件篇

Spring源码阅读目录

第一章 Spring之最熟悉的陌生人——IOC
第二章 Spring之假如让你来写IOC容器——加载资源篇
第三章 Spring之假如让你来写IOC容器——解析配置文件篇
第四章 Spring之假如让你来写IOC容器——XML配置文件篇
第五章 Spring之假如让你来写IOC容器——BeanFactory和FactoryBean
第六章 Spring之假如让你来写IOC容器——Scope和属性填充
第七章 Spring之假如让你来写IOC容器——属性填充特别篇:SpEL表达式
第八章 Spring之假如让你来写IOC容器——拓展篇
第九章 Spring之源码阅读——环境搭建篇
第十章 Spring之源码阅读——IOC篇



前言

    对于Spring一直都是既熟悉又陌生,说对它熟悉吧,平时用用没啥问题,但面试的时候被问的一脸懵逼,就很尴尬,都不好意思在简历上写着熟悉Spring了
在这里插入图片描述

    所以决定花点时间研究研究Spring的源码。主要参考的书籍是:《Spring源码深度解析(第2版)》、《Spring揭秘》、《Spring技术内幕:深入解析Spring架构与设计原理(第2版)》


    书接上回,在上篇 第三章 Spring源码阅读之假如让我来写IOC容器——解析配置文件篇 已经实现了加载不同的配置文件,接下来重点看下如何加载XML的配置文件吧

在这里插入图片描述

尝试动手写IOC容器

    出场人物:A君(苦逼的开发)、老大(项目经理)

    背景:老大要求A君在一周内开发个简单的IOC容器

    前情提要:在A君抽取BeanDefinition之后,老大又提出XML文件加载成DOM可以有多种方式,不能把他写死,希望定义个接口,来支持不同的实现

第六版 抽取DOC加载、注册接口

    接口!接口!又是接口!A君从未像这时一样痛恨过接口。之前的版本被打回来,大多数都是因为这个原因

    既然老大说加载DOM可以有多种实现,那就再定义个DocumentLoader接口,用以支持不同形式的DOM加载方式,DocumentLoader接口代码如下:

import com.hqd.ch03.v6.io.Resource;
import org.w3c.dom.Document;

/**
 * xml dom加载接口
 */
public interface DocumentLoader {
    /**
     * 加载dom
     *
     * @param resource
     * @return
     */
    Document loadDocument(Resource resource);
}

再提供个DefaultDocumentLoader类作为默认的实现,代码如下:

import com.hqd.ch03.v6.io.Resource;
import com.hqd.ch03.v6.reader.xml.doc.DocumentLoader;
import org.w3c.dom.Document;
import org.xml.sax.InputSource;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;

public class DefaultDocumentLoader implements DocumentLoader {
    @Override
    public Document loadDocument(Resource resource) {
        try {
            InputSource inputSource = new InputSource(resource.getInputStream());
            DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
            DocumentBuilder docBuilder = factory.newDocumentBuilder();
            return docBuilder.parse(inputSource);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
}

    经历过这么多次,A君已经麻了,已经预感到老大会说什么了:从DOM解析成BeanDefinition 也可能有多种实现,巴拉巴拉。A君索性一步到位,又定义了BeanDefinitionDocumentReader接口,用以规范从Dom解析成BeanDefinition的注册过程。接口代码如下:


import com.hqd.ch03.v6.registry.BeanDefinitionRegistry;
import org.w3c.dom.Document;

public interface BeanDefinitionDocumentReader {
    void registerBeanDefinitions(Document doc, BeanDefinitionRegistry registry);
}

老样子,提供个DefaultBeanDefinitionDocumentReader作为默认实现,负责具体解析xml标签。代码如下:


import com.hqd.ch03.v6.config.BeanDefinition;
import com.hqd.ch03.v6.config.MutablePropertyValues;
import com.hqd.ch03.v6.reader.xml.parse.BeanDefinitionDocumentReader;
import com.hqd.ch03.v6.registry.BeanDefinitionRegistry;
import org.apache.commons.lang3.StringUtils;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

public class DefaultBeanDefinitionDocumentReader implements BeanDefinitionDocumentReader {

    public static final String DESCRIPTION_ELEMENT = "description";
    public static final String NAME_ATTRIBUTE = "name";
    public static final String BEAN_ELEMENT = "bean";
    public static final String ID_ATTRIBUTE = "id";
    public static final String CLASS_ATTRIBUTE = "class";
    public static final String SCOPE_ATTRIBUTE = "scope";
    public static final String LAZY_INIT_ATTRIBUTE = "lazy-init";
    public static final String INIT_METHOD_ATTRIBUTE = "init-method";
    public static final String DESTROY_METHOD_ATTRIBUTE = "destroy-method";
    public static final String FACTORY_METHOD_ATTRIBUTE = "factory-method";
    public static final String FACTORY_BEAN_ATTRIBUTE = "factory-bean";
    public static final String REF_ATTRIBUTE = "ref";
    public static final String VALUE_ATTRIBUTE = "value";
    private static final String SINGLETON_ATTRIBUTE = "singleton";


    @Override
    public void registerBeanDefinitions(Document doc, BeanDefinitionRegistry registry) {
        parseBeanDefinitions(doc.getDocumentElement(), registry);
    }

    protected void parseBeanDefinitions(Element root, BeanDefinitionRegistry registry) {
        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;
                processBeanDefinition(ele, registry);
            }
        }
    }

    private void processBeanDefinition(Element ele, BeanDefinitionRegistry registry) {
        BeanDefinition bd = new BeanDefinition();
        String id = ele.getAttribute(ID_ATTRIBUTE);
        bd.setId(id);
        bd.setScope(ele.getAttribute(SCOPE_ATTRIBUTE));
        String lazyInit = ele.getAttribute(LAZY_INIT_ATTRIBUTE);
        bd.setLazyInit(StringUtils.isBlank(lazyInit) ? true : Boolean.valueOf(lazyInit));
        bd.setBeanClass(ele.getAttribute(CLASS_ATTRIBUTE));
        bd.setProperties(parsePropertyElements(ele.getChildNodes()));
        registry.registerBeanDefinition(bd.getId(), bd);
    }

    /**
     * 转换bean下的property节点
     *
     * @param nodes
     * @return
     */
    private MutablePropertyValues parsePropertyElements(NodeList nodes) {
        MutablePropertyValues propertyValues = new MutablePropertyValues();
        if (nodes.getLength() != 0) {
            for (int i = 0; i < nodes.getLength(); i++) {
                Node node = nodes.item(i);
                if (node instanceof Element) {
                    Element element = (Element) node;
                    propertyValues.addProperty(element.getAttribute(NAME_ATTRIBUTE), element.getAttribute(VALUE_ATTRIBUTE));
                }
            }
        }
        return propertyValues;
    }

}

由于DOM的加载、解析已经从XmlBeanDefinitionReader剥离出去,所以还需要对其进行修改,删除解析xml相关操作,直接调用具体实现即可。改造如下:

在这里插入图片描述

完整代码:


import com.hqd.ch03.v6.io.Resource;
import com.hqd.ch03.v6.reader.BeanDefinitionReader;
import com.hqd.ch03.v6.reader.xml.doc.DocumentLoader;
import com.hqd.ch03.v6.reader.xml.doc.support.DefaultDocumentLoader;
import com.hqd.ch03.v6.reader.xml.parse.BeanDefinitionDocumentReader;
import com.hqd.ch03.v6.reader.xml.parse.spport.DefaultBeanDefinitionDocumentReader;
import com.hqd.ch03.v6.registry.BeanDefinitionRegistry;
import org.w3c.dom.Document;

public class XmlBeanDefinitionReader implements BeanDefinitionReader {
    private final BeanDefinitionRegistry beanDefinitionRegistry;
    private DocumentLoader documentLoader = new DefaultDocumentLoader();
    private BeanDefinitionDocumentReader documentReader = new DefaultBeanDefinitionDocumentReader();

    public XmlBeanDefinitionReader(BeanDefinitionRegistry beanDefinitionRegistry) {
        this.beanDefinitionRegistry = beanDefinitionRegistry;
    }


    @Override
    public void loadBeanDefinition(Resource resource) {
        Document doc = documentLoader.loadDocument(resource);
        registerBeanDefinitions(doc);
    }

    private void registerBeanDefinitions(Document doc) {
        this.documentReader.registerBeanDefinitions(doc, beanDefinitionRegistry);
    }

    public void setDocumentLoader(DocumentLoader documentLoader) {
        this.documentLoader = (documentLoader != null ? documentLoader : new DefaultDocumentLoader());
    }

    public void setDocumentReader(BeanDefinitionDocumentReader documentReader) {
        this.documentReader = documentReader;
    }

}

在做个简单的测试,测试代码如下:

@Test
    public void v6() {
        System.out.println("############# 第六版:提取Document加载接口 #############");
        SpringImitationV6 beanFactory = new SpringImitationV6Xml("classpath:v6/bean.xml");
        User user = beanFactory.getBean("user", User.class);
        System.out.println("基于类路径加载:" + user);
    }

输出如下:

在这里插入图片描述

    “哼哼,这次看老大还有什么好说的”。看到测试结果后,A君心里暗想。正想着,老大突然就杀了过来,A君心里咯噔一下,顿觉不妙

    老大笑着拍了拍A君的肩膀,说:A君,用户提了个需要,他们想要自定义XML标签,这个你一起回去想一想

第七版 自定义XML标签

    接到新需求后,虽然A君心里早有预感,但心里瞬间千万只草尼玛奔腾而过。还不个简单的需求,自定义个锤子自定义,又得开始加班了,害

在这里插入图片描述

    对于自定义XML标签,A君之前接触甚少,不经挠头,无从下手。几经波折(不停的百度),A君决定先从XML约束开始入手

    不论撸代码,还是其他,总是先有规范,再有具体实现,这样就不会变成一堆乱麻,难以维护。XML也是一样的道理,用XML描述一个类,十个人可能会有十种不同的写法。所以,为了方便维护和实现,必须对XML格式进行约束

    对于XML的约束,主要有两种:xsd、dtd。举个栗子,以下面的XML为例,如下:

<beans>
	<user id="testbean" name="testbean" age="222"/>
</beans>

dtd格式

如果是dtd,则XML如下:

<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN 2.0//EN" "http://www.lexueba.com/schema/user/dtd/user-test.dtd">
<beans>
    <user age="22" name="user" id="user"/>
</beans>

dtd约束文件如下:

<!ELEMENT beans (
        (user)*
        )>
        <!ELEMENT user EMPTY>
        <!ATTLIST user id CDATA #IMPLIED>
        <!ATTLIST user name CDATA #IMPLIED>
        <!ATTLIST user age CDATA #IMPLIED>

xsd格式

如果是xsd,则XML如下:

<beans xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns="http://www.lexueba.com/schema/user"
       xsi:schemaLocation="http://www.lexueba.com/schema/user http://www.lexueba.com/schema/user-test.xsd
">
    <user id="testbean" name="testbean" age="222"/>
</beans>


xsd约束文件如下:

<?xml version="1.0" encoding="UTF-8"?>
<schema xmlns="http://www.w3.org/2001/XMLSchema"
        targetNamespace="http://www.lexueba.com/schema/user"
        elementFormDefault="qualified">
    <element name="beans">
        <complexType>
            <sequence>
                <element name="user">
                    <complexType>
                        <attribute name="id" type="string"/>
                        <attribute name="name" type="string"/>
                        <attribute name="age" type="int"/>
                    </complexType>
                </element>
            </sequence>
        </complexType>
    </element>
</schema>

了解完两者的区别后(这里具体看xsd),A君又一阵头大。这才开始而已,自定义标签又可以分为几种:

  1. 自定义<bean>节点同类型的标签
  2. 自定义<bean>节点的子标签
  3. 自定义<bean>节点的属性

    用户具体要怎么定义标签,A君不得而知。但是作为容器的开发者,A君可以定义接口让用户实现,而后回调即可

    因为存在多种情况,A君先定义BeanDefinitionParser接口负责解析<bean>节点同类型的标签,其代码如下:

import com.hqd.ch03.v7.config.BeanDefinition;
import org.w3c.dom.Element;

/**
 * 自定义标签解析
 */
public interface BeanDefinitionParser {
    BeanDefinition parse(Element element);
}

再定义一个BeanDefinitionDecorator接口,负责解析<bean>节点下自定义的子节点和属性,其代码如下:


import com.hqd.ch03.v7.config.BeanDefinition;
import org.w3c.dom.Node;

/**
 * 解析<bean>的自定义子节点和自定义属性
 */
public interface BeanDefinitionDecorator {
    BeanDefinition decorate(Node node, BeanDefinition beanDefinition);
}

    有了这两个接口后,用户可以个性化实现,但是此时,容器还无法感知到用户实现类。还需要有个注册接口,可以把用户实现的类注册到容器中去。而后,A君有定义了负责注册(NamespaceHandler)接口,代码如下:

import com.hqd.ch03.v6.config.BeanDefinition;
import org.w3c.dom.Element;

import com.hqd.ch03.v7.config.BeanDefinition;
import org.w3c.dom.Element;
import org.w3c.dom.Node;

/**
 * 标签解析注册接口
 */
public interface NamespaceHandler {
    void init();

    /**
     * 解析BeanDefinition自定义标签,eg:
     * <beans>
     * *     <a></a>
     * </beans>
     */
    BeanDefinition parse(Element element);

    /**
     * 解析bean子标签或者属性,eg:
     * <beans>
     * *     <bean name="xxx"  a="xxx">
     * *         <a></a>
     * *     </bean>
     * </beans>
     */
    BeanDefinition decorate(Node source, BeanDefinition definition);
}

为了方便用户使用,A君又定义了NamespaceHandlerSupport抽象类,做个简单的封装。代码如下:


import com.hqd.ch03.v7.config.BeanDefinition;
import com.hqd.ch03.v7.reader.xml.namespace.NamespaceHandler;
import com.hqd.ch03.v7.reader.xml.parse.BeanDefinitionDecorator;
import com.hqd.ch03.v7.reader.xml.parse.BeanDefinitionParser;
import org.w3c.dom.Attr;
import org.w3c.dom.Element;
import org.w3c.dom.Node;

import java.util.HashMap;
import java.util.Map;

public abstract class NamespaceHandlerSupport implements NamespaceHandler {
    /**
     * BeanDefinition自定义标签解析器,eg:
     * <beans>
     * *     <a></a>
     * </beans>
     */
    private final Map<String, BeanDefinitionParser> parsers = new HashMap<>();
    /**
     * bean子标签解析器,eg:
     * <beans>
     * *     <bean>
     * *         <a></a>
     * *     </bean>
     * </beans>
     */
    private final Map<String, BeanDefinitionDecorator> decorators = new HashMap<>();
    /**
     * 自定义属性,eg:
     * <beans>
     * *     <bean name="xx" a="xx"></bean>
     * </beans>
     */
    private final Map<String, BeanDefinitionDecorator> attributeDecorators = new HashMap<>();

    @Override
    public BeanDefinition parse(Element element) {
        BeanDefinitionParser parser = findParserForElement(element);
        return (parser != null ? parser.parse(element) : null);
    }

    protected BeanDefinitionParser findParserForElement(Element element) {
        String localName = element.getLocalName();
        return this.parsers.get(localName);
    }

    @Override
    public BeanDefinition decorate(Node node, BeanDefinition definition) {
        BeanDefinitionDecorator decorator = findDecoratorForNode(node);
        return (decorator != null ? decorator.decorate(node, definition) : null);
    }

    private BeanDefinitionDecorator findDecoratorForNode(Node node) {
        String localName = node.getLocalName();
        if (node instanceof Element) {//自定义子节点
            return this.decorators.get(localName);
        } else if (node instanceof Attr) {//自定义标签属性
            return this.attributeDecorators.get(localName);
        } else {
            throw new RuntimeException(String.format("【%s】节点解析异常", node));
        }
    }

    protected final void registerBeanDefinitionParser(String elementName, BeanDefinitionParser parser) {
        this.parsers.put(elementName, parser);
    }

    protected final void registerBeanDefinitionDecorator(String elementName, BeanDefinitionDecorator dec) {
        this.decorators.put(elementName, dec);
    }

    protected final void registerBeanDefinitionDecoratorForAttribute(String attrName, BeanDefinitionDecorator dec) {
        this.attributeDecorators.put(attrName, dec);
    }
}

    做完这些之后,A君又开始犯难了:

  1. 自定义标签要怎么和用户实现类在xml解析前就关联上?
  2. xml约束定义的是http,如果网络连接慢,甚至断网了怎么办?

    针对上面的问题,A君设想了以下解决方案

  1. 自定义标签可以通过URI关联NamespaceHandler实现类,可以通过配置文件,来维护URI和NamespaceHandler实现类的关系
  2. 约束定义的是http,可以通过本地文件,实现的方式类似,用配置文件来维护URI和xsd约束文件的关系

有了思路之后,A君继续撸袖子忙活了起来,分别定义了两个文件:spring.handlersspring.schemas
spring.handlers文件用来维护URI和实现类的关系(key是URI,value是类全路径),spring.schemas用来维护URI和约束的关系(key是URI,value是xsd路径

    确定了配置文件之后,接下来就是针对两个配置文件的解析。先来看下spring.handlers文件的解析,老规矩,定义个NamespaceHandlerResolver接口,用来注册用户配置的实现类,接口代码如下:

/**
 * 读取自定义的解析类
 */
public interface NamespaceHandlerResolver {
    NamespaceHandler resolve(String namespaceUri);
}

接着是默认实现类DefaultNamespaceHandlerResolver, 这个类只需加载spring.handlers 文件,初始化相关配置实现类后放进缓存就行了,代码如下:


import com.hqd.ch03.v7.reader.xml.namespace.NamespaceHandler;
import com.hqd.ch03.v7.reader.xml.namespace.NamespaceHandlerResolver;

import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;

public class DefaultNamespaceHandlerResolver implements NamespaceHandlerResolver {
    public static final String DEFAULT_HANDLER_MAPPINGS_LOCATION = "META-INF/spring.handlers";
    private Map<String, NamespaceHandler> handlerMappings = new HashMap<>();

    public DefaultNamespaceHandlerResolver() {
        this.init();
    }

    protected void init() {
        Properties properties = new Properties();
        try {
            /**
             * 加载配置文件
             */
            properties.load(this.getClass().getClassLoader().getResourceAsStream(DEFAULT_HANDLER_MAPPINGS_LOCATION));
            Enumeration<Object> keys = properties.keys();
            while (keys.hasMoreElements()) {
                String uri = keys.nextElement().toString();
                String handlerClass = properties.getProperty(uri);
                /**
                 * 初始化配置的类
                 */
                NamespaceHandler handler = (NamespaceHandler) Class.forName(handlerClass).getConstructor().newInstance();
                handler.init();
                /**
                 * 放入缓存
                 */
                handlerMappings.put(uri, handler);
            }
        } catch (Exception e) {
            throw new RuntimeException(e);
        }

    }

    @Override
    public NamespaceHandler resolve(String namespaceUri) {
        return this.handlerMappings.get(namespaceUri);
    }
}

接着是spring.schemas文件的解析。不过在此之前,需要先改造下DocumentLoader接口,代码如下:

import com.hqd.ch03.v7.io.Resource;
import org.w3c.dom.Document;
import org.xml.sax.EntityResolver;
import org.xml.sax.ErrorHandler;

/**
 * xml dom加载接口
 */
public interface DocumentLoader {
    /**
     * 加载dom
     *
     * @param resource       xml资源
     * @param entityResolver 本地xml验证器
     * @param errorHandler   xml解析失败处理器
     * @return
     */
    Document loadDocument(Resource resource, EntityResolver entityResolver, ErrorHandler errorHandler);

}

在修改下实现类DefaultDocumentLoader,代码如下:



import com.hqd.ch03.v7.io.Resource;
import com.hqd.ch03.v7.reader.xml.doc.DocumentLoader;
import org.w3c.dom.Document;
import org.xml.sax.EntityResolver;
import org.xml.sax.ErrorHandler;
import org.xml.sax.InputSource;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;

public class DefaultDocumentLoader implements DocumentLoader {
    protected static final String SCHEMA_LANGUAGE_ATTRIBUTE = "http://java.sun.com/xml/jaxp/properties/schemaLanguage";
    protected static final String XSD_SCHEMA_LANGUAGE = "http://www.w3.org/2001/XMLSchema";

    protected InputSource getInputSource(Resource resource) throws Exception {
        return new InputSource(resource.getInputStream());
    }

    @Override
    public Document loadDocument(Resource resource, EntityResolver resolver, ErrorHandler errorHandler) {
        try {
            InputSource inputSource = getInputSource(resource);
            DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
            /**
             * 设置为true,获取namespaceURI
             * dtd需要设置成false
             */
            factory.setNamespaceAware(true);
            /**
             * 开启验证模式
             */
            factory.setValidating(true);
            /**
             * xsd需要设置,dtd不需要
             */
            factory.setAttribute(SCHEMA_LANGUAGE_ATTRIBUTE, XSD_SCHEMA_LANGUAGE);
            DocumentBuilder docBuilder = factory.newDocumentBuilder();
            /**
             * 设置验证失败处理器
             */
            docBuilder.setErrorHandler(errorHandler);
            /**
             * 指定xml验证器
             */
            docBuilder.setEntityResolver(resolver);
            return docBuilder.parse(inputSource);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
}

之所以这么修改,是因为验证XML,默认会去读取XML文档上的声明,即HTTP请求去下载约束文件。如果要改成读取本地约束文件,则需要提供个EntityResolver实现类。另外A君虽然定义约束,但是架不住用户乱写或者写错了,出现这种情况时A君希望解析XML时候就报错,毕竟错误越早暴露越好嘛,于是A君再提供个ErrorHandler 实现类用来处理验证异常的情况

    完成了DocumentLoader接口的相关修改,再来就是ErrorHandlerEntityResolver的实现了,由于ErrorHandler 比较简单,先来实现ErrorHandler 接口,实现类SimpleSaxErrorHandler代码如下:


import org.xml.sax.ErrorHandler;
import org.xml.sax.SAXException;
import org.xml.sax.SAXParseException;

/**
 * xml验证异常处理实现类
 */
public class SimpleSaxErrorHandler implements ErrorHandler {
    @Override
    public void warning(SAXParseException exception) throws SAXException {
        exception.printStackTrace();
    }

    @Override
    public void error(SAXParseException exception) throws SAXException {
        throw exception;
    }

    @Override
    public void fatalError(SAXParseException exception) throws SAXException {
        throw exception;
    }
}

接着就是EntityResolver的实现了。前面被老大折腾了这么久,A君也有点明白套路了:由于XML约束存在DTD和XSD,先定义一个总的入口,然后再交给具体的处理类。就像上头把需求分给老大,而实际干活的却是A君。。。

    想到这里,A君定义了ResourceEntityResolver作为入口类,包含了dtd和xsd的验证,代码如下:


import org.xml.sax.EntityResolver;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;

import java.io.IOException;

/**
 * xml校验入口。提供dtd、xsd校验
 * 具体校验委托给具体实现类
 */
public class ResourceEntityResolver implements EntityResolver {
    public static final String DTD_SUFFIX = ".dtd";

    public static final String XSD_SUFFIX = ".xsd";


    private final EntityResolver dtdResolver;

    private final EntityResolver schemaResolver;

    public ResourceEntityResolver() {
        this.dtdResolver = new BeansDtdResolver();
        this.schemaResolver = new PluggableSchemaResolver();
    }

    @Override
    public InputSource resolveEntity(String publicId, String systemId) throws SAXException, IOException {
        if (systemId != null) {
            if (systemId.endsWith(DTD_SUFFIX)) {
                return this.dtdResolver.resolveEntity(publicId, systemId);
            } else if (systemId.endsWith(XSD_SUFFIX)) {
                return this.schemaResolver.resolveEntity(publicId, systemId);
            }
        }

        return null;
    }
}

有了入口类之后,接下来看下spring.schemas文件的解析,A君定义了 PluggableSchemaResolver 作为xsd约束的具体处理类(这里重点说下xsd)。这个实现也简单,解析spring.schemas文件存进map缓存,根据uri获取对应路径即可。代码如下:


import com.hqd.ch03.v7.io.Resource;
import com.hqd.ch03.v7.io.support.ClassPathResource;
import org.xml.sax.EntityResolver;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;

import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;

/**
 * 读取本地xsd校验文件进行校验
 */
public class PluggableSchemaResolver implements EntityResolver {
    public static final String DEFAULT_SCHEMA_MAPPINGS_LOCATION = "META-INF/spring.schemas";
    private final String schemaMappingsLocation;
    private volatile Map<String, String> schemaMappings;

    public PluggableSchemaResolver(String schemaMappingsLocation) {
        this.schemaMappingsLocation = schemaMappingsLocation;
    }

    public PluggableSchemaResolver() {
        this(DEFAULT_SCHEMA_MAPPINGS_LOCATION);
    }

    @Override
    public InputSource resolveEntity(String publicId, String systemId) throws SAXException, IOException {
        if (systemId != null) {
            String resourceLocation = getSchemaMappings().get(systemId);
            /**
             * systemId以https开头,且资源不存在,尝试以http的形式获取
             */
            if (resourceLocation == null && systemId.startsWith("https:")) {
                resourceLocation = getSchemaMappings().get("http:" + systemId.substring(6));
            }
            /**
             * 资源存在,则尝试加载
             */
            if (resourceLocation != null) {
                Resource resource = new ClassPathResource(resourceLocation);
                try {
                    InputSource source = new InputSource(resource.getInputStream());
                    source.setPublicId(publicId);
                    source.setSystemId(systemId);
                    return source;
                } catch (FileNotFoundException ex) {
                    throw new RuntimeException(ex);
                }
            }
        }

        return null;
    }

    /**
     * 读取指定目录下的配置文件,缓存起来
     *
     * @return
     */
    private Map<String, String> getSchemaMappings() {
        Map<String, String> schemaMappings = this.schemaMappings;
        if (schemaMappings == null) {
            synchronized (this) {
                schemaMappings = this.schemaMappings;
                if (schemaMappings == null) {
                    try {
                        Properties mappings = new Properties();
                        mappings.load(this.getClass().getClassLoader().getResourceAsStream(schemaMappingsLocation));
                        Set<Object> objects = mappings.keySet();
                        schemaMappings = new ConcurrentHashMap<>(objects.size());
                        for (Object key : objects) {
                            String keyStr = key.toString();
                            String value = mappings.getProperty(keyStr);
                            schemaMappings.put(keyStr, value);
                        }
                        this.schemaMappings = schemaMappings;
                    } catch (IOException ex) {
                        throw new RuntimeException(ex);
                    }
                }
            }
        }
        return schemaMappings;
    }
}

这些都做好之后,接着需要对DefaultBeanDefinitionDocumentReader进行一番改造,以支持xml约束和解析自定义标签。这个可以根据标签的URI进行区分是用默认的标签解析器还是用户自定义的标签解析器。先是parseBeanDefinitions方法改造,如下:

在这里插入图片描述

isDefaultNamespace方法

在这里插入图片描述

parseCustomElement方法

在这里插入图片描述

再是processBeanDefinition方法相关改造,需要添加解析自定义标签。如下:

在这里插入图片描述

decorateBeanDefinitionIfRequired方法

在这里插入图片描述

decorateIfRequired方法

在这里插入图片描述

完整代码:


import com.hqd.ch03.v7.config.BeanDefinition;
import com.hqd.ch03.v7.config.MutablePropertyValues;
import com.hqd.ch03.v7.reader.xml.namespace.NamespaceHandler;
import com.hqd.ch03.v7.reader.xml.namespace.NamespaceHandlerResolver;
import com.hqd.ch03.v7.reader.xml.namespace.support.DefaultNamespaceHandlerResolver;
import com.hqd.ch03.v7.reader.xml.parse.BeanDefinitionDocumentReader;
import com.hqd.ch03.v7.registry.BeanDefinitionRegistry;
import org.apache.commons.lang3.StringUtils;
import org.w3c.dom.*;

public class DefaultBeanDefinitionDocumentReader implements BeanDefinitionDocumentReader {
    public static final String BEANS_NAMESPACE_URI = "http://www.springframework.org/schema/beans";

    public static final String DESCRIPTION_ELEMENT = "description";
    public static final String NAME_ATTRIBUTE = "name";
    public static final String BEAN_ELEMENT = "bean";
    public static final String ID_ATTRIBUTE = "id";
    public static final String CLASS_ATTRIBUTE = "class";
    public static final String SCOPE_ATTRIBUTE = "scope";
    public static final String LAZY_INIT_ATTRIBUTE = "lazy-init";
    public static final String INIT_METHOD_ATTRIBUTE = "init-method";
    public static final String DESTROY_METHOD_ATTRIBUTE = "destroy-method";
    public static final String FACTORY_METHOD_ATTRIBUTE = "factory-method";
    public static final String FACTORY_BEAN_ATTRIBUTE = "factory-bean";
    public static final String REF_ATTRIBUTE = "ref";
    public static final String VALUE_ATTRIBUTE = "value";
    private static final String SINGLETON_ATTRIBUTE = "singleton";

    protected NamespaceHandlerResolver resolver = new DefaultNamespaceHandlerResolver();

    @Override
    public void registerBeanDefinitions(Document doc, BeanDefinitionRegistry registry) {
        parseBeanDefinitions(doc.getDocumentElement(), registry);
    }

    /**
     * 解析xml标签为BeanDefinition
     *
     * @param root
     * @param registry
     */
    protected void parseBeanDefinitions(Element root, BeanDefinitionRegistry registry) {
        if (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 (isDefaultNamespace(ele)) {
                        processBeanDefinition(ele, registry);
                    } else {//自定义标签
                        parseCustomElement(ele, registry);
                    }
                }
            }
        } else {
            parseCustomElement(root, registry);
        }
    }

    /**
     * 转换bean底下的自定义标签和属性
     *
     * @param ele
     * @param originalDef
     * @return
     */
    private BeanDefinition decorateBeanDefinitionIfRequired(
            Element ele, BeanDefinition originalDef) {
        NamedNodeMap attributes = ele.getAttributes();
        for (int i = 0; i < attributes.getLength(); i++) {
            Node node = attributes.item(i);
            originalDef = decorateIfRequired(node, originalDef);
        }
        NodeList children = ele.getChildNodes();
        for (int i = 0; i < children.getLength(); i++) {
            Node node = children.item(i);
            if (node.getNodeType() == Node.ELEMENT_NODE) {
                originalDef = decorateIfRequired(node, originalDef);
            }
        }
        return originalDef;
    }

    private BeanDefinition decorateIfRequired(Node node, BeanDefinition originalDef) {
        String namespaceURI = node.getNamespaceURI();
        if (!isDefaultNamespace(namespaceURI)) {
            NamespaceHandler handler = resolver.resolve(namespaceURI);
            if (handler != null) {
                return handler.decorate(node, originalDef);
            }
        }
        return originalDef;
    }

    /**
     * 处理bean标签
     *
     * @param ele
     * @param registry
     */
    private void processBeanDefinition(Element ele, BeanDefinitionRegistry registry) {
        BeanDefinition bd = new BeanDefinition();
        String id = ele.getAttribute(ID_ATTRIBUTE);
        bd.setId(id);
        bd.setScope(ele.getAttribute(SCOPE_ATTRIBUTE));
        String lazyInit = ele.getAttribute(LAZY_INIT_ATTRIBUTE);
        bd.setLazyInit(StringUtils.isBlank(lazyInit) ? true : Boolean.valueOf(lazyInit));
        bd.setBeanClass(ele.getAttribute(CLASS_ATTRIBUTE));
        bd.setProperties(parsePropertyElements(ele.getChildNodes()));
        /**
         * 解析自定义标签
         */
        BeanDefinition beanDefinition = decorateBeanDefinitionIfRequired(ele, bd);
        registry.registerBeanDefinition(bd.getId(), beanDefinition);
    }

    /**
     * 转换bean下的property节点
     *
     * @param nodes
     * @return
     */
    private MutablePropertyValues parsePropertyElements(NodeList nodes) {
        MutablePropertyValues propertyValues = new MutablePropertyValues();
        if (nodes.getLength() != 0) {
            for (int i = 0; i < nodes.getLength(); i++) {
                Node node = nodes.item(i);
                if (node instanceof Element) {
                    Element element = (Element) node;
                    propertyValues.addProperty(element.getAttribute(NAME_ATTRIBUTE), element.getAttribute(VALUE_ATTRIBUTE));
                }
            }
        }
        return propertyValues;
    }

    /**
     * 调用自定义解析器
     *
     * @param ele
     * @param registry
     */
    private void parseCustomElement(Element ele, BeanDefinitionRegistry registry) {
        NamespaceHandler resolve = resolver.resolve(getNamespaceURI(ele));
        BeanDefinition bd = resolve.parse(ele);
        registry.registerBeanDefinition(bd.getId(), bd);
    }

    /**
     * 判断是否是spring-bean的标签
     *
     * @param namespaceUri
     * @return
     */
    public boolean isDefaultNamespace(String namespaceUri) {
        return StringUtils.isBlank(namespaceUri) || BEANS_NAMESPACE_URI.equals(namespaceUri);
    }

    /**
     * 判断是否是spring-bean的标签
     *
     * @param element
     * @return
     */
    public boolean isDefaultNamespace(Element element) {
        return isDefaultNamespace(getNamespaceURI(element));
    }

    public String getNamespaceURI(Node node) {
        return node.getNamespaceURI();
    }
}

    对应的代码改造已经完成,还需要测试一下来验证努力的成果

    A君新创建了个Student实体类,代码如下:


import lombok.Data;

@Data
public class Student extends User {
    private String school;
    private String hobbies;

    @Override
    public String toString() {
        return "User{" +
                "name='" + getName() + '\'' +
                ", age=" + getAge() +
                ", school='" + school + '\'' +
                ", hobbies=" + hobbies +
                '}';
    }
}

在定义个xsd约束文件,xsd文件如下:

<?xml version="1.0" encoding="UTF-8"?>
<schema xmlns:loc="http://www.lexueba.com/schema/user"
        xmlns="http://www.w3.org/2001/XMLSchema"
        targetNamespace="http://www.lexueba.com/schema/user"
        elementFormDefault="qualified">

    <attribute name="school"/>
    <element name="hobbies">
        <complexType>
            <attribute name="value" type="string"/>
        </complexType>
    </element>

    <complexType name="userType">
        <attribute name="id" type="string"/>
        <attribute name="name" type="string"/>
        <attribute name="age" type="int"/>
    </complexType>

    <element name="user" type="loc:userType"/>
    <element name="beans">
        <complexType>
            <sequence>
                <choice minOccurs="0" maxOccurs="unbounded">
                    <element ref="loc:user"/>
                </choice>
            </sequence>
        </complexType>
    </element>
</schema>

重新写个包含自定义标签的bean.xml文件,并加入spring的相关约束,如下:

<beans xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:custom="http://www.lexueba.com/schema/user"
       xmlns="http://www.springframework.org/schema/beans"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
	http://www.springframework.org/schema/beans/spring-beans.xsd
	http://www.lexueba.com/schema/user http://www.lexueba.com/schema/user.xsd
">
    <custom:user id="testbean" name="testbean" age="222"/>
    <bean id="student" class="com.hqd.ch03.bean.Student" custom:school="南山幼儿园">
        <property name="name" value="法外狂徒-张三"/>
        <property name="age" value="14"/>
        <custom:hobbies value="唱,跳,rap"/>
    </bean>
</beans>

准备完xml后,还要实现相关接口来完成解析。先是自定义<bean>同级标签的解析类UserBeanDefinitionParser,代码如下:


import com.hqd.ch03.bean.User;
import com.hqd.ch03.v7.config.BeanDefinition;
import com.hqd.ch03.v7.config.MutablePropertyValues;
import com.hqd.ch03.v7.reader.xml.parse.BeanDefinitionParser;
import org.apache.commons.lang3.StringUtils;
import org.w3c.dom.Element;

public class UserBeanDefinitionParser implements BeanDefinitionParser {

    @Override
    public BeanDefinition parse(Element element) {
        BeanDefinition bd = new BeanDefinition();
        String id = element.getAttribute("id");
        String userName = element.getAttribute("name");
        String age = element.getAttribute("age");
        bd.setBeanClass(User.class.getName());
        bd.setId(id);
        MutablePropertyValues propertyValues = new MutablePropertyValues();
        if (StringUtils.isNotBlank(userName)) {
            propertyValues.addProperty("name", userName);
        }
        if (StringUtils.isNotBlank(age)) {
            propertyValues.addProperty("age", age);
        }
        bd.setProperties(propertyValues);
        return bd;
    }
}

<bean>自定义子标签解析类,代码如下:

import com.hqd.ch03.v7.config.BeanDefinition;
import com.hqd.ch03.v7.reader.xml.parse.BeanDefinitionDecorator;
import org.apache.commons.lang3.StringUtils;
import org.w3c.dom.Element;
import org.w3c.dom.Node;

import java.util.HashMap;
import java.util.Map;

public class HobbiesBeanDefinitionDecorator implements BeanDefinitionDecorator {

    private Map<String, String> parsePropertyValues(Element ele) {
        String hobbiesStr = ele.getAttribute("value");
        if (StringUtils.isNoneBlank(hobbiesStr)) {
            HashMap<String, String> map = new HashMap<>();
            map.put("hobbies", hobbiesStr);
            return map;
        }
        return null;
    }

    @Override
    public BeanDefinition decorate(Node node, BeanDefinition definition) {
        if (node instanceof Element
                && StringUtils.equalsIgnoreCase(node.getLocalName(), "hobbies")) {
            Element ele = (Element) node;
            Map<String, String> map = parsePropertyValues(ele);
            if (map != null) {
                definition.getProperties()
                        .addPropertyValues(map);
            }
        }
        return definition;
    }
}

最后是<bean>自定义属性解析类,代码如下:

import com.hqd.ch03.v7.config.BeanDefinition;
import com.hqd.ch03.v7.reader.xml.parse.BeanDefinitionDecorator;
import com.sun.org.apache.xerces.internal.dom.DeferredAttrNSImpl;
import org.apache.commons.lang3.StringUtils;
import org.w3c.dom.Node;

public class SchoolBeanDefinitionDecorator implements BeanDefinitionDecorator {
    @Override
    public BeanDefinition decorate(Node node, BeanDefinition definition) {
        if (node instanceof DeferredAttrNSImpl
                && StringUtils.equalsIgnoreCase(node.getLocalName(), "school")) {
            DeferredAttrNSImpl attrNS = (DeferredAttrNSImpl) node;
            definition.getProperties()
                    .addProperty(attrNS.getLocalName(), attrNS.getValue());
        }
        return definition;
    }
}

准备完xml和标签实现类后,还要实现NamespaceHandler接口注册这几个实现类,MyNamespaceHandler代码如下:

import com.hqd.ch03.v7.reader.xml.namespace.support.NamespaceHandlerSupport;

public class MyNamespaceHandler extends NamespaceHandlerSupport {
    @Override
    public void init() {
        registerBeanDefinitionParser("user", new UserBeanDefinitionParser());
        registerBeanDefinitionDecorator("hobbies", new HobbiesBeanDefinitionDecorator());
        registerBeanDefinitionDecoratorForAttribute("school", new SchoolBeanDefinitionDecorator());

    }
}

这些都做完之后,还需要配置下spring.handlersspring.schemas,将定义的xsd和注册类与URI关联起来

spring.handlers配置如下:

http\://www.lexueba.com/schema/user=com.hqd.test.MyNamespaceHandler

spring.schemas配置如下:

http\://www.lexueba.com/schema/user.xsd=schema/xsd/user.xsd
http\://www.springframework.org/schema/beans/spring-beans.xsd=schema/xsd/spring-beans.xsd

好了,现在一切都准备完了,可以进行测试了,测试代码如下:

 	/**
     * 增加自定义标签
     */
    @Test
    public void v7() {
        System.out.println("############# 第七版:增加自定义标签 #############");
        SpringImitationV7 beanFactory = new SpringImitationV7Xml("classpath:v7/bean.xml");
        User user = beanFactory.getBean("student", User.class);
        System.out.println("加载默认标签:" + user);
        User testbean = beanFactory.getBean("testbean", User.class);
        System.out.println("加载自定义标签:" + testbean);
    }

输入如下:

在这里插入图片描述

    总算做好了,A君喜悦之情溢于言表。为了需要求,A君都不知道掉了多少头发哩!升职加薪,指日可待,嘿嘿

在这里插入图片描述

    老大看着代码,难得没再提什么建议了,反复看了几遍,说道:A君,干得不错,准备上线吧


总结

    历经千辛万苦,项目终于可以上线了,A君后边还会遇到什么问题,让我们拭目以待吧。欲知后事如何,请看下回分解(✪ω✪)

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

穷儒公羊

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值