Spring 自定义 XML 配置扩展

版权声明: https://blog.csdn.net/Dongguabai/article/details/84553612

XML 配置用的越来越少了,不过还是有比较了解一下 Spring 对 XML 的扩展机制。Spring 是基于 Dom 进行操作的。

可以先看看官方文档的介绍:

9.2. XML Schema Authoring

9.2.1. Introduction

Since version 2.0, Spring has featured a mechanism for schema-based extensions to the basic Spring XML format for defining and configuring beans. This section is devoted to detailing how you would go about writing your own custom XML bean definition parsers and integrating such parsers into the Spring IoC container.

To facilitate the authoring of configuration files using a schema-aware XML editor, Spring’s extensible XML configuration mechanism is based on XML Schema. If you are not familiar with Spring’s current XML configuration extensions that come with the standard Spring distribution, please first read the appendix entitled[xsd-config].

Creating new XML configuration extensions can be done by following these (relatively) simple steps:

  • Authoring an XML schema to describe your custom element(s).

  • Coding a custom NamespaceHandler implementation (this is an easy step, don’t worry).

  • Coding one or more BeanDefinitionParser implementations (this is where the real work is done).

  • Registering the above artifacts with Spring (this too is an easy step).

在 resources 下新建 context.xml 文件,从 Spring 官网(https://docs.spring.io/spring/docs/5.0.11.BUILD-SNAPSHOT/spring-framework-reference/core.html#xml-custom)找一段 Demo :

可以复制这段 XML :

xmlns="http://www.springframework.org/schema/beans"

修改为: 

其中:

"http://www.springframework.org/schema/context"

就称之为命名空间,同时有一个前缀 context。

根据 XML Schema 的机制,需要配置地址,而且还需要成对出现:

为什么要成对出现呢,因为这里就相当于是个 Key-Value 结构一样:

这个 xsd 从网页上打开是不存在的(主要是因为这里的路径写错了,后面会有说明):

但是我们可以通过 Ctrl + 鼠标左键看到文件地址:

这个文件在 jar 里面:

再多说一个,这里有很多人喜欢带上版本:

其实不带版本更好一点,不带版本会跟着 jar 包走。

这样在配置文件中可以使用 <context> 标签:

其实有这么一个概念:prefix:local-element-name。比如在上面这个 XML 中,context 就是 prefix,component-scan 就是 local-element-name。

在 xsd 文件中也是这么定义的:

也能够看到一些子属性:

也就是说可以在 context 标签中增加 这个子属性:

再比如这里还有一个 sequence:

也就是说这两个属性是有顺序要求的,比如这么使用就报错了:

必须要严格遵守顺序:

再比如属性就是使用 attribute 来表达,同时还可以定义类型,这里定义的 string:

那么 XML 进行了配置后,必然会有一些相应的类去处理,比如我们常用的这个配置:

新建一个 User 类,并在 application.properties 文件中添加内容:

配置 User Bean:

因为这里使用了中文,先看看 application.properties 文件的编码:

在 PropertyPlaceholderConfigurer 中指定编编码一直:

简单使用 ClassPathXmlApplicationContext 测试一下,那么为什么 ClassPathXmlApplicationContext 可以进行 Spring 测试呢,因为在 ClassPathXmlApplicationContext 的构造中执行了 refresh() 方法:

测试代码:

输出:

Spring 3.1 之后也有一个读取配置文件的新的写法:

而本质也是使用的这个类:

这个类和之前使用的 PropertyPlaceholderConfigurer 还是不一样的。但是都是继承自同一个类:

Spring 3.1 后基于 Schema 的扩展替代了之前的写法。

在运行测试代码:

发现出现了异常:

org.xml.sax.SAXParseException: schema_reference.4: 无法读取方案文档 'http://www.springframework.org/schema/spring-context.xsd', 原因为 1) 无法找到文档; 2) 无法读取文档; 3) 文档的根元素不是 <xsd:schema>。
	at com.sun.org.apache.xerces.internal.util.ErrorHandlerWrapper.createSAXParseException(ErrorHandlerWrapper.java:203)
	at com.sun.org.apache.xerces.internal.util.ErrorHandlerWrapper.warning(ErrorHandlerWrapper.java:99)
	at com.sun.org.apache.xerces.internal.impl.XMLErrorReporter.reportError(XMLErrorReporter.java:392)
	at com.sun.org.apache.xerces.internal.impl.XMLErrorReporter.reportError(XMLErrorReporter.java:306)
	at com.sun.org.apache.xerces.internal.impl.xs.traversers.XSDHandler.reportSchemaErr(XSDHandler.java:4162)
	at com.sun.org.apache.xerces.internal.impl.xs.traversers.XSDHandler.reportSchemaWarning(XSDHandler.java:4153)
	at com.sun.org.apache.xerces.internal.impl.xs.traversers.XSDHandler.getSchemaDocument1(XSDHandler.java:2486)
	at com.sun.org.apache.xerces.internal.impl.xs.traversers.XSDHandler.getSchemaDocument(XSDHandler.java:2183)
	at com.sun.org.apache.xerces.internal.impl.xs.traversers.XSDHandler.parseSchema(XSDHandler.jav
......
......
Exception in thread "main" org.springframework.beans.factory.xml.XmlBeanDefinitionStoreException: Line 21 in XML document from class path resource [context.xml] is invalid; nested exception is org.xml.sax.SAXParseException; lineNumber: 21; columnNumber: 92; cvc-complex-type.2.4.c: 通配符的匹配很全面, 但无法找到元素 'context:property-placeholder' 的声明。
	at org.springframework.beans.factory.xml.XmlBeanDefinitionReader.doLoadBeanDefinitions(XmlBeanDefinitionReader.java:404)
	at org.springframework.beans.factory.xml.XmlBeanDefinitionReader.loadBeanDefinitions(XmlBeanDefinitionReader.java:336)

应该是这部分出现了问题:

那到底是哪里出现了问题呢,其实 Spring XML 扩展主要基于 Schema 配置(META-INF/spring.schema)和 Namespace Hander 配置(META-INF/spring.handlers),可以搜索一下 spring.schema 文件,选择和 context 有关的:

找到原因了,这里少了一个 context。

修改 context.xml 文件:

重新运行测试代码,运行成功:

有了 Schema 就肯定需要定义 namespace,即 Handler ,也就是要定义处理类,可以搜索 META-INF/spring.handlers 文件,选择 context :

 也就是存在这样的关联关系:

进入 context 的那个类,可以看到很熟悉的 property-placeholder:

这个就是之前写的这个:

而后面有一个处理类:

Spring XML 扩展

      Schema 配置

           META-INF/spring.schemas(作为 properties 文件处理,所以 : 需要转义,类似 k = v 或者 k : v )

              schema 绝对路径 = schema 相对路径

 在 Scheme 中需要定义 namespace ,也就是需要定义处理类。

       Namespace Handler 配置

             META-INF/spring.handlers

Bean 的定义:BeanDefination

Bean 定义解析器:BeanDefinitionParser

Spring XML Schema -> 自定义元素 -> 解析Bean的定义

总的来说,存在一个映射的关系。

所以照葫芦画瓢:

第一步:定义 Schema.xsd:

从 spring-context.xsd 文件中复制过来:

定义元素 user -> User

定义 targetNamespace:

第二步:建立 schema 绝对路径和相对路径的映射(META-INF/spring.schemas)

绝对的XSD = 相对的XSD

创建相关的文件:

第三步:添加 my.xsd 到 context.xml文件中

引入 namespace(就是刚刚的 targetNamespace):

配置 namespace Schema 映射:

需要成对出现:

引入配置:

第四步:建立 Schema namespace 与 Handler 映射( META-INF/spring.handlers)

建立  META-INF/spring.handlers 文件:

将刚刚的 namespace 复制过来,并实现 NamespaceHandlerSupport:

创建 user 元素的 BeanDefinitionParser(实现 BeanDefinitionParser):

package com.example.demoXML.config;

import com.example.demoXML.model.User;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.xml.BeanDefinitionParser;
import org.springframework.beans.factory.xml.ParserContext;
import org.springframework.lang.Nullable;
import org.w3c.dom.Element;

/**
 * @author Dongguabai
 * @date 2018/11/26 20:44
 */
public class MyParser implements BeanDefinitionParser{

    /**
     *  解析  <my:user age="12" name="${name}" />
     *
     */

    @Nullable
    @Override
    public BeanDefinition parse(Element element, ParserContext parserContext) {

        //获取 age(原始为String)
        String age = element.getAttribute("age");

        //这里需要处理 Placeholder
        String name = element.getAttribute("name");

        //构建BeanDefination
        BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(User.class);

        //添加 Property的值
        builder.addPropertyValue("age",age);
        builder.addPropertyValue("name",name);

        //创建 BeanDefinition
        return builder.getBeanDefinition();
    }

}

建立 local-element-name 与 BeanDefinitionParser 的联系:

运行测试类:

出现了异常:

Exception in thread "main" org.springframework.beans.factory.parsing.BeanDefinitionParsingException: Configuration problem: Cannot locate BeanDefinitionParser for element [user]
Offending resource: class path resource [context.xml]
	at org.springframework.beans.factory.parsing.FailFastProblemReporter.fatal(FailFastProblemReporter.java:62)
	at org.springframework.beans.factory.parsing.ReaderContext.fatal(ReaderContext.java:90)
	at org.springframework.beans.factory.parsing.ReaderContext.fatal(ReaderContext.java:68)
	at org.springframework.beans.factory.xml.NamespaceHandlerSupport.findParserForElement(NamespaceHandlerSupport.java:86)
	at org.springframework.beans.factory.xml.NamespaceHandlerSupport.parse(NamespaceHandlerSupport.java:73)

原来刚刚这个地方写错了 element,要更改为 user:

再启动测试类:

Exception in thread "main" org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type 'com.example.demoXML.model.User' available
	at org.springframework.beans.factory.support.DefaultListableBeanFactory.getBean(DefaultListableBeanFactory.java:343)
	at org.springframework.beans.factory.support.DefaultListableBeanFactory.getBean(DefaultListableBeanFactory.java:335)
	at org.springframework.context.support.AbstractApplicationContext.getBean(AbstractApplicationContext.java:1101)
	at com.example.demoXML.SpringTest.main(SpringTest.java:14)

又出现异常了,说 User 没有注册。为什么会没有注册呢。检查一下到底是什么原因。找到 org.springframework.context.support.AbstractApplicationContext#refresh 这个方法,这个方法应该很熟悉了,初始化整个容器的方法。在 finishBeanFactoryInitialization()方法上打个断点,这个方法是初始化所有的 BeanDefinition:

接下来再在这个方法上打个断点,看看两个断点的执行顺序:

的确是先执行的这个方法:

但是发现此时 User 并未注册进 Spring 容器:

也就是说我们需要在 MyParser 中将 User 的BeanDefinition 注册进去:

再来运行测试代码,可以发现此时 User 已经注册进去了:

也输出了想要的结果:

我这里注册的时候 Bean 的 id 是写死了,可以扩展一下,在 my.xsd 文件中:


 

修改 MyParser:

在 context.xml 文件中也定义一下:

再次运行测试代码,出现了异常:

Exception in thread "main" org.springframework.beans.factory.NoUniqueBeanDefinitionException: No qualifying bean of type 'com.example.demoXML.model.User' available: expected single matching bean but found 2: myUser1,myUser2
	at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveNamedBean(DefaultListableBeanFactory.java:1139)
	at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveBean(DefaultListableBeanFactory.java:407)
	at org.springframework.beans.factory.support.DefaultListableBeanFactory.getBean(DefaultListableBeanFactory.java:341)
	at org.springframework.beans.factory.support.DefaultListableBeanFactory.getBean(DefaultListableBeanFactory.java:335)
	at org.springframework.context.support.AbstractApplicationContext.getBean(AbstractApplicationContext.java:1101)
	at com.example.demoXML.SpringTest.main(SpringTest.java:14)

这个异常的原因就很好解释了,修改测试代码:

再次运行,测试成功:

Demo 源码地址:https://gitee.com/white_melon_white/spring-xml

猜你喜欢

转载自blog.csdn.net/Dongguabai/article/details/84553612