概述
在《spring源码解析之一 ----- 默认标签的解析注册》中,已经介绍了Spring对于默认标签(beans、bean、alias、import)的解析注册。看到这个时候,你也会明白,我们常用的aop标签,并不是spring的默认标签,而是自定义标签。spring对自定义标签,提供了良好的扩展和支持。在上篇文章中,按照源码看bean的解析过程中,在下方的代码里,是自定义标签和Default标签的分开点。
再谈具体实现自定义标签之前,通过下面的代码,我们可以简单推测出一些东西。Node是存放标签的节点元素,在if语句中,通过node instanceof Element,来判断node节点是不是Element元素,如果是Element类型,name就会解析。所以Element类型,对应着一个标签,比如bean标签就是Element类型。
protected void parseBeanDefinitions(Element root, BeanDefinitionParserDelegate delegate) {
if(delegate.isDefaultNamespace(root)) {
NodeList nl = root.getChildNodes();
for(int i = 0; i < nl.getLength(); ++i) {
Node node = nl.item(i);
if(node instanceof Element) {
Element ele = (Element)node;
if(delegate.isDefaultNamespace(ele)) {
this.parseDefaultElement(ele, delegate);//进入
} else {
delegate.parseCustomElement(ele);
}
}
}
} else {
delegate.parseCustomElement(root);
}
}
这里先宏观介绍一下自定义标签的实现过程
1.实现一个POJO类,定义标签对应的相关参数
2.实现一个XSD文件,用来描述POJO类
3.实现一个解析类,用来解析XSD和POJO。Spring为我们提供了抽象类 AbstractSingleBeanDefinitionParser
4.实现一个Handler类,该类被Spring使用,用来注册bean到容器中。Spring为我们提供了抽象类 NamespaceHandlerSupport
5.添加我们自定义的handler,schema和location到spring.handlers 和 spring.schemas
二、User类型的POJO
package com.heitian.ssm.model;
/**
* Created by weili on 17/6/27.
*/
public class UserInfo {
String name;
String id;
String tel;
String address;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getTel() {
return tel;
}
public void setTel(String tel) {
this.tel = tel;
}
public String getAddress() {
return address;
}
public void setAddress(String address) {
this.address = address;
}
}
三、定义XSD描述POJO
XSD的文件格式规范和入门学习,可以去这个网址 http://www.phpstudy.net/e/schema/ 。
<?xml version="1.0" encoding="UTF-8"?>
<schema xmlns="http://www.w3.org/2001/XMLSchema"
targetNamespace="http://www.weili.com/schema/userinfo"
xmlns:tns="http://www.weili.com/schema/userinfo"
elementFormDefault="qualified">
<element name="userinfo">
<complexType>
<attribute name="id" type="string" />
<attribute name="name" type="string" />
<attribute name="address" type="string" />
<attribute name="tel" type="string" />
</complexType>
</element>
</schema>
package com.heitian.ssm.model;
import org.springframework.beans.factory.support.AbstractBeanDefinition;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.xml.AbstractBeanDefinitionParser;
import org.springframework.beans.factory.xml.AbstractSingleBeanDefinitionParser;
import org.springframework.beans.factory.xml.ParserContext;
import org.springframework.util.StringUtils;
import org.w3c.dom.Element;
/**
* Created by weili on 17/6/27.
*/
public class UserBeanDefinitionParser extends AbstractSingleBeanDefinitionParser{
@Override
protected Class<?> getBeanClass(Element element) {
return UserInfo.class;
}
@Override
protected void doParse(Element element, BeanDefinitionBuilder builder) {
String id=element.getAttribute("id");
String name=element.getAttribute("name");
String tel=element.getAttribute("tel");
String address=element.getAttribute("address");
//将提取到的数据放入beanDefinitionBuilder 中,待完成所有的bean解析后统一放到beanfactory
if(StringUtils.hasText(id)){
builder.addPropertyValue("id", id);
}
if(StringUtils.hasText(name)){
builder.addPropertyValue("name", name);
}
if(StringUtils.hasText(tel)){
builder.addPropertyValue("tel", tel);
}
if(StringUtils.hasText(address)){
builder.addPropertyValue("address", address);
}
}
}
五、实现Handler类
该类的作用是将解析器和相应的标签名字,关联起来,告诉Spring
package com.heitian.ssm.model;
import org.springframework.beans.factory.xml.NamespaceHandlerSupport;
/**
* Created by weili on 17/6/27.
*/
public class userInfoNamespaceHandler extends NamespaceHandlerSupport {
public void init() {
registerBeanDefinitionParser("userinfo", new UserBeanDefinitionParser());
}
}
六、修改spring的schema和handler
我用的Intellij建立的maven工程,所以工程下没有spring.handlers和spring.shcemas这两个文件。为了让maven能够读取这俩文件,需要在你工程的Resource目录下新建一个META-INF目录,然后再这个目录下新建spring.handlers和spring.schemas。同时需要把你定义的xsd文件也移动到META-INF文件下。
spring.handlers:
http://www.weili.com/schema/userinfo = com.heitian.ssm.model.userInfoNamespaceHandler
spring.schemas:
http://www.weili.com/schema/userinfo/userinfo.xsd = META-INF/userinfo.xsd
spring.handlers文件中增加(告诉spring,这样的URI应该去用哪个handler去调用解析)
spring.schemas文件中增加(告诉spring,这样的URI应该去什么地方找到xsd文件描述)
这个时候你的resource目录下应该和图上的差不多:因为我是在一个SSM Demo上直接写的,所以无关的配置打了马赛克。
七、配置spring-bean.xml
注意第四行的xmlns:myname 需要和定义的标签<myname:userinfo>相一致。
通过下面的xml,我们就配置了一个userinfo的标签,也可以说是我们自己实现的bean。最后就可以在测试类里面读取这个配置文件了。
<?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:myname="http://www.weili.com/schema/userinfo"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-4.0.xsd
http://www.weili.com/schema/userinfo
http://www.weili.com/schema/userinfo/userinfo.xsd">
<myname:userinfo id="userinfoId" name="userinfoName" tel="18801289635" address="bupt"></myname:userinfo>
</beans>
八、测试类
最后就是写个测试类,从classpath中加载xml,然后获取我们自己定义的bean。可以看到,我们在xml文件中自己定义了一个<userinfo>标签或者bean,然后通过这个标签的id属性的名字,获取相应的标签元素。
package com.heitian.ssm;
import com.heitian.ssm.model.UserInfo;
import org.springframework.context.support.ClassPathXmlApplicationContext;
/**
* Created by weili on 17/6/25.
*/
public class SpringBean {
public static void main (String args[]){
ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext("spring-bean.xml");
UserInfo usrInfo = (UserInfo) ctx.getBean("userinfoId");
System.out.print(usrInfo.getName());
}
}
以上就是自己实现自定义标签的过程,我们通过spring提供的相应接口和类,实现了一个userinfo的标签,这个标签类似于bean标签,可以配置id、name等属性。
这个方法虽然简单,但是确实自定义标签的基本步骤。在阿里巴巴的开源分布式服务框架Dubbo中,dubbo-config-spring中就是dubbo和spring结合的配置文件。里面充分用了这个方法,可能你很清楚的直到一个文件dubbo.xsd。
二、自定义bean的源码解析
自定义bean和spring的默认bean的流程基本是一致的。除了之前提到的,spring的default bean和自定义bean进入的parse方法不同外,基本没啥大区别。本文的源码解析,着重看自定义的标签的shecma和handler是怎么联系和加载的,然后又是怎么通过handler调用了我们自己的解析方法。
还是和之前spring默认bean的加载过程一样,我们一路debug直到下面这个地方:
XmlBeanDefinitionReader 下的doLoadBeanDefinitions
protected int doLoadBeanDefinitions(InputSource inputSource, Resource resource) throws BeanDefinitionStoreException {
try {
Document doc = this.doLoadDocument(inputSource, resource);
return this.registerBeanDefinitions(doc, resource);
} catch (BeanDefinitionStoreException var4) {
throw var4;
} catch (SAXParseException var5) {
throw new XmlBeanDefinitionStoreException(resource.getDescription(), "Line " + var5.getLineNumber() + " in XML document from " + resource + " is invalid", var5);
} catch (SAXException var6) {
throw new XmlBeanDefinitionStoreException(resource.getDescription(), "XML document from " + resource + " is invalid", var6);
} catch (ParserConfigurationException var7) {
throw new BeanDefinitionStoreException(resource.getDescription(), "Parser configuration exception parsing XML from " + resource, var7);
} catch (IOException var8) {
throw new BeanDefinitionStoreException(resource.getDescription(), "IOException parsing XML document from " + resource, var8);
} catch (Throwable var9) {
throw new BeanDefinitionStoreException(resource.getDescription(), "Unexpected exception parsing XML document from " + resource, var9);
}
}
在上一篇,spring默认bean的加载中我们并没有进去看文档DOM树是怎么加载的。在这篇文章里,我们将简单看doc的生成过程,
进入这个方法,一直到如下这个方法。
public Document loadDocument(InputSource inputSource, EntityResolver entityResolver, ErrorHandler errorHandler, int validationMode, boolean namespaceAware) throws Exception {
DocumentBuilderFactory factory = this.createDocumentBuilderFactory(validationMode, namespaceAware);
if(logger.isDebugEnabled()) {
logger.debug("Using JAXP provider [" + factory.getClass().getName() + "]");
}
DocumentBuilder builder = this.createDocumentBuilder(factory, entityResolver, errorHandler);
return builder.parse(inputSource);
}
在上面的方法里,获取了一个文档的构建对象builder,这个builder并没有做什么特别的事情,只是构建一个对象。然后对inputSource进行解析,inputSource里面存放着配置文件spring.xml文件的存放位置。j进入parse方法。进去后,我们可以看到这里对xml文档进行了DOM树的解析,然后将xml文件中的标签转成DOM树的Node。如果你想看看,DOM树的解析,那么进入domParser.parse(is)。这里仅仅简单了解一下过程就行。如果对DOM树不了解,可以去随意找一些资料,了解xml和DOM的转换。
public Document parse(InputSource is) throws SAXException, IOException {
if (is == null) {
throw new IllegalArgumentException(
DOMMessageFormatter.formatMessage(DOMMessageFormatter.DOM_DOMAIN,
"jaxp-null-input-source", null));
}
if (fSchemaValidator != null) {
if (fSchemaValidationManager != null) {
fSchemaValidationManager.reset();
fUnparsedEntityHandler.reset();
}
resetSchemaValidator();
}
domParser.parse(is); //在这里对inputSource进行解析
Document doc = domParser.getDocument(); //将解析的结果传给doc
domParser.dropDocumentReferences();
return doc; //结果返回
}
经过上面的DOM树的解析,xml中的标签就被转成了doc对象中的节点,我们可以看一下doc中的数据
我们看到,在NodeName里面已经有我们自己定义的标签的名字和相应的属性。
那么doc对象生成了,接下来就该解析bean(myname:userinfo),然后注册。那么接下来就进入解析的方法。
进入之后,获取doc对象的根节点。xml文档的根节点是beans,然后进入真正的解析方法。
public void registerBeanDefinitions(Document doc, XmlReaderContext readerContext) {
this.readerContext = readerContext;
this.logger.debug("Loading bean definitions");
Element root = doc.getDocumentElement();
this.doRegisterBeanDefinitions(root);
}
到下面的方法后,就是默认bean和自定义bean的分界点了。在这个方法里,不同的bean,进入不同的解析方法。现在我们都是自定义bean,所以走下面的parseCustomElement(ele)。这个ele是我们自己定义的标签:myname:userinfo
protected void parseBeanDefinitions(Element root, BeanDefinitionParserDelegate delegate) {
if(delegate.isDefaultNamespace(root)) {
NodeList nl = root.getChildNodes();
for(int i = 0; i < nl.getLength(); ++i) {
Node node = nl.item(i);
if(node instanceof Element) {
Element ele = (Element)node;
if(delegate.isDefaultNamespace(ele)) {
this.parseDefaultElement(ele, delegate);
} else {
delegate.parseCustomElement(ele);
}
}
}
} else {
delegate.parseCustomElement(root);
}
}
进入自定义bean的解析方法后,代码如下图所示。
这个方法首先从ele中获取namespaceUri,这个namespaceUri = “www.weili.com/schema/userinfo”。
然后获取handler。
public BeanDefinition parseCustomElement(Element ele, BeanDefinition containingBd) {
String namespaceUri = this.getNamespaceURI(ele);
NamespaceHandler handler = this.readerContext.getNamespaceHandlerResolver().resolve(namespaceUri);
if(handler == null) {
this.error("Unable to locate Spring NamespaceHandler for XML schema namespace [" + namespaceUri + "]", ele);
return null;
} else {
return handler.parse(ele, new ParserContext(this.readerContext, this, containingBd));
}
}
进去获取handler的方法,你可以发现这个handler的获取非常简单。就是从map里面取出来的,key就是namespaceUri,取出来的value就是我们在spring.handlers中说明的 自定义的handler类
com.heitian.ssm.model.userInfoNamespaceHandler
既然获取到了类的权限定名,那么接下来自然想到的就是反射生成对应的类,也就是反射生成namespaceHandler。
这个namespaceHandler就是我们自己定义的userInfoNamespaceHandler。
有的人可能好奇,我们自己定义handler的时候,继承的是NamespaceHandlerSupport,这里的类型是NamespaceHandler。
原因是这样的:NamespaceHandler是一个接口,而
NamespaceHandlerSupport实现了NamespaceHandler接口,而我们自定义的类userInfoNamespaceHandler又继承了NamespaceHandlerSupport.
理清关系后,我们继续看。
接下里namespaceHandler调用了初始化方法init(),也就是我们写的userInfoNamespaceHandler中的init()方法。在初始化方法里,我们做了如下的事情
registerBeanDefinitionParser("userinfo", new UserBeanDefinitionParser());这个注册告诉spring,userinfo这个bean,应该用什么样的解析器。
到了这里,所有的一切就都串联起来了。
public NamespaceHandler resolve(String namespaceUri) {
Map<String, Object> handlerMappings = this.getHandlerMappings();
Object handlerOrClassName = handlerMappings.get(namespaceUri);
if(handlerOrClassName == null) {
return null;
} else if(handlerOrClassName instanceof NamespaceHandler) {
return (NamespaceHandler)handlerOrClassName;
} else {
String className = (String)handlerOrClassName;
try {
Class<?> handlerClass = ClassUtils.forName(className, this.classLoader);
if(!NamespaceHandler.class.isAssignableFrom(handlerClass)) {
throw new FatalBeanException("Class [" + className + "] for namespace [" + namespaceUri + "] does not implement the [" + NamespaceHandler.class.getName() + "] interface");
} else {
NamespaceHandler namespaceHandler = (NamespaceHandler)BeanUtils.instantiateClass(handlerClass);
namespaceHandler.init();//这个初始化方法,向spring注册我们的bean名和handler的对应关系。
handlerMappings.put(namespaceUri, namespaceHandler);
return namespaceHandler;
}
} catch (ClassNotFoundException var7) {
throw new FatalBeanException("NamespaceHandler class [" + className + "] for namespace [" + namespaceUri + "] not found", var7);
} catch (LinkageError var8) {
throw new FatalBeanException("Invalid NamespaceHandler class [" + className + "] for namespace [" + namespaceUri + "]: problem with handler class file or dependent class", var8);
}
}
}
接下里,方法返回我们自己定义的handler,然后开始进入下面的方法
return handler.parse(ele, new ParserContext(this.readerContext, this, containingBd));
进入上面的方法,到了下面这个方法里。我们可以看到,程序开始查找 bean标签element 对应的解析器。因为在上面已经向spring注册过,bean名字和解析器的对应关系,
所以在这里找解析器骗人色弱,就是一个从map中取出value的过程。
找到我们自己定义的解析器后,就开始调用parse方法。进入相应的方法,直到下面。在这个方法里,我们看到了BeanDefinitionBuilder,public BeanDefinition parse(Element element, ParserContext parserContext) { return this.findParserForElement(element, parserContext).parse(element, parserContext); } private BeanDefinitionParser findParserForElement(Element element, ParserContext parserContext) { String localName = parserContext.getDelegate().getLocalName(element); BeanDefinitionParser parser = (BeanDefinitionParser)this.parsers.get(localName); if(parser == null) { parserContext.getReaderContext().fatal("Cannot locate BeanDefinitionParser for element [" + localName + "]", element); } return parser; }
如果你还记得,在我们自己的实现的解析器里,有一个builder,我们将解析的属性名字和其对应的value,全部都放到了这个builder里。
如果你忘了,也没关系,进入这个方法的倒数第二行,doparse方法。
protected final AbstractBeanDefinition parseInternal(Element element, ParserContext parserContext) { BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(); String parentName = this.getParentName(element); if(parentName != null) { builder.getRawBeanDefinition().setParentName(parentName); } Class<?> beanClass = this.getBeanClass(element); if(beanClass != null) { builder.getRawBeanDefinition().setBeanClass(beanClass); } else { String beanClassName = this.getBeanClassName(element); if(beanClassName != null) { builder.getRawBeanDefinition().setBeanClassName(beanClassName); } } builder.getRawBeanDefinition().setSource(parserContext.extractSource(element)); if(parserContext.isNested()) { builder.setScope(parserContext.getContainingBeanDefinition().getScope()); } if(parserContext.isDefaultLazyInit()) { builder.setLazyInit(true); } this.doParse(element, parserContext, builder); return builder.getBeanDefinition(); }
进入之后是如图的调用,
再进去这个方法,然后你就会惊喜的发现,进入我们自己实现的解析器的parse方法了。
解析完毕后,开始一步步返回。
接下里的过程就是将,刚刚解析的beanDefinition注册到容器map的过程。这个和spring默认bean的注册时一致的。
相信如果你看懂了上篇文章,这篇文章也不会很难。
到这里,自定义标签的实现和解析原理,就讲完了。
有任何问题,欢迎留言交流!
最后
以上就是畅快高跟鞋为你收集整理的Spring源码解析之二 ------ 自定义标签的解析和注册(IOC的第一步)的全部内容,希望文章能够帮你解决Spring源码解析之二 ------ 自定义标签的解析和注册(IOC的第一步)所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复