0x00 概览
在这篇博客中,会为大家带来《Spring源码深度解析》一书中的第二章的读书笔记,即“容器的基本实现”
0x01 简单的Spring使用例子
- 对于Spring的使用,我们已经熟的不能再烂了。即使用
ClassPathXmlApplicationContext
加载xml,然后使用getBean()
方法获取我们想要的bean,然后进行我们下一步操作。但是,在这里我们使用另外一种获取Bean的方式。即使用XmlBeanFactory
.详见以下代码:1
2
3
4
5
6
7
8public void test() {
//加载资源。把我们写好的xml传进去,得到一个Resource
ClassPathResource resource = new ClassPathResource("testSpring.xml");
//创建XmlBeanFactory
XmlBeanFactory factory = new XmlBeanFactory(resource);
//获取Bean
factory.getBean("xxx",String.class);
} - 上面的代码对于我们并不陌生,我们可以通过简单的3行代码,就可以得到我们想要的Bean。看似简单,实际上Spring在背后为我们做了太多事。下面来就一起来初略的看看Spring为我们做了些什么事情吧~
0x02 容器的基础–XmlFactoryBean
-
在进入正题前,我们不妨猜测下上面的源码大致干了一些什么事情。
new ClassPathResource("testSpring.xml")
:这一步很好理解,无非就是把我们放在ClassPath下面对应的xml文件加载到内存,封装成Resource对象。new XmlBeanFactory(resource);
:这一步也不难,大致想一下,这里把Resource对象传递过去,肯定是得先验证xml文件语法正确与否,然后就是解析xml文件。得到我们写在xml中的<bean>
标签,再然后就是根据我们定义的<bean>
标签去实例化对象,放在容器中。事实上,Spring就是这样玩的。factory.getBean("xxx",String.class)
:这个就更加简单了。从容器中获取bean就不用多说了。- 总结下(时序图),画的不太正规,凑合着看看吧:
接下来我们一点一点的去分析。
-
配置文件的封装
- 在上面的第一步,是创建一个ClassPathResource对象。也就是把我们的xml读入内存,暴露获取InputStream的方法,供后面使用。我们来一起看看ClassPathResource的层次结构图:
- 我们都知道,在Java中会将不同来源的资源抽象成URL,通过注册不同的Handler(URLStreamHandler)来处理不同来源的资源的读取逻辑,一般handler的类型使用不同的前缀(协议)来标识。如“file:”、“http:”、“jar:”等,然而URL没有默认定义相对ClassPath或ServletContext等资源的handler,虽然可以注册自己的URLStreamHandler来解析特定的URL前缀,比如"classpath:",然而这需要了解URL的实现机制,而且URL也没有提供一些基本方法,如检查当前资源是否存在、检查当前资源是否可读等方法。因而Spring对其内部使用到的资源实现了自己的抽象结构:
Resource
接口来封装底层资源。(以上摘自《Spring源码深度解析》)。Resource
源码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17public interface InputStreamSource {
InputStream getInputStream() throws IOException;
}
public interface Resource extends InputStreamSource {
boolean exists(); //文件是否存在
boolean isReadable; //是否可读
boolean isOpen(); //是否打开
URL getURL() throws IOException; //获取URL
URI getURI() throws IOException; //获取URI
File getFile() throws IOException; //获取文件
long contentLength() throws IOException;
long lastModified() throws IOException;
Resource createRelative(String relativePath) throws IOException;
String getFilename(); //获取文件名(不带路径)
String getDescription();
}- 在有了Resource接口后,对于资源文件就可以统一处理。而且其实现也是简单。这里拿
ClassPathResource
的getInputStream()
方法来说。源码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18public InputStream getInputStream() throws IOException {
InputStream is;
//如果clazz不为空,就使用class获取资源文件
if (this.clazz != null) {
is = this.clazz.getResourceAsStream(this.path);
}
//如果classloader不为空,就使用classloader获取资源文件
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;
}通过源码发现,
ClassPathResource
就是通过class或者ClassLoader提供的底层方法获取InputStream- 对于其他来源的资源文件,Spring都有相应的实现,层次结构图如下:
对于资源文件的解析就到这结束了。接下来就让我们看看第2步做了些什么吧。
- 在上面的第一步,是创建一个ClassPathResource对象。也就是把我们的xml读入内存,暴露获取InputStream的方法,供后面使用。我们来一起看看ClassPathResource的层次结构图:
-
加载Bean
- 第2步是
new XmlFactory(resource)
.那么,我们就从XmlFactory
的构造方法开始吧~1
2
3
4
5
6
7
8
9
10public XmlBeanFactory(Resource resource) throws BeansException {
//调用重载的构造方法
this(resource, null);
}
public XmlBeanFactory(Resource resource, BeanFactory parentBeanFactory) throws BeansException {
//执行父类的构造方法
super(parentBeanFactory);
//通过resource加载Bean的定义
this.reader.loadBeanDefinitions(resource);
} - 我们先看
XmlBeanFactory(Resource resource, BeanFactory parentBeanFactory)
在执行父类的构造方法中做了什么。
1
2
3
4
5
6public AbstractAutowireCapableBeanFactory() {
super();
ignoreDependencyInterface(BeanNameAware.class);
ignoreDependencyInterface(BeanFactoryAware.class);
ignoreDependencyInterface(BeanClassLoaderAware.class);
}这里得介绍下
ignoreDependencyInterface()
。该方法的作用是忽略给定接口的自动装配功能。在Spring中,加入加载A类的时候,发现A类中使用B类作为属性,而加载A类的时候发现B类没有加载,那么Spring会自动加载B类。这也是Spring中的一个特性。那么,这关ignoreDependencyInterface()
什么事呢?我们在有些时候,不希望B类加载,也就是B类不会初始化。这时候B类就可以实现上面的这些接口,如:BeanNameAware/BeanFactoryAware/BeanClassLoaderAware
- 调用父类构造函数我们已经分析完了,接下来就分析下核心的操作
this.reader.loadBeanDefinitions(resource)
,直接上源码吧。
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
41
42
43
44
45
46
47
48
49public int loadBeanDefinitions(Resource resource) throws BeanDefinitionStoreException {
//调用重载的方法。new EncodedResource():把resource转成带编码格式的resource对象
return loadBeanDefinitions(new EncodedResource(resource));
}
public int loadBeanDefinitions(EncodedResource encodedResource) throws BeanDefinitionStoreException {
//断言resource不为空
Assert.notNull(encodedResource, "EncodedResource must not be null");
if (logger.isInfoEnabled()) {
logger.info("Loading XML bean definitions from " + encodedResource.getResource());
}
//获取当前正在加载的XML的bean定义资源
Set<EncodedResource> currentResources = this.resourcesCurrentlyBeingLoaded.get();
if (currentResources == null) {
currentResources = new HashSet<EncodedResource>(4);
this.resourcesCurrentlyBeingLoaded.set(currentResources);
}
//如果该Resource已经存在,则抛出异常
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不是Spring的类),判断Resource是否有设置encoding,如果有设置,则对相应的InputSource也设置encoding
InputSource inputSource = new InputSource(inputStream);
if (encodedResource.getEncoding() != null) {
inputSource.setEncoding(encodedResource.getEncoding());
}
//真正处理加载Bean的方法,返回的是加载bean的个数
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();
}
}
}该方法我们粗略的分析了下。梳理下大致操作。
- 首先进入loadBeanDefinitions(Resource resource)
方法的时候,就开始对resource进行进一步的封装,变成EncodedResource
- 判断该resource是否已经被处理过。如果被处理过则抛出异常。
- 获取输入流,构造InputSource(注意该InputSource是Sax解析中的),并设置编码(如果存在的话)
- 调用真正处理加载Bean的方法doLoadBeanDefinitions
- 分析了这么久,还没有到达关键地方,到现在,都还是在准备阶段,准备了这么久。接下来他要干些什么呢?详见
doLoadBeanDefinitions()
方法。
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
35protected int doLoadBeanDefinitions(InputSource inputSource, Resource resource)
throws BeanDefinitionStoreException {
try {
//获取xml的验证模式
int validationMode = getValidationModeForResource(resource);
//加载xml文档。使用委托设计模式,把操作委托给documentLoader
Document doc = this.documentLoader.loadDocument(
inputSource, getEntityResolver(), this.errorHandler, validationMode, isNamespaceAware());
//注册Bean
return registerBeanDefinitions(doc, resource);
}
catch (BeanDefinitionStoreException ex) {
throw ex;
}
catch (SAXParseException ex) {
throw new XmlBeanDefinitionStoreException(resource.getDescription(),
"Line " + ex.getLineNumber() + " in XML document from " + resource + " is invalid", ex);
}
catch (SAXException ex) {
throw new XmlBeanDefinitionStoreException(resource.getDescription(),
"XML document from " + resource + " is invalid", ex);
}
catch (ParserConfigurationException ex) {
throw new BeanDefinitionStoreException(resource.getDescription(),
"Parser configuration exception parsing XML from " + resource, ex);
}
catch (IOException ex) {
throw new BeanDefinitionStoreException(resource.getDescription(),
"IOException parsing XML document from " + resource, ex);
}
catch (Throwable ex) {
throw new BeanDefinitionStoreException(resource.getDescription(),
"Unexpected exception parsing XML document from " + resource, ex);
}
}纵观源码,发现除了异常外。该方法就执行了三个操作。即:
- 获取xml的验证方式
- 加载xml文档,得到Sax解析的Document对象
- 注册Bean
到此,我们越来越接近真相了。嘿嘿(。◕ˇ∀ˇ◕)。这三个操作支撑着整个Spring容器部分的实现基础,尤其是注册Bean信息,逻辑相当复杂。接下来我们一个一个去攻克。一个一个的去分析。
- 第2步是
0x03 获取xml的验证方式
- Xml的两种验证方式
- DTD
- 全称是Document Type Definition,即文档类型定义,是一种xml约束模式语言。是xml文件的验证机制,属于xml文档的一部分
- 一个DTD文档包含如下部分:
- 元素的定义规则
- 元素间关系的定义规则
- 元素可使用的属性
- 可使用的实体或符号规则
- 使用DTD验证模式需要在xml文件的头部进行声明。以下是Spring3.x的声明
1
2<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN 2.0//EN"
"http://www.springframework.org/dtd/spring-beans-2.0.dtd"> - XSD
- 全称是XML Schemas Definition。Xml schemas描述了XML文档的结构。
- Xml schemas本身就是一个xml文档,符合Xml的语法结构
- Xml schemas的声明除了要声明命名空间外(xmlns=xxxxxx),还必须指定该名称空间所对应的Xml schemas文档的存储位置。而存储位置又由两部分组成。即:
- 名称空间的URI
- 该名称空间所对应的Xml schemas文件位置或URL地址。
- 整个的文档存储位置声明如下:
- DTD
- 上面粗略的介绍了下xml的两种验证方式,下面就让我们进入正题,看看Spring是怎么得到该xml是哪种验证方式吧。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17protected int getValidationModeForResource(Resource resource) {
int validationModeToUse = getValidationMode();
//如果配置了xml验证模式,就使用配置的
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).
//默认为xsd模式
return VALIDATION_XSD;
}- 总的来说,Spring会去检查是否我们自己手动配置了xml的验证模式,如果没有就会去自动检测。如果再得不到,那就默认返回xsd模式验证。自动检测的代码如下:
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
30protected int detectValidationMode(Resource resource) {
//如果该资源打开了,就抛出异常。即如果文件打开了,就得等它关闭后再操作
if (resource.isOpen()) {
throw new BeanDefinitionStoreException(
"Passed-in Resource [" + resource + "] contains an open stream: " +
"cannot determine validation mode automatically. Either pass in a Resource " +
"that is able to create fresh streams, or explicitly specify the validationMode " +
"on your XmlBeanDefinitionReader instance.");
}
InputStream inputStream;
try {
inputStream = resource.getInputStream();
}
catch (IOException ex) {
throw new BeanDefinitionStoreException(
"Unable to determine validation mode for [" + resource + "]: cannot open InputStream. " +
"Did you attempt to load directly from a SAX InputSource without specifying the " +
"validationMode on your XmlBeanDefinitionReader instance?", ex);
}
try {
//委托设计模式
return this.validationModeDetector.detectValidationMode(inputStream);
}
catch (IOException ex) {
throw new BeanDefinitionStoreException("Unable to determine validation mode for [" +
resource + "]: an error occurred whilst reading from the InputStream.", ex);
}
}- 这里把检测验证模式的任务委托给了一个专一类。那我们就去看看这个
detectValidationMode
方法把。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
35public int detectValidationMode(InputStream inputStream) throws IOException {
// Peek into the file to look for DOCTYPE.
//通过DOCTYPE来判断是否是DTD约束模式。
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;
}
//如果有DOCTYPE就是DTD。否则就是XSD
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();
}
}
<!DOCTYPE>
.如果有就是DTD,如果没有就是XSD - xml验证模式得到了,接下来就是加载xml文档,封装成Document对象了。这也是委托给一个专门的类进行实现,代码见下:
1
Document doc = this.documentLoader.loadDocument(inputSource, getEntityResolver(), this.errorHandler, validationMode, isNamespaceAware());
- 这里我们说下
loadDocument
方法的第二个参数getEntityResolver()
是干嘛用的。- 我们知道,两种验证方式都会带上该schema的地址。大多数情况下,这个地址都是需要连接网络才能下载下来,然后对xml进行验证。一旦涉及网络,就会有一大堆的问题浮现。例如:传输失败、传输速度慢等。而一旦download schema出现问题,那么验证就会失败,整个项目也会运行失败。于是为了能够快速的定位到schema的位置,如本地。这样做就不会出现上面所述的问题。
- Sax就是使用
EntityResolver
接口中的resolveEntity()
方法来实现自定义如何寻找DTD的方法,即我们可以自己定义怎么去找DTD。下面就是getEntityResolver()
方法的代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19protected EntityResolver getEntityResolver() {
//EntityResolver的作用就是为了能够更好的找到DTD声明。
//xml在解析的时候,sax解析会先读取该文件的声明,根据声明去找DTD定义,以便对文档进行验证。
//而默认的寻找规则是通过网络寻找,如果计算机不能上网,那就会GG。所以可以通过EntityResolve定义寻找DTD定义的规则。
//也就是说,sax在解析的时候,会通过我们定义的EntityResolver寻找到DTD定义,这样就不用去网上download。
if (this.entityResolver == null) {
// Determine default EntityResolver to use.
//获取资源加载器
ResourceLoader resourceLoader = getResourceLoader();
if (resourceLoader != null) {
this.entityResolver = new ResourceEntityResolver(resourceLoader);
}
else {
//Spring默认使用
this.entityResolver = new DelegatingEntityResolver(getBeanClassLoader());
}
}
return this.entityResolver;
}- 在Spring中,默认使用的是
DelegatingEntityResolver
,resolveEntity()
方法代码如下:1
2
3
4
5
6
7
8
9
10
11
12public InputSource resolveEntity(String publicId, String systemId) throws SAXException, IOException {
if (systemId != null) {
//通过systemId判断是DTD约束还是XSD约束,执行不同的方法
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;
} - 这里具体就不细看了。粗略的说下,如果加载了DTD类型,则是使用
BeansDtdResolver
直接截取systemId后面的xx.dtd去当前路径下找,而如果加载了XSD类型,则是使用PluggableSchemaResolver
到META-INF/Spring.schemas
文件中找到systemId相对应的XSD文件进行加载。有兴趣的可以看下代码。
- 这里我们说下
0x04 解析并注册Bean
- 上面把加载xml文档说完了。到现在为止,我们已经拿到了解析后的Document对象。没错,接下来就是重头戏–解析并注册Bean。
- 在加载完xml后,随即就会执行
return registerBeanDefinitions(doc, resource);
,我们看下registerBeanDefinitions()
方法执行了什么逻辑。这里的doc参数就是上面我们经过重重分析后,1
2
3
4
5
6
7
8
9
10
11
12public int registerBeanDefinitions(Document doc, Resource resource) throws BeanDefinitionStoreException {
//默认创建DefaultBeanDefinitionDocumentReader
BeanDefinitionDocumentReader documentReader = createBeanDefinitionDocumentReader();
//设置环境
documentReader.setEnvironment(getEnvironment());
//返回注册表中定义的bean的数量。
int countBefore = getRegistry().getBeanDefinitionCount();
//从给定的DOM文档中读取bean定义,并在给定的阅读器上下文中向注册中心注册它们。
documentReader.registerBeanDefinitions(doc, createReaderContext(resource));
//记录本次注册的BeanDefinition的数量
return getRegistry().getBeanDefinitionCount() - countBefore;
}loadDocument
后得到的Document对象。这个方法里面最重要的步骤是documentReader.registerBeanDefinitions(doc, createReaderContext(resource));
,其中documentReader默认创建的是DefaultBeanDefinitionDocumentReader
,为什么这样说呢?大家点进去就知道了,这里就不过多描述。我们接下来看registerBeanDefinitions()
方法。 registerBeanDefinitions()
方法看字面意思就是注册bean的定义。他是怎么玩的呢?里面又有何种套路呢?他在这里获取了xml的root,然后继续去加载bean。1
2
3
4
5
6
7public void registerBeanDefinitions(Document doc, XmlReaderContext readerContext) {
this.readerContext = readerContext;
logger.debug("Loading bean definitions");
Element root = doc.getDocumentElement();
//加载bean的定义
doRegisterBeanDefinitions(root);
}- 接下来越来越到最核心了。是不是有点兴奋呢?经过这么多分析才到达这。来一起看看
doRegisterBeanDefinitions()
方法把可以看到,一开始就从bean中看是否配置了profile标签。对于profile标签,我这里就不细说了。处理了profile标签后,下面就真正到了解析我们的bean了。一起再来看看1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25protected void doRegisterBeanDefinitions(Element root) {
//从beans中的属性中看是否定义profile。使用profile可以方便的指定哪些配置是开发环境,哪些配置是生产环境
String profileSpec = root.getAttribute(PROFILE_ATTRIBUTE);
//如果定义了profile,则从环境变量中获取当前激活的环境
if (StringUtils.hasText(profileSpec)) {
//分隔
String[] specifiedProfiles = StringUtils.tokenizeToStringArray(
profileSpec, BeanDefinitionParserDelegate.MULTI_VALUE_ATTRIBUTE_DELIMITERS);
//与环境变量配置的active环境进行匹配。如果不一样。则忽略注册bean
if (!getEnvironment().acceptsProfiles(specifiedProfiles)) {
return;
}
}
BeanDefinitionParserDelegate parent = this.delegate;
this.delegate = createDelegate(this.readerContext, root, parent);
//解析前处理,这里preProcessXml是一个空方法,等子类去重写它
preProcessXml(root);
//解析bean
parseBeanDefinitions(root, this.delegate);
//解析后处理,这里postProcessXml是一个空方法,等子类去重写它
postProcessXml(root);
this.delegate = parent;
}parseBeanDefinitions()
方法把 parseBeanDefinitions()
源码如下:逻辑一览无余,首先判断他的namespace,是默认的还是自定义的。如果是默认的namespace,则使用默认的解析器,如果是自定义的,则使用自定义的命名空间解析。至于里面的实现,咱们下一章再聊~~~1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23protected void parseBeanDefinitions(Element root, BeanDefinitionParserDelegate delegate) {
//判断是不是默认的namespace
if (delegate.isDefaultNamespace(root)) {
NodeList nl = root.getChildNodes();//获取子节点列表 <bean></bean>节点
for (int i = 0; i < nl.getLength(); i++) {
Node node = nl.item(i);
if (node instanceof Element) {
Element ele = (Element) node;
//判断当前节点是否属于默认的namespace http://www.springframework.org/schema/beans
if (delegate.isDefaultNamespace(ele)) { //是默认的namespace,则使用默认的解析
parseDefaultElement(ele, delegate);
}
else { //否则使用自定的解析
delegate.parseCustomElement(ele);
}
}
}
}
else {
//自定义命名空间解析
delegate.parseCustomElement(root);
}
}
0x05 总结
- Spring源码深度解析第二章到此就结束了。在这章,我们可以知道简单的3行Spring使用代码,在底层做了些什么事,可谓是经历山路十八弯,最终终于柳暗花明又一村。
- 接下来就进入Spring源码深度解析第三章的阅读了。下一篇文章可能会有点晚。
- 本人才疏学浅,文章中难免会有不当或错误之处,望大家不吝指出。谢谢大家。
评论