Osheep

时光不回头,当下最重要。

浅析Spring自定义标签的使用

作者: 一字马胡
转载标志 【2017-11-17】

更新日志

日期 更新内容 备注
2017-11-17 新建文章 初版
2017-11-18 修改几个错误 xx

导入

Spring框架的一大强大之处就是框架的设计具有很好的可扩展性,所以只要有想象力,就可以在Spring框架上作出扩展,比如,在学会了熟练使用Spring的内置标签之后,如果我们想要设计自己的标签,Spring是支持这种创新的,本文将结合实际的例子来说明如何使用Spring提供的扩展接口来设计自己的自定义标签,并且实现一些动作。在阅读本文之前,你可以首先阅读下面的两篇链接文章,以更快的属性Spring的生命周期等内容,可以更流畅的阅读和理解本文的内容:

Spring的BeanFactory和FactoryBean
Spring Bean 的生命周期

下面再次放上Spring Bean的生命周期图,因为本文的内容涉及到Bean的生命周期,自定义标签需要在Bean的生命周期内做一些事情来操作bean,所以属性Spring Bean的生命周期在阅读本文之前是必须的:

上面的流程图已经展示了Spring bean生命周期的详细细节,我们知道了这些加载、初始化、设置等一系列流程之后,就可以在合适的环节加上我们想要的动作,比如,我们可以使用BeanFactoryPostProcessor的postProcessBeanFactory方法来修改bean的属性,例如,我们有一个bean的一个属性A在spring配置文件中找不到,但是我们可以在BeanFactoryPostProcessor的postProcessBeanFactory方法里面使用方法的参数beanFactory来注册一个A。我们还可以使用BeanPostProcessor来修改我们的bean的属性值,比如一个bean的一个属性A,我们可以在BeanPostProcessor的postProcessBeforeInitialization方法和postProcessAfterInitialization方法来修改其值,这些方法需要配合其他的与Spring bean生命周期相关的类来做。

可以将Spring bean的生命周期根据不同特点划分为下面的几类:

Bean自身的方法

包括我们在配置bean时候设置的init-method方法和destroy-method方法。

Spring Bean级别的生命周期方法

包括BeanNameAware、BeanFactoryAware、InitializingBean和DiposableBean这些接口的方法。

Spring容器级别生命周期方法

包括InstantiationAwareBeanPostProcessor、BeanPostProcessor、BeanFactoryPostProcessor的实现类的方法。

特别说明,本文仅结合实际的例子来说明Spring 自定义标签的使用方法,而在此过程中涉及到的额外的技术点(比如xsd文档的编写规则)将不再本文的描述范围之内,需要自行查找资料来学习,本文的定位是学会使用Spring自定义标签做一些事情,所以需要自行去查阅相关技术资料来学习一些内容来理解Spring自定义标签。

自定义标签以实现bean注册

首先,如何自定义一个Spring标签来实现bean的注册呢?我想要实现的功能是类似于<bean …/>这样的,下面将一步一步来说明如何进行操作,达到最后的效果。

编写xsd文件

第一步是需要编写xsd文件,下面是一个例子:


<?xml version="1.0" encoding="UTF-8"?>
<xsd:schema xmlns="http://code.hujian.com/schema/ok"
            xmlns:xsd="http://www.w3.org/2001/XMLSchema"
            xmlns:beans="http://www.springframework.org/schema/beans"
            targetNamespace="http://code.hujian.com/schema/ok"
            elementFormDefault="qualified" attributeFormDefault="unqualified">

    <xsd:complexType name="server">
        <xsd:attribute name="id" type="xsd:string">
            <xsd:annotation>
                <xsd:documentation><![CDATA[ The unique identifier for a bean. ]]></xsd:documentation>
            </xsd:annotation>
        </xsd:attribute>
        <xsd:attribute name="serverName" type="xsd:string">
            <xsd:annotation>
                <xsd:documentation><![CDATA[ The name of the bean. ]]></xsd:documentation>
            </xsd:annotation>
        </xsd:attribute>
    </xsd:complexType>
    
        <xsd:element name="service" type="server">
        <xsd:annotation>
            <xsd:documentation><![CDATA[ The service config ]]></xsd:documentation>
        </xsd:annotation>
    </xsd:element>

</xsd:schema>

将这个文件命名为任意你喜欢的名字,后缀为.xsd,比如例子中的该文件被命名为ok-1.0.xsd,这个名字将在后文中用到。上面定义的xsd文件中,我想要实现类似于:


<service id = "" serverName= "" />

看起来很简单,并且我希望可以通过加载配置文件后可以获取到这个bean(根据id来获取)。但是看起来很奇怪的是这个bean的类似是什么呢?你当然可以在xsd文件中增加一个attr叫做“class”来控制生成的bean的类型,但是本文中的例子为了简单,只可以配置一个属性,具体返回的类似后面会说到。

编写Schema文件和handler文件

这一步是比较关键的一步,你需要编写两个文件,分别为spring.schemas和spring.handlers,然后将这两个文件放在resource文件夹下的META-INF文件夹下,在spring.schemas文件里面,你需要写上;类似下面的内容:


http\://code.hujian.com/schema/ok/ok-1.0.xsd=./ok-1.0.xsd

前面的http://code.hujian.com/schema/ok/ok-1.0.xsd是我们的命名空间,后面是我们上面编写的xsd文件,这里需要注意文件名。写好spring.schemas文件后,需要写spring.handlers文件,在这个文件里面你需要定义一个处理器来处理你自定义的哪些标签,我们可以在里面做很丰富的事情,下面是为本文例子编写的spring.handlers文件的内容:


http\://code.hujian.com/schema/ok=com.hujian.spring.handler.CommonNamespaceHandler


编写Handler

经过上面两步之后,现在我们可以开始写处理我们的自定义标签的Handler了,下面首先展示了代码:


class CommonNamespaceHandler extends NamespaceHandlerSupport{
    @Override
    public void init() {
        this.registerBeanDefinitionParser("service",
                new OkServerDefinitionParser(ServerBean.class));
    }
}

class OkServerDefinitionParser implements BeanDefinitionParser {

    private final Class<?> clazz;
    private static final String default_prefix = "ok-";
    private static final AtomicLong COUNT = new AtomicLong(0);

    public OkServerDefinitionParser(Class<?> clazz) {
        this.clazz = clazz;
    }

    @Override
    public BeanDefinition parse(Element element, ParserContext parserContext) {
        return parseHelper(element, parserContext, this.clazz);
    }

    private BeanDefinition parseHelper(Element element, ParserContext parserContext, Class<?> clazz) {
        RootBeanDefinition bd = new RootBeanDefinition();

        bd.setLazyInit(false);
        String id = element.getAttribute("id");
        if (id == null || id.isEmpty()) {
            id = default_prefix + COUNT.getAndDecrement();
        }

        String serverName = element.getAttribute("serverName");

        bd.setBeanClass(clazz);
        bd.setInitMethodName("init");

        MutablePropertyValues propertyValues = bd.getPropertyValues();
        propertyValues.addPropertyValue("serverName", serverName);

        parserContext.getRegistry().registerBeanDefinition(id, bd);

        return bd;
    }
}

上面说到我们自定义的标签还不知道返回的bean是什么类型的,为了简单,上面的代码中将返回的类型定义为了ServerBean这个类型,下面是这个类的信息:


class ServerBean {
    private String serverName;

    //init method
    public void init() {
        System.out.println("bean ServerBean init.");
    }

    @Override
    public String toString() {
        return "[Service]=>" + serverName;
    }

    public String getServerName() {
        return serverName;
    }

    public void setServerName(String serverName) {
        this.serverName = serverName;
    }
}


其实流程还是比较容易看懂的,首先我们需要注册一个bean,而Spring中注册的bean是AbstractBeanDefinition的子类,所以你可以使用任意AbstractBeanDefinition的子类来注册你的bean,上面的例子中使用了RootBeanDefinition这个AbstractBeanDefinition的子类来注册一个bean,设置一些配置信息之后就使用ParserContext的注册器来将我们自定义的bean注册到Spring中去了,需要注意的是,我们在<service id = “” …/>中配置的id就是我们往Spring容器中注册的bean的id,所以在我们想要使用该bean的时候就可以使用这个id来获取这个bean了。

测试

经过上面的步骤之后,下面来测试一下我自定义的标签是否可以正常工作,首先需要编写Spring xml配置文件:


<?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:ok="http://code.hujian.com/schema/ok"

       xsi:schemaLocation="http://www.springframework.org/schema/beans
           http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
           http://code.hujian.com/schema/ok
           http://code.hujian.com/schema/ok/ok-1.0.xsd">


    <ok:service id="testServer" serverName="HelloWorldService"/>

</beans>

需要注意的是需要引入我们自定义的命名空间: xmlns:ok=”http://code.hujian.com/schema/ok“,并且需要将我们的Schema位置也告诉Spring,也就是需要在xsi:schemaLocation中设置我们的Schema路径。然后就可以使用我们的自定义标签<ok:service …/>了,可以看出上面我们配置了一个自定义bean,id为testServer,serverName属性为HelloWorldService,下面是测试代码:


    public static void main(String ... args) {

        String xmlFile = "tagTest.xml";
        String beanId = "testServer";

        ApplicationContext context = new ClassPathXmlApplicationContext(xmlFile);

        ServerBean bean = (ServerBean) context.getBean(beanId);

        System.out.println(bean);

    }

下面是输出的结果:


[Service]=>HelloWorldService

可以看到,我们自定义的标签可以正常工作了,更为复杂的Spring自定义标签可以借助这个例子来扩展。

自定义标签以实现bean扫描

上面展示了一个简单的Spring自定义标签的用法,当然任意复杂的自定义标签都可以基于这个简单的标签来模仿出来,下面一个例子和注解有关,有时候我们希望借助Spring来帮我们解析代码中的注解,下面的例子可以在xml中使用自定义的标签设定需要扫描的package,Spring会扫描我们配置的这个package,然后我希望可以找到这个package下所有注解了OkService的类,并且基于该注解做一些统计,比如将这些注解的信息收集起来,然后最后展示出这些收集到的注解信息,因为步骤和上面的例子一样,所以不再赘述:

首先是xsd文件:

<?xml version="1.0" encoding="UTF-8"?>
<xsd:schema xmlns="http://code.hujian.com/schema/ok"
            xmlns:xsd="http://www.w3.org/2001/XMLSchema"
            xmlns:beans="http://www.springframework.org/schema/beans"
            targetNamespace="http://code.hujian.com/schema/ok"
            elementFormDefault="qualified" attributeFormDefault="unqualified">

    <xsd:complexType name="annotationType">
        <xsd:attribute name="id" type="xsd:ID">
            <xsd:annotation>
                <xsd:documentation><![CDATA[ The unique identifier for a bean. ]]></xsd:documentation>
            </xsd:annotation>
        </xsd:attribute>
        <xsd:attribute name="scan" type="xsd:string" use="optional">
            <xsd:annotation>
                <xsd:documentation><![CDATA[ The scan package. ]]></xsd:documentation>
            </xsd:annotation>
        </xsd:attribute>
        <xsd:attribute name="url" type="xsd:string" use="optional">
            <xsd:annotation>
                <xsd:documentation><![CDATA[ The url string ]]></xsd:documentation>
            </xsd:annotation>
        </xsd:attribute>
    </xsd:complexType>

    <xsd:element name="annotation" type="annotationType">
        <xsd:annotation>
            <xsd:documentation><![CDATA[ The annotation config ]]></xsd:documentation>
        </xsd:annotation>
    </xsd:element>
</xsd:schema>

接下来编写handler:


class CommonNamespaceHandler extends NamespaceHandlerSupport{
    @Override
    public void init() {
        this.registerBeanDefinitionParser("annotation",
                new OkAnnotationDefinitionParser(ScanBeanReference.class));
    }
}


class OkAnnotationDefinitionParser implements BeanDefinitionParser {
    private final Class<?> clazz;
    private static final String default_prefix = "scan-";
    private static final AtomicLong COUNT = new AtomicLong(0);

    public OkAnnotationDefinitionParser(Class<?> clazz) {
        this.clazz = clazz;
    }

    @Override
    public BeanDefinition parse(Element element, ParserContext parserContext) {
        return parseHelper(element, parserContext, clazz);
    }

    private BeanDefinition parseHelper(Element element, ParserContext parserContext, Class<?> clazz) {
        RootBeanDefinition bd = new RootBeanDefinition();

        bd.setLazyInit(false);
        String id = element.getAttribute("id");
        if (id == null || id.isEmpty()) {
            id = default_prefix + COUNT.getAndDecrement();
        }

        String scanPackage = element.getAttribute("scan");
        String url = element.getAttribute("url");

        bd.setBeanClass(ScanBeanParser.class);
        bd.setInitMethodName("init");

        MutablePropertyValues propertyValues = bd.getPropertyValues();
        propertyValues.addPropertyValue("scan", scanPackage);
        propertyValues.addPropertyValue("url", url);

        parserContext.getRegistry().registerBeanDefinition(id, bd);

        return bd;
    }
}


上面的代码和上面的例子中的代码没有什么区别,但是有一个地方需要特别注意:


bd.setBeanClass(ScanBeanParser.class);

而这个ScanBeanParser类的信息如下:


class ScanBeanParser implements BeanPostProcessor,
        BeanFactoryPostProcessor, ApplicationContextAware, PriorityOrdered {

    private static final Pattern COMMA_SPLIT_PATTERN = Pattern.compile("\\s*[,]+\\s*");
    private String scan; // the scan package
    private String url; // the url

    public void setScan(String scan) {
        this.scan = scan;
    }

    public void setUrl(String url) {
        this.url = url;
    }

    public void init() {
        System.out.println("ScanBeanParser start to run...");
    }

    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory)
            throws BeansException {
        String annotationPackage = scan == null || scan.isEmpty() ? "com.hujian" : scan;

        System.out.println("get the scan package:" + annotationPackage);

        if (beanFactory instanceof BeanDefinitionRegistry) {
            try {
                // init scanner
                Class<?> scannerClass = ClassUtils
                        .loadClass("org.springframework.context.annotation.ClassPathBeanDefinitionScanner");
                Object scanner = scannerClass.getConstructor(
                        new Class<?>[] { BeanDefinitionRegistry.class, boolean.class }).newInstance(
                                beanFactory, true);
                // add filter
                Class<?> filterClass = ClassUtils
                        .loadClass("org.springframework.core.type.filter.AnnotationTypeFilter");
                Object filter = filterClass.getConstructor(Class.class).newInstance(OkService.class);
                Method addIncludeFilter = scannerClass.getMethod("addIncludeFilter",
                        ClassUtils.loadClass("org.springframework.core.type.filter.TypeFilter"));
                addIncludeFilter.invoke(scanner, filter);
                // scan packages
                String[] packages = COMMA_SPLIT_PATTERN.split(annotationPackage);
                Method scan = scannerClass.getMethod("scan", String[].class);
                scan.invoke(scanner, new Object[] { packages });
            } catch (Throwable e) {
                e.printStackTrace();
            }
        }
    }

    @Override
    public Object postProcessBeforeInitialization(Object o, String s) throws BeansException {
        return o;
    }

    @Override
    public Object postProcessAfterInitialization(Object o, String s) throws BeansException {
        Class<?> beanClass = AopUtils.getTargetClass(o);
        if (beanClass == null) {
            return o;
        }

        OkService service = beanClass.getAnnotation(OkService.class);
        if (service != null) {
            ScanBeanReference scanBeanReference = new ScanBeanReference();
            scanBeanReference.setScan(service.scan());
            scanBeanReference.setUrl(service.url());
            scanBeanReference.setMsg(service.msg());

            System.out.println("get a scan bean:" + scanBeanReference);

            ScanStorageFactory.addScanBean(scanBeanReference);
        }

        return o;
    }

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        System.out.println("get the ApplicationContext:" + applicationContext);
    }

    @Override
    public int getOrder() {
        return 0;
    }
}

在OkAnnotationDefinitionParser中获取了xml中的配置(比如scan属性),然后将获取到的属性值传递给ScanBeanParser这个类,这个类里面做了我们想要做的事情,就是收集所有注解了OKService的类的信息,并存储起来。读到这里就需要回头看一下文章开头的那张Spring Bean的生命周期图,ScanBeanParser实现了很多涉及Spring Bean生命周期的类。下面是测试代码:


    public static void main(String ... args) {

        String xmlFile = "tagTest.xml";
        String beanId = "testServer";

        ApplicationContext context = new ClassPathXmlApplicationContext(xmlFile);

        ScanStorageFactory.getScanBeanReferenceList()
                .forEach(System.out::println);
    }

@OkService(scan = "com.hujian.io", url = "http://www.meituan.com", msg = "ScanTestClass1")
class ScanTestClass1 {

}

@OkService(scan = "com.hujian.rpc", url = "http://www.dianping.com", msg = "ScanTestClass2")
class ScanTestClass2 {

}

@OkService(scan = "io.hujian.com", url = "http://www.ok.com", msg = "ScanTestClass3")
class ScanTestClass3 {

}

测试的结果如下:


scanPackage:com.hujian.io, url:http://www.meituan.com, msg:ScanTestClass1
scanPackage:com.hujian.rpc, url:http://www.dianping.com, msg:ScanTestClass2
scanPackage:io.hujian.com, url:http://www.ok.com, msg:ScanTestClass3

结语

本文较为粗浅的解析了Spring中自定义标签的使用方法,可以将本文中的代码作为模板来进行Spring自定义标签的设计和处理,更为深入的分析与总结将在未来进行,关于Spring的相关分析总结会持续更新,本文相当于一个Spring自定义标签的“最佳实践”吧!

点赞

发表评论

电子邮件地址不会被公开。