本文学习自马士兵MAC课程-源码五班
添加自定义标签步骤 添加 xsd(XML Schema Definition) 文件 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 <?xml version="1.0" encoding="UTF-8" standalone="no"?> <xsd:schema xmlns:xsd ="http://www.w3.org/2001/XMLSchema" targetNamespace ="http://eitan-blog.github.io/schema/eitan" elementFormDefault ="qualified" attributeFormDefault ="unqualified" > <xsd:element name ="user" > <xsd:complexType > <xsd:attribute name ="id" type ="xsd:string" /> <xsd:attribute name ="userName" type ="xsd:string" /> <xsd:attribute name ="password" type ="xsd:string" /> <xsd:attribute name ="email" type ="xsd:string" /> </xsd:complexType > </xsd:element > </xsd:schema >
xmlns:xsd=”http://www.w3.org/2001/XMLSchema":表示该引用该命名空间的编写规范
targetNamespace=”http://eitan-blog.github.io/schema/eitan":表示当前命名空间的地址
elementFormDefault=”qualified” attributeFormDefault=”unqualified”:表示使用该命名空间的规范时,元素必须使用前缀短名,而属性可以不用,如 **<eitan:user id=”user1” userName=”eitan” email=”eitan_blog@163.com “ password=”123456”></eitan:user>中 eitan 就是前缀短名, user* 是元素,*userName 是属性
添加 spring.handlers 1 http\://eitan-blog.github.io/schema/eitan =com.eitan.tag.handler.UserNamespaceHandler
http://eitan-blog.github.io/schema/eitan:表示你的命名空间
com.eitan.tag.handler.UserNamespaceHandler:表示你对该命名空间的处理器的全限定类名
添加 Spring.shemas 1 http\://eitan-blog.github.io/schema/eitan/user.xsd =com.eitan.tag/user.xsd
编写命名空间映射关系,目的是为了在 xml 中找到映射对于的 xsd 文件
编写 UserNamespaceHandler 1 2 3 4 5 6 public class UserNamespaceHandler extends NamespaceHandlerSupport { @Override public void init () { registerBeanDefinitionParser("user" , new UserBeanDefinitionPaser()); } }
该类主要是用于初始化 xml 每一个元素对应的解析类
编写 UserBeanDefinitionPaser 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 public class UserBeanDefinitionPaser extends AbstractSingleBeanDefinitionParser { @Override protected Class<?> getBeanClass(Element element) { return User.class; } @Override protected void doParse (Element element, BeanDefinitionBuilder builder) { String userName = element.getAttribute("userName" ); String email = element.getAttribute("email" ); String password = element.getAttribute("password" ); if (StringUtils.hasText(userName)) { builder.addPropertyValue("userName" , userName); } if (StringUtils.hasText(email)) { builder.addPropertyValue("email" , email); } if (StringUtils.hasText(password)) { builder.addPropertyValue("password" , password); } } }
使用 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 <?xml version="1.0" encoding="UTF-8"?> <beans xmlns ="http://www.springframework.org/schema/beans" xmlns:context ="http://www.springframework.org/schema/context" xmlns:xsi ="http://www.w3.org/2001/XMLSchema-instance" xmlns:eitan ="http://eitan-blog.github.io/schema/eitan" xsi:schemaLocation ="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://eitan-blog.github.io/schema/eitan http://eitan-blog.github.io/schema/eitan/user.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd" > <eitan:user id ="user1" userName ="eitan" email ="eitan_blog@163.com" password ="123456" > </eitan:user > <context:property-placeholder location ="classpath*:db.properties" > </context:property-placeholder > <bean id ="person" class ="com.eitan.quickstart.entity.Person" > <property name ="id" value ="1" > </property > <property name ="name" value ="zs" > </property > </bean > </beans >
在文件中引入自己的 xsd
xmlns:eitan=”http://eitan-blog.github.io/schema/eitan"
xsi:schemaLocation=http://eitan-blog.github.io/schema/eitan http://eitan-blog.github.io/schema/eitan/user.xsd
在文件中使用自己的标签:*<eitan:user id=”user1” userName=”eitan” email=”eitan_blog@163.com “ password=”123456”></eitan:user>*
效果 1 2 3 4 5 6 7 public class MyApplicationContext { public static void main (String[] args) { ApplicationContext ac = new ClassPathXmlApplicationContext("spring-${username}.xml" ); User user = ac.getBean(User.class); System.out.println(user); } }
在 MyApplicationContext 中获取并打印 User.class ,可以发现写在 xml 中的属性成功注入进 user 中
Connected to the target VM, address: ‘127.0.0.1:12821’, transport: ‘socket’ User{userName=’eitan’, email=’eitan_blog@163.com ‘, password=’123456’}
源码解析
该过程发生在 AbstractApplicationContext 的 refresh() 方法中
在 refresh() 的 obtainFreshBeanFactory() 的 loadBeanDefinitions(beanFactory) 重载方法里
在 doLoadBeanDefinitions(inputSource, encodedResource.getResource()) 的 registerBeanDefinitions(doc, resource) 方法中,调用 documentReader.registerBeanDefinitions(doc, createReaderContext(resource)) 方法完成对 Document 对象完成具体的解析过程
核心处理代码在 doRegisterBeanDefinitions(doc.getDocumentElement()) 的 parseBeanDefinitions(root, this.delegate) 里
parseBeanDefinitions 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 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)) { parseDefaultElement(ele, delegate); } else { delegate.parseCustomElement(ele); } } } } else { delegate.parseCustomElement(root); } }
这里是判断元素是默认命名空间的元素 delegate.isDefaultNamespace(ele)
我们这里现在不是默认的命名空间,所以调用的是解析自定义元素的方法 delegate.parseCustomElement(ele)
parseCustomElement 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public BeanDefinition parseCustomElement (Element ele, @Nullable BeanDefinition containingBd) { String namespaceUri = getNamespaceURI(ele); if (namespaceUri == null ) { return null ; } NamespaceHandler handler = this .readerContext.getNamespaceHandlerResolver().resolve(namespaceUri); if (handler == null ) { error("Unable to locate Spring NamespaceHandler for XML schema namespace [" + namespaceUri + "]" , ele); return null ; } return handler.parse(ele, new ParserContext(this .readerContext, this , containingBd)); }
我们先从上下文中获取对应的 handler
再调用 handler 的 parse(Element element, ParserContext parserContext) 方法
resolver 方法 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 public NamespaceHandler resolve (String namespaceUri) { Map<String, Object> handlerMappings = 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" ); } NamespaceHandler namespaceHandler = (NamespaceHandler) BeanUtils.instantiateClass(handlerClass); namespaceHandler.init(); handlerMappings.put(namespaceUri, namespaceHandler); return namespaceHandler; } catch (ClassNotFoundException ex) { throw new FatalBeanException("Could not find NamespaceHandler class [" + className + "] for namespace [" + namespaceUri + "]" , ex); } catch (LinkageError err) { throw new FatalBeanException("Unresolvable class definition for NamespaceHandler class [" + className + "] for namespace [" + namespaceUri + "]" , err); } } }
映射关系是什么时候加载到程序中的 首先再创造上下文对象的时候会对路径赋值 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 public int registerBeanDefinitions (Document doc, Resource resource) throws BeanDefinitionStoreException { BeanDefinitionDocumentReader documentReader = createBeanDefinitionDocumentReader(); int countBefore = getRegistry().getBeanDefinitionCount(); documentReader.registerBeanDefinitions(doc, createReaderContext(resource)); return getRegistry().getBeanDefinitionCount() - countBefore; } public XmlReaderContext createReaderContext (Resource resource) { return new XmlReaderContext(resource, this .problemReporter, this .eventListener, this .sourceExtractor, this , getNamespaceHandlerResolver()); } public NamespaceHandlerResolver getNamespaceHandlerResolver () { if (this .namespaceHandlerResolver == null ) { this .namespaceHandlerResolver = createDefaultNamespaceHandlerResolver(); } return this .namespaceHandlerResolver; } protected NamespaceHandlerResolver createDefaultNamespaceHandlerResolver () { ClassLoader cl = (getResourceLoader() != null ? getResourceLoader().getClassLoader() : getBeanClassLoader()); return new DefaultNamespaceHandlerResolver(cl); } public DefaultNamespaceHandlerResolver (@Nullable ClassLoader classLoader) { this (classLoader, DEFAULT_HANDLER_MAPPINGS_LOCATION); } public static final String DEFAULT_HANDLER_MAPPINGS_LOCATION = "META-INF/spring.handlers" ;
在 resolve 方法时会调用 getHandlerMappings() 获取对应的映射关系 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 private Map<String, Object> getHandlerMappings () { Map<String, Object> handlerMappings = this .handlerMappings; if (handlerMappings == null ) { synchronized (this ) { handlerMappings = this .handlerMappings; if (handlerMappings == null ) { if (logger.isTraceEnabled()) { logger.trace("Loading NamespaceHandler mappings from [" + this .handlerMappingsLocation + "]" ); } try { Properties mappings = PropertiesLoaderUtils.loadAllProperties(this .handlerMappingsLocation, this .classLoader); if (logger.isTraceEnabled()) { logger.trace("Loaded NamespaceHandler mappings: " + mappings); } handlerMappings = new ConcurrentHashMap<>(mappings.size()); CollectionUtils.mergePropertiesIntoMap(mappings, handlerMappings); this .handlerMappings = handlerMappings; } catch (IOException ex) { throw new IllegalStateException( "Unable to load NamespaceHandler mappings from location [" + this .handlerMappingsLocation + "]" , ex); } } } } return handlerMappings; }
趣事:在 Debug 中,代码没有调用 getHandlerMappings() 方法,但是我们却能看到对应的映射关系 原因是 Debug 过程中,IDEA 自动调用了 toString() 方法,而 toString() 方法中有 getHandlerMappings()
1 2 3 4 @Override public String toString () { return "NamespaceHandlerResolver using mappings " + getHandlerMappings(); }
小问题-为什么 Spring.schemas 文件开头要大写 当该文件改成 spring.schemas 时,gradle 会报错
1 2 3 4 5 Cause: assert shortName != key | | | | | 'http://eitan-blog.github.io/schema/eitan/user.xsd' | false 'http://eitan-blog.github.io/schema/eitan/user.xsd'
原因是发生在 gradle 文件夹下面的 docs.gradle 的注释代码,注释掉后不管大小写都能正常运行。