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

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

相关推荐
s:1036 分钟前
【框架】参考 Spring Security 安全框架设计出,轻量化高可扩展的身份认证与授权架构
java·开发语言
南山十一少3 小时前
Spring Security+JWT+Redis实现项目级前后端分离认证授权
java·spring·bootstrap
闲猫5 小时前
go orm GORM
开发语言·后端·golang
427724005 小时前
IDEA使用git不提示账号密码登录,而是输入token问题解决
java·git·intellij-idea
丁卯4045 小时前
Go语言中使用viper绑定结构体和yaml文件信息时,标签的使用
服务器·后端·golang
chengooooooo5 小时前
苍穹外卖day8 地址上传 用户下单 订单支付
java·服务器·数据库
李长渊哦5 小时前
常用的 JVM 参数:配置与优化指南
java·jvm
计算机小白一个5 小时前
蓝桥杯 Java B 组之设计 LRU 缓存
java·算法·蓝桥杯
Tirzano6 小时前
springsecurity自定义认证
spring boot·spring
南宫生8 小时前
力扣每日一题【算法学习day.132】
java·学习·算法·leetcode