Spring源码阅读之编写自定义标签

>>强大,10k+点赞的 SpringBoot 后台管理系统竟然出了详细教程!

基于昨天的那篇文章,我们今天来聊一个比较简单轻松点的话题,今天我们来实现一个自定义的标签。

先明确下整个项目的结构,网上有很多关于自定义标签的实现方式,但是教程包结构不清晰,导致测试无法正常进行,博主自己也栽了一个坑,为了让朋友们可以快速验证结果,我们首先来介绍一下包结构。

Spring源码阅读之编写自定义标签

第一个工程包结构,该工程的作用是为了实现一个自定义标签。

Spring源码阅读之编写自定义标签

第二个工程是为了验证自定义标签是否能够正常工作。

这里说一下为什么创建了两个工程,博主就是在这里遇到了一个坑,最开始博主在一个工程下实现了标签并且去做验证,遇到了一个比较怪异的报错,如下,大概意思就是说 'mytag:annotation' 找不到,苦思冥想命名定义了为什么会找不到呢?朋友们混个眼熟,后面会讲为啥。

Caused by: org.xml.sax.SAXParseException; lineNumber: 10; columnNumber: 42; cvc-complex-type.2.4.c: 通配符的匹配很全面, 但无法找到元素 'mytag:annotation' 的声明。
at com.sun.org.apache.xerces.internal.util.ErrorHandlerWrapper.createSAXParseException(ErrorHandlerWrapper.java:203)
at

  1. 清楚了包结构,我们先来介绍第一个工程涉及文件的作用
  • MyNamespaceDefinitionParse:用于解析标签的具体实现
  • MyNamespaceHandler:命名空间处理器,用于注册标签对应的解析器
  • MyTagBean:自定义BeanClass
  • spring.handlers:指定使用的命名空间处理器,即指定MyNamespaceHandler
  • spring.schemas:用于指定.xsd文件位置的
  • resource-1.0.xsd:用于定义标签,也可理解成规范标签编写规则的
  1. 我们先来介绍下resource-1.0.xsd文件内容。
<?xml version="1.0" encoding="UTF-8"?>
<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns="http://www.zxz.com/resource"
targetNamespace="http://www.zxz.com/resource">
<!-- 定义一个类型,该类型内可以指定一个或多个标签 -->
<xsd:complexType name="annotationType">
<!-- 定义属性,名字叫id,可以理解成我们定义bean标签的时候写的 id 那个属性 -->
<xsd:attribute name="id" type="xsd:ID">
<xsd:annotation>
<xsd:documentation><![CDATA[ The unique identifier for a bean. ]]></xsd:documentation>
</xsd:annotation>
</xsd:attribute>
<!-- 定义属性,,名字叫name -->
<xsd:attribute name="name" type="xsd:string">
<xsd:annotation>
<xsd:documentation><![CDATA[ The name of bean. ]]></xsd:documentation>
</xsd:annotation>
</xsd:attribute>
<!-- 定义属性,名字叫myPackage -->
<xsd:attribute name="myPackage" type="xsd:string">
<xsd:annotation>
<xsd:documentation><![CDATA[ The scan package. ]]></xsd:documentation>
</xsd:annotation>
</xsd:attribute>
</xsd:complexType>
<!-- 定义一个标签,名字叫annotation,对应的类型是我们上面定义的annotationType类型 -->
<!-- 即annotation标签包含了 id、name、myPackage 三个属性 -->
<xsd:element name="annotation" type="annotationType">
<xsd:annotation>
<xsd:documentation><![CDATA[ The annotation config ]]></xsd:documentation>
</xsd:annotation>
</xsd:element>
</xsd:schema>

  1. spring.schemas文件内容如下,用于指定resource-1.0.xsd文件的位置,左边为 key,右边为该文件在本地的位置,如果该文件在本地找不到会尝试去远程去找,即左边 key 指定的远程位置去获取。
http://www.zxz.com/resource/resource-1.0.xsd=/resource-1.0.xsd

  1. spring.handlers文件内容如下,用于指定自定义标签的解析器位置,左边为 key,右边为该文件在本地的位置。
http://www.zxz.com/resource=com.zxz.resource.MyNamespaceHandler

  1. MyTagBean.java文件,设置了 name myPackage两个属性,注意这个字段名和resource-1.0.xsd文件定义的名字实际上没有什么直接联系,只是名字相同而已。
package com.zxz.resource;

/**
* @author zxz
*/

public class MyTagBean {
private String name;
private String myPackage;

public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getMyPackage() {
return myPackage;
}
public void setMyPackage(String myPackage) {
this.myPackage = myPackage;
}

@Override
public String toString() {
return "MyTagBean{" + "name='" + name + ''' + ", myPackage='" + myPackage + ''' + '}';
}
}

  1. MyNamespaceHandler.java文件内容如下,其继承了NamespaceHandlerSupport,实现了init方法。用于注册具体annotation 标签对应的解析器,这个和上一讲的<context:component-scan>的做法是一样的,不熟悉的朋友可以先看上一讲。
package com.zxz.resource;

import org.springframework.beans.factory.xml.NamespaceHandlerSupport;

/**
* @author zxz
*/

public class MyNamespaceHandler extends NamespaceHandlerSupport {
public void init() {
registerBeanDefinitionParser("annotation", new MyNamespaceDefinitionParse());
}
}

  1. MyNamespaceDefinitionParse.java内容如下,其实现了BeanDefinitionParse接口,来实现parse方法。
package com.zxz.resource;

import org.springframework.beans.MutablePropertyValues;
import org.springframework.beans.factory.config.BeanDefinition;
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;

/**
* @author zxz
*/

public class MyNamespaceDefinitionParse implements BeanDefinitionParser {

public BeanDefinition parse(Element element, ParserContext parserContext) {
RootBeanDefinition beanDefinition = new RootBeanDefinition();
// 设置beanClass
beanDefinition.setBeanClass(MyTagBean.class);
MutablePropertyValues mutablePropertyValues = beanDefinition.getPropertyValues();
// 添加name属性
if (element.hasAttribute("name")) {
mutablePropertyValues.addPropertyValue("name", element.getAttribute("name"));
}
// 添加package属性
if (element.hasAttribute("myPackage")) {
mutablePropertyValues.addPropertyValue("myPackage", element.getAttribute("myPackage"));
}
String id = element.getAttribute("id");
// 拿到注册表, 注册BeanDefinition
parserContext.getRegistry().registerBeanDefinition(id, beanDefinition);
return beanDefinition;
}
}

至此,我们的自定义标签就制作完成了,接下来打成 jar 包,在第二个测试工程中进行测试。


  1. 第二个工程文件介绍。
  • applicationContext.xml:编写定义标签内容
  • Test.java:测试方法
  • pom.xml:引入第一个工程的 jar 包

  1. applicationContext.xml文件内容如下,在测试启动的时候会找 http://www.zxz.com/resource/resource-1.0.xsdhttp://www.zxz.com/resource对应的.xsd文件和对应的处理器。回答下前面的那个报错原因,因为在 spring 启动的时候会先扫描本地所有的.handlers.schemas文件,如果本地找不到回去远程找。但是从我们的报错可以看出,应该是没有扫描到我们自定义的两个文件,但是我们将其打成 jar 包,在另外一个工程里引用就可以扫到,所以我猜想 spring 启动只扫描在 pom 文件指定的那些包下文件。
<?xml version="1.0" encoding="UTF-8"?>
<!-- xmlns:mytag="http://www.zxz.com/resource" 的标签名,可以理解成指定一级标签名,和 '<context:component-scan>' 的context一样 -->
<beans xmlns:mytag="http://www.zxz.com/resource"
xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.zxz.com/resource
http://www.zxz.com/resource/resource-1.0.xsd"

<!-- http://www.zxz.com/resource 是用于指定handler类的位置 -->
<!-- http://www.zxz.com/resource/resource-1.0.xsd 用于指定 .xsd文件的位置 -->
<!-- 自定义标签 -->
<mytag:annotation id="myTagBean" name="zxz" myPackage="com.zxz.demo"/>
</beans>

  1. Test.xml文件内容如下。
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class Test {
public static void main(String[] args) {
ApplicationContext context = new ClassPathXmlApplicationContext("classpath:applicationContext.xml");
System.out.println(context.getBean("myTagBean"));
}
}

11. 运行测试代码,最终输出结果如下。

Spring源码阅读之编写自定义标签

总结

以上即为创建一个自定义标签的流程,全部代码都经过测试并得出预期结果。具体的运行原理我在上一篇讲解<context:component-scan>标签的源码解析讲解过了,不熟悉的朋友先看看上一篇内容。至此,我们关于标签解析成BeanDefinition对象并将其注册到BeanFactory的全部流程就讲完了,涉及到的其他细节朋友大家自行了解,或者可以和我交流。下一篇文章我们来讲讲下一个比较重要的流程invokeBeanFactoryPostProcessors,希望大家和我继续坚持下去,搞定 Spring 最核心的灵魂。


Spring源码阅读之编写自定义标签

欢迎关注我,共同学习

原文始发于微信公众号(z小赵):Spring源码阅读之编写自定义标签