Spring容器的基本实现
通过Spring容器的设计及加载机制来探秘Spring,彻底揭开其神秘的面纱。
Spring上下文和容器
Core Container模块是Spring整个架构的根基,其核心概念是BeanFactory,也正是这个概念让Spring成为一个容器,帮助Spring管理Bean,并提供DI(依赖注入)功能来实现对Bean的依赖管理,使用配置方式来达到与业务代码及框架代码的分离。
Context模块即Spring上下文模块(也叫Spring Context模块),是Core Container模块的子模块,它让 Spring 真正成为一个可执行框架。这个模块扩展实现了 BeanFactory,让它不仅仅是 BeanFactory。笔者认为,Context 模块虽然是 BeanFactory 的实现者,但更是一个框架,这才是它的主要职责。这个模块为Spring的扩展和架构继承提供了非常多的可能,比如校验框架、调度框架、缓存框架、模板渲染框架,等等。
Spring上下文的设计
Spring Context模块是Spring Core Container模块中的Spring Context子模块,这个模块让Spring成为一个执行框架,而不仅仅是对BeanFactory概念的扩展。也正是Spring上下文的设计,让Spring可以提供很多企业级服务的定制。下面从Spring Context的核心类图设计入手,来详细讲解Spring上下文。
Spring容器的设计和实现是对抽象模板设计模式的灵活驾驭,其中大量使用了 Java 语言中的继承关键字来实现代码的高复用,若不能很好地掌握继承的用法,则非常有风险,比如子类重写了父类的方法实现,但是子类的子类仍然依赖这个基类(父类的父类)的原始实现,此时一旦重写程序,将要发生的变化则不可预估(因为它的子类可能还在依赖它的父类的实现逻辑,此时它的子类无法感知它的父类重写,所以无法预测子类的执行结果是什么样的),而且在继承层级较多时更有风险。而 Spring大神们用合理的分层职责划分及强大的抽象设计能力完美利用了继承的优点并且规避了风险,是我们使用继承设计的教科书式框架设计。
下面说说核心抽象类的职责。
- ApplicationContext 是整个容器的基本功能定义类,继承了 BeanFactory,这说明容器也是工厂的多态实现。其实它利用了代理的设计方法,内部持有一个 BeanFactory 实例,这个实例替它执行 BeanFactory 接口定义的功能,这种典型的代理模式应用也是非常巧妙的。
- AbstractApplicationContext是整个容器的核心处理类,是真正的Spring容器的执行者,在内部使用了模板方法,实现了高复用、高扩展,实现了Spring的启动、停止、刷新、事件推送、BeanFactory方法的默认实现及虚拟机回调的注册等。在本书中讲解Spring容器的加载方式时,会以这个类作为主线来讲解。
- GenericApplicationContext是Spring Context模块中最容易构建Spring环境的实体类,涵盖了Spring Context的核心功能,在不需要特殊定制的场景下可以实现开箱即用。如图2-1所示,AnnotationConfigApplicationContext完美利用了GenericApplicationContext的封装性和对外简便性,如果想扩展适合自己业务的轻量级 Spring 容器,使用GenericApplicationContext这个基类则会非常容易上手。AnnotationConfigApplicationContext的构造方法先传入一个 class 数组,再创建一个可执行的上下文实例来构造一个可运行的Spring运行环境,使用起来非常简便。
- AbstractRefreshableApplicationContext是 XmlWebApplicationContext的核心父类,如果当前上下文持有BeanFactory,则关闭当前BeanFactory,然后为上下文生命周期的下一个阶段初始化一个新的BeanFactory,并且在创建新容器时仍然保持对其父容器的引用。
- EmbeddedWebApplicationContext是在Spring Boot中新增的上下文实现类,是使用自嵌容器启动Web应用的核心上下文基类。
Spring Boot在启动时,启动Spring环境并使用内嵌Tomcat容器加载运行Web环境
容器基本用法
bean是Spring中最核心的东西,因为Spring就像是个大水桶,而bean就像是容器中的水,水桶脱离了水便也没什么用处了,那么我们先看看bean的定义。
public class MyTestBean {
private String testStr = "testStr";
public String getTestStr() {
return testStr;
}
public void setTestStr(String testStr) {
this.testStr = testStr;
}
}
这么看来bean并没有任何特别之处,的确,Spring的目的就是让我们的bean能成为一个纯粹的POJO,这也是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="bean.MyTestBean"/>
</beans>
在上面的配置中我们看到了bean的声明方式,尽管Spring中bean的元素定义着N种属性来支撑我们业务的各种应用,但是我们只要声明成这样,基本上就已经可以满足我们的大多数应用了。好了,你可能觉得还有什么,但是,真没了,Spring的入门示例到这里已经结束,我们可以写测试代码测试了。
@SuppressWarnings("deprecation")
public class BeanFactoryTest {
@Test
public void testSimpleLoad(){
BeanFactory bf = new XmlBeanFactory(new ClassPathResource("beanFactoryTest.xml"));
MyTestBean bean=(MyTestBean) bf.getBean("myTestBean");
assertEquals("testStr",bean.getTestStr());
}
}
直接使用BeanFactory作为容器对于Spring的使用来说并不多见,甚至是甚少使用,因为在企业级的应用中大多数都会使用的是ApplicationContext,这里只是用于测试,让读者更快更好地分析Spring的内部原理。
核心类介绍
DefaultListableBeanFactory XmlBeanFactory继承自DefaultListableBeanFactory,而DefaultListableBeanFactory是整个bean加载的核心部分,是Spring注册及加载bean的默认实现,而对于XmlBeanFactory与DefaultListableBeanFactory不同的地方其实是在XmlBeanFactory中使用了自定义的XML读取器XmlBeanDefinitionReader,实现了个性化的BeanDefinitionReader读取,DefaultListableBeanFactory继承了AbstractAutowireCapableBeanFactory并实现了ConfigurableListableBeanFactory以及BeanDefinitionRegistry接口。
- 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并对接口Autowire Capable BeanFactory进行实现。
- ConfigurableListableBeanFactory:BeanFactory配置清单,指定忽略类型及接口等。
- DefaultListableBeanFactory:综合上面所有功能,主要是对bean注册后的处理。
XmlBeanFactory对DefaultListableBeanFactory类进行了扩展,主要用于从XML文档中读取BeanDefinition,对于注册及获取bean都是使用从父类DefaultListableBeanFactory继承的方法去实现,而唯独与父类不同的个性化实现就是增加了XmlBeanDefinitionReader类型的reader属性。在XmlBeanFactory中主要使用reader属性对资源文件进行读取和注册。
XmlBeanDefinitionReader
XML配置文件的读取是Spring中重要的功能,因为Spring的大部分功能都是以配置作为切入点的,那么我们可以从XmlBeanDefinitionReader中梳理一下资源文件读取、解析及注册的大致脉络,首先我们看看各个类的功能。
- ResourceLoader:定义资源加载器,主要应用于根据给定的资源文件地址返回对应的Resource。
- BeanDefinitionReader:主要定义资源文件读取并转换为BeanDefinition的各个功能。
- EnvironmentCapable:定义获取Environment方法。
- DocumentLoader:定义从资源文件加载到转换为Document的功能。
- AbstractBeanDefinitionReader:对EnvironmentCapable、BeanDefinitionReader类定义的功能进行实现。
- BeanDefinitionDocumentReader:定义读取Docuemnt并注册BeanDefinition功能。
- BeanDefinitionParserDelegate:定义解析Element的各种方法。
配置文件读取 通过继承自AbstractBeanDefinitionReader中的方法,来使用ResourLoader将资源文件路径转换为对应的Resource文件。 通过DocumentLoader对Resource文件进行转换,将Resource文件转换为Document文件。 通过实现接口BeanDefinitionDocumentReader的DefaultBeanDefinitionDocumentReader类对Document进行解析,并使用BeanDefinitionParserDelegate对Element进行解析。
容器的基础XmlBeanFactory
BeanFactory bf = new XmlBeanFactory(new ClassPathResource("beanFactoryTest.xml"));
时序图从BeanFactoryTest测试类开始,通过时序图我们可以一目了然地看到整个逻辑处理顺序。在测试的BeanFactoryTest中首先调用ClassPathResource的构造函数来构造Resource资源文件的实例对象,这样后续的资源处理就可以用Resource提供的各种服务来操作了,当我们有了Resource后就可以进行XmlBeanFactory的初始化了。那么Resource资源是如何封装的呢?
配置文件封装
Spring的配置文件读取是通过ClassPathResource进行封装的,如new ClassPathResource (“beanFactoryTest.xml”),那么ClassPathResource完成了什么功能呢?
在Java中,将不同来源的资源抽象成URL,通过注册不同的handler(URLStreamHandler)来处理不同来源的资源的读取逻辑,一般handler的类型使用不同前缀(协议,Protocol)来识别,如“file:”“http:”“jar:”等,然而URL没有默认定义相对Classpath或ServletContext等资源的handler,虽然可以注册自己的URLStreamHandler来解析特定的URL前缀(协议),比如“classpath:”,然而这需要了解URL的实现机制,而且URL也没有提供基本的方法,如检查当前资源是否存在、检查当前资源是否可读等方法。因而Spring对其内部使用到的资源实现了自己的抽象结构:Resource接口封装底层资源。
public interface InputStreamSource {
InputStream getInputStream() throws IOException;
}
public interface Resource extends InputStreamSource {
boolean exists();
boolean isReadable();
boolean isOpen();
URL getURL() throws IOException;
URI getURI() throws IOException;
File getFile() throws IOException;
long lastModified() throws IOException;
Resource createRelative(String relativePath) throws IOException;
String getFilename();
String getDescription();
}
InputStreamSource封装任何能返回InputStream的类,比如File、Classpath下的资源和Byte Array等。它只有一个方法定义:getInputStream(),该方法返回一个新的InputStream对象。
Resource接口抽象了所有Spring内部使用到的底层资源:File、URL、Classpath等。首先,它定义了3个判断当前资源状态的方法:存在性(exists)、可读性(isReadable)、是否处于打开状态(isOpen)。另外,Resource接口还提供了不同资源到URL、URI、File类型的转换,以及获取lastModified属性、文件名(不带路径信息的文件名,getFilename())的方法。为了便于操作,Resource还提供了基于当前资源创建一个相对资源的方法:createRelative()。在错误处理中需要详细地打印出错的资源文件,因而Resource还提供了getDescription()方法用来在错误处理中打印信息。
对不同来源的资源文件都有相应的Resource实现:文件(FileSystemResource)、Classpath资源(ClassPathResource)、URL资源(UrlResource)、InputStream资源(InputStreamResource)、Byte数组(ByteArrayResource)等。
在日常的开发工作中,资源文件的加载也是经常用到的,可以直接使用Spring提供的类,比如在希望加载文件时可以使用以下代码:
Resource resource=new ClassPathResource("beanFactoryTest.xml");
InputStream inputStream=resource.getInputStream();
得到inputStream后,我们就可以按照以前的开发方式进行实现了,并且我们可以利用Resource及其子类为我们提供的诸多特性。
有了Resource接口便可以对所有资源文件进行统一处理。至于实现,其实是非常简单的,以getInputStream为例,ClassPathResource中的实现方式便是通过class或者classLoader提供的底层方法进行调用,而对于FileSystemResource的实现其实更简单,直接使用FileInputStream对文件进行实例化。
ClassPathResource.java
if (this.clazz != null) {
is = this.clazz.getResourceAsStream(this.path);
}else {
is = this.classLoader.getResourceAsStream(this.path);
}
FileSystemResource.java
public InputStream getInputStream() throws IOException {
return new FileInputStream(this.file);
}
当通过Resource相关类完成了对配置文件进行封装后配置文件的读取工作就全权交给XmlBeanDefinitionReader 来处理了。
了解了Spring中将配置文件封装为Resource类型的实例方法后,我们就可以继续探寻XmlBeanFactory的初始化过程了,XmlBeanFactory的初始化有若干办法,Spring中提供了很多的构造函数,在这里分析的是使用Resource实例作为构造函数参数的办法,代码如下: XmlBeanFactory.java
public XmlBeanFactory(Resource resource) throws BeansException {
//调用XmlBeanFactory(Resource,BeanFactory)构造方法
this(resource, null);
}
构造函数内部再次调用内部构造函数:
//parentBeanFactory为父类BeanFactory用于factory合并,可以为空
public XmlBeanFactory(Resource resource, BeanFactory parentBeanFactory) throws
BeansException {
super(parentBeanFactory);
this.reader.loadBeanDefinitions(resource);
}
上面函数中的代码this.reader.loadBeanDefinitions(resource) 才是资源加载的真正实现,也是我们分析的重点之一。我们可以看到时序图中提到的XmlBeanDefinitionReader加载数据就是在这里完成的,但是在XmlBeanDefinitionReader加载数据前还有一个调用父类构造函数初始化的过程:super(parentBeanFactory),跟踪代码到父类AbstractAutowireCapableBeanFactory的构造函数中:
public AbstractAutowireCapableBeanFactory() {
super();
ignoreDependencyInterface(BeanNameAware.class);
ignoreDependencyInterface(BeanFactoryAware.class);
ignoreDependencyInterface(BeanClassLoaderAware.class);
}
这里有必要提及ignoreDependencyInterface方法。ignoreDependencyInterface的主要功能是忽略给定接口的自动装配功能,那么,这样做的目的是什么呢?会产生什么样的效果呢?
举例来说,当A中有属性B,那么当Spring在获取A的Bean的时候如果其属性B还没有初始化,那么Spring会自动初始化B,这也是Spring中提供的一个重要特性。但是,某些情况下,B不会被初始化,其中的一种情况就是B实现了BeanNameAware接口。Spring中是这样介绍的:自动装配时忽略给定的依赖接口,典型应用是通过其他方式解析Application上下文注册依赖,类似于BeanFactory通过BeanFactoryAware进行注入或者ApplicationContext通过ApplicationContextAware进行注入。
加载Bean
在XmlBeanFactory构造函数中调用了XmlBeanDefinitionReader类型的reader属性提供的方法this.reader.loadBeanDefinitions(resource),而这句代码则是整个资源加载的切入点。 处理过程如下。
- 封装资源文件。当进入XmlBeanDefinitionReader后首先对参数Resource使用EncodedResource类进行封装。
- 获取输入流。从Resource中获取对应的InputStream并构造InputSource。
- 通过构造的InputSource实例和Resource实例继续调用函数doLoadBeanDefinitions。