Spring Start Here 读书笔记:第5章 The Spring context: Bean scopes and life cycle

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 可以归结为三点:

  1. 仅当需要 Spring 管理 Bean 时,才在 Spring 上下文中创建对象 Bean,以便框架能够为其添加特定功能。如果对象不需要框架提供的任何功能,则无需将其创建为 Bean。
  2. 如果需要在 Spring 上下文中创建对象 Bean,则只有当它是不可变时,才应该将其设计为单例 Bean。避免设计可变的单例 Bean。
  3. 如果 Bean 需要可变,可以使用原型作用域,即5.2节。

5.1.3 使用即时实例化和延迟实例化

大多数情况下,Spring 在初始化上下文时会创建所有单例 Bean——这是 Spring 的默认行为,也称为即时实例化(eager instantiation)。

此外,还有一种是延迟实例化(lazy instantiation),不是在上下文初始化时,而是在第一次被引用时实例化。

以构造型注解为例,延迟实例化是通过在类的上方添加@Lazy注解实现的。

示例sq-ch5-ex3sq-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 的区别。

SingletonPrototype
框架将名称与实际的对象实例关联。名称与类型相关联。
每次引用 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 设置为原型作用域的目的是为了每次使用时都获取不同的实例。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值