Spring Bean Scope 指南

在本教程中,将带你学习 Spring Framework 的重要组成部分 Spring Bean Scope(作用域)。你将了解它们是什么、如何工作以及何时使用。最后,你将对 Spring Bean Scope 有一个清晰的了解,从而帮助你构建更好的 Spring 应用程序。

Spring Bean 介绍

在 Spring Framework 的世界里,“Spring Bean” 是一个非常重要的术语。从本质上讲,Spring Bean 是由 Spring IoC(反转控制)容器实例化、组装和管理的对象。这些 Bean 是根据你提供给容器的配置元数据创建的,例如,以 XML 定义或源代码注解的形式。

Bean 可以看作是任何 Spring 应用程序的基本构件。它们是构成应用程序主干的对象,由 Spring IoC 容器管理。这些 Bean 是根据项目中提供的定义创建的,用于履行应用程序中的各种角色,如 service 类、数据访问对象 (DAO)、Spring MVC Controller 等对象,甚至是简单对象。

Spring Bean Scope 是什么

Spring Bean Scope(或换句话说,Bean 的作用域)决定了这些 Bean 在应用程序各种上下文中的生命周期和可见性。

Spring Bean 的 scope 定义了 Bean 存在的边界、与之绑定的上下文以及存活时间。简而言之,它定义了何时创建 Bean 的新实例,以及何时删除该特定实例。

Spring 提供多种 scope,例如:

  • Singleton
  • Prototype
  • Request
  • Session
  • Application
  • Websocket

每个 scope 都意味着不同的生命周期和它们定义的 Bean 的可见性。正确理解和使用这些 scope 对于构建稳健高效的 Spring 应用程序至关重要。

在本指南中,我们将探讨这些不同的 Bean scope,说明它们的用例,并提供示例帮助你更好地理解并在 Spring 应用程序中使用它们。本文的目标是让你全面了解这些 scope 的工作原理,从而更高效地设计你的 Spring 应用程序。

Singleton Scope

在 Spring Framework 的世界中,Singleton 是默认的 Bean scope。当一个 Bean 被定义为 Singleton 时,Spring IoC 容器会为该 Bean 定义所定义的对象创建一个实例。此单个实例存储在此类单例 Bean 的缓存中,对该命名 Bean 的所有后续请求和引用都会返回缓存对象。请记住,单例作用域仅在每个容器中有效,而不是在整个应用程序中有效。因此,如果运行多个 Spring 容器,每个容器都将拥有自己的 Singleton 实例。

Singleton Scope 的应用场景

在使用 singleton scope 时,最佳方案通常涉及无状态 Bean。这意味着 bean 不会存储特定于客户端的数据。为什么这一点很重要?因为在数据库连接池、业务逻辑组件和日志记录器等服务中,没有必要维护具有各自状态的多个实例。单个共享实例(Singleton)非常适合这些情况。

不过,也有相反的一面。假设你的 Bean 需要为每个客户保持不同的状态,也就是通常所说的对话状态。在这种情况下,Singleton 可能不是最合适的选择。其他 scope 可以更有效地实现你的目的。

Singleton Scope 示例

让我们看一个简单的例子来演示 singleton scope:

@Configuration
public class AppConfig {

    @Bean
    @Scope("singleton")
    public MySingletonBean myBean() {
        return new MySingletonBean();
    }
}

public class MySingletonBean {
    // class body...
}

在上述代码中,myBean Bean 是以 singleton scope 声明的。如果我们多次从容器中请求这个 Bean,每次都会得到完全相同的实例。

我们可以用以下测试方法进行测试。

@RunWith(SpringRunner.class)
@ContextConfiguration(classes = AppConfig.class)
public class MySingletonBeanTest {

    @Autowired
    private ApplicationContext context;

    @Test
    public void whenSingletonScope_thenSingleInstanceCreated() {
        MySingletonBean firstInstance = context.getBean(MySingletonBean.class);
        MySingletonBean secondInstance = context.getBean(MySingletonBean.class);

        assertSame(firstInstance, secondInstance);
    }
}

在测试中,firstInstancesecondInstance 是指向同一个 MySingletonBean 对象的两个变量,因为默认情况下 Spring 中的 Bean 都是 singleton scope 的。assertSame 断言验证了这两个实例确实相同。

这个简单的测试验证了我们的 Bean 具有 Singleton scope,因为它证明 Spring IoC 容器只创建了 MySingletonBean 的单个实例。你也可以按照类似的步骤为其他 scope 创建测试,但可能需要为某些 scope(如 request、session 或 websocket)设置额外的配置。

Singleton Scope 最佳实践

While using Singleton Scope, you should be aware of some best practices:

在使用 singleton Scope 时,你应该注意一些最佳实践:

  1. 无状态: 尽可能使你的 singleton Bean 无状态。有状态的 Singleton 会导致与共享状态相关的难以调试的问题,尤其是在多线程环境中。
  2. 线程安全: 如果你的 Singleton Bean 有状态,请确保它们是线程安全的,因为 Singleton Bean 是跨多个线程共享的。
  3. 延迟初始化: 如果你的 Singleton Bean 在启动过程中会占用大量资源,请考虑对其使用延迟初始化。这可以通过在 Bean 定义中添加 @Lazy 注解来实现。

请记住,Singleton scope 的关键在于了解每个 Spring IoC 容器只有一个实例。因此,与 Singleton Bean 的每次交互都将与相同的状态交互,因此应进行相应的设计。

Prototype Scope

在 Spring 框架中,prototype(原型) scope 表示将创建一个 Bean 的新实例,并在每次请求该 Bean 时返回。简单地说,当你将 Bean 设置为 prototype scope 时,Spring 不会管理该 Bean 的整个生命周期;容器会实例化、配置并以其他方式组装一个 prototype 对象,然后将其交给客户端,不再记录该实例。

Prototype Scope 的应用场景

现在,你可能想知道这样的 scope 会在哪里发挥作用。prototype scope 非常适合有状态 Bean,在这种情况下,每个实例都可以保存独立于其他实例的数据。因此,如果你的应用程序中的多个对象或多个操作都打算独立使用和操作一个 Bean,那么 prototype scope 就非常适合。

Prototype Scope 示例

比方说,我们有一个 Message Bean,其目的是携带一条带有时间戳的消息。每次使用该 Bean 时,我们都希望创建一个新实例,以确保时间戳与创建 Bean 的时间一致。以下是你在 Spring 配置中的定义方式:

@Configuration
public class AppConfig {

    @Bean
    @Scope("prototype")
    public Message message() {
        return new Message();
    }
}

public class Message {
    private LocalDateTime timestamp;
    private String content;

    public Message() {
        this.timestamp = LocalDateTime.now();
    }

    // getter 和 setter
}

在此配置中,message() Bean 是以 “prototype” scope 定义的。每次请求 Message Bean 时,都会创建一个带有唯一时间戳的新实例。这就确保了Message 的每个实例都能拥有独立的状态和行为。

你可以使用以下测试方法测试上述 Message 类。

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.ApplicationContext;

import java.time.LocalDateTime;

import static org.junit.jupiter.api.Assertions.*;

@SpringBootTest
public class MessageTest {

    @Autowired
    private ApplicationContext applicationContext;

    @Test
    public void givenPrototypeScope_whenSetContent_thenDifferentContent() {
        Message messagePrototypeA = (Message) applicationContext.getBean("message");
        Message messagePrototypeB = (Message) applicationContext.getBean("message");

        messagePrototypeA.setContent("Hello");
        messagePrototypeB.setContent("World");

        assertEquals("Hello", messagePrototypeA.getContent());
        assertEquals("World", messagePrototypeB.getContent());
    }

    @Test
    public void givenPrototypeScope_whenGetTimestamp_thenDifferentTimestamp() {
        Message messagePrototypeA = (Message) applicationContext.getBean("message");
        Message messagePrototypeB = (Message) applicationContext.getBean("message");

        LocalDateTime timestampA = messagePrototypeA.getTimestamp();
        LocalDateTime timestampB = messagePrototypeB.getTimestamp();

        assertNotEquals(timestampA, timestampB);
    }
}

第一个测试方法是 givenPrototypeScope_whenSetContent_thenDifferentContent,它检查从同一个 prototype scope 创建的两个 message Bean 在被设置为不同的值时是否有不同的内容。这将验证 prototype scope 是否在每次请求时都会创建一个新的 message Bean 实例。

第二个测试方法 givenPrototypeScope_whenGetTimestamp_thenDifferentTimestamp 会检查从同一个 prototype scope 创建的两个 message Bean 在初始化时是否有不同的时间戳。这可验证 prototype scope 不会在不同的请求中重复使用同一个 message Bean 实例。

Prototype Scope 最佳实践

使用 prototype scope 非常有益,但记住一些最佳实践也很重要。

  • 首先,请记住 Spring 不会管理 prototype Bean 的整个生命周期:不会调用销毁生命周期回调。客户端代码必须清理 prototype scope 对象,并释放 prototype Bean 持有的资源。
  • 其次,尽量少用 prototype scope。每次需要创建一个新的 Bean 实例时,都会耗费大量内存和处理时间,特别是对于重量级的有状态 Bean。
  • 最后,如果你将具有 prototype scope 的 Bean 注入到 singleton Bean 中,则 prototype Bean 的行为仍与 singleton Bean 相同。这是因为 singleton Bean 只创建一次,因此只会注入 prototype Bean 的一个实例。要解决这个问题,你可以使用 Spring 的方法注入功能。

Request Scope

Spring 中的 Request Scope 是框架提供的 Web 感知 scope 之一。顾名思义,它将单个 Bean 定义的 scope 限定为单个 HTTP 请求的生命周期。换句话说,每个 HTTP 请求都有自己的 Bean 实例,该实例是根据单个 Bean 定义创建的。因此,每当你发出一个新的 HTTP 请求时,就会创建一个新的 Bean 实例,并且该实例对该请求是唯一的。

Request Scope 的应用场景

在很多情况下,request scope 都非常有用。一个典型的用例就是在处理表单数据提交和绑定时。假设你正在开发一个用户可以填写表单(form)的 web 应用程序。每个用户的表单数据对于特定的 HTTP 请求都是唯一的。在这种情况下,表单 Bean 应具有 request scope,以保持数据对单个请求的唯一性。

另一个场景是,当你需要在单个 HTTP 请求中跨多个组件维护状态,但又不想将状态存储在类似 HTTP Session 这样的共享全局对象中时。

Request Scope 示例

为了使这一概念更加具体,让我们来看看如何在 Spring 配置中定义具有 request scope 的 Bean:

@Bean
@Scope(value = WebApplicationContext.SCOPE_REQUEST, proxyMode = ScopedProxyMode.TARGET_CLASS)
public MyClass myClass() {
    return new MyClass();
}

在本示例中,MyClass 类被定义为具有 request scope 的 Spring Bean。@Scope 注解用于指定 Bean 的 scope。WebApplicationContext.SCOPE_REQUEST 将 scope 设置为 request,这意味着将为每个 HTTP 请求创建该 Bean 的新实例。

@Scope 注解的 proxyMode 属性被设置为 ScopedProxyMode.TARGET_CLASS,这意味着 Spring 将为 Bean 创建一个基于 class 的代理。简单来说,代理是 Spring 创建的一个 “替身” 对象,它可以确保在需要的地方注入 Bean 的正确实例(本例中每个 request 一个),即使注入目标是一个 singleton Bean 也是如此。

或者,你也可以使用 @RequestScope 注解来代替 @Scope(value=WebApplicationContext.SCOPE_REQUEST)。这是专用于 request scope 的 @Scope 注解的一种特殊形式:

import org.springframework.web.context.annotation.RequestScope;
import org.springframework.stereotype.Component;

@Component
@RequestScope
public class MyRequestScopedBean {

    private String message;

    public MyRequestScopedBean() {
        message = "Initial Message";
    }

    public String getMessage() {
        return this.message;
    }

    public void setMessage(String message) {
        this.message = message;
    }
}

由于 MyRequestScopedBean 被注解为 @RequestScope,因此将为每个 HTTP 请求创建一个新的 Bean 实例,并且每个请求的 message 字段都将是唯一的。

Request Scope 的最佳实践

在 Spring 框架中使用 Request Scope Bean 时,需要遵循以下一些最佳实践:

  • 请注意,Request Scoped Bean 是有状态的,不能在多个请求之间共享。这些 Bean 不应在不同 HTTP 请求或用户之间共享信息。
  • 保持该 scope 中的 Bean 的轻量级。由于这些 Bean 的生命周期与 HTTP 请求息息相关,因此构造函数中的任何繁重操作都可能会减慢响应时间,从而导致糟糕的用户体验。
  • 请记住,其他非 Request Scope 的 Bean(如 Singleton Bean)不会为每个请求重新创建。如果你将一个 Request Scope Bean 注入到一个单例 Bean,除非你使用代理,否则 Request Scope Bean 的行为将不符合预期。
  • 始终确保在请求结束前清理 Request Scope Bean 可能持有的任何资源。这包括数据库连接、文件流等资源,以防止任何资源泄漏。

Session Scope

在 Spring 框架中,当一个 Bean 被定义为 session scope 时,这意味着将为每个 HTTP session 创建和管理该 Bean 的单个实例。简单地说,Bean 存储在 HTTP session 中,每次在同一 session 中需要它时,都会返回相同的实例。这与 singleton scope(整个应用程序共享一个实例)或 prototype scope(每次需要 bean 时都会创建一个新实例)截然不同。

什么时候使用 Session Scope?

当你需要在单个 session 中,在用户与应用程序交互的整个过程中保留 Bean 的状态时,session scope 尤其有用。网上商店的购物车就是一个很好的例子。你希望购物车在每个用户会话中都是唯一的,并在他们浏览网站时存储他们选择的项目。session scope 非常适合这种情况,因为它可以确保 Bean 的状态在每个用户 session 中都是独立和持久的。

Session Scope 示例

下面是一个如何使用 @Scope 注解定义 session scope Bean 的简单示例:

import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;

@Component
@Scope("session")
public class ShoppingCart {
    // 购物车实现
}

此外,@Scope 注解允许两个参数,即 valueproxyMode,这在你要将 session scope Bean 注入单例 Bean 时非常有用:

import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.context.annotation.ScopedProxyMode;

@Component
@Scope(value = WebApplicationContext.SCOPE_SESSION, proxyMode = ScopedProxyMode.TARGET_CLASS)
public class ShoppingCart {
    // 购物车实现
}

在 Spring 的最新版本中,你还可以使用 @SessionScope 注解,它更加具体:

import org.springframework.web.context.annotation.SessionScope;
import org.springframework.stereotype.Component;

@Component
@SessionScope
public class ShoppingCart {
    // 购物车实现
}

在所有这些情况下,ShoppingCart Bean 都将特定于 HTTP session。请记住,proxyMode 有助于创建注入到 Bean 中的代理,从而允许 Spring 容器确保 Bean 在正确的上下文中使用,即使注入到 singleton Bean 中也是如此。

Session Scope 最佳实践

在使用 session scope Bean 时,有几种最佳做法需要牢记:

  1. 轻量化 Session Bean: 尽量保持会 session scope Bean 的轻量级。请记住,每个用户 session 都会创建一个新实例,因此过大的 Bean 可能会导致内存问题。
  2. 非必要,不使用: Session scope 很方便,但会增加复杂性和内存使用量。只有在需要在单个 session 中的多个请求之间保持状态时才使用它。
  3. 线程安全: 虽然每个 session 都有自己的 bean 实例,但请记住,HTTP session 不是线程安全的。如果 session 中涉及多个线程,可能需要额外的同步处理。
  4. Session 结束: 请注意 session 结束时会发生什么。Bean 的生命周期与 session 息息相关,因此一旦 session 结束,Bean 的生命周期也随之结束。请在设计 Bean 时考虑到这一点。

Application Scope

在 Spring Framework 中,Application Scope 是多个可用 Bean scope 之一。使用 Application Scope 定义 Bean 时,意味着整个 ServletContext 将共享 Bean 的同一实例。这意味着,一旦初始化了 Bean,它就会在应用程序的整个生命周期中存在,并在所有 request 和 session 中共享。这与其他 scope 不同,在其他 scope 中,Bean 的生命周期可以短至一个 HTTP 请求,也可以长至一个 HTTP session。

什么时候使用 Application Scope?

当你需要在整个应用程序中保持一致性,并且需要所有用户和组件与同一个 Bean 实例进行交互时,Application Scope Bean 就会大有用武之地。例如,当你有全应用程序范围的设置,或者您想缓存检索成本较高的数据并在整个应用程序中使用时,这可能会很有用。

Application Scope 示例

下面是一个基本示例,展示了如何使用 @Scope 注解定义具有 Application Scope 的 Bean:

@Component 
@ApplicationScope 
public class ApplicationScopeBean {
  // 忽略类的详情定义...
}

或者,你也可以使用 @ApplicationScope 注解来代替 @Scope(WebApplicationContext.SCOPE_APPLICATION)

@ApplicationScope@Scope 的一种特殊化,用于生命周期与当前 web 应用绑定的组件。它还将默认的 proxyMode 设置为 TARGET_CLASS,这意味着将为 scope 的 Bean 创建一个 CGLIB 代理。

Application Scope 最佳实践

在使用 Application Scope 时,有必要了解一些最佳实践,以避免潜在问题。

  1. 线程安全: 由于 Application Scope bean 在所有 request 和 session 中共享,因此多个线程可能会并发访问它们。因此,它们需要是线程安全的,以防止数据不一致。
  2. 无状态: 一般来说,保持 Application Scope bean 无状态或不可变是个好主意,因为它们在应用程序的整个生命周期中都存在,并在所有组件中共享。
  3. 内存消耗: 鉴于这些 Bean 在应用程序的整个生命周期中都存在,如果不小心处理,它们可能会消耗大量内存。因此,应确保这些 bean 不会在超出必要的时间内占用大量资源。
  4. 初始化时间: 由于 Application Scope Bean 是在启动时创建的,因此它们可能会增加应用程序的初始化时间。在规划应用程序的启动时间时,请考虑这一点。

WebSocket Scope

首先,让我们来了解一下什么是 WebSocket Scope。我们已经了解到,Spring 中的 Scope 决定了 Spring Bean 的生命周期和可见性。WebSocket Scope,顾名思义,与 WebSocket 的生命周期息息相关。在此 Scope 中定义的 Bean 将在 WebSocket 的持续时间内存活,并在 WebSocket 关闭时被废弃。

何时使用 WebSocket Scope?

那么,什么时候应该使用 WebSocket Scope 呢?答案在于应用程序的性质。如果你的应用程序依赖 WebSockets 实现服务器和客户端之间的双向实时通信,并且你需要一个特定于每个 WebSocket Session 的 Bean,那么这种 Scope 就非常合适。这样,你就可以拥有有状态的 Bean,其数据是 WebSocket Session 所特有的,并与其他会话隔离。

WebSocket Scope 示例

现在,让我们来看一个实际例子。下面是一个 WebSocket Scope Bean 的简单声明:

@Configuration
public class AppConfig {

    @Bean
    @Scope(scopeName = "websocket", proxyMode = ScopedProxyMode.TARGET_CLASS)
    public MyBean myBean() {
        return new MyBean();
    }
}

在此代码片段中,@Scope 的 scope 名称为 websocket,表示此 Bean 专用于 WebSocket session。proxyMode 属性被设置为 ScopedProxyMode.TARGET_CLASS,以便为该 scope Bean 创建一个 CGLIB 代理。请记住,必须显式配置 WebSocket scope 才能使其在应用程序中可用。

WebSocket Scope 最佳实践

最后,让我们来谈谈处理 WebSocket Scope 时的一些最佳实践。

  1. 合理使用: 只有在需要特定于 WebSocket session 的数据时,才能使用 WebSocket scope Bean。如果数据不是针对 session 的,请考虑使用不同的 scope。
  2. 管理资源: 由于 WebSocket scope 的 Bean 在 WebSocket 的持续时间内都是有效的,因此要确保有效管理资源,防止内存泄漏。
  3. 理解代理: 在使用 scope Bean 时,请务必了解使用代理的影响。代理模式的不正确使用会导致意想不到的行为。

总结

在本教程中,你已经深入了解 Spring Bean 的 Scope。以下是总结的要点:

  1. Bean Scope: Spring Bean Scope 是控制应用程序中 Spring Bean 生命周期和可见性的基础。
  2. Scope 类型: 你已经了解了 Spring 框架中各种类型的 scope,包括 Singleton、Prototype、Request、Session、Application 和 WebSocket scope。
  3. 何时使用哪种 Scope: 每个 scope 都有不同的目的,在不同的情况下使用。当需要在多个组件中共享单个实例时,就会使用 Singleton Bean。当每次请求 Bean 时都需要一个新实例时,就会使用 Prototype Bean。Request、Session 和 Application scope 用于在 Web 应用程序中创建生命周期与 HTTP request、session 或整个 web application 相关联的 Bean。WebSocket scope 用于将 bean 的生命周期与 WebSocket session 绑定。
  4. Scope 注解: Spring 提供了 @Scope 注解,用于在声明 Bean 时指定其 scope。
  5. 代理与 Scope: 你已经了解了proxy mode(TARGET_CLASSNODEFAULT)及其在正确管理 scope Bean 中的重要性。
  6. 最佳实践: 你已经了解了在使用各种 Spring Bean Scope 时应遵循的一些最佳实践,例如根据应用程序的需求使用适当的 Scope,以及有效管理资源以防止内存泄漏。

记住,了解 Spring Bean Scope 的正确使用方法会极大地影响 Spring 应用程序的效率和效果。请不断探索和实践,充分利用 Spring 框架提供的这一强大功能。


原文: https://www.appsdeveloperblog.com/a-guide-to-spring-bean-scopes/