1. 前言
在 Spring 框架中,context 模块的定位是一个门面,对内整合了 core、beans、context、expression 等基础模块,隐藏了底层的实现细节;对外负责与用户(开发者)打交道,提供一系列强大易用的功能。在实际使用中,开发者主要与 context 模块的 API 打交道,只有很少的操作可能会涉及到底层模块。
本章的内容比较多,大体可以分为以下四个部分:
- 初步介绍
ApplicationContext
的继承体系和基本实现,包括资源加载和解析、组件加载的功能。 - 在拥有加载组件的基本功能后,
ApplicationContext
还实现了灵活多样的加载方式,这一过程是由配置类完成的。 - 配置类的主要作用是加载组件,除此之外还有一些常用的功能,比如条件判定、生命周期管理、事件机制等。在实际开发中,这些都是强大易用的功能。
- 对于 Spring 容器来说,对对象进行处理是必不可少了,因此数据处理也是很重要的部分。我们主要介绍了格式化、数据校验和
DataBinder
。
2. ApplicationContext
2.1 概述
Spring 容器的概念有狭义和广义之分,狭义上指的是 BeanFactory
,广义上指的是 ApplicationContext
。不论是狭义的还是广义的,它们都是各自模块的核心接口。ApplicationContext
与 BeanFactory
之间不是简单的组合,而是通过「装饰模式」联系在一起的。装饰模式有两大特点,一是扩展原对象的功能 ,二是以组合代替继承的结构。
装饰模式的结构包含四个角色,ApplicationContext
的继承结构中也有对应的接口和类,简单分析对比如下:
- 抽象组件角色:
BeanFactory
定义了管理Bean
的相关方法,比如getBean
等方法 - 具体组件角色:
DefaultListableBeanFactory
实现了getBean
等方法 - 抽象装饰角色:
ApplicationContext
继承了BeanFactory
接口,此外还定义其他扩展功能 - 具体装饰角色:
GenericApplication
实现了ApplicationContext
接口,持有一个组件类DefaultListableBeanFactory
2.2 Spring 应用分类
在 ApplicationContext
的众多实现类中,我们可以从两个维度来划分:
- 按应用类型划分:一是普通应用,二是 web 应用(进一步可分为传统 web 应用和内嵌 web 应用)
- 按配置方式划分:一是配置文件,二是注解声明
目前主流的开发模式中,使用的都是注解声明的方式,因此我们重点关注以下三个实现类:
-
AnnotationConfigApplicationContext
:普通应用,本模块实现 -
AnnotationConfigWebApplicationContext
:传统 web 应用,web 模块实现 -
AnnotationConfigEmbeddedWebApplicationContext
:内嵌 web 应用,第二部 Spring Boot 的 boot 模块实现
2.3 组件加载
AnnotationConfigApplicationContext
作为 Spring 应用的实现类,首先要解决 Java 源文件如何变成 Spring 容器中的一个 Bean 的问题。一般来说,组件加载的过程可以分为三步:
-
资源加载:Spring 核心包使用
Resource
接口对资源进行抽象,其中ClassPathResource
负责解析类路径下的文件。ResourceLoader
的作用是加载指定路径下的文件。DefaultResourceLoader
作为默认的资源加载器,AbstractApplicationContext
继承了该类,因此 Spring 容器拥有加载资源的能力。 -
资源解析:由于资源的文件类型不同,因此需要对加载后的资源进行解析。以字节码文件为例,源代码的后缀是
.java
,编译成字节码的后缀为.class
,加载之后变成ClassPathResource
对象。Spring 提供了两种解析方式,一是通过 JDK 反射来解析类的信息,二是使用 ASM 框架处理。 -
注册
BeanDefinition
:Spring 提供了两个强大易用的组件,一是AnnotatedBeanDefinitionReader
,加载指定的单例。二是ClassPathBeanDefinitionScanner
,可以批量扫描指定目录下的所有字节码文件,加载声明了@Component
及其子注解的单例。
3. 配置类
3.1 概述
在早期的 Spring 项目中,使用 XML 文件进行配置。XML 配置文件支持使用 bean
标签注册组件,使用 context:component-scan
标签批量扫描组件,使用 import
标签引入其他的 XML 文件。此外,有的标签带有前缀,需要引入相应的名称空间,操作起来异常繁琐。鉴于此,Spring 提供了一套声明式的解决方案,以配置类为核心,为我们提供了更加方便快捷、灵活多样的加载组件的方式。
Spring 为了处理配置类,提供了三个重要的组件,此外还使用 CongigurationClass
来描述已解析的配置类。简单介绍如下:
ConfigurationClassPostProcessor
:寻找容器中已存在的配置类,并对其进行解析。ConfigurationClassParser
:负责具体的解析过程,将已解析的配置类封装成CongigurationClass
对象。ConfigurationClassBeanDefinitionReader
:对CongigurationClass
集合进行处理,将所有的组件以BeanDefinition
的方式注册到容器中。
3.2 基本解析
ConfigurationClassParser
负责对配置类进行解析,一共包括六个部分,我们先看四个比较简单的实现。
- 内部类:寻找配置类的所有内部类,递归地方式进行解析。需要注意的是,内部类是一个独立的配置类。
- 父类:可能存在多个父类,都作为配置类的一部分,不是独立的配置类。
- 属性文件:解析
@PropertySource
注解,通过ResourceLoader
加载指定的配置文件,并将配置项保存到Environment
中。 - 组件扫描:解析
@ComponentScan
注解,实际上委托给ClassPathBeanDefinitionScanner
批量加载组件。
3.3 工厂方法
我们在第一章讲过 FactoryBean
,特点是通过工厂方法来创建对象。FactoryBean
是编程式的解决方案,按照 Spring 的一贯作风,对于同样的功能还会提供更为便捷的声明式解决方案。BeanMethod 是指声明了 @Bean
注解的方法,返回的对象会注册到 Spring 容器中。
一个类中可能存在多个重载方法,不同的类也可能存在同名的方法,因此我们必须考虑该由哪个 BeanMethod 来创建单例。这就涉及到 BeanMethod 的覆盖机制,具体情况如图所示。
3.4 导入机制
导入机制相当于 XML 配置文件的 import 标签。Spring 处理了三种类型的导入,一是普通类,主要是导入新的配置类。二是 ImportBeanDefinitionRegistrar
接口,可以直接注册 BeanDefinition
。三是 ImportSelector
接口,返回全类名数组,最终需要通过另外两种方式解析。在实际使用中,这三种情况可以互相配合,比如 Spring Boot 通过它们实现了非常灵活的自动配置功能。
此外还提供了延迟导入的功能,Spring Boot 的自动配置就用到了延迟导入。目的是优先加载用户定义的组件,最后导入框架默认的组件作为兜底措施,而这正符合 Spring Boot 标榜的约定大于配置的理念。
3.5 三种加载方式对比
总的来说,配置类最重要功能是加载组件,三个方式各有侧重点。在特定的场合选择恰当的方式,可以达到事半功倍的效果。
- 组件扫描:主要加载用户定义的 Bean,这是标准化的过程,可以自动完成
- 工厂方法:用于整合第三方组件,Spring 不知道如何创建对象,一般由用户自定义处理
- 导入机制:主要是框架使用,比如 Spring Boot 通过导入机制,实现了众多配置类的连锁导入
4. 常用功能
4.1 条件判定
配置类提供了多种加载组件的方式,但有时需要根据一定的条件决定是否加载组件。鉴于此,Spring 提供条件判定的功能,允许实现自定义的加载规则。条件判定与加载 Bean 的组件密切配合,为我们提供了非常灵活的判定方式。条件判定的主要由四部分组成:
-
注解类:
@Conditional
作为元注解,声明在其他条件注解类上,指向一个具体的条件类 -
条件类:
Condition
接口定义了判定的行为,子类可以实现自定义的判定逻辑。 -
条件上下文:
ConditionContext
接口定义了在条件判定的过程中使用的相关组件 -
条件评估器:
ConditionEvaluator
是条件判定的核心类,通过条件注解获取对应的条件类,然后综合判定是否应当加载组件
条件判定在 Spring 中有着广泛的应用,context 模块实现了 ProfileCondition
接口,@Profile
注解可以用于加载不同的运行环境。此外,Spring Boot 扩展了 SpringBootCondition
接口,提供了更多的判定方式。这里列举了三种比较常用的方式:
OnPropertyCondition
:支持@ConditionalOnProperty
注解,根据某个属性值作为判定依据。OnClassCondition
:支持@ConditionalOnClass
和@ConditionalOnMissingClass
注解,根据工程中是否存在某个类作为判定依据。OnBeanCondition
:支持@ConditionalOnBean
和@ConditionalOnMissingBean
注解,根据 Spring 容器中是否存在某个 Bean 作为判定依据。
4.2 生命周期管理
生命周期管理包括两个层面,一是组件的生命周期,我们之前介绍过 Bean 的生命周期,但仅涉及了普通单例的初始化和销毁。事实上,Spring 还提供了两种特殊的组件:
SmartInitializingSingleton
组件在所有单例初始化之后执行,避免提前实例化依赖项。Lifecycle
组件支持异步操作,是否抛出异常由开发者决定,不像普通单例初始化失败会导致容器创建失败。
二是 Spring 容器的生命周期。总的来说,外层容器的生命周期驱动着内部组件生命周期的变化,而容器的生命周期又受外部环境的影响。
- Spring 容器的初始化就是调用
refresh
方法,依次对普通单例、SmartInitializingSingleton
和Lifecycle
进行初始化。 ConfigurableApplicationContext
接口定义了两种容器关闭的方式,close
方法表示手动关闭,registerShutdownHook
方法则是向虚拟机注册一个钩子函数,在虚拟机停止运行之前,以回调的方式通知 Spring 容器,执行销毁流程。
4.3 事件机制
Spring 的事件机制提供了容器和组件、组件和组件之间进行通信的功能。事件机制是基于观察者模式实现的。观察者模式可以分为两类,一是订阅发布模式,二是监听器模式。两者的区别在于,订阅发布模式只能处理一种事件,监听器模式则可以同时处理多种事件。Spring 的事件机制是基于监听器模式实现的,使用的范围更广泛。监听器模式需要实现三个基本组件:
- 事件对象:
ApplicationEvent
扩展自EventObject
,代表事件在传播过程中携带的数据。 - 监听器:
ApplicationListener
接口代表观察者角色,作用是对发生的事件进行响应。 - 事件多播器:
ApplicationEventMulticaster
接口代表主题角色,持有一组监听器,负责发布事件。
Spring 提供了两种监听器实现,一种是编程式,通过实现 ApplicationListener
接口,称之为监听器类。一种是声明式,将 @EventListener
注解声明在方法上,称为监听器方法。事件多播器对这两种类型的监听器进行了适配,其中监听器类被包装成 GenericApplicationListenerAdapter
对象,监听器方法被包装成 ApplicationListenerMethodAdapter
对象。
5. 数据处理
5.1 格式化器
格式化是指将指定类型转换成字符串,以文本的方式进行展示。这意味着对同一个原始值来说,以不同的文本形式进行展示,这是一种一对多的关系 。java.text
包定义了常见类型的格式化类,比如 DateFormat
、NumberFormat
、MessageFormat
等。Spring 的格式化器实际上是一个包装类,底层逻辑主要由 Java 的格式化工具类完成。
格式化器与转换器的区别在于,前者是两个类型之间的单向转换 ,因此类名必须写明 A 转换到 B。后者转换的一端固定是字符串,因此只需言明另一端的类型即可,比如 DateFormatter
表示 Date
与字符串的转换。同时,格式化器与转换器也有密切的联系,因为格式化器最终是通过转换服务来发挥作用的。FormattingConversionService
负责注册格式化器,一个格式化器会分解成 PrinterConverter
和 ParserConverter
,然后注册到转换器列表中。
5.2 数据校验
在处理外部数据时,Spring 通常会封装成对象再进一步处理,这就涉及到两个操作。首先是赋值阶段,将外部数据赋给一个空对象。但仅仅为对象赋值还不够,有时候还要根据业务对数据进行校验。
JDK 提供了数据校验的功能,javax.validation.Validator
接口对校验行为进行抽象,具体实现是由第三方框架提供。Spring 框架通过适配的方式,将数据校验的功能委托给 Hibernate 框架的 ValidatorImpl
实现类完成。Spring 验证器的工作是将校验结果保存在 BindingResult
中。
BindingResult
作为中间桥梁,将赋值和校验两个操作联系在一起。具体表现在两个方面,一是赋值和校验针对的都是同一个目标对象,二是赋值和校验阶段产生的异常都保存在 BindingResult
中。用户可以通过统一地方式来管理异常,并访问目标对象。
5.3 DataBinder
DataBinder
是一个强大的数据处理工具,本身没有实现新的功能,而是完成了对众多组件的整合。总的来说,DataBinder
提供了三大功能:
- 数据绑定:Spring 通过属性访问将外部数据绑定到一个对象上,绑定结果则包含了绑定对象和异常信息
- 数据校验:JDK 定义了验证器接口,Hibernate 框架提供了实现类,Spring 的验证器通过适配 Hibernate 验证器的方式发挥作用
- 类型转换:之前介绍了属性编辑器和转换器两种方式,格式化器可以看作是转换器的特殊形式
欢迎关注公众号【Java编程探微】,加群一起讨论。
原创不易,觉得内容不错请关注、点赞、收藏。