【重写SpringFramework】第三章小结(chapter 3-16)

1. 前言

在 Spring 框架中,context 模块的定位是一个门面,对内整合了 core、beans、context、expression 等基础模块,隐藏了底层的实现细节;对外负责与用户(开发者)打交道,提供一系列强大易用的功能。在实际使用中,开发者主要与 context 模块的 API 打交道,只有很少的操作可能会涉及到底层模块。

本章的内容比较多,大体可以分为以下四个部分:

  1. 初步介绍 ApplicationContext 的继承体系和基本实现,包括资源加载和解析、组件加载的功能。
  2. 在拥有加载组件的基本功能后,ApplicationContext 还实现了灵活多样的加载方式,这一过程是由配置类完成的。
  3. 配置类的主要作用是加载组件,除此之外还有一些常用的功能,比如条件判定、生命周期管理、事件机制等。在实际开发中,这些都是强大易用的功能。
  4. 对于 Spring 容器来说,对对象进行处理是必不可少了,因此数据处理也是很重要的部分。我们主要介绍了格式化、数据校验和 DataBinder

2. ApplicationContext

2.1 概述

Spring 容器的概念有狭义和广义之分,狭义上指的是 BeanFactory,广义上指的是 ApplicationContext。不论是狭义的还是广义的,它们都是各自模块的核心接口。ApplicationContextBeanFactory 之间不是简单的组合,而是通过「装饰模式」联系在一起的。装饰模式有两大特点,一是扩展原对象的功能 ,二是以组合代替继承的结构。

装饰模式的结构包含四个角色,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 方法,依次对普通单例、SmartInitializingSingletonLifecycle 进行初始化。
  • ConfigurableApplicationContext 接口定义了两种容器关闭的方式,close 方法表示手动关闭,registerShutdownHook 方法则是向虚拟机注册一个钩子函数,在虚拟机停止运行之前,以回调的方式通知 Spring 容器,执行销毁流程。

4.3 事件机制

Spring 的事件机制提供了容器和组件、组件和组件之间进行通信的功能。事件机制是基于观察者模式实现的。观察者模式可以分为两类,一是订阅发布模式,二是监听器模式。两者的区别在于,订阅发布模式只能处理一种事件,监听器模式则可以同时处理多种事件。Spring 的事件机制是基于监听器模式实现的,使用的范围更广泛。监听器模式需要实现三个基本组件:

  • 事件对象:ApplicationEvent 扩展自 EventObject,代表事件在传播过程中携带的数据。
  • 监听器:ApplicationListener 接口代表观察者角色,作用是对发生的事件进行响应。
  • 事件多播器:ApplicationEventMulticaster 接口代表主题角色,持有一组监听器,负责发布事件。

Spring 提供了两种监听器实现,一种是编程式,通过实现 ApplicationListener 接口,称之为监听器类。一种是声明式,将 @EventListener 注解声明在方法上,称为监听器方法。事件多播器对这两种类型的监听器进行了适配,其中监听器类被包装成 GenericApplicationListenerAdapter 对象,监听器方法被包装成 ApplicationListenerMethodAdapter 对象。

5. 数据处理

5.1 格式化器

格式化是指将指定类型转换成字符串,以文本的方式进行展示。这意味着对同一个原始值来说,以不同的文本形式进行展示,这是一种一对多的关系java.text 包定义了常见类型的格式化类,比如 DateFormatNumberFormatMessageFormat等。Spring 的格式化器实际上是一个包装类,底层逻辑主要由 Java 的格式化工具类完成。

格式化器与转换器的区别在于,前者是两个类型之间的单向转换 ,因此类名必须写明 A 转换到 B。后者转换的一端固定是字符串,因此只需言明另一端的类型即可,比如 DateFormatter 表示 Date 与字符串的转换。同时,格式化器与转换器也有密切的联系,因为格式化器最终是通过转换服务来发挥作用的。FormattingConversionService 负责注册格式化器,一个格式化器会分解成 PrinterConverterParserConverter,然后注册到转换器列表中。

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编程探微】,加群一起讨论。

原创不易,觉得内容不错请关注、点赞、收藏。

相关推荐
hikktn18 分钟前
Java 兼容读取WPS和Office图片,结合EasyExcel读取单元格信息
java·开发语言·wps
迪迦不喝可乐19 分钟前
软考 高级 架构师 第十一章 面向对象分析 设计模式
java·设计模式
檀越剑指大厂1 小时前
【Java基础】使用Apache POI和Spring Boot实现Excel文件上传和解析功能
java·spring boot·apache
苹果酱05671 小时前
Golang的网络流量分配策略
java·spring boot·毕业设计·layui·课程设计
小青柑-1 小时前
Go语言中的接收器(Receiver)详解
开发语言·后端·golang
孑么1 小时前
GDPU Android移动应用 重点习题集
android·xml·java·okhttp·kotlin·android studio·webview
未命名冀2 小时前
微服务面试相关
java·微服务·面试
Heavydrink2 小时前
ajax与json
java·ajax·json
阿智智2 小时前
纯手工(不基于maven的pom.xml、Web容器)连接MySQL数据库的详细过程(Java Web学习笔记)
java·mysql数据库·纯手工连接
fangxiang20083 小时前
spring boot 集成 knife4j
java·spring boot