Spring 实现自定义 bean 的扩展

Spring mvc 提供了扩展 xml 的机制,用来编写自定义的 xml bean ,例如 dubbo 框架,就利用这个机制实现了好多的 dubbo bean,比如 <dubbo:application> 、<dubbo:registry> 等等,只要安装这个标准的扩展方式实现配置即可。

扩展自定义 bean 的意义何在

假设我们要使用一个开源框架或者一套 API,我们肯定希望以下两点:

  1. 易用性,即配置简单,要配置的地方越少越好
  2. 封装性,调用简单,也就是越高层封装越好,少暴露底层实现

基于以上两点,假设我们要实现一个自定义功能,用现有的 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("======================\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 结构描述,内容如下:

<?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-版本号 模块中。

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏Java Web

Java 面试知识点解析(四)——版本特性篇(1)

在遨游了一番 Java Web 的世界之后,发现了自己的一些缺失,所以就着一篇深度好文:知名互联网公司校招 Java 开发岗面试知识点解析 ,来好好的对 Jav...

52760
来自专栏nnngu

记录某公司(简称SMKJ) 的一次面试

昨天去了一家公司面试 Java 开发岗位,这篇文章主要是做一个面试的记录以及总结。 这家公司的规模大概100-200人,环境还可以,在一栋大厦租了两层办公室(3...

33160
来自专栏Java架构沉思录

如何优雅实现优雅停机?

其实优雅停机,就是在要关闭服务之前,不是立马全部关停,而是做好一些善后操作,比如:关闭线程、释放连接资源等。

24210
来自专栏屈定‘s Blog

设计模式--Builder模式的思考

在日常开发中总是会遇到多参数的情况,那么对于多参数,尤其是可选参数众多的情况,可能有如下的一些解决方案.

29690
来自专栏LanceToBigData

OOAD-设计模式(四)结构型模式之适配器、装饰器、代理模式

前言   前面我们学习了创建型设计模式,其中有5中,个人感觉比较重要的是工厂方法模式、单例模式、原型模式。接下来我将分享的是结构型模式! 一、适配器模式 1.1...

21190
来自专栏Java学习网

高性能Java解析器实现过程详解

高性能Java解析器实现过程详解 如果你没有指定数据或语言标准的或开源的Java解析器, 可能经常要用Java实现你自己的数据或语言解析器。或者,可能有很多解析...

48560
来自专栏腾讯Bugly的专栏

iOS: ARM64不定函数传参问题调试剖析

| 导语  ABI(Application Binary Interface)描述了应用程序和OS之间的底层接口。其中,通过查阅调用约定(Calling Co...

47120
来自专栏不会写文章的程序员不是好厨师

日志那些事儿——Logback源码解析

在上篇文章日志漫谈中谈到,日志在监控报警、查错分析等方面有着非常重要的应用。Logback作为目前最火的日志系统,本文就简单分析一下logback日志打印的过程...

57820
来自专栏IT米粉

你必须了解的反射——反射来实现实体验证

开发工作中,都会需要针对传入的参数进行验证,特别是针对实体进行验证,确保传入的参数格式正确。这里做了一个非常简单的组件进行验证。抛砖引玉,让大家深入思考下反射的...

34370
来自专栏大内老A

ASP.NET MVC Controller激活系统详解:默认实现

Controller激活系统最终通过注册的ControllerFactory创建相应的Conroller对象,如果没有对ControllerFactory类型或...

23990

扫码关注云+社区

领取腾讯云代金券