浅析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自定义标签的“最佳实践”吧!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,445评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,889评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,047评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,760评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,745评论 5 367
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,638评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,011评论 3 398
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,669评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,923评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,655评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,740评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,406评论 4 320
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,995评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,961评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,197评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,023评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,483评论 2 342

推荐阅读更多精彩内容