作者: 一字馬胡
轉(zhuǎn)載標(biāo)志 【2017-11-17】
更新日志
日期 | 更新內(nèi)容 | 備注 |
---|---|---|
2017-11-17 | 新建文章 | 初版 |
2017-11-18 | 修改幾個(gè)錯(cuò)誤 | xx |
導(dǎo)入
Spring框架的一大強(qiáng)大之處就是框架的設(shè)計(jì)具有很好的可擴(kuò)展性,所以只要有想象力陆赋,就可以在Spring框架上作出擴(kuò)展旺罢,比如纱控,在學(xué)會(huì)了熟練使用Spring的內(nèi)置標(biāo)簽之后猫十,如果我們想要設(shè)計(jì)自己的標(biāo)簽鸟蜡,Spring是支持這種創(chuàng)新的艺糜,本文將結(jié)合實(shí)際的例子來(lái)說(shuō)明如何使用Spring提供的擴(kuò)展接口來(lái)設(shè)計(jì)自己的自定義標(biāo)簽微王,并且實(shí)現(xiàn)一些動(dòng)作蔼夜。在閱讀本文之前兼耀,你可以首先閱讀下面的兩篇鏈接文章,以更快的屬性Spring的生命周期等內(nèi)容求冷,可以更流暢的閱讀和理解本文的內(nèi)容:
Spring的BeanFactory和FactoryBean
Spring Bean 的生命周期
下面再次放上Spring Bean的生命周期圖瘤运,因?yàn)楸疚牡膬?nèi)容涉及到Bean的生命周期,自定義標(biāo)簽需要在Bean的生命周期內(nèi)做一些事情來(lái)操作bean匠题,所以屬性Spring Bean的生命周期在閱讀本文之前是必須的:
上面的流程圖已經(jīng)展示了Spring bean生命周期的詳細(xì)細(xì)節(jié)拯坟,我們知道了這些加載、初始化韭山、設(shè)置等一系列流程之后郁季,就可以在合適的環(huán)節(jié)加上我們想要的動(dòng)作,比如钱磅,我們可以使用BeanFactoryPostProcessor的postProcessBeanFactory方法來(lái)修改bean的屬性梦裂,例如,我們有一個(gè)bean的一個(gè)屬性A在spring配置文件中找不到盖淡,但是我們可以在BeanFactoryPostProcessor的postProcessBeanFactory方法里面使用方法的參數(shù)beanFactory來(lái)注冊(cè)一個(gè)A年柠。我們還可以使用BeanPostProcessor來(lái)修改我們的bean的屬性值,比如一個(gè)bean的一個(gè)屬性A褪迟,我們可以在BeanPostProcessor的postProcessBeforeInitialization方法和postProcessAfterInitialization方法來(lái)修改其值冗恨,這些方法需要配合其他的與Spring bean生命周期相關(guān)的類來(lái)做答憔。
可以將Spring bean的生命周期根據(jù)不同特點(diǎn)劃分為下面的幾類:
Bean自身的方法
包括我們?cè)谂渲胋ean時(shí)候設(shè)置的init-method方法和destroy-method方法。
Spring Bean級(jí)別的生命周期方法
包括BeanNameAware掀抹、BeanFactoryAware虐拓、InitializingBean和DiposableBean這些接口的方法。
Spring容器級(jí)別生命周期方法
包括InstantiationAwareBeanPostProcessor傲武、BeanPostProcessor蓉驹、BeanFactoryPostProcessor的實(shí)現(xiàn)類的方法。
特別說(shuō)明谱轨,本文僅結(jié)合實(shí)際的例子來(lái)說(shuō)明Spring 自定義標(biāo)簽的使用方法戒幔,而在此過(guò)程中涉及到的額外的技術(shù)點(diǎn)(比如xsd文檔的編寫規(guī)則)將不再本文的描述范圍之內(nèi),需要自行查找資料來(lái)學(xué)習(xí)土童,本文的定位是學(xué)會(huì)使用Spring自定義標(biāo)簽做一些事情,所以需要自行去查閱相關(guān)技術(shù)資料來(lái)學(xué)習(xí)一些內(nèi)容來(lái)理解Spring自定義標(biāo)簽工坊。
自定義標(biāo)簽以實(shí)現(xiàn)bean注冊(cè)
首先献汗,如何自定義一個(gè)Spring標(biāo)簽來(lái)實(shí)現(xiàn)bean的注冊(cè)呢?我想要實(shí)現(xiàn)的功能是類似于<bean .../>這樣的王污,下面將一步一步來(lái)說(shuō)明如何進(jìn)行操作罢吃,達(dá)到最后的效果。
編寫xsd文件
第一步是需要編寫xsd文件昭齐,下面是一個(gè)例子:
<?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>
將這個(gè)文件命名為任意你喜歡的名字尿招,后綴為.xsd,比如例子中的該文件被命名為ok-1.0.xsd阱驾,這個(gè)名字將在后文中用到就谜。上面定義的xsd文件中,我想要實(shí)現(xiàn)類似于:
<service id = "" serverName= "" />
看起來(lái)很簡(jiǎn)單里覆,并且我希望可以通過(guò)加載配置文件后可以獲取到這個(gè)bean(根據(jù)id來(lái)獲壬ゼ觥)。但是看起來(lái)很奇怪的是這個(gè)bean的類似是什么呢喧枷?你當(dāng)然可以在xsd文件中增加一個(gè)attr叫做“class”來(lái)控制生成的bean的類型虹统,但是本文中的例子為了簡(jiǎn)單,只可以配置一個(gè)屬性隧甚,具體返回的類似后面會(huì)說(shuō)到车荔。
編寫Schema文件和handler文件
這一步是比較關(guān)鍵的一步,你需要編寫兩個(gè)文件戚扳,分別為spring.schemas和spring.handlers忧便,然后將這兩個(gè)文件放在resource文件夾下的META-INF文件夾下,在spring.schemas文件里面咖城,你需要寫上茬腿;類似下面的內(nèi)容:
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文件,在這個(gè)文件里面你需要定義一個(gè)處理器來(lái)處理你自定義的哪些標(biāo)簽悴品,我們可以在里面做很豐富的事情禀综,下面是為本文例子編寫的spring.handlers文件的內(nèi)容:
http\://code.hujian.com/schema/ok=com.hujian.spring.handler.CommonNamespaceHandler
編寫Handler
經(jīng)過(guò)上面兩步之后,現(xiàn)在我們可以開(kāi)始寫處理我們的自定義標(biāo)簽的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;
}
}
上面說(shuō)到我們自定義的標(biāo)簽還不知道返回的bean是什么類型的定枷,為了簡(jiǎn)單,上面的代碼中將返回的類型定義為了ServerBean這個(gè)類型届氢,下面是這個(gè)類的信息:
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;
}
}
其實(shí)流程還是比較容易看懂的欠窒,首先我們需要注冊(cè)一個(gè)bean,而Spring中注冊(cè)的bean是AbstractBeanDefinition的子類退子,所以你可以使用任意AbstractBeanDefinition的子類來(lái)注冊(cè)你的bean岖妄,上面的例子中使用了RootBeanDefinition這個(gè)AbstractBeanDefinition的子類來(lái)注冊(cè)一個(gè)bean,設(shè)置一些配置信息之后就使用ParserContext的注冊(cè)器來(lái)將我們自定義的bean注冊(cè)到Spring中去了寂祥,需要注意的是荐虐,我們?cè)?lt;service id = "" .../>中配置的id就是我們往Spring容器中注冊(cè)的bean的id,所以在我們想要使用該bean的時(shí)候就可以使用這個(gè)id來(lái)獲取這個(gè)bean了丸凭。
測(cè)試
經(jīng)過(guò)上面的步驟之后福扬,下面來(lái)測(cè)試一下我自定義的標(biāo)簽是否可以正常工作,首先需要編寫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中設(shè)置我們的Schema路徑。然后就可以使用我們的自定義標(biāo)簽<ok:service .../>了向拆,可以看出上面我們配置了一個(gè)自定義bean亚茬,id為testServer,serverName屬性為HelloWorldService浓恳,下面是測(cè)試代碼:
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);
}
下面是輸出的結(jié)果:
[Service]=>HelloWorldService
可以看到刹缝,我們自定義的標(biāo)簽可以正常工作了,更為復(fù)雜的Spring自定義標(biāo)簽可以借助這個(gè)例子來(lái)擴(kuò)展颈将。
自定義標(biāo)簽以實(shí)現(xiàn)bean掃描
上面展示了一個(gè)簡(jiǎn)單的Spring自定義標(biāo)簽的用法梢夯,當(dāng)然任意復(fù)雜的自定義標(biāo)簽都可以基于這個(gè)簡(jiǎn)單的標(biāo)簽來(lái)模仿出來(lái),下面一個(gè)例子和注解有關(guān)晴圾,有時(shí)候我們希望借助Spring來(lái)幫我們解析代碼中的注解颂砸,下面的例子可以在xml中使用自定義的標(biāo)簽設(shè)定需要掃描的package,Spring會(huì)掃描我們配置的這個(gè)package,然后我希望可以找到這個(gè)package下所有注解了OkService的類人乓,并且基于該注解做一些統(tǒng)計(jì)勤篮,比如將這些注解的信息收集起來(lái),然后最后展示出這些收集到的注解信息色罚,因?yàn)椴襟E和上面的例子一樣碰缔,所以不再贅述:
首先是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>
接下來(lái)編寫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;
}
}
上面的代碼和上面的例子中的代碼沒(méi)有什么區(qū)別,但是有一個(gè)地方需要特別注意:
bd.setBeanClass(ScanBeanParser.class);
而這個(gè)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這個(gè)類金抡,這個(gè)類里面做了我們想要做的事情,就是收集所有注解了OKService的類的信息腌且,并存儲(chǔ)起來(lái)梗肝。讀到這里就需要回頭看一下文章開(kāi)頭的那張Spring Bean的生命周期圖,ScanBeanParser實(shí)現(xiàn)了很多涉及Spring Bean生命周期的類铺董。下面是測(cè)試代碼:
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 {
}
測(cè)試的結(jié)果如下:
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
結(jié)語(yǔ)
本文較為粗淺的解析了Spring中自定義標(biāo)簽的使用方法巫击,可以將本文中的代碼作為模板來(lái)進(jìn)行Spring自定義標(biāo)簽的設(shè)計(jì)和處理,更為深入的分析與總結(jié)將在未來(lái)進(jìn)行柄粹,關(guān)于Spring的相關(guān)分析總結(jié)會(huì)持續(xù)更新喘鸟,本文相當(dāng)于一個(gè)Spring自定義標(biāo)簽的“最佳實(shí)踐”吧!