单例设计模式与 Spring Boot 中的 Singleton Bean

1、概览

单例对象经常被开发人员使用,因为应用程序中的许多对象都需要重复使用一个单例。在 Spring 中,我们可以通过 使用 Spring 的单例 Bean 或自己实现单例设计模式 来创建单例对象。

在本教程中,我们将首先了解单例设计模式及其线程安全的实现。然后,我们将了解 Spring 中的 Singleton Bean Scope,并将 singleton Bean 与使用单例设计模式创建的对象进行比较。

最后,我们将介绍一些可行的最佳实践。

本文中的 “Singleton Bean”,即“单例 bean”。

2、单例设计模式

单例是 “GoF” (si人帮?)于 1994 年发布的最简单的设计模式之一。它被归类于创建模式,因为单例提供了一种只创建一个实例的方法。

2.1、模式定义

单例模式是指由一个类负责创建对象,并确保只创建一个实例。我们经常使用单例来共享状态或减少创建多个对象的成本。

单例模式实现可以确保只创建一个实例:

  • 通过实现单个私有构造函数来隐藏所有构造函数。
  • 仅在实例不存在时创建实例,并将其存储在私有静态变量中。
  • 使用公共静态 getter 方法访问该单例。

让我们看看几个使用单例对象的类的示例:

单件设计模式的类

在上面的类图中,我们可以看到多个服务如何使用只创建一次的同一个单例。

2.2、懒加载

单例模式实现通常使用懒加载来延迟实例创建(也称为“懒汉式”),直到第一次实际需要时才创建。为了确保延迟实例化,我们可以在首次调用静态 getter 方法时创建实例:

public final class ThreadSafeSingleInstance {

    private static volatile ThreadSafeSingleInstance instance = null;

    private ThreadSafeSingleInstance() {}

    public static ThreadSafeSingleInstance getInstance() {
        if (instance == null) {
            synchronized(ThreadSafeSingleInstance.class) {
                if (instance == null) {
                    instance = new ThreadSafeSingleInstance();
                }
            }
        }
        return instance;
    }

    // 标准的 getter 方法

}

在多线程应用中,延迟加载可能会导致并发问题。因此,我们还应用了双重检查锁,以防止不同线程创建多个实例。

3、Spring 中的 Singleton Bean

Spring 框架中的 bean 是在 Spring IoC 容器中创建、管理和销毁的对象。

3.1、Bean Scope

通过 Spring Bean,我们可以使用反转控制 (IoC) 通过元数据将对象注入 Spring 容器。实际上,一个对象可以定义其依赖,而无需创建它们,并将这项工作委托给 IoC 容器。

最新版本的 Spring 框架定义了六种 scope:

  • singleton
  • prototype
  • request
  • session
  • application
  • websocket

Bean 的 scope 定义了它的生命周期和可见性。它还决定了如何创建 bean 的实际实例。例如,我们可能想创建一个全局实例,或者每次请求 bean 时都创建一个不同的实例。

3.2、Singleton Bean

我们可以使用配置类中的 @Bean 注解在 Spring 中声明 Bean。Spring 中的 singleton scope 为容器中的每个 Bean 标识创建一个 Bean:

@Configuration
public class SingletonBeanConfig {

    @Bean
    @Scope(value = ConfigurableBeanFactory.SCOPE_SINGLETON)
    public SingletonBean singletonBean() {
        return new SingletonBean();
    }

}

Singleton 是 Spring 中定义的所有 Bean 的默认 scope。因此,即使我们不使用 @Scope 注解指定特定的 scope,我们仍然会得到一个单例 Bean。这里包含的 scope 仅供参考。它通常用于表达其他可用的 scope。

3.3、Bean 标识符(Identifier)

与纯粹的单例设计模式不同,我们可以从同一个类中创建多个单例 Bean:

@Bean
@Scope(value = ConfigurableBeanFactory.SCOPE_SINGLETON)
public SingletonBean singletonBean() {
    return new SingletonBean();
}

@Bean
@Scope(value = ConfigurableBeanFactory.SCOPE_SINGLETON)
public SingletonBean anotherSingletonBean() {
    return new SingletonBean();
}

对具有匹配标识符的 Bean 的所有请求都将导致框架返回一个特定的 Bean 实例。当我们在方法上使用 @Bean 注解时,Spring 会将方法名称用作 Bean 标识符。

注入 Bean 时,如果容器中存在多个相同类型的 Bean,框架会抛出 NoUniqueBeanDefinitionException

@Autowired
private SingletonBeanConfig.SingletonBean bean; //抛出异常

在这种情况下,我们可以使用 @Qualifier 注解来指定要注入 Bean 的标识符:

@Autowired
@Qualifier("singletonBean")
private SingletonBeanConfig.SingletonBean beanOne;

@Autowired
@Qualifier("anotherSingletonBean")
private SingletonBeanConfig.SingletonBean beanThree;

另外,当存在多个相同类型的 Bean 时,还可以使用另一个注解 @Primary 来定义主 Bean(存在多个相同类型的 Bean 注入时,优先注入 @Primary 标识的 Bean)。

4、对比

现在,让我们比较一下这两种方法,并总结出 Spring 的最佳实践。

4.1、单例反模式

有些人认为单例是一种反模式,因为它引入了应用程序级的全局状态。使用单例的任何其他对象都直接依赖于单例。这就造成了类和模块之间不必要的相互依赖。

单例模式还违反了单一责任原则。因为单例对象至少要对两件事负责:

  • 确保只创建一个实例。
  • 执行正常业务。

此外,在多线程环境中,单例需要特殊处理,以确保独立线程不会创建多个实例。它们还可能增加单元测试和 mock 的难度。由于许多 mock 框架依赖于继承,私有构造函数使得单例对象难以 mock。

4.2、推荐方法

使用 Spring 的单例 Bean 而不是实现单例设计模式,可以消除上述许多缺点。

Spring 框架在所有使用 Bean 的类中注入 Bean,但保留了替换或扩展 Bean 的灵活性。该框架通过保持对 bean 生命周期的控制来实现这一点。因此,以后可以用另一种方法替换它,而无需更改任何代码。

此外,Spring Bean 还让单元测试变得更加简单。Spring Bean 易于 mock,框架可以将其注入测试类。我们可以选择注入实际的 Bean 实现或它们的 mock。

我们应该注意的是,单例 bean 不会只创建一个类的实例,而是在容器中为每个 bean 标识符创建一个 bean。

5、总结

在本文中,我们探讨了如何在 Spring 框架中创建单例。我们研究了单例设计模式的实现,以及如何使用 Spring 的 singleton Bean。

我们探索了如何通过懒加载和线程安全实现单例模式。然后,我们研究了 Spring 中的单例 Bean scope,并探索了如何实现和注入单例 Bean。我们还了解了单例 Bean 与使用单例设计模式创建的对象之间的区别。

最后,我们了解了在 Spring 中使用 singleton Bean 如何消除单例设计模式传统实现的一些缺点。


参考:https://www.baeldung.com/spring-boot-singleton-vs-beans