Spring mvc 提供了扩展 xml 的机制,用来编写自定义的 xml bean ,例如 dubbo 框架,就利用这个机制实现了好多的 dubbo bean,比如 <dubbo:application> 、<dubbo:registry> 等等,只要安装这个标准的扩展方式实现配置即可。
扩展自定义 bean 的意义何在
假设我们要使用一个开源框架或者一套 API,我们肯定希望以下两点:
-
易用性,即配置简单,要配置的地方越少越好
-
封装性,调用简单,也就是越高层封装越好,少暴露底层实现
基于以上两点,假设我们要实现一个自定义功能,用现有的 Spring 配置项也可以实现,但可能要配置的内容较多,而且还有可能要加入代码辅助。导致逻辑分散,不便于维护。
所以我们用扩展 Spring 配置的方式,将一些自定义的复杂功能封装,实现配置最小化。
实现自定义扩展的步骤
本例只做简单示范,功能简单,即实现一个可配置参数的 Hacker bean,然后提供一个toString() 方法,输入参数信息。
我们最终实现的 bean 配置如下:
<kite:hacker id="hacker" name="moon" language="english" age="18" isHide="true"/>
用 Spring 自带的配置做个比较,例如
<context:component-scan base-package="com.ebanghu"></context:component-scan>
1、实现自定义 bean 类,命名为 Hacker ,并在方法中重载toString()方法,输入属性名称,代码如下:
package kite.lab.spring.config; /** * Hacker * @author fengzheng */ public class Hacker { private String name; private String age; private String language; private boolean isHide; public String getName() { return name; } public void setName(String name) { this.name = name; } public String getAge() { return age; } public void setAge(String age) { this.age = age; } public String getLanguage() { return language; } public void setLanguage(String language) { this.language = language; } public boolean isHide() { return isHide; } public void setHide(boolean hide) { isHide = hide; } @Override public String toString() { StringBuilder builder = new StringBuilder(); builder.append("====================== "); builder.append(String.format("hacker's name is :%s ", this.getName())); builder.append(String.format("hacker's age is :%s ", this.getAge())); builder.append(String.format("hacker's language is :%s ", this.getLanguage())); builder.append(String.format("hacker's status is :%s ", this.isHide())); builder.append("====================== "); return builder.toString(); } }
2、编写 xsd schema 属性描述文件,命名为 hacker.xsd ,这里把它放到项目 resources 目录下的 META-INF 目录中(位置可以自己决定),可以理解为:这个文件就是对应刚刚创建的实体类作一个 xml 结构描述,内容如下:
<?xml version="1.0" encoding="UTF-8"?> <xsd:schema xmlns="http://code.fengzheng.com/schema/kite" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:beans="http://www.springframework.org/schema/beans" targetNamespace="http://code.fengzheng.com/schema/kite" elementFormDefault="qualified" attributeFormDefault="unqualified"> <xsd:import namespace="http://www.springframework.org/schema/beans"/> <xsd:complexType name="hackType"> <xsd:complexContent> <xsd:extension base="beans:identifiedType"> <xsd:attribute name="name" type="xsd:string" use="required"> <xsd:annotation> <xsd:documentation> <![CDATA[ The name of hacker ]]> </xsd:documentation> </xsd:annotation> </xsd:attribute> <xsd:attribute name="age" type="xsd:int" use="optional" default="0"> <xsd:annotation> <xsd:documentation><![CDATA[ The age of hacker. ]]></xsd:documentation> </xsd:annotation> </xsd:attribute> <xsd:attribute name="language" type="xsd:string" use="optional"> <xsd:annotation> <xsd:documentation><![CDATA[ The language of hacker. ]]></xsd:documentation> </xsd:annotation> </xsd:attribute> <xsd:attribute name="isHide" type="xsd:boolean" use="optional"> <xsd:annotation> <xsd:documentation><![CDATA[ The status of hacker. ]]></xsd:documentation> </xsd:annotation> </xsd:attribute> </xsd:extension> </xsd:complexContent> </xsd:complexType> <xsd:element name="hacker" type="hackType"> <xsd:annotation> <xsd:documentation><![CDATA[ The hacker config ]]></xsd:documentation> </xsd:annotation> </xsd:element> </xsd:schema>
注意上面的
xmlns="http://code.fengzheng.com/schema/kite" 和 targetNamespace="http://code.fengzheng.com/schema/kite"
一会儿有地方要用到
3、实现 NamespaceHandler 类,代码如下:
package kite.lab.spring.config; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.xml.NamespaceHandlerSupport; /** * HackNamespaceHandler * @author fengzheng */ public class HackNamespaceHandler extends NamespaceHandlerSupport { private static final Logger logger = LoggerFactory.getLogger(HackNamespaceHandler.class); @Override public void init() { logger.info("执行 HackNamespaceHandler 的 init 方法"); registerBeanDefinitionParser("hacker",new HackBeanDefinitionParser(Hacker.class)); logger.info("注册 「hacker」 定义转换器成功"); } }
此类功能非常简单,就是继承 NamespaceHandlerSupport 类,并重载 init 方法,调用 registerBeanDefinitionParser 方法,其中第一个参数 hacker 即是我们之后在 spring 配置文件中要使用的名称,即<kite:hacker> 这里的hacker;
第二个参数是下一步要说的。
4、实现 BeanDefinitionParser 类,这个类的作用简单来说就是将第一步实现的类和 Spring xml中生命的 bean 做关联,实现属性的注入,来看代码:
package kite.lab.spring.config; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.MutablePropertyValues; import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.support.BeanDefinitionRegistry; import org.springframework.beans.factory.support.RootBeanDefinition; import org.springframework.beans.factory.xml.BeanDefinitionParser; import org.springframework.beans.factory.xml.ParserContext; import org.w3c.dom.Element; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.lang.reflect.Parameter; /** * HackBeanDefinitionParser * * @author fengzheng */ public class HackBeanDefinitionParser implements BeanDefinitionParser { private static final Logger logger = LoggerFactory.getLogger(HackBeanDefinitionParser.class); private final Class<?> beanClass; public HackBeanDefinitionParser(Class<?> beanClass) { this.beanClass = beanClass; } @Override public BeanDefinition parse(Element element, ParserContext parserContext) { logger.info("进入 HckBeanDefinitionParser 的 parse 方法"); try { String id = element.getAttribute("id"); RootBeanDefinition rootBeanDefinition = new RootBeanDefinition(); rootBeanDefinition.setBeanClass(beanClass); rootBeanDefinition.setLazyInit(false); //必须注册才可以实现注入 parserContext.getRegistry().registerBeanDefinition(id, rootBeanDefinition); String name = element.getAttribute("name"); String age = element.getAttribute("age"); String language = element.getAttribute("language"); String isHide = element.getAttribute("isHide"); MutablePropertyValues pvs = rootBeanDefinition.getPropertyValues(); pvs.add("name", name); pvs.add("age", Integer.valueOf(age)); pvs.add("language", language); pvs.add("hide", isHide.equals(null) ? false : Boolean.valueOf(isHide)); return rootBeanDefinition; } catch (Exception e) { e.printStackTrace(); } return null; } }
此类实现自 BeanDefinitionParser,并且重载 parse 方法,parse 方法有两个参数,第一个Element可以理解为 Spring xml 配置的 bean 的实体对应,通过 element.getAttribute 方法可以获取 配置的参数值,第二个参数 ParserContext ,可以理解为 Spring 提供的接口对象,通过它实现注册 bean 的注入。
通过 RootBeanDefinition 实体对象的 getPropertyValues 方法可获取自定义bean的属性 kv 集合,然后像其中添加属性值。
注意:kv 集合中的 key 并不是实体类中的属性名称,而是属性对应的 setter 方法的参数名称,例如布尔型参数如果命名为 is 开头的,使用编辑器自动生成 setter 方法时,对应的 setter 方法的参数就会去掉 is ,并把后面的字符串做驼峰命名规则处理。当然了如果要规避的话,可以自己写 setter 方法。
5、注册 handler 和 xsd schema
Spring 规定了两个 xml 注册文件,并且规定这两个文件必须项目资源目录下的 META-INF 目录中,并且文件名称和格式要固定。
spring.handlers 用于注册第三步实现的 Handler 类
内容如下:
http://code.fengzheng.com/schema/kite=kite.lab.spring.config.HackNamespaceHandler
这是一个键值对形式,等号前面为命名空间,第一步已经提到,这里就用到了,等号后面是 Handler 类的完全类名称。注意冒号前面加转义符
spring.schemas 用于注册第二步中的 xsd 文件
内容如下:
http://code.fengzheng.com/schema/kite/kite.xsd=META-INF/hacker.xsd
等号前面是声明的 xsd 路径,后面是实际的 xsd 路径。
6、 在 Spring 配置文件中使用
<?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:kite="http://code.fengzheng.com/schema/kite" xsi:schemaLocation=" http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://code.fengzheng.com/schema/kite http://code.fengzheng.com/schema/kite/kite.xsd"> <kite:hacker id="hacker" name="moon" language="english" age="18" isHide="true"/> </beans>
注意前面引入了命名空间 xmlns:kite="http://code.fengzheng.com/schema/kite”,后面指定了 xsd 文件位置
http://code.fengzheng.com/schema/kite http://code.fengzheng.com/schema/kite/kite.xsd
7、测试
直接获取配置文件的方式测试
public static void main(String[] args){ ApplicationContext ac = new ClassPathXmlApplicationContext("application.xml"); Hacker hacker = (Hacker) ac.getBean("hacker"); System.out.println(hacker.toString()); }
使用 SpringJUnit4ClassRunner 测试
@RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(locations = { "classpath:application.xml" }) public class HackTest { @Resource(name = "hacker") private Hacker hacker; @Test public void propertyTest() { System.out.println(hacker.toString()); } }
测试结果如图:
本文只是简要说明实现步骤,具体负责操作可参考 dubbo ,代码在 dubbo-config-spring 模块中,当然也可以阅读 Spring 源码,例如 查看 <context:component-scan> 的实现,在 spring-context-版本号 模块中。
古时的风筝 【微信公众号】gushidefengzheng