开篇
本文主要基于SpringFramework5.2.0.RELEASE
版本,源码的下载步骤在别的文章中已经讲过,这里就不再赘述。
容器的基本用法
我们先创建一个简单的示例来看一下容器的基本用法。
创建一个简单的 Java Bean。
/** * @author 神秘杰克 * 公众号: Java菜鸟程序员 * @date 2022/3/15 * @Description 简单的bean实例 */ public class MyTestBean { private String testStr = "testStr"; public String getTestStr() { return testStr; } public void setTestStr(String testStr) { this.testStr = testStr; } }
创建一个简单 Spring 配置文件。
<?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="cn.jack.MyTestBean"/> </beans>
ok,编写一个测试类进行测试。
/** * @author 神秘杰克 * 公众号: Java菜鸟程序员 * @date 2022/3/15 * @Description 测试类 */ @SuppressWarnings("deprecation") public class BeanFactoryTest { @Test public void testSimpleLoad(){ final BeanFactory beanFactory = new XmlBeanFactory(new ClassPathResource("beanFactoryTest.xml")); final MyTestBean myTestBean = (MyTestBean) beanFactory.getBean("myTestBean"); assertEquals("testStr",myTestBean.getTestStr()); } }
运行之后可以看到执行成功,示例就这么简单。
平常我们并不会直接使用 BeanFactory,而是使用 ApplicationContext,这里只是为了之后可以更好地分析 Spring 原理。
功能分析
这部分代码完成的功能如下:
- 读取配置文件 beanFactoryTest.xml
- 根据配置文件中的配置找到对应类的配置,并实例化
- 调用实例化后的实例
按照这些描述,我们可以了解需要至少三个类来实现。
ConfigReader
:用于读取及验证配置文件,随后放到内存中ReflectionUtil
:根据配置文件内容,进行反射实例化,也就是我们配置文件中的<bean id="myTestBean" class="cn.jack.MyTestBean"/>
App
:用于完成整个逻辑的串联
按照原始的思维方式,整个过程就是这样,但是这么优秀的 Spring 框架的结构组成是什么样子?
Spring 的层级架构
我们首先尝试梳理 Spring 的框架结构,用全局的角度了解 Spring 的构成。
Beans 包的层级架构
实现刚才的示例,我们主要使用的就是 org.springframework.beans.jar。
分析源码前,我们首先了解两个核心类。
1.DefaultListableBeanFactory
我们刚才使用的 XmlBeanFactory 继承自DefaultListableBeanFactory
,而 DefaultListableBeanFactory 是整个加载的核心部分,是 Spring 注册及加载 bean 的默认实现,而在 XmlBeanFactory 中使用了自定义的 XML 读取器XmlBeanDefinitionReader
,实现了个性化的 BeanDefinitionReader 读取。
DefaultListableBeanFactory 继承了 AbstractAutoWireCapableBeanFactory 并实现了 ConfigurableListableBeanFactory 以及 BeanDefinitionRegistry 接口。
从该类图我们可以清晰了解 DefaultListableBeanFactory 的脉络,我们先简单了解一下各个类的作用。
AliasRegistry
:定义对 alias 的简单增删改操作SimpleAliasRegistry
:主要使用 map 作为 alias 缓存,并对接口 AliasRegistry 进行实现SingletonBeanRegistry
:定义对单例的注册和获取BeanFactory
:定义获取 bean 和 bean 的各种属性DefaultSingletonBeanRegistry
:对接口 SingletonBeanRegistry 的各个函数实现HierarchicalBeanFactory
:继承 BeanFactory,在 BeanFactory 定义的功能基础上增加了对 parentFactory 的支持BeanDefinitionRegistry
:定义对 BeanDefinition 的各种增删改操作FactoryBeanRegistrySupport
:在 DefaultSingletonBeanRegistry 基础上增加了对 FactoryBean 的特殊处理功能ConfigurableBeanFactory
:提供配置 Factory 的各种方法ListableBeanFactory
:根据各种条件获取 bean 的配置清单AbstractBeanFactory
:综合了 FactoryBeanRegistrySupport、ConfigurableBeanFactory 的功能AutowireCapableBeanFactory
:提供创建 bean、自动注入、初始化以及应用 bean 的后处理器AbstractAutowireCapableBeanFactory
:综合 AbstractBeanFactory 功能并对接口 AutowireCapableBeanFactory 进行实现ConfigurableListableBeanFactory
:BeanFactory 配置清单,指定忽略类型及接口等DefaultListableBeanFactory
:综合上面所有功能,主要对 bean 注册后的处理
XmlBeanFactory 继承 DefaultListableBeanFactory 进行了扩展,主要用于从 XML 中读取 BeanDefinition,对于注册和获取 bean 都是使用从父类 DefaultListableBeanFactory 继承的方法中实现,与父类不同的是 XmlBeanFactory 增加了 XmlBeanDefinitionReader 类型的 reader 属性,进行对资源文件的读取和注册。
2.XmlBeanDefinitionReader
XML 配置文件的读取是 Spring 中重要的功能,我们可以从 XmlBeanDefinitionReader 中梳理一下资源文件读取、解析及注册的大致脉络。
我们首先看一下各个类的功能:
ResourceLoader
:定义资源加载器,主要应用于根据给定的资源文件地址返回对应的 ResourceBeanDefinitionReader
:主要定义资源文件读取并转换为 BeanDefinition 的各个功能EnvironmentCapable
:定义获取 Environment 方法DocumentLoader
:定义从资源文件加载到转换为 Document 的功能AbstractBeanDefinitionReader
:对 EnvironmentCapable、BeanDefinitionReader 类定义的方法进行实现BeanDefinitionDocumentReader
:定义读取 Document 并注册 BeanDefinition 功能BeanDefinitionParserDelegate
:定义解析 Element 的各种方法
经过上面的分析,我们可以梳理出 XML 配置文件读取的大概流程。
- 通过继承自 AbstractBeanDefinitionReader 中的方法,来使用 ResourLoader 将资源文件路径转换为对应的 Resource 文件
- 通过 DocumentLoader 对 Resource 文件进行转换,将 Resource 文件转换为 Document 文件
- 通过实现接口 BeanDefinitionDocumentReader 的 DefaultBeanDefinitionDocumentReader 类对 Document 进行解析,并使用 BeanDefinitionParserDelegate 对 Element 进行解析
容器的基础 XmlBeanFactory
在有了对 Spring 容器的大致了解后,我们接下来分析下面代码的实现。
final BeanFactory beanFactory = new XmlBeanFactory(new ClassPathResource("beanFactoryTest.xml"));
通过时序图我们可以看到首先调用了 ClassPathResource 的构造函数来构造 Resource 资源文件的实例对象,之后的资源处理就可以用 Resource 提供的服务来操作了,之后就可以进行对 XmlBeanFactory 进行初始化了,接下来我们看一下 Resource 资源是如何封装的。
配置文件封装
首先读取文件是通过 ClassPathResource 进行封装的,比如new ClassPathResource("beanFactoryTest.xml")
,那么 ClassPathResource 完成了什么功能?
在 Java 中,将不同来源的资源抽象为 URL,然后注册不同的 handler(URLStreamHandler)来处理不同资源的读取逻辑,但是 URL 没有默认定义相对 Classpath 或 ServletContext 等资源的 handler,虽然可以通过注册自己的 URLStreamHandler 来解析特定的 URL 前缀协议,然而这需要了解 URL 的实现机制,而且 URL 也没有提供基本的方法(比如资源是否可读、是否存在等)。因而在 Spring 中对其内部使用到的资源实现了自己的抽象结构,Resource 接口封装底层资源。
public interface InputStreamSource { InputStream getInputStream() throws IOException; }
public interface Resource extends InputStreamSource { boolean exists(); default boolean isReadable() { return exists(); } default boolean isOpen() { return false; } default boolean isFile() { return false; } URL getURL() throws IOException; URI getURI() throws IOException; File getFile() throws IOException; default ReadableByteChannel readableChannel() throws IOException { return Channels.newChannel(getInputStream()); } long contentLength() throws IOException; long lastModified() throws IOException; Resource createRelative(String relativePath) throws IOException; @Nullable String getFilename(); String getDescription(); }
InputStreamSource 中只有一个方法 getInputStream(),返回一个新的 InputStream 对象。
Resource 接口抽象了 Spring 内部使用到的底层资源,File、URL、Classpath 等。定义了 4 个判断资源状态的方法:exists()、isReadable()、isOpen()、isFile(),另外,Resource 接口也提供了不同资源到 URL、URI、File 类型的转换。
createRelative()方法可以基于当前资源创建一个相对资源的方法。
getDescription()方法用来在错误处理中打印信息。
大致了解了 Spring 中将配置文件封装为 Resource 类型的实例方法后,我们继续看 XmlBeanFactory 的初始化过程,我们这里通过使用 Resource 实例作为构造函数参数的方法。
public XmlBeanFactory(Resource resource) throws BeansException { this(resource, null); }
public XmlBeanFactory(Resource resource, BeanFactory parentBeanFactory) throws BeansException { super(parentBeanFactory); this.reader.loadBeanDefinitions(resource); }
上面的方法中this.reader.loadBeanDefinitions(resource);
就是资源加载的真正实现,也是分析的重点之一。
我们上面的时序图 3.1 就是这里完成的。在调用到该方法之前,我们还要调用父类构造器进行初始化。
我们直接跟踪到父类 AbstractAutowireCapableBeanFactory 的构造函数中:
public AbstractAutowireCapableBeanFactory() { super(); ignoreDependencyInterface(BeanNameAware.class); ignoreDependencyInterface(BeanFactoryAware.class); ignoreDependencyInterface(BeanClassLoaderAware.class); }
ignoreDependencyInterface 的主要功能是忽略给定接口的自动装配功能。为什么要这样做?
比如:当 A 中有属性 B,当 Spring 在获取 A 的 bean 的时候如果 B 还没初始化,那么 Spring 会自动初始化 B,但是在某些情况下,B 不会被初始化,其中一个情况就是 B 实现了 BeanNameAware 接口。
Spring 中这样介绍的:自动装配时忽略给定的依赖接口,典型应用就是通过其他方式解析 Application 上下文注册依赖,类似于 BeanFactory 通过 BeanFactoryAware 进行注入或者 ApplicationContext 通过 ApplicationContextAware 进行注入。
加载 Bean
回到刚才,我们在 XmlBeanFactory 构造函数中调用了this.reader.loadBeanDefinitions(resource);
这句代码就是整个资源加载的入口,我们看一下这个方法的时序图。
我们尝试梳理一下处理过程:
- 使用 EncodedResource 类对参数 Resource 资源文件进行封装
- 从 Resource 中获取对应的 InputStream,随后构造 InputSource
- 随后使用构造的 InputSource 实例和 Resource 实例继续调用 doLoadBeanDefinitions 方法
接下来看一下 loadBeanDefinitions 方法具体实现过程。
public int loadBeanDefinitions(Resource resource) throws BeanDefinitionStoreException { return loadBeanDefinitions(new EncodedResource(resource)); }
首先我们看一下 EncodedResource 的作用是什么,通过名字可以看出来是对资源文件进行编码处理,主要逻辑就在其中的getReader()方法中,如果设置了编码属性,Spring 会使用相应的编码作为输入流的编码。
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()); } }
该方法构造了一个 InputStreamReader。当构造完 EncodedResource 之后,调用了 loadBeanDefinitions 重载方法。、
该方法内部就是真正的数据准备阶段了。
public int loadBeanDefinitions(EncodedResource encodedResource) throws BeanDefinitionStoreException { Assert.notNull(encodedResource, "EncodedResource must not be null"); if (logger.isTraceEnabled()) { logger.trace("Loading XML bean definitions from " + encodedResource); } //resourcesCurrentlyBeingLoaded是一个ThreadLocal,里面存放着Resource类的set集合 Set<EncodedResource> currentResources = this.resourcesCurrentlyBeingLoaded.get(); if (currentResources == null) { currentResources = new HashSet<>(4); this.resourcesCurrentlyBeingLoaded.set(currentResources); } //如果set中已有这个元素则返回false并抛出异常 if (!currentResources.add(encodedResource)) { throw new BeanDefinitionStoreException( "Detected cyclic loading of " + encodedResource + " - check your import definitions!"); } try { //从encodedResource中获取已经封装的Resource对象并再次从Resource中获取InputStream InputStream inputStream = encodedResource.getResource().getInputStream(); try { //准备解析xml文件,全路径为org.xml.sax.InputSource 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 { //资源加载完毕,移除该Resource currentResources.remove(encodedResource); //如果没有其他资源了,则remove if (currentResources.isEmpty()) { this.resourcesCurrentlyBeingLoaded.remove(); } } }
该方法首先将传入的 Resource 参数进行封装,目的是为了考虑到 Resource 可能存在编码要求的情况,其次,通过 SAX 读取 XML 文件的方式来创建 InputSource 对象,最后将参数传入到核心处理部分
doLoadBeanDefinitions(inputSource,encodedResource.getResource())
protected int doLoadBeanDefinitions(InputSource inputSource, Resource resource) throws BeanDefinitionStoreException { try { Document doc = doLoadDocument(inputSource, resource); int count = registerBeanDefinitions(doc, resource); if (logger.isDebugEnabled()) { logger.debug("Loaded " + count + " bean definitions from " + resource); } return count; } catch (BeanDefinitionStoreException ex) { throw ex; } // ...省略catch }
该方法主要做三件事:
- 获取对 XML 文件的验证模式
- 加载 XML 文件,得到对应的 Document
- 根据返回的 Document 注册 Bean 信息
我们一步步看,从 XML 验证模式开始。
获取 XML 的验证模式
比较常用的验证 XML 正确性有两种:DTD 和 XSD。
DTD(Document Type Definition)即文档类型定义,是一种 XML 约束模式语言,是 XML 文件的验证机制。
DTD 即文档类型定义,是一种 XML 约束模式语言,是 XML 文件的验证机制,属于 XML 文件组成的一部分。
DTD 是一种保证 XML 文档格式正确的有效方法,可以通过比较 XML 文档和 DTD 文件来看文档是否符合规范,元素和标签使用是否正确。 一个 DTD 文档包含:元素的定义规则,元素间关系的定义规则,元素可使用的属性,可使用的实体或符号规则。
DTD 和 XSD 相比:DTD 是使用非 XML 语法编写的。
DTD 不可扩展,不支持命名空间,只提供非常有限的数据类型。
XSD(XML Schemas Definition),即 XML Schema 语言,针对 DTD 的缺陷有 W3C 在 2001 年推出。XML Schema 本身就是一个 XML 文档,使用的是 XML 语法,因此可以很方便地解析 XSD 文档。
相对于 DTD, XSD 具有如下优势:
- XML Schema 基于 XML,没有专门的语法
- XML Schema 可以像其他 XML 文件一样解析和处理
- XML Schema 相比于 DTD 提供了更丰富的数据类型
- XML Schema 提供可扩展的数据模型
- XML Schema 支持综合命名空间
- XML Schema 支持属性组
在 Spring 源码中,基于 XML 文件配置 Bean 的验证模式,一般情况下是 XSD 模式。
验证模式的读取
protected int getValidationModeForResource(Resource resource) { int validationModeToUse = getValidationMode(); //如果手动指定了验证模式则使用指定的验证模式 if (validationModeToUse != VALIDATION_AUTO) { return validationModeToUse; } // 如果未指定则使用自动检测 int detectedMode = detectValidationMode(resource); if (detectedMode != VALIDATION_AUTO) { return detectedMode; } return VALIDATION_XSD; }
Spring 检测验证模式就是通过判断是否包含 DOCTYPE,包含就是 DTD,否则是 XSD。
获取 Document
经过验证模式准备后,就可以进行 Document 加载了,这里的 documentLoader 是一个接口,真正实现是下面的 DefaultDocumentLoader。
protected Document doLoadDocument(InputSource inputSource, Resource resource) throws Exception { return this.documentLoader.loadDocument(inputSource, getEntityResolver(), this.errorHandler, getValidationModeForResource(resource), isNamespaceAware()); }
@Override public Document loadDocument(InputSource inputSource, EntityResolver entityResolver, ErrorHandler errorHandler, int validationMode, boolean namespaceAware) throws Exception { //创建文档构建器工厂对象,并初始化一些属性 //如果验证模式为XSD,那么强制支持XML名称空间,并加上schema属性 DocumentBuilderFactory factory = createDocumentBuilderFactory(validationMode, namespaceAware); if (logger.isTraceEnabled()) { logger.trace("Using JAXP provider [" + factory.getClass().getName() + "]"); } DocumentBuilder builder = createDocumentBuilder(factory, entityResolver, errorHandler); //按照XML文档解析给定inputSource的内容,然后返回一个新的DOM对象 return builder.parse(inputSource); }
这段代码主要创建了一个 DocumentBuilderFactory 实例,再通过 DocumentBuilderFactory 创建了一个 DocumentBuilder,最后解析 inputSource 返回 Document 对象。
解析及注册 BeanDefinitions
我们再回到 doLoadBeanDefinitions 方法,拿到 Document 对象后,我们就可以注册 bean 对象,调用 registerBeanDefinitions 方法。
public int registerBeanDefinitions(Document doc, Resource resource) throws BeanDefinitionStoreException { //实例化BeanDefinitionDocumentReader BeanDefinitionDocumentReader documentReader = createBeanDefinitionDocumentReader(); //记录统计前BeanDefinition的加载个数 int countBefore = getRegistry().getBeanDefinitionCount(); //加载及注册bean(关键) documentReader.registerBeanDefinitions(doc, createReaderContext(resource)); //记录本次加载个数 return getRegistry().getBeanDefinitionCount() - countBefore; }
调用 registerBeanDefinitions 方法,选择实现类为DeDefaultBeanDefinitionDocumentReader
。
@Override public void registerBeanDefinitions(Document doc, XmlReaderContext readerContext) { this.readerContext = readerContext; //拿到了xml文档对象的根元素 并调用该方法 doRegisterBeanDefinitions(doc.getDocumentElement()); }
doRegisterBeanDefinitions 开始真正地解析。
protected void doRegisterBeanDefinitions(Element root) { // 任何被嵌套的<beans>元素都会导致此方法的递归。为了正确的传播和保存<beans>的默认属性、 // 保持当前(父)代理的跟踪,它可能为null // 为了能够回退,新的(子)代理具有父的引用,最终会重置this.delegate回到它的初始(父)引用。 // 这个行为模拟了一堆代理,但实际上并不需要一个代理 BeanDefinitionParserDelegate parent = this.delegate; //代码(1) this.delegate = createDelegate(getReaderContext(), root, parent); //默认名称空间是"http://www.springframework.org/schema/beans" if (this.delegate.isDefaultNamespace(root)) { String profileSpec = root.getAttribute(PROFILE_ATTRIBUTE); //在xml配置文件中对profile的设置 区分是生产环境还是线上环境 if (StringUtils.hasText(profileSpec)) { String[] specifiedProfiles = StringUtils.tokenizeToStringArray( profileSpec, BeanDefinitionParserDelegate.MULTI_VALUE_ATTRIBUTE_DELIMITERS); if (!getReaderContext().getEnvironment().acceptsProfiles(specifiedProfiles)) { if (logger.isDebugEnabled()) { logger.debug("Skipped XML bean definition file due to specified profiles [" + profileSpec + "] not matching: " + getReaderContext().getResource()); } return; } } } //解析前处理,留给子类实现 preProcessXml(root); //生成BeanDefinition,并注册在工厂中,代码(2) parseBeanDefinitions(root, this.delegate); //解析后处理,留给子类实现 postProcessXml(root); this.delegate = parent; }
我们先看一下代码(1)的方法,该方法创建了一个 BeanDefinitionParserDelegate 对象,该对象是对 XML 中属性值解析的委派。
protected BeanDefinitionParserDelegate createDelegate( XmlReaderContext readerContext, Element root, @Nullable BeanDefinitionParserDelegate parentDelegate) { BeanDefinitionParserDelegate delegate = new BeanDefinitionParserDelegate(readerContext); delegate.initDefaults(root, parentDelegate); return delegate; }
我们看下 BeanDefinitionParserDelegate 类的常量。
public class BeanDefinitionParserDelegate { public static final String BEANS_NAMESPACE_URI = "http://www.springframework.org/schema/beans"; public static final String MULTI_VALUE_ATTRIBUTE_DELIMITERS = ",; "; /** * Value of a T/F attribute that represents true. * Anything else represents false. Case seNsItive. */ public static final String TRUE_VALUE = "true"; public static final String FALSE_VALUE = "false"; public static final String DEFAULT_VALUE = "default"; public static final String DESCRIPTION_ELEMENT = "description"; public static final String AUTOWIRE_NO_VALUE = "no"; public static final String AUTOWIRE_BY_NAME_VALUE = "byName"; public static final String AUTOWIRE_BY_TYPE_VALUE = "byType"; public static final String AUTOWIRE_CONSTRUCTOR_VALUE = "constructor"; public static final String AUTOWIRE_AUTODETECT_VALUE = "autodetect"; public static final String NAME_ATTRIBUTE = "name"; public static final String BEAN_ELEMENT = "bean"; public static final String META_ELEMENT = "meta"; public static final String ID_ATTRIBUTE = "id"; public static final String PARENT_ATTRIBUTE = "parent"; //... }
我们发现 Spring 配置文件的属性全都在这里 现在我们知道 BeanDefinitionParserDelegate 对象确实是来对 XML 配置文件的解析后,继续回到 createDelegate 方法,创建了 BeanDefinitionParserDelegate 对象后,还执行了 initDefaults 方法,来初始化一些默认值。
解析并注册 BeanDefinition
现在我们回到代码(2),进入 parseBeanDefinitions 方法。
protected void parseBeanDefinitions(Element root, BeanDefinitionParserDelegate delegate) { //对beans的处理,默认名称空间是"http://www.springframework.org/schema/beans" if (delegate.isDefaultNamespace(root)) { //获取根元素下的子Node NodeList nl = root.getChildNodes(); for (int i = 0; i < nl.getLength(); i++) { Node node = nl.item(i); if (node instanceof Element) { //拿到了<beans>下的子标签 Element ele = (Element) node; if (delegate.isDefaultNamespace(ele)) { //如果该标签属于beans的名称空间,则进入这个方法 //xmlns="http://www.springframework.org/schema/beans" parseDefaultElement(ele, delegate); } else { //如果该标签属于其他的名称空间比如:context,aop等 //xmlns:aop="http://www.springframework.org/schema/aop" //xmlns:context="http://www.springframework.org/schema/context" delegate.parseCustomElement(ele); } } } } else { delegate.parseCustomElement(root); } }
在 Spring 中的 XML 分为两大类,一种是默认的如下:
<bean id="test" class="cn.jack.Test"/>
另一种就是自定义的:
<tx:annotation-driven/>
如果是自定义实现的话,则需要用户实现自定义配置,如果根节点或者节点是使用默认命名的话则使用 parseDefaultElement 进行解析,否则使用 delegate.parseCustomElement 方法对自定义命名空间进行解析。
而判断是默认命名还是自定义命名空间则使用 isDefaultNamespace 方法中的 node.getNamespaceURI()获取命名空间,随后与固定的命名空间http://www.springframework.org/schema/beans
进行对比,不一致则为自定义命名空间。
对于默认标签解析与自定义标签解析则在下一篇文章中。