Spring mvc 提供了扩展 xml 的机制,用来编写自定义的 xml bean ,例如 dubbo 框架,就利用这个机制实现了好多的 dubbo bean,比如 <dubbo:application> 、<dubbo:registry> 等等,只要安装这个标准的扩展方式实现配置即可。
扩展自定义 bean 的意义何在
假设我们要使用一个开源框架或者一套 API,我们肯定希望以下两点:
-
易用性,即配置简单,要配置的地方越少越好
-
封装性,调用简单,也就是越高层封装越好,少暴露底层实现
基于以上两点,假设我们要实现一个自定义功能,用现有的 Spring 配置项也可以实现,但可能要配置的内容较多,而且还有可能要加入代码辅助。导致逻辑分散,不便于维护。
所以我们用扩展 Spring 配置的方式,将一些自定义的复杂功能封装,实现配置最小化。
实现自定义扩展的步骤
本例只做简单示范,功能简单,即实现一个可配置参数的 Hacker bean,然后提供一个toString() 方法,输入参数信息。
我们最终实现的 bean 配置如下:
1 |
<kite:hacker id= "hacker" name= "moon" language= "english" age= "18" isHide= "true" /> |
用 Spring 自带的配置做个比较,例如
1 |
<context:component-scan base- package = "com.ebanghu" ></context:component-scan> |
1、实现自定义 bean 类,命名为 Hacker ,并在方法中重载toString()方法,输入属性名称,代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 |
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( "======================\n" ); builder.append(String.format( "hacker's name is :%s \n" , this .getName())); builder.append(String.format( "hacker's age is :%s \n" , this .getAge())); builder.append(String.format( "hacker's language is :%s \n" , this .getLanguage())); builder.append(String.format( "hacker's status is :%s \n" , this .isHide())); builder.append( "======================\n" ); return builder.toString(); } } |
2、编写 xsd schema 属性描述文件,命名为 hacker.xsd ,这里把它放到项目 resources 目录下的 META-INF 目录中(位置可以自己决定),可以理解为:这个文件就是对应刚刚创建的实体类作一个 xml 结构描述,内容如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 |
<? 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 > |
注意上面的
1 2 3 |
xmlns="http://code.fengzheng.com/schema/kite" 和 targetNamespace="http://code.fengzheng.com/schema/kite" |
一会儿有地方要用到
3、实现 NamespaceHandler 类,代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
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 做关联,实现属性的注入,来看代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 |
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 类
内容如下:
1 |
http\://code.fengzheng.com/schema/kite=kite.lab.spring.config.HackNamespaceHandler |
这是一个键值对形式,等号前面为命名空间,第一步已经提到,这里就用到了,等号后面是 Handler 类的完全类名称。注意冒号前面加转义符
spring.schemas 用于注册第二步中的 xsd 文件
内容如下:
1 |
http\://code.fengzheng.com/schema/kite/kite.xsd=META-INF/hacker.xsd |
等号前面是声明的 xsd 路径,后面是实际的 xsd 路径。
6、 在 Spring 配置文件中使用
1 2 3 4 5 6 7 8 9 10 11 12 |
<? 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、测试
直接获取配置文件的方式测试
1 2 3 4 5 |
public static void main(String[] args){ ApplicationContext ac = new ClassPathXmlApplicationContext( "application.xml" ); Hacker hacker = (Hacker) ac.getBean( "hacker" ); System.out.println(hacker.toString()); } |
使用 SpringJUnit4ClassRunner 测试
1 2 3 4 5 6 7 8 9 10 11 |
@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-版本号 模块中。