spring可以说是目前全球使用最为广泛的java框架之一,spring提供的ioc,aop等功能大大的提高了开发的效率。spring在提高效率的同时,提供很多了扩展槽。自定义标签就是扩展槽之一,其他框架可以无缝的嵌入到spring中,方便开发者使用,比如阿里巴巴之前开源的rpc框架dubbo就实现了自己的标签<dubbo:application>、<dubbo:registry>、 <dubbo:service>等。下面我们会介绍如何在spring中使用自定义标签。首先我们先介绍下我们要实现的标签的功能:

用户使用标签,在spring容器启动结束后,打印出jvm和系统的一些参数。

定义标签名称

完事开头难,起名字尤其难,为了方便开发者使用,一个好的名字既能明确表达框架作者的意图又能方便记忆。既然是打印虚拟机相关的参数,所以我们将标签定义为<vm>,子标签定义为info,这样完整的标签就是<vm:info>。之所以我们需要定义子标签,是为了后面扩容其他功能。

定义Namespace

如果你之前阅读过一些spring的源码,应该知道每一个标签对应着一个Namespace,spring会根据Namespace查找Handler进行处理。所以接下来我们需要定一个Namespace,根据spring的命名规则,我们将其定义为http://www.springframework.org/schema/vm

定义Handler

有了Namespace后,就需要定义Handler了。

public class VMHandler extends NamespaceHandlerSupport {

    @Override
    public void init() {
        registerBeanDefinitionParser("info", new JavaInfoBeanDefinitionParser());
    }
}

参照<context><vm>标签对应的Handler也继承自NamespaceHandlerSupportNamespaceHandlerSupport的功能不做过多的介绍了。VMHandler的初始化函数主要负责注册子标签的Parser,在这里就是info

定义Parser

Spring调用Handler的时候,会根据子标签的名字调用到具体的Parser,上面我们已经定义了info对应的Parser,下面看一下其具体实现。

public class JavaInfoBeanDefinitionParser implements BeanDefinitionParser {

    @Override
    public BeanDefinition parse(Element element, ParserContext parserContext) {
        BeanDefinitionBuilder bdb = BeanDefinitionBuilder.genericBeanDefinition(JavaInfoTool.class);
        bdb.setDependencyCheck(DEPENDENCY_CHECK_SIMPLE);
        bdb.setScope(SCOPE_SINGLETON);

        BeanDefinition definition = bdb.getRawBeanDefinition();
        BeanDefinitionHolder holder = new BeanDefinitionHolder(definition, "vm-java-info", null);
        registerBeanDefinition(holder, parserContext.getRegistry());

        return definition;
    }
}

Parser对应的功能比较简单,注册BeanDefinition,这个BeanDefinition对应的类是JavaInfoTool,也就是我们实现打印vm信息的类,同时给这个BeanDefinition定义一个id,id是vm-java-info

定义JavaInfoTool

为了完成在Spring容器启动后打印vm信息的功能,JavaInfoTool需要保证在最后执行,最直接的方法是继承ApplicationListener,在Spring容器状态发生变化的时候调用。

public class JavaInfoTool implements ApplicationListener<ContextRefreshedEvent> {

    public void printJavaInfo() {
        Properties properties = System.getProperties();
        for (Map.Entry entry : properties.entrySet()) {
            System.out.printf("%20s -> %s\r\n", entry.getKey(), entry.getValue());
        }
    }

    @Override
    public void onApplicationEvent(ContextRefreshedEvent event) {
        if(event.getApplicationContext().getParent() == null) {
            printJavaInfo();
        }
    }
}

实现打印vm信息的代码逻辑比较简单,这里就不做过多介绍了。

添加配置文件

到这里代码层面的工作都差不多完成了,但是还是不能直接使用。我们需要定义三个文件,

  • spring.handlers
  • spring.schemas
  • spring-vm-xx.xsd

spring.handlers

这个文件用于指定Namespace对应的Handler,Spring在启动过程中会扫描这个文件,加载对应关系。

http\://www.springframework.org/schema/vm=com.lpmoon.spring.VMInfoHandler

spring.schemas

这个文件用于定义对应Schema文件所在位置,

http\://www.springframework.org/schema/vm/spring-vm-2.5.xsd=com/lpmoon/spring/spring-vm-2.5.xsd
http\://www.springframework.org/schema/vm/spring-vm-3.0.xsd=com/lpmoon/spring/spring-vm-3.0.xsd
http\://www.springframework.org/schema/vm/spring-vm-3.1.xsd=com/lpmoon/spring/spring-vm-3.1.xsd
http\://www.springframework.org/schema/vm/spring-vm-3.2.xsd=com/lpmoon/spring/spring-vm-3.2.xsd
http\://www.springframework.org/schema/vm/spring-vm-4.0.xsd=com/lpmoon/spring/spring-vm-4.0.xsd
http\://www.springframework.org/schema/vm/spring-vm-4.1.xsd=com/lpmoon/spring/spring-vm-4.1.xsd
http\://www.springframework.org/schema/vm/spring-vm-4.2.xsd=com/lpmoon/spring/spring-vm-4.2.xsd
http\://www.springframework.org/schema/vm/spring-vm-4.3.xsd=com/lpmoon/spring/spring-vm-4.3.xsd
http\://www.springframework.org/schema/vm/spring-vm.xsd=com/lpmoon/spring/spring-vm-4.3.xsd

spring-vm-xx.xsd

因为我们需要支持从2-x到4-x多个版本的Spring,所以我们需要根据spring.schemas重定义的schema创建多个版本的xsd文件,文件放置在com/lpmoon/spring/目录下。

<?xml version="1.0" encoding="UTF-8"?>

<xsd:schema xmlns="http://www.springframework.org/schema/vm"
            xmlns:xsd="http://www.w3.org/2001/XMLSchema"
            xmlns:tool="http://www.springframework.org/schema/tool"
            targetNamespace="http://www.springframework.org/schema/vm"
            elementFormDefault="qualified"
            attributeFormDefault="unqualified">

    <xsd:annotation>
        <xsd:documentation><![CDATA[
	Defines the vm elements for the Spring Framework's application
	context support.
		]]></xsd:documentation>
    </xsd:annotation>

    <xsd:element name="info">
        <xsd:annotation>
            <xsd:documentation><![CDATA[
java info.
			]]></xsd:documentation>
            <xsd:appinfo>
                <tool:annotation>
                    <tool:exports type="com.lpmoon.spring.JavaInfoTool"/>
                </tool:annotation>
            </xsd:appinfo>
        </xsd:annotation>
    </xsd:element>

</xsd:schema>

上面的文件定义了vm标签,以及子标签info的含义。

打包

到这里所有的文件都已经定义好了,我们来看一看具体的文件结构,

|____pom.xml
|____src
| |____main
| | |____java
| | | |____com
| | | | |____lpmoon
| | | | | |____spring
| | | | | | |____JavaInfoBeanDefinitionParser.java
| | | | | | |____JavaInfoTool.java
| | | | | | |____spring-vm-2.5.xsd
| | | | | | |____spring-vm-3.0.xsd
| | | | | | |____spring-vm-3.1.xsd
| | | | | | |____spring-vm-3.2.xsd
| | | | | | |____spring-vm-4.0.xsd
| | | | | | |____spring-vm-4.1.xsd
| | | | | | |____spring-vm-4.2.xsd
| | | | | | |____spring-vm-4.3.xsd
| | | | | | |____VMHandler.java
| | |____resources
| | | |____META-INF
| | | | |____spring.handlers
| | | | |____spring.schemas

为了方便他人使用我们需要将我们定义的文件打包成jar文件,配置pom文件如下,

    <build>
        <plugins>
            <plugin>
                <artifactId>maven-resources-plugin</artifactId>
                <version>2.5</version>
                <executions>
                    <execution>
                        <id>copy-xmls</id>
                        <phase>process-sources</phase>
                        <goals>
                            <goal>copy-resources</goal>
                        </goals>
                        <configuration>
                            <outputDirectory>${basedir}/target/classes</outputDirectory>
                            <resources>
                                <resource>
                                    <directory>${basedir}/src/main/java</directory>
                                    <includes>
                                        <include>**/*.xsd</include>
                                    </includes>
                                </resource>
                            </resources>
                        </configuration>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>

上面的配置的主要作用是将xsd文件打包到对应的目录下,否则使用该jar启动的时候会报错。执行

mvn clean install

后对应的jar包里面的内容为

|____com
| |____lpmoon
| | |____spring
| | | |____JavaInfoBeanDefinitionParser.class
| | | |____JavaInfoTool.class
| | | |____spring-vm-2.5.xsd
| | | |____spring-vm-3.0.xsd
| | | |____spring-vm-3.1.xsd
| | | |____spring-vm-3.2.xsd
| | | |____spring-vm-4.0.xsd
| | | |____spring-vm-4.1.xsd
| | | |____spring-vm-4.2.xsd
| | | |____spring-vm-4.3.xsd
| | | |____VMInfoHandler.class
|____META-INF
| |____spring.handlers
| |____spring.schemas

使用

建立项目,在对应的配置文件中添加如下配置,

<vm:info/>

然后启动代码,可以看到如下输出,

   java.runtime.name -> Java(TM) SE Runtime Environment
sun.boot.library.path -> /Library/Java/JavaVirtualMachines/jdk1.8.0_111.jdk/Contents/Home/jre/lib

......