原创作品未经作者同意严禁私自转载,或私自修改后据为自有。违者将追究法律责任
深夜12点驾驶着 Chrome
汽车飞驰在 Tomcat
高速公路上,速度表飞速逼近160码,而我的心情却越来越忧虑。
我是 design pattern
市刑事探长 高Sir
,因 code
市发生一起重大凶杀案件,我被火速遣往 code
市参与破案。
以下是案情背景
在 code
市 javascript
大道 webpack
大酒店 444
号房间内发生一起谋杀,受害者 configuration.xml
先生于晚上11点被酒店服务员发现死亡。
ClassPathResource
匕首,以及,一行神秘的注释 —— "
configuration.xml
先生在某种意义上来说并没有死,只是转化为另一种形式,另一层更高境界的艺术..."
这是什么意思?没有人知道!但毫无疑问为了人民的安全,我必须找出这幕后的凶手!
我相信有很多朋友刚接触Spring就迫不及待地想要一探实现原理,但其实这样很快就会发现自己难以驾驭Spring的源码而自信心受挫。
要理解一个框架的工作原理,最重要的前提是: 你必须真的在实践中用过这个框架,了解它的基本使用方法
纸上谈兵是大忌,所以请还不熟悉Spring用法的朋友不要急功近利
千里之行,始于足下 —— 老子
任何事情都要有个头,解剖源代码更是如此,解析IOC容器的起点就是下面这段代码
XmlBeanFactory bf = new XmlBeanFactory(new ClassPathResource("configuration.xml")); Dog dog = (Dog) bf.getBean("dog");
XML配置文件的内容如下, Dog
类是个空类(当前讲解解析流程不需要复杂的对象)
<?xml version="1.0" encoding="UTF-8" ?> <beans 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"> <bean id="dog" class="com.bean.Dog" /> </beans>
我先来教大家快速读懂源码的秘技
读懂Spring源代码最有效的方式只有一个 —— Debug代码一步一步跟进
ClassPathResource
开始 就是这把利刃刺穿了 configuration.xml
先生,造成他的大量出血并死亡,它是如此的锋利和轻盈,我们推断 configuration.xml
先生甚至都没有发觉自己受了重伤,直到大量的失血让他失去了行动力。
可是究竟是什么人要下此毒手呢?
new ClassPathResource("configuration.xml")
下面是 ClassPathResource
的家族成员
可以看到 ClassPathResource
的基本接口是 InputStreamSource
和 Resource
Spring框架定制了自己的 Resource
抽象,从最基础的接口 InputStreamSource
的名称也可以看出,它们其实无非是对底层的 InputStream
做了些封装以方便使用罢了
具体的接口定义大家可以自行查阅源码, ClassPathResource
顾名思义就是用来操作位于 类装载路径上的资源
,它有两个构造器
public ClassPathResource(String path) { this(path, (ClassLoader) null); } public ClassPathResource(String path, ClassLoader classLoader) { Assert.notNull(path, "Path must not be null"); String pathToUse = StringUtils.cleanPath(path); if (pathToUse.startsWith("/")) { pathToUse = pathToUse.substring(1); } this.path = pathToUse; this.classLoader = (classLoader != null ? classLoader : ClassUtils.getDefaultClassLoader()); }
主要的工作集中在 StringUtils.cleanPath(path)
方法中,这个方法做了两件事
path
中的Windows风格的路径分隔符 '/'
转换成 '/'
path
,比如将 file://core/../core/io/Resource.class
这样的路径压缩成 file://core/io/Resource.class
在 new XmlBeanFactory(new ClassPathResource("configuration.xml"))
的后续执行流程中,会调用 ClassPathResource.getInputStream()
这个 InputStreamSource
父接口中的方法,我们来看下这个方法的实现
@Override public InputStream getInputStream() throws IOException { InputStream is; if (this.clazz != null) { is = this.clazz.getResourceAsStream(this.path); } else if (this.classLoader != null) { is = this.classLoader.getResourceAsStream(this.path); } else { is = ClassLoader.getSystemResourceAsStream(this.path); } if (is == null) { throw new FileNotFoundException(getDescription() + " cannot be opened because it does not exist"); } return is; }
可以看到获取ClassPath中的资源流十分简单,直接调用 ClassLoader.getResourceAsStream()
方法,剩下和操作系统的互动交给JVM就好了。
另外还需要注意的是 ClassPathResource
的 hashCode
实现,它会返回关联资源路径的 String
哈希值。关注这个是因为加载xml文件会去根据 ClassPathResource
的哈希码检测循环加载配置文件的情况
@Override public int hashCode() { return this.path.hashCode(); }
午夜1点,唯一的线索分析完了,可是凶手很狡猾,除此之外犯罪现场没有留下任何有用的信息,我还是一筹莫展。无奈只能先去休息
XmlBeanFactory
和 XmlBeanDefinitionReader
凌晨2点。"叮铃铃"一阵急促的电话声将我惊醒,原来 code
市的破案人员通过监控系统 Intellj Idea
对 ClassPathResource
对象进行调用分析,发现了 XmlBeanFactory
和 XmlBeanDefinitionReader
曾经与 ClassPathResource
有过接触。
这两个恶棍喝得酩酊大醉,经过一系列的审讯 XmlBeanFactory
只肯招供他仅仅把 ClassPathResource
交给了 XmlBeanDefinitionReader
,其他的一概不知,可恶!
而 XmlBeanDefinitionReader
则是醉得不省人事,趴在审讯桌上呼呼大睡!
根据我们的代码入口
new XmlBeanFactory(new ClassPathResource("configuration.xml"))
其实它做的事情就是这个
public XmlBeanFactory(Resource resource) throws BeansException { this(resource, null); } public XmlBeanFactory(Resource resource, BeanFactory parentBeanFactory) throws BeansException { super(parentBeanFactory); this.reader.loadBeanDefinitions(resource); }
将工作都交给 XmlBeanDefinitionReader.loadBeanDefinitions()
方法完成
XmlBeanDefinitionReader
做的事情就是把Xml配置文件内容转换成 Bean Definition
对象表示。它主要依靠以下两个类的协作完成解析工作
DefaultDocumentLoader
javax.xml
类库完成Xml文件到 Document
对象的转换 BeanDefinitionDocumentReader
Document
对象完成从xml标签到 Bean Definition
的转换,并将 Bean Definition
注册到IOC容器中 下面我们从 XmlBeanDefinitionReader
的入口方法一步一步跟进
/* XmlBeanDefinitionReader.class */ @Override public int loadBeanDefinitions(Resource resource) throws BeanDefinitionStoreException { return loadBeanDefinitions(new EncodedResource(resource)); }
在第Ⅲ节中,我们了解到Spring将资源都抽象成 Resource
,而这个 EncodedResource
只是多了个编码属性的设置,当调用 EncodedResource.getReader()
时,会使用设置的编码
/* EncodedResource.class */ public Reader getReader() throws IOException { if (this.charset != null) { return new InputStreamReader(this.resource.getInputStream(), this.charset); } else if (this.encoding != null) { return new InputStreamReader(this.resource.getInputStream(), this.encoding); } else { return new InputStreamReader(this.resource.getInputStream()); } }
loadBeanDefnitions()
又调用了另一个重载方法
/* XmlBeanDefinitionReader.class */ public int loadBeanDefinitions(EncodedResource encodedResource) throws BeanDefinitionStoreException { Assert.notNull(encodedResource, "EncodedResource must not be null"); Set<EncodedResource> currentResources = this.resourcesCurrentlyBeingLoaded.get(); if (currentResources == null) { currentResources = new HashSet<EncodedResource>(4); this.resourcesCurrentlyBeingLoaded.set(currentResources); } if (!currentResources.add(encodedResource)) { throw new BeanDefinitionStoreException( "Detected cyclic loading of " + encodedResource + " - check your import definitions!"); } try { InputStream inputStream = encodedResource.getResource().getInputStream(); try { InputSource inputSource = new InputSource(inputStream); if (encodedResource.getEncoding() != null) { inputSource.setEncoding(encodedResource.getEncoding()); } return doLoadBeanDefinitions(inputSource, encodedResource.getResource()); } finally { inputStream.close(); } } catch (IOException ex) { throw new BeanDefinitionStoreException( "IOException parsing XML document from " + encodedResource.getResource(), ex); } finally { currentResources.remove(encodedResource); if (currentResources.isEmpty()) { this.resourcesCurrentlyBeingLoaded.remove(); } } }
可以看到其内部是通过 HashSet<EncodedResource> currentResources
这个HashSet集合进行重复加载相同配置文件的检测的,具体操作自然是检查新加入的 EncodedResource
的 hashCode()
是否重复
/* EncodedResource.class*/ @Override public int hashCode() { return this.resource.hashCode(); }
前面说过, ClassPathResource
的哈希值就是 String
表示的配置文件路径的哈希值,这就意味着如果重复加载了路径相同的xml资源则会抛出 Cyclic Loading
异常
/* ClassPathResource.class */ @Override public int hashCode() { return this.path.hashCode(); }
最后的工作交给了 doLoadBeanDefinitions()
方法
/* XmlBeanDefinitionReader.class */ protected int doLoadBeanDefinitions(InputSource inputSource, Resource resource) throws BeanDefinitionStoreException { try { Document doc = doLoadDocument(inputSource, resource); return registerBeanDefinitions(doc, resource); } catch (...) { ... } }
下面我们来分析 doLoadDocument()
方法
/* XmlBeanDefinitionReader.class */ protected Document doLoadDocument(InputSource inputSource, Resource resource) throws Exception { return this.documentLoader.loadDocument(inputSource, getEntityResolver(), this.errorHandler, getValidationModeForResource(resource), isNamespaceAware()); }
this.documentLoader
就是前面提到的 DefaultDocumentLoader
,利用 javax.xml
类库完成配置文件到 Document
对象的转换
/* XmlBeanDefinitionReader.class */ private DocumentLoader documentLoader = new DefaultDocumentLoader();
DefaultDocumentLoader
需要 EntityResolver
和 ValidationMode
的辅助进行配置文件到 Document
的转换
/* XmlBeanDefinitionReader.class */ protected EntityResolver getEntityResolver() { if (this.entityResolver == null) { // Determine default EntityResolver to use. ResourceLoader resourceLoader = getResourceLoader(); if (resourceLoader != null) { this.entityResolver = new ResourceEntityResolver(resourceLoader); } else { this.entityResolver = new DelegatingEntityResolver(getBeanClassLoader()); } } return this.entityResolver; }
EntityResolver
的作用是什么呢?请看下面这个简单的配置文件
<?xml version="1.0" encoding="UTF-8" ?> <beans 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"> <bean id="myTestBean" class="com.bean.MyTestBean" /> </beans>
DefaultDocumentLoader
在使用 javax.xml
内置库去解析xml文件的过程中,当遇到命名空间对应的 schema
文件时
(本例即 http://www.springframework.org/schema/beans/spring-beans.xsd
),它会去访问这个URL尝试获得 schema
文件内容,
然而网络环境是很复杂的,在不同地域不同的机器上,难以保证网络的畅通性,如果网络一直拥塞则解析xml过程就会一直失败。
为了避免依赖不可靠的网络,用户可以编写自定义的 EntityResolver
来为解析过程提供其他途径获取 schema
文件内容。
所谓 schema
就是一组规则,用来定义xml命名空间下能够合法出现的标签和每个标签合法的属性。
比如<beans>命名空间对应的 schema
—— spring-beans.xsd
就定义了<beans>标签的合法属性 default-autowire
、 default-lazy-init
等等,
以及<beans>下能够出现的标签<bean>、<import>等以及它们各自合法的属性。
我们重点来关注下 ResourceEntityResolver
的父类 DelegatingEntityResolver
的 resolveEntity()
方法
/* DelegatingEntityResolver.class */ @Override public InputSource resolveEntity(String publicId, String systemId) throws SAXException, IOException { if (systemId != null) { if (systemId.endsWith(DTD_SUFFIX)) { return this.dtdResolver.resolveEntity(publicId, systemId); } else if (systemId.endsWith(XSD_SUFFIX)) { return this.schemaResolver.resolveEntity(publicId, systemId); } } return null; }
systemId
就是 "http://www.springframework.org/schema/beans/spring-beans.xsd"
,然后具体解析过程交给 schemaResolver
/* DelegatingEntityResolver.class */ public DelegatingEntityResolver(ClassLoader classLoader) { this.dtdResolver = new BeansDtdResolver(); this.schemaResolver = new PluggableSchemaResolver(classLoader); }
在子类 ResourceEntityResolver
构造时会调用上面的构造器,可以看到 schemaResolver
被赋值为 PluggableSchemaResolver
,我们进入它的 resolveEntity()
方法
/* PluggableSchemaResolver.class */ 1. public static final String DEFAULT_SCHEMA_MAPPINGS_LOCATION = "META-INF/spring.schemas"; 2. public PluggableSchemaResolver(ClassLoader classLoader) { this.classLoader = classLoader; this.schemaMappingsLocation = DEFAULT_SCHEMA_MAPPINGS_LOCATION; } @Override 3. public InputSource resolveEntity(String publicId, String systemId) throws IOException { if (systemId != null) { String resourceLocation = getSchemaMappings().get(systemId); if (resourceLocation != null) { Resource resource = new ClassPathResource(resourceLocation, this.classLoader); try { InputSource source = new InputSource(resource.getInputStream()); source.setPublicId(publicId); source.setSystemId(systemId); if (logger.isDebugEnabled()) { logger.debug("Found XML schema [" + systemId + "] in classpath: " + resourceLocation); } return source; } catch (FileNotFoundException ex) { if (logger.isDebugEnabled()) { logger.debug("Couldn't find XML schema [" + systemId + "]: " + resource, ex); } } } } return null; }
getSchemaMappings()
方法会根据 META-INF/spring.schemas
文件中的内容找到对应 systemId
的物理文件位置,然后就能获得对应的文件内容。以下是我本地 spring-beans-4.3.14.RELEASE.jar
中的 spring/schemas
文件的部分截图
可以看到真正的规则文件都位于 org.springframework.beans.factory.xml
包下,所以这就避免了网络环境的不可靠导致解析xml失败。
使用 javax.xml
类库进行解析工作所需要的 EntityResolver
和 Validation Mode
还剩下一个需要分析
/* XmlBeanDefinitionReader.class */ 1. private int validationMode = VALIDATION_AUTO; 2. protected int getValidationModeForResource(Resource resource) { int validationModeToUse = getValidationMode(); if (validationModeToUse != VALIDATION_AUTO) { return validationModeToUse; } int detectedMode = detectValidationMode(resource); if (detectedMode != VALIDATION_AUTO) { return detectedMode; } // Hmm, we didn't get a clear indication... Let's assume XSD, // since apparently no DTD declaration has been found up until // detection stopped (before finding the document's root tag). return VALIDATION_XSD; }
validation mode
有四种 —— VALIDATION_NONE
, VALIDATION_AUTO
, VALIDATION_DTD
, VALIDATION_XSD
,在上面的方法中只有在验证模式为 VALIDATION_AUTO
时才需要进一步检测。 Validation Mode
在 XmlBeanDefinitionReader
中初始化了为 AUTO
模式,用户可以调用 XmlBeanDefinitionReader.setValidationMode()
进行设置
/* XmlBeanDefinitionReader.class */ 1. private final XmlValidationModeDetector validationModeDetector = new XmlValidationModeDetector(); 2. protected int detectValidationMode(Resource resource) { ... try { return this.validationModeDetector.detectValidationMode(inputStream); } catch (IOException ex) { ... } }
继续分析 ValidationModeDetector.detectValidationMode()
/* XmlValidationModeDetector.class */ public int detectValidationMode(InputStream inputStream) throws IOException { BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream)); try { boolean isDtdValidated = false; String content; while ((content = reader.readLine()) != null) { content = consumeCommentTokens(content); // 跳过注释 if (this.inComment || !StringUtils.hasText(content)) { continue; } if (hasDoctype(content)) { isDtdValidated = true; break; } if (hasOpeningTag(content)) { // End of meaningful data... break; } } return (isDtdValidated ? VALIDATION_DTD : VALIDATION_XSD); } catch (CharConversionException ex) { // Choked on some character encoding... // Leave the decision up to the caller. return VALIDATION_AUTO; } finally { reader.close(); } }
可以看到核心的分析在于 hasDoctype()
方法,这个方法很无脑...如果xml文件的非注释内容的第一行含有" DOCTYPE
"字符串,则认为是 VALIDATION_DTD
模式,否则是 VALIDATION_XSD
模式
获得了这些必要信息后,就可以将Xml文件解析为 Document
对象了
/* XmlBeanDefinitionReader.class */ protected Document doLoadDocument(InputSource inputSource, Resource resource) throws Exception { return this.documentLoader.loadDocument(inputSource, getEntityResolver(), this.errorHandler, getValidationModeForResource(resource), isNamespaceAware()); }
解析Xml文件是个复杂的过程,而现在我们才刚刚把解析的前期工作 —— 将Xml文件解析为 Document
对象讲完。为了更好地理解整个解析过程,我制作了一份时序图给大家提供参考。
然而很多细节是没法体现在图中的,要想理解代码逻辑就必须要 Debug
代码步步跟进
后面的篇章就是重点了 —— 遍历 Document
的元素,注册 Bean Definition
如果读者对本文有疑问,欢迎在评论区留言交流