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

Spring源码阅读目录

第一部分——IOC篇

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

第二部分——AOP篇

第十一章 Spring之不太熟的熟人——AOP
第十二章 Spring之不得不了解的内容——概念篇
第十三章 Spring之假如让你来写AOP——AOP联盟篇
第十四章 Spring之假如让你来写AOP——雏形篇
第十五章 Spring之假如让你来写AOP——Joinpoint(连接点)篇
第十六章 Spring之假如让你来写AOP——Pointcut(切点)篇
第十七章 Spring之假如让你来写AOP——Advice(通知)上篇
第十八章 Spring之假如让你来写AOP——Advice(通知)下篇
第十九章 Spring之假如让你来写AOP——番外篇:Spring早期设计
第二十章 Spring之假如让你来写AOP——Aspect(切面)篇
第二十一章 Spring之假如让你来写AOP——Weaver(织入器)篇
第二十二章 Spring之假如让你来写AOP——Target Object(目标对象)篇
第二十三章 Spring之假如让你来写AOP——融入IOC容器篇
第二十四章 Spring之源码阅读——AOP篇

第三部分——事务篇

第二十五章 Spring之曾经的老朋友——事务
第二十六章 Spring之假如让你来写事务——初稿篇
第二十七章 Spring之假如让你来写事务——铁三角篇
第二十八章 Spring之假如让你来写事务——属性篇
第二十九章 Spring之假如让你来写事务——状态篇
第三十章 Spring之假如让你来写事务——管理篇
第三十一章 Spring之假如让你来写事务——融入IOC容器篇
第三十二章 Spring之源码阅读——事务篇

第四部分——MVC篇

第三十三章 Spring之梦开始的地方——MVC
第三十四章 Spring之假如让你来写MVC——草图篇
第三十五章 Spring之假如让你来写MVC——映射器篇
第三十六章 Spring之假如让你来写MVC——拦截器篇
第三十七章 Spring之假如让你来写MVC——控制器篇
第三十八章 Spring之假如让你来写MVC——适配器篇
第三十九章 Spring之假如让你来写MVC——番外篇:类型转换
第四十章 Spring之假如让你来写MVC——ModelAndView篇
第四十一章 Spring之假如让你来写MVC——番外篇:数据绑定
第四十二章 Spring之假如让你来写MVC——视图篇
第四十三章 Spring之假如让你来写MVC——上传文件篇
第四十四章 Spring之假如让你来写MVC——异常处理器篇
第四十五章 Spring之假如让你来写MVC——国际化篇
第四十六章 Spring之假如让你来写MVC——主题解析器篇
第四十七章 Spring之假如让你来写MVC——闪存管理器篇
第四十八章 Spring之假如让你来写MVC——请求映射视图篇
第四十九章 Spring之假如让你来写MVC——番外篇:属性操作
第五十章 Spring之假如让你来写MVC——融入IOC容器篇
第五十一章 Spring之源码阅读——MVC篇

第五部分——Boot篇

第五十二章 Spring之再进一步——Boot
第五十三章 Spring之假如让你来写Boot——环境篇
第五十四章 Spring之假如让你来写Boot——注解篇(上)
第五十五章 Spring之假如让你来写Boot——注解篇(下)
第五十六章 Spring之假如让你来写Boot——SPI篇
第五十七章 Spring之假如让你来写Boot——配置文件篇(上)
第五十八章 Spring之假如让你来写Boot——配置文件篇(下)
第五十九章 Spring之假如让你来写Boot——番外篇:再谈Bean定义
第六十章 Spring之假如让你来写Boot——自动装配篇
第六十一章 Spring之假如让你来写Boot——番外篇:杂谈Starter
第六十二章 Spring之假如让你来写Boot——番外篇:重构BeanFactory
第六十三章 Spring之假如让你来写Boot——番外篇:再谈ApplicationContext
第六十四章 Spring之假如让你来写Boot——内嵌Web容器篇
第六十五章 Spring之假如让你来写Boot——Main方法启动篇
第六十六章 Spring之最终章——结语篇



前言

    对于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君后边还会遇到什么问题,让我们拭目以待吧。欲知后事如何,请看下回分解(✪ω✪)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

穷儒公羊

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

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

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

打赏作者

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

抵扣说明:

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

余额充值