Spring源码深度解析笔记(9)——Spring容器的基本实现

蹲街弑〆低调 提交于 2019-12-25 16:57:50

2.1 容器的基本用法

bean是Spring中最核心的东西,因为Spring就像一个大水桶,而bean就像容器中的水,水桶脱离了水也没什么用处。

2.2 功能分析

Spring创建对象的功能就是一下几点:

  1. 读取配置文件;
  2. 根据配置文件中的配置找到对应的类的配置,并实例化;
  3. 调用实例化后的实例。

如果想完成上述功能,至少需要三个类:

  1. ConfigReader:用于读取以及验证配置文件,然后放置在内存中;
  2. ReflectionUtil:用于根据配置文件中的配置进行反射实例化,
  3. App:用于完成整个逻辑的串联。

2.4 Spring的结构组成

2.4.1 beans包的层级结构

整个beans工程的源码包的功能如下:

  1. src/main/java:用于展示Spring的主逻辑;
  2. src/main/resource:用于存放系统的配置文件;
  3. src/test/java:用于主要逻辑进行单元测试;
  4. src/test/resource:用于存放测试用的配置文件。
2.4.2 核心类介绍
  1. DefaultListableBeanFactory:XmlBeanFactory继承自DefaultListableBeanFactory,而DefaultListableBeanFactory是整个Bean加载的核心部分,是Spring注册以及加载bean的默认实现,而对于XmlBeanFactory与DefaultListableBeanFactory不同的地方其实是在XmlBeanFactory中使用了自定义的XML读取器XmlBeanDefinitionReader,实现了个性化的BeanDefinitionReader读取,DefaultListableBeanFactory继承了AbstractAutowireCapableBeanFactory并实现了ConfigurableListableBeanFactory以及BeanDefinitionRegistry接口。
  2. XmlBeanDefinitionReader:XML配置文件的读取是Spring中重要的功能,因为Spring的大部分功能都是以配置文件作为切入点的XmlBeanDefinitionReader的主要功能就是资源文件读取、解析以及注册。

经过以上分析可以梳理出整个XML配置文件读取的大致力流程:

  1. 通过继承自AbstractBeanDefinitionReader中的方法,来使用RourceLoader将资源文件路径转换为对应的Resource文件;
  2. 通过DocumentLoader对Resource文件进行转换,将Resource文件转换为Document文件;
  3. 通过实现接口BeanDefinitionDocumentReader的DefaultBeanDefinitionDocumentReader类对Document进行解析,并使用BeanDefinitionParserDelegate对Element进行解析。

2.5 容器的基础XmlBeanFactory

在这里插入图片描述

2.5.1 配置文件封装

Spring的配置文件读取是通过ClassPathResource进行封装的,那么ClassPathResource完成了什么功能呢?
在Java中,将不同来源的资源抽象成URL,通过注册不同Handler来处理不同来源的资源读取逻辑,一般handle的类型使用不同前缀来识别,如“file:”、“http:”、“jar:”等,然而URL没有默认定义相对Classpath和ServletContext等资源的handle,虽然可以注册自己的URLStreamHandler来解析特定的URL前缀。因而Spring对其内部使用到的资源实现了自己的抽象结构:Resource接口来封装地底层资源。

InputStreamSource封装任何能返回InputStream的类,比如File、Classpath下的资源和ByteArray等。它只有一个方法定义:getInputStream(),该方法返回一个新的InputStream对象。

Resource接口抽象了所有Spring内部使用的底层资源:File、URL、Classpath等,首先,它定义了3个判断当前资源状态的方法:存在性(exist)、可读性(isReadable)、是否处于打开状态(isOpen)。另外,Resource接口还提供了不同资源到URL、URI、File类型的转换,以及获得lastModified属性、文件名的方法。为了便于操作,Resource会提供了基于当前资源创建一个相对资源的方法:createRelative()。在错误处理中需要详细的打印出错的资源文件,因而Resource还提供了getDescription()方法用于在错误处理中的打印时间。

对不同来源的资源文件都有相应的Resource实现:文件(FileSystemResource)、Classpath资源(ClasspathResource)、URL资源(UrlResource)、InputResource(InputStreamResource)、Byte数组(ByteArrayResource)等。

当通过Resource相关类完成了对配置文件进行封装后配置文件的读取工作就全权交给XmlBeanDefinitionReader来处理。XmlBeanFactory的初始化有若干方法中,this.reader
.loadBeanDefinitions(resource)才是资源加载的真正实现,但在XmlBeanDefinitionReader加载数据前还有一个父类构造函数初始化过程:super(parentBeanFactory),跟踪代码到父类AbstractAutowireCapableBeanFactory的构造函数。

2.5.2 加载Bean

之前提到的在XmlBeanFactory构造函数中调用了XmlBeanDefinitionReader类型Reader属性提供的方法this.reader.loadBeanDefinitions(resource)。

在这里插入图片描述
从上面的时序图中梳理整个处理过程如下:

  1. 封装资源文件。当进入XmlBeanDefinitionReader后首先对参数Resource使用EncodeResource类进行封装;
  2. 获取输入流。从Resource中获取对应的InputStream并构造InputSource;
  3. 通过构造的InputSource实例和Resource实例继续调用函数doLoadBeanDefinitions。

那么EncodeResource的作用是什么呢?通过名称,可以大致推断出这个类主要是用于对资源文件的编码进行处理的,其中的主要逻辑提现在getReader()方法中,当设置了编码属性的时候Spring会使用相应的编码作为输入流的编码。

当构造好encodeResource对象后,再次转如可复用方法loadBeanDefinitions(new EncodeResource(resource))。这个方法内部才是真正的数据准备阶段,也就是时序图所描述的逻辑。

再次整理一下数据准备阶段的逻辑,首先对出入的resource参数做封装,目的是考虑到Resource可能存在编码要求的情况,其次,通过SAX读取XML文件的方式来准备InputSource对象,最后将准备的数据通过参数传入真正核心处理部分doLoadBeanDefinitions(inputSource,encodeResource.getResource())。

该方法中如果不考虑异常类的代码,其实做了三件事,这三件事的没一件都必不可少。

  1. 获取对XML文件的验证模式;
  2. 加载XML文件,获取对应的Document;
  3. 根据返回的Document注册Bean。

2.6 获取XML的验证模式

XML文件的验证模式保证了XML文件的正确性,而表常用的验证模式有两种:DTD和XSD。它们之间有什么区别呢?

2.6.1 DTD与XSD区别

DTD(Document Type Definition)即文档类型定义,是一种XML约束模式语言,是XML文件的验证机制,属于XML文件组成的一部分。DTD是一种保证XML文档格式正确的有效方法,可以通过比较XML文档和DTD文件来看文档是否符合规范,元素和标签使用是否正确。一个DTD文件包含:元素的定义规范,元素间关系的定义规则,元素可使用的属性,可使用的实体或符号规则。

XML Schema语言就是XSD(XML Schema Definition)。XML Schema描述了XML文档的结构。可以用一个指定的XML Schema来验证一个XML文档,以检查XML文档是否符合要求。文档设计者可通过XML Schema指定一个XML文档所允许的结构和内容,并可据此检查一个XML文档是否是有效的。XML Schema本身是一个XML文档,它符合XML语法结构。可以通过XML解析器解析它。

在使用XML Schema文档对XML实例文档进行检验,除了要声明名称空间,还必须指定该名称空间所对应的XML Schema文档存储位置。通过schemaLocation属性指定名称空间所对应的XML Schema文档的存储位置,它包含两个部分,一部分是名称空间的URI,另一部分就是该名称空间所标识的XML Schema文件位置或URL地址。

2.6.2 验证模式的读取

了解了DTD和XSD的区别后再分析Spring中对于验证模式的提取就更容易理解了。通过之前的分析锁定了Spring通过getValidationModeForResource方法来获取对应资源的验证模式。

方法的实现其实很简单,无法是如果设定了验证模式则使用设定的验证模式(可以通过对调用XmlBeanDefinitionReader中的setValidationMode方法进行设定),否则使用自动检测的方法,而自动检测验证模式的功能是在函数detectValidationMode方法中实现的,在detectValidationMode函数中又将自动检测验证模式的工作委托给了专门处理类XmlValidationModeDetector,调用XmlValidationModeDetector的validationModeDetector方法。

只要理解了XSD和DTD的使用方法,Spring检测验证模式办法就是判断是否包含DOCTYPE,如果包含就是DTD,否则就是XSD。

2.7 获取Document

经过了验证模式准备步骤就可以进行Document加载了,同样XmlBeanFactoryReader对于文档读取并没有亲力亲为,而是委托给了DocumentLoader去执行,这里DocumentLoader只是一个接口,而真正调用的是DefaultDocumentLoader。

对于这部分代码其实并没有太对可以描述的,因为通过SAX解析XML文档的套路大致都差不多,Spring在这里没有什么特殊的地方,同样首先创建DocumentBuilderFactory,在通过DocumentBuilderFactory创建DocumentBuilder,进而解析inputSource返回Document对象。这里有必要提及一下EntityResolver,那么EntityResolver到底有什么作用呢?

2.7.1 EntityResolver用法

在loadDocument方法中涉及一个参数EntityResolver,何为EntityResolver?官方这样解释:如果SAX应用程序需要实现自定义处理外部实体,则必须实现此接口并使用setEntityResolver方法想SAX驱动器注册一个实例。也就是说,对于解析一个XML,SAX首先读取该XML文档上的声明,根据声明去寻找相应的DTD定义,以便对于文档进行一个验证。默认的寻找规则,即通过网络来下载DTD声明,并进行验证,下载过程是一个漫长的过程,而且当网络终端或不可用时,这里就会报错,就是因为相应的DTD声明没有找到的原因。

EntityResolver的作用是项目本身可以提供一个如何寻找DTD声明的方法,即有程序来实现寻找DTD声明的过程,比如将DTD文件放到项目中某处,在实现时之间此文档读取并返回给SAX即可,这样就避免了通过网络来寻找相应的声明。

2.8 解析及注册BeanDefinitions

当文件转换为Document后,接下来的提取以及注册bean就是重头戏,继续上面的分析,当程序已经拥有XML文档文件的Document实例对象时,就会引入registerBeanDefinition(Document doc,Resource resource)个方法,其中的参数doc是通过上一节loadDocument加载转换出来的。在这个方法中很好的应用了面向对象中单一职责的原则,将逻辑委托给单一的类进行处理,而这个逻辑处理类就是BeanDefinitionDocumentReader。BeanDefinitionDocumentReader是一个接口,而实例化的工作是在createBeanDefinitionDocumentReader()中完成,而通过此方法,BeanDefinitionDocumentReader真正类型其实已经是DefaultBeanDefinitionDocumentReader了,进入DefaultBeanDefinitionDocumentReader后,发行这个方法的重要目的之一就是提取root,以便于再次将root作为参数继续BeanDefinition的注册。即核心逻辑的底层doRegisterBeanDefinitions(root)。

如果说以前一直是XML加载解析的准备阶段,那么doRegisterBeanDefinitions算是真正的开始进行解析了,期待的核心部分真正开始了。

protected void doRegisterBeanDefinitions(Element root)
// 处理profile属性
// 专门处理解析
// 解析前处理,留给子类实现
parseBeanDefinition(root,this.detegate);
// 解析后处理,留个子类实现

通过以上代码看到了处理流程,首选是对profile的处理,然后开始进行解析,可是preProcessXml(root)或者postProcessXml(root)发现代码是空的,既然是空的写着还有什么用呢?就像面向对象设计方法学常说的一句话,一个类要么是面向继承的设计的,要么就用final修饰,在DefaultBeanDefinitionDocumentReader中并没有final修饰,所以它是面向继承而设计的。这两个方法正式为子类设计而设计的,这是模板方法模式,如果继承自DefaultBeanDefinitionDocumentReader的子类需要在Bean解析前后做一些处理的话,那么只需要重写这两个方法就可以了。

2.8.1 profile属性的使用

在注册Bean的最开始是对PROFILE_ATTRIBUTE属性的解析,可能对于我们说,profile并不是很常用,分析profile前我们先来了解一下profile的用法,该属性的主要作用是可以方便的进行切换开发、部署环境,最常用的就是更换不同的数据库。

了解了profile的使用再来分析代码就会清洗很多,首先获取beans节点是否定义了profile属性,如果定义了则需要到环境变量中去寻找,所以这里首先断言environment不可能为空,因为profile是可以同时指定多个的,需要程序对其拆分,并解析每个profile是都符合环境变量中所定义的,不定义则不会浪费性能去解析。

2.8.2 解析并注册BeanDefinition

处理了profile后就可以进行XML的读取了,跟踪代码进入parseBeanDefinitions(root,this.detegate)。

protected void parseBeanDefinitions(Elment root,BeanDefinitionParserDelegate detegate)
// 对beans的处理

上面的代码看起来逻辑还是蛮清晰的,因为在Spring的XML配置里面有两大类Bean声明,一个是默认的(< bean id=“test” class = “test.TestBean”/>),另一类是自定义的(< tx:annotation-driver/>),而这两种的读取和解析差别是非常大的,如果采用Spring默认的配置,Spring当然知道该怎么做,但是如果是自定义的,那么就需要用户实现一些接口及配置了。对于根节点或者子节点如果是默认命名空间的话则采用parseDfaultElment方法进行解析,否则使用delegate.paeseCustomElment方法对自定义名称空间进行解析。而判断是否默认命名空间还是自定义命名空间的办法其实是使用node.getNameSpaceURI()获取命名空间,并与Spring中固定的命名空间进行对比。如果一致则认为是默认,否则就认为是自定义。

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!