1. 概述
单例对象通常被需要单个实例的开发人员使用,该实例旨在被应用程序中的许多对象重用。在Spring中,我们可以通过使用Spring的单例bean或自己实现单例设计模式来创建它们。
在本教程中,我们将首先了解单例设计模式及其线程安全实现。然后,我们将查看Spring中的单例bean作用域,并将单例bean与使用单例设计模式创建的对象进行比较。
最后,我们将介绍一些可能的最佳实践。
2. 单例设计模式
Singleton是四人帮于1994年发布的最简单的设计模式之一。它被分组在创建型模式下,因为单例提供了一种创建只有一个实例的对象的方法。
2.1 模式定义
单例模式涉及负责创建对象并确保只创建一个类的单个实例。我们经常使用单例来共享状态或避免设置多个对象的成本。
单例模式实现通过执行以下操作确保仅创建一个实例:
让我们看一个使用单例对象的几个类的例子:
在上面的类图中,我们可以看到多个服务如何使用同一个只创建一次的单例实例。
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;
}
// standard getters
}
在多线程应用程序中,惰性实例化会导致争用条件。因此,我们还应用了双重检查锁定来防止不同线程创建多个实例。
3. Spring中的单例Bean
Spring框架中的bean是在Spring IoC容器中创建、管理和销毁的对象。
3.1 Bean作用域
使用Spring bean,我们可以使用控制反转(IoC)通过元数据将对象注入到Spring容器中。实际上,一个对象可以在不创建它们的情况下定义它的依赖关系,并将该工作委托给IoC容器。
最新版本的Spring框架定义了六种作用域:
- singleton
- prototype
- request
- session
- application
- websocket
bean的作用域定义了它的生命周期和可见性。它还确定将如何创建bean的实际实例。例如,我们可能希望在每次请求bean时创建一个全局实例或不同的实例。
3.2 单例Bean
我们可以在配置类使用@Bean注解在Spring中声明bean。Spring中的单例作用域在容器中为每个bean标识符创建一个bean:
@Configuration
public class SingletonBeanConfig {
@Bean
@Scope(value = ConfigurableBeanFactory.SCOPE_SINGLETON)
public SingletonBean singletonBean() {
return new SingletonBean();
}
}
单例是Spring中定义的所有bean的默认作用域。因此,即使我们没有使用@Scope注解指定特定作用域,我们仍然会得到一个单例bean。此处包含的作用域仅用于说明目的。它通常用于表示其他可用作用域。
3.3 Bean标识符
与纯单例设计模式不同,我们可以从同一个类创建多个单例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; //throws exception
在这种情况下,我们可以使用@Qualifier注解来指定要注入的正确bean标识符:
@Autowired
@Qualifier("singletonBean")
private SingletonBeanConfig.SingletonBean beanOne;
@Autowired
@Qualifier("anotherSingletonBean")
private SingletonBeanConfig.SingletonBean beanThree;
或者,当存在多个相同类型的bean时,可以使用另一个注解@Primary来定义主bean。
4. 比较
现在让我们比较这两种方法并确定在Spring中遵循的最佳实践。
4.1 单例反模式
有些人认为单例是一种反模式,因为它引入了应用程序级全局状态。使用单例的任何其他对象都直接依赖于它。这会导致类和模块之间不必要的相互依赖。
单例模式也违反了单一职责原则。因为单例对象至少负责了两件事:
- 确保只创建一个实例
- 执行他们的正常操作
此外,单例需要在多线程环境中进行特殊处理,以确保单独的线程不会创建多个实例。它们还可能使单元测试和Mock变得更加困难。由于许多Mock框架依赖于继承,私有构造函数使得单例对象难以Mock。
4.2 推荐方法
使用Spring的单例bean而不是实现单例设计模式可以消除上述许多缺点。
Spring框架在所有使用它的类中注入一个bean,但保留了替换或扩展它的灵活性。该框架通过保持对bean生命周期的控制来实现这一点。因此,以后可以将其替换为另一种方法,而无需更改任何代码。
此外,Spring bean使单元测试更加简单。Spring bean很容易Mock,框架可以将它们注入到测试类。我们可以选择注入实际的bean实现或它们的Mock版本。
我们应该注意,单例bean不会只创建类的一个实例,而是在容器中为每个bean标识符创建一个bean。
5. 总结
在本文中,我们探讨了如何在Spring框架中创建单例实例。我们着眼于实现单例设计模式,以及使用Spring的单例bean。
我们探讨了如何实现具有延迟加载和线程安全的单例模式。然后我们研究了Spring中的单例bean作用域,并探讨了如何实现和注入单例bean。我们还看到了单例bean如何区别于使用单例设计模式创建的对象。
最后,我们了解了如何在Spring中使用单例bean来消除传统单例设计模式实现的一些缺点。
与往常一样,本教程的完整源代码可在GitHub上获得。