Spring 有多种创建 Bean 并管理其生命周期的方法,称为作用域(scopes)。本章将讨论 Spring 应用中常见的两种作用域:单例(singleton)和原型(prototype)。
5.1 使用单例 bean 作用域
单例 Bean 作用域是 Spring 在其上下文中管理 Bean 的默认方法。在生产应用中最常见。
5.1.1 单例 bean 的工作原理
对于Spring,singleton 作用域允许同一类型的多个实例,单例意味着每个名称唯一。这和singleton pattern的每个应用唯一不同。
使用@BEAN声明单例范围的Bean
💡 当使用 @Bean 注解方法将 Bean 添加到上下文时,使用 @Bean 注解的方法名称将成为 bean 的名称。
参见示例sq-ch5-ex1
,较简单,略。
使用构造型注解声明单例 Bean
假设两个service类依赖repository类实现用例,则Spring上下文包括这3个类的实例。
💡 当使用构造型注解时,类的名称即为bean的名称。
在构造型注解中,@Component,@Service 和 @Repository添加bean到Spring上下文,@Autowired从上下文中获取bean。当使用 @Autowired 请求 Spring 注入 bean 引用时,框架会在所有请求的位置注入对单例 bean 的引用。
参见示例sq-ch5-ex2
,较简单,略。
与sq-ch5-ex1
不同,此项目中的配置类为空。我们只需要使用 @ComponentScan 注解告诉 Spring 在哪里可以找到带有构造型注解的类。
5.1.2 实际场景中的单例 bean
由于单例 bean 作用域假设应用的多个组件可以共享一个对象实例,因此最重要的一点是这些 bean 必须是不可变(immutable)的。现实世界中的应用通常会在多个线程上执行操作(如 Web 应用)。在这种情况下,多个线程共享同一个对象实例。如果这些线程更改了该实例,就会遇到竞争条件(race condition)。
💡 如果 Bean 不是为并发设计的,竞争条件会导致意外结果或执行异常。
竞争条件是指在多线程架构中,当多个线程尝试更改共享资源时可能发生的情况。如果发生竞争条件,开发人员需要正确同步线程,以避免出现意外的执行结果或错误。
如果想要可变的单例 Bean(其属性可以发生变化),则需要自行使这些 Bean 并发(主要通过使用线程同步)。但单例 Bean 并非为同步设计。它们通常用于定义应用的主干类设计,并将职责委托给彼此。从技术上讲,同步是可行的,但这并非好的做法。在并发实例上同步线程会显著影响应用的性能。在大多数情况下,会找其他方法来解决同步问题并避免线程并发。
第三章已经提到,构造函数注入(constructor DI)是一种很好的实践,比字段注入更受欢迎。构造函数注入的优点之一是它允许你将实例设置为不可变(将 bean 的字段定义为 final),例如:
@Service
public class CommentService {
private final CommentRepository commentRepository;
private final CommentNotificationProxy commentNotificationProxy;
public CommentService(CommentRepository commentRepository,
CommentNotificationProxy commentNotificationProxy) {
this.commentRepository = commentRepository;
this.commentNotificationProxy = commentNotificationProxy;
}
public void publishComment(Comment comment) {
commentRepository.storeComment(comment);
commentNotificationProxy.sendComment(comment);
}
}
💡 在Java中,如果某字段声明为final,则该字段不应也不能被修改。该字段只能被赋值一次,且赋值后不能再更改。对于基本数据类型(如 int, double, boolean 等),一旦赋值后,数值不能再改变。对于引用类型(如对象、数组等),引用本身不可变,但对象的内容可以变。
使用 Bean 可以归结为三点:
- 仅当需要 Spring 管理 Bean 时,才在 Spring 上下文中创建对象 Bean,以便框架能够为其添加特定功能。如果对象不需要框架提供的任何功能,则无需将其创建为 Bean。
- 如果需要在 Spring 上下文中创建对象 Bean,则只有当它是不可变时,才应该将其设计为单例 Bean。避免设计可变的单例 Bean。
- 如果 Bean 需要可变,可以使用原型作用域,即5.2节。
5.1.3 使用即时实例化和延迟实例化
大多数情况下,Spring 在初始化上下文时会创建所有单例 Bean——这是 Spring 的默认行为,也称为即时实例化(eager instantiation)。
此外,还有一种是延迟实例化(lazy instantiation),不是在上下文初始化时,而是在第一次被引用时实例化。
以构造型注解为例,延迟实例化是通过在类的上方添加@Lazy注解实现的。
示例sq-ch5-ex3
和sq-ch5-ex4
分别演示了即时实例化和延迟实例化,通过在构造函数中增加打印语句,可以清楚的演示在何时实例化。
即时实例化的好处在于:
- 性能。他无需检查实例是否已存在。
- 及早发现问题。由于某些问题导致无法创建bean时,应用启动时即可知道。
延迟实例化的好处是:
- 节省资源(内存)。不会消耗不必要的资源。
何时使用即时实例化或延迟实例化?大多数情况下,应让框架在上下文实例化时(立即实例化)创建所有实例;这样,当一个实例委托给另一个实例时,第二个 Bean 已经存在。
5.2 使用原型 bean 作用域
5.2.1 原型 bean 的工作原理
每次请求对原型范围 bean 的引用时,Spring 都会创建一个新的对象实例。
作者给了个比喻,对于原型范围bean,@Bean添加到上下文的是一棵咖啡树,每次getBean时,则从咖啡树上摘一颗咖啡豆。
💡 有了原型 bean,我们不再有并发问题,因为每个请求 bean 的线程都会获得不同的实例,所以定义可变原型 bean 不再是问题。
使用@BEAN声明原型范围的Bean
参见示例sq-ch5-ex5
。以下为代码片段:
@Configuration
public class ProjectConfig {
@Bean
@Scope(BeanDefinition.SCOPE_PROTOTYPE)
public CommentService commentService() {
return new CommentService();
}
}
使用构造型注解声明原型范围的 Bean
参见示例sq-ch5-ex6
。在此例中,repository类使用了原型范围,而两个service类仍使用单例范围。不知道为何要这么定义,也许只是示例而已。
@Repository
@Scope(BeanDefinition.SCOPE_PROTOTYPE)
public class CommentRepository {
}
5.2.2 实际场景中的原型 bean
虽然singleton bean更常见,但prototype bean也有一些适用场景,如需要可变对象时,参见示例sq-ch5-ex7
。
此例设计了一个名为 CommentProcessor 的对象,用于处理并验证评论。一个服务使用 CommentProcessor 对象来实现一个用例。但是,CommentProcessor 对象将要处理的评论存储为一个属性,并且它的方法会更改此属性。这是符合原型bean的场景的。
这个示例需要注意两个问题,第一是,CommentProcessor 对象可以不是bean,但由于其要使用已加入Spring上下文的CommentRepository,因此也需要加入到Spring上下文中。
第二个注意的问题,是需要在合适的位置注入bean。
先来看正确的方式:
@Service
public class CommentService {
@Autowired
private ApplicationContext context;
public void sendComment(Comment c) {
CommentProcessor p = context.getBean(CommentProcessor.class);
p.setComment(c);
p.processComment(c);
p.validateComment(c);
c = p.getComment();
// do something further
}
}
在以上代码中,获取CommentProcessor bean放在了sendComment方法中。
以下是错误的实现方式:
@Service
public class CommentService {
@Autowired
private CommentProcessor p;
public void sendComment(Comment c) {
p.setComment(c);
p.processComment(c);
p.validateComment(c);
c = p.getComment();
// do something further
}
}
这是因为,CommentService bean 是单例的,这意味着 Spring 只会创建该类的一个实例。因此,Spring 在创建 CommentService bean 本身时,也会只注入一次该类的依赖项。在这种情况下,您最终只会得到一个 CommentProcessor 的实例。
总的来说,prototype bean应尽量少用。
以下是singleton bean 和 prototype bean 的区别。
Singleton | Prototype |
---|---|
框架将名称与实际的对象实例关联。 | 名称与类型相关联。 |
每次引用 Bean 名称时,都会获得相同的对象实例。 | 每次引用 Bean 名称时,都会获得一个新的实例。 |
您可以配置 Spring 在上下文加载时或首次引用时创建实例。 | 引用 Bean 时,框架始终会创建原型范围的对象实例。 |
Spring 中的默认 Bean 作用域为单例 (Singleton)。 | 需要显式将 Bean 标记为原型。 |
不建议单例 Bean 具有可变属性。 | 原型 Bean 可以具有可变的属性。 |
总结
- 在 Spring 中,Bean 的范围定义了框架如何管理对象实例。
- Spring 提供两种 Bean 作用域:单例 (Singleton) 和原型 (Prototype)。
- 使用单例时,Spring 直接在其上下文中管理对象实例。每个实例都有一个唯一的名称,使用该名称始终可以引用该特定实例。单例是 Spring 的默认设置。
- 使用原型时,Spring 仅考虑对象类型。每种类型都有一个与之关联的唯一名称。每次引用该 Bean 名称时,Spring 都会创建该类型的新实例。
- 您可以将 Spring 配置为在上下文初始化时(立即实例化)或首次引用 Bean 时(延迟实例化)创建单例 Bean。默认情况下,Bean 是立即实例化的。
- 在应用中,我们最常使用单例 Bean。因为任何引用相同名称的对象都会获得相同的对象实例,因此多个不同的线程可以访问和使用这个实例。因此,建议将实例设置为不可变的。但是,如果您希望对 Bean 的属性进行可变操作,则您需要负责处理线程同步。
- 如果您需要像 bean 这样的可变对象,使用原型范围可能是一个不错的选择。
- 将原型作用域的 bean 注入单例作用域的 bean 时要小心。当你这样做时,你需要注意,单例实例总是使用同一个原型实例,而 Spring 在创建单例实例时会注入该实例。这通常是一种恶意的设计,因为将 bean 设置为原型作用域的目的是为了每次使用时都获取不同的实例。