详细分析如何扩展Spring中的文档定义
前言
在我们平时使用Spring框架的时候,在90%的情况下都是使用其自带的默认标签,因为其默认提供给我们的标签元素在绝大多数情况下也满足了我们的需求。那么我们为什么还要自己去定义标签去进行扩展呢?其本质原因还是因为你不能保证任何情况下当前的场景就能满足你的需求,在某些特殊情况下,我们不得不对其进行扩展;比如Dubbo框架就对Spring默认的标签元素进行了扩展,加入了自己定制化的一些内容,从而大大简化了我们用户的配置。
Dubbo自定义标签说明.png如果不这样做的话,我们就需要配置一个具体的Bean,这样会造成两个问题:
1.增加用户配置的复杂度
2.破坏封装特性,将底层实现细节强制地暴露给用户
因此,通过上面的分析我们知道,学会如何扩展Spring的文档定义还是非常有必要的,本文的目的就是将详细说明我们应该如何对其进行扩展,并且将其与Spring的Ioc容器进行集成。
如何实现
从Spring2.0开始,Spring拥有了一个新的特征机制:可以基于Schema对基本Spring XML格式进行扩展,从而实现自定义和配置bean。
我们要实现一个自定义标签的具体步骤如下:
1.编写一个XML Schema来描述我们自定义的元素
2.编写一个NamespaceHandler接口的实现(这步非常简单)
3.编写一个或者多个BeanDefinitionParser(BeanDefinitionParser是真正完成XML元素的解析任务)
4.将以上的构建都注册到Spring容器中(这步也非常简单)
下面我们将对上面的每一个步骤做详细的说明。
定义Schema文档
<?xml version="1.0" encoding="UTF-8"?>
<!--这里xmlns是用于说明这份XSD的命名空间
targetNamespace必须指定,因为Spring会根据namespace来进行NamespaceHandler的映射
elementFormDefault="qualified"表示该Schema文件中声明的元素在使用时需要加命名空间
attributeFormDefault="unqualified"表示元素的属性无需加命名空间
-->
<xsd:schema xmlns="http://www.panlingxiao.com/schema/myns"
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns:beans="http://www.springframework.org/schema/beans"
targetNamespace="http://www.panlingxiao.com/schema/myns"
elementFormDefault="qualified" attributeFormDefault="unqualified">
<!--因为在文档定义中又使用到了Spring的XSD文件,因此手动需要引入-->
<xsd:import namespace="http://www.springframework.org/schema/beans"
schemaLocation="http://www.springframework.org/schema/beans/spring-beans.xsd"/>
<!--定义的元素名称为dateformat-->
<xsd:element name="dateformat">
<xsd:complexType>
<xsd:complexContent>
<!---
这里的xsd:extension表示从beans:identifiedType从这个复杂类型中继承
并且在它的基础上又添加了自己的两个属性
-->
<xsd:extension base="beans:identifiedType">
<xsd:attribute name="lenient" type="xsd:boolean"/>
<xsd:attribute name="pattern" type="xsd:string" use="required"/>
</xsd:extension>
</xsd:complexContent>
</xsd:complexType>
</xsd:element>
</xsd:schema>
beans:identifiedType
是Spring自己所定义的一个复杂类型,该类型只有一个id属性。我们自己定义的复杂类型从该类型中继承,那么我们的类型也将拥有id属性,并且还自己多增加了两个额外属性。因此,我们所定义的dateformat
元素是一个复杂类型,它有三个元素,其中pattern是必填属性,而其余两个属性是可选属性。
我们在使用自定义的标签时,其内容是这样:
<!--直接自动继承了一个id属性,其余属性为自己定义-->
<myns:dateformat id="dateFormat" pattern="yyyy-MM-dd HH:mm" lenient="true"/>
如果对上面描述内容还不是特别明白的话,建议参考XML Schema extension 元素说明。
自定义NamespaceHandler
在完成Scheman文件编写之后,接下来我们就是去自己定义一个NamespaceHandler
去解析特定命名空间下的元素,下面是该接口的说明:
NamespaceHandler作为一个基础接口,它被DefaultBeanDefinitionDocumentReader
(可以简单认为它是一个XML文档的解析器)用于处理Spring XML配置文件中所定制化的命名空间。当在解析过程中,出现top-level
标签时,则需要返回一个BeanDefinitionParser
,当出现一个内嵌的定制标签时,则需要返回一个BeanDefinitionDecorator
。下面简单说明一下top-level tag
与nested tags
之间的区别:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:myns="http://www.panlingxiao.com/schema/myns"
xsi:schemaLocation="
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.panlingxiao.com/schema/myns http://www.panlingxiao.com/schema/myns/myns.xsd">
<!-- as a top-level bean -->
<myns:dateformat id="defaultDateFormat" pattern="yyyy-MM-dd HH:mm" lenient="true"/>
<bean class="com.panlingxiao.spring.ioc.tag.TagBean" id="tagBean">
<!--nested tag-->
<myns:hello/>
</bean>
</beans>
上面的文档最后还告诉我们,作为开发者,通常无需直接实现NamespaceHandler接口,而只需充分使用已经提供的NamespaceHandlerSupport即可。
package com.panlingxiao.spring.ioc.tag;
import org.springframework.beans.factory.xml.NamespaceHandlerSupport;
/**
* 自定义NamespaceHandler,直接继承NamespaceHandlerSupport即可
* NamespaceHandler只提供解析器的注册与管理,并不提供真正的解析逻辑
*/
public class MyNamespaceHandler extends NamespaceHandlerSupport {
/**
* init方法会被Spring自动调用
*/
@Override
public void init() {
/*
* 当解析到dateformat元素时,使用SimpleDateFormatBeanDefinitionParser解析器进行解析
*/
registerBeanDefinitionParser("dateformat", new SimpleDateFormatBeanDefinitionParser());
}
}
自定义BeanDefinitionParser
当NamespaceHandler遇到一个特定类型的XML标签被映射到对应的BeanDefinitionParser
时,就会通过它来对元素进行解析。换而言之,BeanDefinitionParser
是真正意义上完成元素解析逻辑功能的类,它会将一个元素转换成一个BeanDefinition
,然后注册到BeanDefinitionRegistry
中,下面是我们的实现,我们希望将我们自定义的元素转换成一个SimpleDateFormat对象。
package com.panlingxiao.spring.ioc.tag;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.xml.AbstractSingleBeanDefinitionParser;
import org.springframework.util.StringUtils;
import org.w3c.dom.Element;
import java.text.SimpleDateFormat;
/**
* Created by panlingxiao on 2017/6/14.
*
* 自定义BeanDefinitionParser,完成XML元素的解析处理
*/
public class SimpleDateFormatBeanDefinitionParser extends AbstractSingleBeanDefinitionParser { //①
/**
* 返回元素对应的Class,Class对象会被注入到BeanDefinition中
* @param element
* @return
*/
protected Class getBeanClass(Element element) {
return SimpleDateFormat.class; //②
}
/**
* 执行真正的解析逻辑
* @param element
* @param bean
*/
protected void doParse(Element element, BeanDefinitionBuilder bean) {
// this will never be null since the schema explicitly requires that a value be supplied
String pattern = element.getAttribute("pattern");
//因为SimpleDateFormat需要一个pattern字符串作为构造方法的参数,因此设置一个构造方法参数
bean.addConstructorArgValue(pattern);
// this however is an optional property
String lenient = element.getAttribute("lenient");
//判断是否设置了lenient属性,如果设置,则设置lenient属性为true
if (StringUtils.hasText(lenient)) {
bean.addPropertyValue("lenient", Boolean.valueOf(lenient));
}
}
}
① 我们直接使用了Spring为我们所提供的AbstractSingleBeanDefinitionParser
,它会帮我们去自动地去处理创建BeanDefinition
的任务,我们只需向其提供的参数中设置内容即可。
② 我们指定了当前被解析的元素所对应的对象类型,返回的Class信息会被自动地设置到BeanDefinition中,用于后续对象的创建。
注册Handler和Schema
上面我们已经完成所有的处理逻辑,下面我们该考虑如何让Spring的能够解析到我们自定义标签,使用我们给定的解析器去处理;我们只需将我们定制的nameSpaceHandler和XSD文件注册到两个特殊的Properites文件中即可。这两个文件都位于ClassPath路径下的META-INF目录下。
META-INF/spring.handlers
通过META-INF/spring.handlers
配置文件,我们可以将某个命名空间与对应的NamespaceHandler进行映射。
#指定该命名空间下的元素使自己给定的NameSpaceHandler进行处理
http\://www.panlingxiao.com/schema/myns=com.panlingxiao.spring.ioc.tag.MyNamespaceHandler
(由于:
在Java的Properties中是一个有效的分隔符,因为我们在这里需要进行一次转义的处理)
META-INF/spring.schemas
META-INF/spring.schemas
用于将在XML中声明的XSD的URL地址与本地的XSD进行关联,这样避免了从远程下面文件到本地的处理,并且即使在远程服务器上不存在这个文件,也不会有任何问题。
http\://www.panlingxiao.com/schema/myns/myns.xsd=META-INF/spring-custom-tag.xsd
文件最终结果.png
测试结果
在完成上面的配置操作后,我们现在可以验证我们最终的结果了~
package com.panlingxiao.spring.ioc.tag;
import java.text.SimpleDateFormat;
/**
* Created by panlingxiao on 2017/6/14.
*/
public class TagBean {
private SimpleDateFormat dateFormat;
public SimpleDateFormat getDateFormat() {
return dateFormat;
}
public void setDateFormat(SimpleDateFormat dateFormat) {
this.dateFormat = dateFormat;
}
}
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:myns="http://www.panlingxiao.com/schema/myns"
xsi:schemaLocation="
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.panlingxiao.com/schema/myns http://www.panlingxiao.com/schema/myns/myns.xsd">
<!-- as a top-level bean -->
<myns:dateformat id="defaultDateFormat" pattern="yyyy-MM-dd HH:mm" lenient="true"/>
<bean class="com.panlingxiao.spring.ioc.tag.TagBean" id="tagBean">
<property name="dateFormat">
<!--直接根据id引用,自定义的元素最终会被转换成SimpleDateFormat对象-->
<ref bean="defaultDateFormat"/>
</property>
</bean>
</beans>
package com.panlingxiao.spring.ioc.tag;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import java.io.IOException;
import java.util.Date;
/**
* Created by panlingxiao on 2017/6/4.
* Spring自定义标签
*/
public class SpringCustomTagExample {
public static void main(String[] args) throws IOException {
ApplicationContext ctx = new ClassPathXmlApplicationContext("tag/custom-tag.xml");
TagBean tagBean = ctx.getBean("tagBean", TagBean.class);
System.out.println(tagBean.getDateFormat().format(new Date()));
}
}
测试结果
至此,已经分析完了如何扩展Spring中的文档定义。通过自己定义的元素,我们很好地向使用方屏蔽了底层的具体实现,其他的好处可以参考Webx中对使用Schema来扩展Spring的优势。