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