5. [进阶配置] Bean 的高级管理

5. [进阶配置] Bean 的高级管理
Prorise5. [进阶配置] Bean 的高级管理
摘要: 掌握了基础的 DI 配置后,本章我们将深入探索 Bean 的更多高级特性。我们将学习 Bean 的作用域如何影响其实例数量,Bean 的完整生命周期流程,以及 Spring 创建 Bean 的多种底层方式,包括强大的 FactoryBean。
5.1. Bean 的作用域 (Scope)
在 Spring 中,作用域 (Scope) 决定了 Spring IoC 容器如何创建和管理 Bean 的实例。简单来说,它回答了这样一个问题:“当我从容器中请求一个 Bean 时,是每次都给我一个新的对象,还是始终给我同一个?”。正确地使用作用域对于应用的性能和状态管理至关重要。
Spring 定义了多种作用域,但最核心、最常用的只有两种:singleton
(单例) 和 prototype
(原型/多例)。
5.1.1. 详解 singleton
:唯一的实例
singleton
是 Spring 默认 的作用域。当一个 Bean 的作用域为 singleton
时,无论你从容器中获取多少次该 Bean,Spring 容器都 只会返回同一个共享的实例。
1. 特性与创建时机
- 唯一实例: 在整个 Spring IoC 容器的生命周期内,一个 Bean ID 只对应一个对象实例。
- 创建时机: 默认情况下,
singleton
作用域的 Bean 在 容器启动和初始化时 就会被创建并放入一个缓存区(俗称“单例池”),等待后续的注入和调用。
2. 实践验证
我们在 spring6
项目中进行验证。
文件路径: src/main/java/com/example/spring6/bean/SpringBean.java
(新增文件)
1 | package com.example.spring6.bean; |
文件路径: src/main/resources/spring-scope.xml
(新增配置文件)
1 |
|
测试代码:
文件路径: src/test/java/com/example/spring6/test/ScopeTest.java
(新增测试类)
1 | package com.example.spring6.test; |
1
2
3
SpringBean 的无参数构造方法执行了...
sb1 = com.example.spring6.bean.SpringBean@2c88b9fc
sb2 = com.example.spring6.bean.SpringBean@2c88b9fc
结果分析: 构造方法只执行了一次,并且两次获取到的对象地址是完全相同的,证明了 singleton
的唯一性。
运行 testSingletonCreateTiming
测试:
1 | SpringBean 的无参数构造方法执行了... |
结果分析: 即使我们没有调用 getBean()
,构造方法依然在容器初始化时就被执行了,证明了其“饿汉式”的创建时机。
5.1.2. 详解 prototype
:多变的实例
与 singleton
相对,prototype
作用域的 Bean 每次被请求时,Spring 容器都会 创建一个全新的实例 返回。
1. 特性与创建时机
- 全新实例: 每一次
getBean()
调用或每一次注入操作,都会触发一次全新的对象创建过程。 - 创建时机:
prototype
作用域的 Bean 是 懒加载 的。只有当它被实际请求(getBean()
)时,容器才会去创建它的实例。 - 生命周期管理: Spring 容器在创建并初始化
prototype
Bean 后,就会将其交给调用方,不再追踪其后续的生命周期。这意味着 Spring 不会为prototype
Bean 调用其销毁方法 (destroy-method
)。
2. 实践验证
文件路径: src/main/resources/spring-scope.xml
(修改)
1 | <bean id="sb" class="com.example.spring6.bean.SpringBean" scope="prototype"/> |
测试代码: 使用与上面完全相同的 ScopeTest.java
。
运行 testSingletonScope
测试 (现在 sb Bean 是 prototype):
1 | SpringBean 的无参数构造方法执行了... |
结果分析: 构造方法被执行了两次,并且两次获取到的对象地址是不同的,证明了 prototype
的多例性。
运行 testSingletonCreateTiming
测试 (现在 sb Bean 是 prototype):
1 | (无任何输出) |
结果分析: 仅创建容器而没有 getBean()
时,构造方法完全没有被执行,证明了其“懒加载”的创建时机。
5.1.3. Spring Boot 对比:使用 @Scope
注解
在 Spring Boot 中,我们使用 @Scope
注解来声明 Bean 的作用域,这比 XML 配置更加直观和便捷。
通过在 <bean>
标签上设置 scope
属性来定义作用域。
1 | <bean id="singletonBean" class="com.example.spring6.bean.SpringBean" /> |
通过在 Bean 的声明注解(如 @Component
, @Service
)之上添加 @Scope
注解来定义作用域。
Java 代码:
1 | // SpringBean.java |
5.2. Bean 的实例化方式
我们知道,Spring 创建 Bean 最常规的方式是调用其无参构造函数。但在真实的开发世界里,对象的创建过程远比 new User()
要复杂得多。本节,我们将聚焦于那些“非常规”的实例化场景,并探索 Spring 是如何通过多种灵活的机制来优雅地应对它们的。
5.2.1. 构造方法实例化 (回顾)
这是标准场景,我们不再赘述。当一个类拥有公开的无参构造器时,Spring 默认通过它来创建实例。
1 | <bean id="userBean" class="com.example.spring6.bean.User"/> |
5.2.2. 静态工厂方法 (factory-method
)
“痛点”场景:
假设我们需要集成一个公司内部的遗留工具库。这个库中有一个
LegacyApiClient
类,它的构造函数是private
的,我们无法直接new
它。幸运的是,这个类提供了一个静态方法public static LegacyApiClient getInstance()
来获取其单例对象。那么,我们如何在 Spring XML 中配置并管理这个由静态方法创建的对象呢?
解决方案:
Spring 提供了 factory-method
属性,专门用于调用一个静态方法来创建 Bean 实例。
1. 准备代码 (模拟遗留库)
文件路径: src/main/java/com/example/spring6/bean/LegacyApiClient.java
1 | package com.example.spring6.bean; |
2. 配置与测试
XML 配置: src/main/resources/spring-instantiation.xml
(新增配置文件)
1 |
|
测试代码:
1 |
|
1
2
遗留 API 客户端实例已创建...
com.example.spring6.bean.LegacyApiClient@636e8cc
结论: 我们成功地让 Spring 通过调用 LegacyApiClient.getInstance()
方法,将这个原本无法直接实例化的对象纳入了容器管理。
5.2.3. 实例工厂方法 (factory-bean
)
“痛点”场景:
现在情况变得更复杂。我们需要一个数据库连接池
ConnectionPool
对象,但这个对象不能直接new
,必须由一个ConnectionPoolManager
的实例来创建。更关键的是,这个manager
对象本身在创建pool
之前,需要先进行配置(比如设置最大连接数maxConnections
)。我们如何让 Spring 先创建并配置好manager
,然后再调用这个配置好的manager
实例的createPool()
方法来创建pool
Bean 呢?
解决方案:
这就是 factory-bean
属性的用武之地。它允许我们指定一个已经存在的 Bean 实例作为工厂,来创建另一个 Bean。
1. 准备代码
文件路径: src/main/java/com/example/spring6/bean/ConnectionPool.java
1 | package com.example.spring6.bean; |
文件路径: src/main/java/com/example/spring6/bean/ConnectionPoolManager.java
1 | package com.example.spring6.bean; |
2. 配置与测试
XML 配置: src/main/resources/spring-instantiation.xml
(修改)
1 | <bean id="poolManager" class="com.example.spring6.bean.ConnectionPoolManager"> |
测试代码:
1 |
|
1
2
实例工厂 ConnectionPoolManager 正在创建连接池,最大连接数:10
com.example.spring6.bean.ConnectionPool@4b45a2f5
5.2.4. FactoryBean
接口:一种特殊的工厂 Bean
“痛点”场景:
在项目中,我们可能需要根据配置文件中的一个
type
值(例如 ‘simple’ 或 ‘complex’)来决定创建一个SimpleMessageConverter
还是ComplexMessageConverter
。这个创建逻辑如果散落在业务代码中会非常混乱。我们希望将这种复杂的、有条件的创建逻辑封装成一个可重用的 Spring 组件,让这个“工厂”本身可以被配置,并由 Spring 来调用它生产最终的 Bean。
解决方案:
Spring 提供了一个完美的、框架原生的解决方案:实现 FactoryBean
接口。它允许我们将复杂的实例化逻辑封装在一个类中,使其成为一个专门为 Spring 生产 Bean 的“工厂 Bean”。
1. 准备代码
文件路径: src/main/java/com/example/spring6/bean/MessageConverter.java
1 | package com.example.spring6.bean; |
文件路径: src/main/java/com/example/spring6/bean/MessageConverterFactoryBean.java
1 | package com.example.spring6.bean; |
2. 配置与测试
XML 配置: src/main/resources/spring-instantiation.xml
(修改)
1 | <bean id="messageConverter" class="com.example.spring6.bean.MessageConverterFactoryBean"> |
测试代码:
1 |
|
1
2
FactoryBean 正在根据 type='complex' 创建 MessageConverter...
获取到的 Bean 类型: ComplexMessageConverter
5.2.5. Spring Boot 对比:使用 @Configuration
和 @Bean
方法
“痛点”场景:
我们已经看到了 XML 中各种工厂方法的威力,但 XML 配置是字符串形式的,类型不安全(写错类名或方法名在编译期无法发现),难以重构(IDE 无法方便地查找引用和重命名),而且配置和逻辑是分离的。在 Spring Boot 中,有没有一种既能实现所有这些复杂创建逻辑,又具备 Java 代码所有优点的现代方式呢?
解决方案:
当然有!这就是 Spring Boot 的核心配置机制:@Configuration
类和 @Bean
方法。它允许我们用纯 Java 代码来定义和配置 Bean。
- 静态工厂:
1
<bean class="...Factory" factory-method="get"/>
- 实例工厂:
1
2<bean id="myFactory" class="...Factory"/>
<bean factory-bean="myFactory" factory-method="get"/> - FactoryBean:
1
<bean id="product" class="...ProductFactoryBean"/>
配置思路解读:
我们创建一个被 @Configuration
注解的类,这个类就相当于一个 XML 配置文件。类中所有被 @Bean
注解的方法,其返回值都会被 Spring 注册为一个 Bean,方法名默认就是 Bean 的 ID。
Java 配置: src/main/java/com/example/spring6boot/config/AppConfig.java
(新增文件)
1 | package com.example.spring6boot.config; |
对比总结: Spring Boot 的 Java 配置方式 (@Configuration
+ @Bean
) 完胜 XML 配置。它提供了完全的类型安全(所有东西都是 Java 代码,编译器会检查错误)、更好的可重构性(可以利用 IDE 的全部功能),并且能以编程方式实现任意复杂的 Bean 创建逻辑。这是现代 Spring 应用配置 Bean 的标准和推荐方式。
5.3. Bean 的生命周期
一个 Bean 在 Spring IoC 容器中,从被创建到最终被销毁,会经历一系列预定义的阶段,这就是 Bean 的生命周期。理解这个过程至关重要,因为它为我们提供了在特定时间点介入、执行自定义逻辑的机会。
“痛点”场景:
假设我们有一个
DatabaseManager
Bean,它负责管理数据库连接。我们必须确保,在它所有必需的属性(如url
,username
)被 Spring 注入之后,立刻调用openConnection()
方法来建立连接;而在整个应用关闭、容器销毁这个 Bean 之前,必须调用closeConnection()
方法来安全地释放资源。我们如何才能将这两个关键操作精确地安插在这两个时间点呢?
解决方案:
Spring 强大的生命周期回调机制,正是为了解决这类问题而设计的。它允许我们指定特定的方法,在 Bean 初始化和销毁时自动执行。
5.3.1. 核心五步:从实例化到销毁
我们可以将一个 Bean 的生命周期粗略地划分为五个核心阶段,使用时间线来展示最为清晰:
bean生命周期
Instantiation
Spring 容器通过构造方法创建 Bean 的实例。
Populate Properties
Spring 容器通过 DI 为 Bean 的属性注入值。
Initialization
执行开发者自定义的初始化逻辑,例如 openConnection()
。
In Use
Bean 处于可用状态,可以被应用程序中的其他对象调用。
Destruction
容器关闭时,执行自定义的销毁逻辑,例如 closeConnection()
。
实践验证
文件路径: src/main/java/com/example/spring6/bean/LifecycleBean.java
1 | package com.example.spring6.bean; |
文件路径: src/main/resources/spring-lifecycle.xml
1 |
|
文件路径: src/test/java/com/example/spring6/test/LifecycleTest.java
1 | package com.example.spring6.test; |
1
2
3
4
5
1. 实例化 Bean (调用构造器)
2. Bean 属性赋值 (调用 setter)
3. 初始化 Bean (调用自定义 init 方法)
4. 使用 Bean
5. 销毁 Bean (调用自定义 destroy 方法)
结论: 通过 init-method
和 destroy-method
,我们成功地将自定义逻辑挂载到了 Bean 的初始化和销毁阶段。
5.3.2. 关键扩展点:BeanPostProcessor
“痛点”场景:
init-method
很好用,但它只能对单个 Bean 生效。如果我们想对容器中所有(或某一批)的 Bean,在它们各自的init
方法执行前后,都统一执行一段逻辑(比如打印日志、创建代理对象等),难道要修改每一个 Bean 的配置吗?这显然不现实。
解决方案:
这是一种典型的“横切关注点”,Spring 提供了其体系中最强大的扩展点之一:Bean 后置处理器 (BeanPostProcessor
)。
加入后置处理器后,生命周期演变为更精细的七个步骤:
- 1.创建 Bean 实例。
- 2.为 Bean 注入依赖。
- 3.Bean后置处理器前置处理:
init-method
执行前的第一个扩展点。 - 4.执行 Bean 自定义的
init-method
。 - 5.Bean后置处理器后置处理:
init-method
执行后的第二个扩展点,常用于创建代理对象。 - 6.Bean 处于可用状态。
- 7.执行 Bean 自定义的
destroy-method
。
实践验证
文件路径: src/main/java/com/example/spring6/bean/LogBeanPostProcessor.java
1 | package com.example.spring6.bean; |
文件路径: src/main/resources/spring-lifecycle.xml
(修改)
1 | <bean class="com.example.spring6.bean.LogBeanPostProcessor"/> |
再次运行上一节的 testLifecycleFiveSteps()
测试。
1 | 1. 实例化 Bean (调用构造器) |
5.3.3. Spring Boot 对比:使用注解定义生命周期
通过在 <bean>
标签上设置 init-method
和 destroy-method
属性来指定回调方法,并通过定义 <bean>
来注册后置处理器。
1 | <bean id="lifecycleBean" |
对比总结: @PostConstruct
和 @PreDestroy
是 JSR-250 标准的一部分,这意味着您的代码不直接依赖于 Spring 的特定 API,具有更好的可移植性和通用性。这是在现代 Spring Boot 应用中处理生命周期回调的首选方式。