1. 前言
在 Spring 框架中,beans 模块是仅次于 core 模块的基础模块。我们知道,IOC 机制是 Spring 框架的两大基石之一,beans 模块的主要任务就是实现控制反转和依赖注入的功能。从具体实现来说,BeanFactory
接口是整个模块的核心接口,几乎所有功能都是围绕对象展开的。BeanFactory
提供了创建对象的功能,并对一部分对象进行管理,这是控制反转的基础。此外,在创建对象的过程中完成依赖注入,这时我们可以说 BeanFactory
是一个典型的 IOC 容器。
上述内容只是 beans 模块的主线,在实现这些功能的过程中,还涉及到了众多的问题。有的问题得到了解决,有的问题只是开了个头,将在后续模块进行处理。我们将对本章所涉及的知识点进行回顾和梳理,巩固加深对 beans 模块的理解。
2. BeanFactory
2.1 Bean 的概述
BeanFactory
是管理 Bean 的容器,Spring 根据作用域对 Bean 进行划分,大体上分为三类:
- 单例:由 Spring 容器负责管理,每个单例的 beanName 是唯一的。
- 原型:Spring 容器只负责创建对象,不负责管理,每次调用
getBean
方法都会创建一个新的对象。 - 其他作用域:Spring 还定义了 request、session 等作用域,且允许用户自定义作用域。
Spring 容器负责对单例进行管理,单例的注册有两种途径。一是由外界创建对象,然后注册到 Spring 容器中。二是由 BeanFactory
自行创建对象,包括普通对象和 FactoryBean
工厂对象两种形式。BeanDefinition
描述了创建对象的相关信息,我们关心两种实现:
RootBeanDefinition
是创建对象的标准依据,各种形式的BeanDefinition
实现都会转换成RootBeanDefinition
。AnnotatedBeanDefinition
接口通过注解声明的方式加载组件,这一过程是由 Spring 框架自行完成的。该接口有三个实现类,使用方式更为灵活便捷。
2.2 BeanFactory 体系
BeanFactory
的继承体系十分庞大,我们对原始的继承体系进行精简,将整个体系分为三个部分。第一部分对单例进行管理,第二部分对 Bean 进行管理,第三部分对 BeanDefinition
进行管理。Spring 容器的继承层次看起来很复杂,但每个类的职责划分非常明确,整体的设计思路是值得借鉴的。这里简单描述一下各个类的作用,如下所示:
-
DefaultSingletonBeanRegistry
:负责存储单例,包括FactoryBean
工厂对象。此外,该类使用三级缓存来处理循环依赖的问题。 -
FactoryBeanRegistrySupport
:主要为FactoryBean
提供支持,负责存储FactoryBean
创建的对象。 -
AbstractBeanFactory
:实现了核心的getBean
工厂方法,实际的查询缓存和创建对象的工作是由父类和子类完成的。该类完成了一些辅助工作,比如获取RootBeanDefinition
作为创建对象的依据,以及区分普通单例和FactoryBean
。 -
AbstractAutowireCapableBeanFactory
:完成了创建对象的工作,包括实例化、填充对象、初始化等操作。该类的特点是拥有自动装配的能力,通过AutowiredAnnotationBeanPostProcessor
和ConstructorResolver
等组件实现依赖注入的功能。 -
DefaultListableBeanFactory
:默认的实现类,完成了依赖解析等工作。该类还实现了BeanDefinitionRegistry
接口,负责管理BeanDefinition
。
2.3 创建对象的流程
创建对象的流程是 Spring 容器的核心部分,也是控制反转和依赖注入的体现。创建对象的过程比较复杂,我们尤其关心以下三个步骤:
- 实例化:Spring 提供了多种实例化对象的方式,一是通过
InstantiationStrategy
组件以反射的方式调用无参构造器,二是通过工厂方法来创建对象,三是调用构造器来创建对象。后两种方式是由ConstructorResolver
组件完成的,且工厂方法和构造器的参数会被依赖解析。 - 填充对象:对象实例化之后,此时的对象只是一个空对象,需要对属性进行赋值。填充对象包括两个部分,一是属性访问,二是依赖注入。从数据来源来说,属性访问的数据是事先准备的,存储在
BeanDefinition
的propertyValues
属性中。依赖注入的数据来源于 Spring 容器中的单例,以及环境变量中的属性。 - 初始化:在填充对象之后,此时的对象是基本可用的,Spring 容器提供了一个机会供用户进行自定义操作。这一过程包括感知接口注入、三种初始化的方式,以及初始化后的回调。
2.4 扩展组件
BeanPostProcessor
接口提供了若干钩子方法,在 Spring 容器创建对象的不同阶段进行回调。BeanPostProcessor
不仅被 Spring 框架使用,同时作为面向用户的扩展接口,允许用户进行自定义操作。BeanPostProcessor
接口的实现类有着广泛地应用,本章介绍了两个比较重要的实现类,如下所示:
-
AutowiredAnnotationBeanPostProcessor
:负责解析@Autowired
、@Value
等注解,在填充对象阶段调用,主要解决依赖注入的问题。 -
InitDestroyAnnotationBeanPostProcessor
:负责对象的初始化和销毁操作,支持对@PostConstruct
和@PreDestroy
注解的解析。
BeanFactoryPostProcessor
接口的作用是在 BeanFactory
实例创建之后,允许以回调的方式对容器进行设置。该接口实际上是提供给 ApplicationContext
使用的,比如配置的处理就是通过子类 ConfigurationClassPostProcessor
完成的。
3. 属性访问
3.1 概述
对于一个 Java Bean 来说,可以调用 setter 方法为对象赋值。但 Spring 容器在创建对象的过程中,用户不能直接进行干预,因此我们需要一种替代方案。属性访问是将一个空对象和一组数据关联起来,并自动完成赋值的操作。
PropertyProcessor
:负责为对象的属性进行赋值,可以处理复杂的嵌套属性。TypeConverter
:在赋值的过程中,外部数据可能与对象的字段类型不一致,因此需要进行类型转换。PropertyValues
:对属性值的来源进行抽象,每一组属性可以使用 key 和 value 来表示。
3.2 PropertyProcessor
属性访问的强大之处在于可以处理多层嵌套的复杂对象,在实际应用中,Spring Boot 的属性类就是通过 PropertyProcessor
处理的。PropertyProcessor
接口有两个实现类,如下所示:
BeanWrapperImpl
:通过内省的方式调用 getter/setter 方法安全地访问对象的属性DirectFieldAccessor
:通过反射的方式直接访问字段
3.3 TypeConverter
TypeConverter
提供了类型转换的功能,其底层的转换逻辑主要由属性编辑器和转换器完成的。其中属性编辑器是由 JDK 提供的内省操作的 API,允许以反射的方式安全地访问字段。转换器则是 Spring 核心包定义的一系列转换器,并通过 ConversionService
提供类型转换的服务。下表列出了属性编辑器和转换器各自的特点:
转换方向 | 转换类型 | 线程安全 | |
---|---|---|---|
属性编辑器 | 双向转换 | 自定义类型与 String 互转 | 否 |
转换器 | 单向转换 | 任意两个类型互转 | 是 |
在 Spring 框架中,TypeConverter
和 ConversionService
是一起使用的。这是因为属性编辑器不是线程安全的,所以虽然 TypeConverter
的功能很强大,但如果需要保证线程安全,可单独使用 ConversionService
。
4. 自动装配
4.1 概述
自动装配又称依赖注入,与控制反转共同构成了 IOC 机制。总的来说,自动装配的流程可以分为两个阶段,首先需要对注入目标进行解析,使用以下两个类来描述:
-
InjectionMetadata
表示一个类的注入信息,称为注入元数据。注入元数据持有一组InjectedElement
,每个InjectedElement
代表一个注入目标,可以是一个字段或 setter 方法。 -
DependencyDescriptor
描述了依赖项的相关信息,作用与BeanDefinition
类似。
在得到注入目标的信息之后,需要对依赖项进行解析。由于 Spring 容器负责管理各项资源,因此依赖解析的工作是由 DefaultListableBeanFactory
完成的。从功能上来说,依赖解析可以分为三种:
- 延迟依赖解析:如果依赖项是
ObjectProvider
或Provider
这种包装类型,不立即解析依赖项,而是推迟到第一次调用对象时再解析。这是一种辅助功能,适用于特殊情况,即依赖项的实例可能尚未存在,因此需要推迟。 - 字符串解析:对声明了
@Value
注解的字段进行解析,通过StringValueResolver
组件寻找环境变量中的属性值。 - 对象解析:最常用的形式,包括单一类型和集合类型,其中集合类型建立在单一类型的基础之上。
4.2 注入方式
Spring 实现了四种依赖注入的方式,根据用途和执行时机可以分为两组。第一组是字段注入与 setter 方法注入,作用是为字段赋值,在填充对象的流程中执行。第二组是工厂方法注入和构造器注入,作用是创建对象,在实例化的流程中执行。
- 字段注入:在字段上声明
@Value
、@Autowired
等注解 - setter 方法注入:在方法上声明
@Autowired
等注解。有时需要在 setter 方法中进行额外操作,可以选择这种方式 - 工厂方法注入:在方法上声明
@Bean
注解,作用是创建对象,这种方式是代替FactoryBean
的声明式实现 - 构造器注入:在构造器上声明
@Autowired
注解
4.3 循环依赖
在依赖解析的过程中,可能遇到一种特殊情况,即当两个对象相互依赖时,如果不进行处理,那么代码的执行会陷入无限循环。Spring 的解决思路是临时存储创建中的对象,在寻找依赖项时返回创建中的对象,从而切断循环依赖的链条。DefaultSingletonBeanRegistry
定义了三级缓存,功能如下所示:
singletonObjects
为一级缓存,负责存储创建完毕的单例。earlySingletonObjects
为二级缓存,存储尚处于创建中单例,用于解决普通对象的循环依赖。singletonFactories
为三级缓存,存储单例工厂,用于解决代理对象的循环依赖。
需要注意的是,二级缓存的作用是确保三级缓存中的 ObjectFactory
对象只回调一次,因为对于代理对象来说,代理对象的创建过程只能有一次。如果是普通对象,只保留一级和三级缓存就够了,因为不管回调多少次,获得的都是同一个对象。
5. Bean的生命周期
5.1 初始化流程
在填充对象之后,此时的对象是基本可用的,Spring 容器提供了一个机会供用户进行自定义操作,这一过程称之为对象的初始化。初始化操作包括以下四个方面:
- 感知接口注入:为实现了
Aware
接口的对象注入组件,比如BeanFactoryAware
接口的实现类会被注入BeanFactory
实例。 - 初始化前处理:调用
BeanPostProcessor
接口的postProcessBeforeInitialization
方法,比如InitDestroyAnnotationBeanPostProcessor
组件处理声明了@PostConstruct
注解的实例,并调用初始化方法。 - 初始化操作:包括两个部分,其一,如果当前实例实现了
InitializingBean
接口,则调用afterPropertiesSet
方法。其二,获取BeanDefinition
的initMethodName
属性,如果存在,则以反射的方式调用方法。 - 初始化后处理:调用
BeanPostProcessor
接口的postProcessAfterInitialization
方法,最典型的应用是AbstractAutoProxyCreator
创建代理对象。
5.2 销毁流程
Bean 的销毁流程包括两个阶段,首先在创建实例的最后阶段,将待销毁的单例包装成 DisposableBeanAdapter
适配器对象,并注册到 Spring 容器中。其次,调用 ConfigurableBeanFactory
接口的 destroySingletons
方法触发销毁流程。单例的销毁操作是由 DisposableBeanAdapter
适配器对象完成的,一共处理了四种情况。如下所示:
- 通过
InitDestroyAnnotationBeanPostProcessor
组件处理声明了@PreDestroy
注解的方法 - 调用
DisposableBean
接口的销毁方法 - 调用指定的销毁方法,分为两种情况。一是自定义的销毁方法,即
BeanDefinition
的destroyMethodName
属性。二是默认的销毁方法,即AutoCloseable
接口实现类的close
或shutdown
方法。
6. FactoryBean
Spring 容器主要通过 BeanDefinition
来创建对象,这种方式有两个特点,一是对象的构建过程较为简单,二是通过依赖注入完成了大部分构建工作。对于一些复杂对象来说,需要更加灵活的创建方式。Spring 的解决思路是将创建对象的权力「转包」出去,这实际上也是工厂模式的体现,即屏蔽了创建对象的细节。
Spring 提供了两种实现方式。一是 FactoryBean
接口,二是配置类中的工厂方法。关于后者将在第三章 context 模块进行讨论,其前置技术就是自动装配的工厂方法注入的功能。
关注公众号【Java编程探微】,加群一起讨论。