美文网首页SpringFrameworkJava技术Spring技术
浅析Spring自定义标签的使用

浅析Spring自定义标签的使用

作者: 一字马胡 | 来源:发表于2017-11-17 23:51 被阅读352次

    作者: 一字马胡
    转载标志 【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自定义标签的“最佳实践”吧!

    相关文章

      网友评论

        本文标题:浅析Spring自定义标签的使用

        本文链接:https://www.haomeiwen.com/subject/wwbsvxtx.html