Skip to content

Spring学习_Spring_Bean的作用域

复习概念

什么是 Spring Bean

Bean 代指的就是那些被 IoC 容器所管理的对象。

我们需要告诉 IoC 容器帮助我们管理哪些对象,这个是通过配置元数据来定义的。配置元数据可以是 XML 文件、注解或者 Java 配置类。

Spring 中的 bean 的作用域有哪些

这个问题是面试题场景的一个问题,我们首先来看一下基本的一个回答。

Spring 中 Bean 的作用域通常有下面几种:

  • singleton : IoC 容器中只有唯一的 bean 实例。Spring 中的 bean 默认都是单例的,是对单例设计模式的应用。
  • prototype : 每次获取都会创建一个新的 bean 实例。也就是说,连续 getBean() 两次,得到的是不同的 Bean 实例。
  • request (仅 Web 应用可用): 每一次 HTTP 请求都会产生一个新的 bean(请求 bean),该 bean 仅在当前 HTTP request 内有效。
  • session (仅 Web 应用可用) : 每一次来自新 session 的 HTTP 请求都会产生一个新的 bean(会话 bean),该 bean 仅在当前 HTTP session 内有效。
  • application/global-session (仅 Web 应用可用):每个 Web 应用在启动时创建一个 Bean(应用 Bean),该 bean 仅在当前应用启动时间内有效。
  • websocket (仅 Web 应用可用):每一次 WebSocket 会话产生一个新的 bean。

如何配置 bean 的作用域

xml 方式:

<bean id="..." class="..." scope="singleton"></bean>

注解方式:

@Bean
@Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public Person personPrototype() {
    return new Person();
}

具体怎么配置的看你实际使用,这里大概讲一下使用场景,通过不同场景下的使用示例对 Spring 中 Bean 的作用域有一个基本认知。

Singleton Bean(默认作用域)

场景: 在一个在线书店应用中,有一个库存管理服务负责跟踪所有书籍的库存。这个服务需要是单例的,因为它维护着整个应用中书籍库存的统一视图。

代码逻辑

import org.springframework.stereotype.Service;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

@Service
public class InventoryService {

    private final Map<String, Integer> bookInventory = new ConcurrentHashMap<>();

    public void addBook(String isbn, int quantity) {
        bookInventory.put(isbn, bookInventory.getOrDefault(isbn, 0) + quantity);
    }

    public boolean checkStock(String isbn) {
        return bookInventory.getOrDefault(isbn, 0) > 0;
    }

    // ... 其他与库存管理相关的方法 ...
}

这里,InventoryService 是一个singleton作用域的Bean,它在应用的整个生命周期内只会有一个实例。

所有对书籍库存的操作都通过这个单一实例进行,确保了库存数据的一致性。

Prototype作用域

场景: 在同一在线书店应用中,当用户想要购买书籍时,每个购物车都应该是独立的。因此,每个用户的购物车应该有其自己的状态。

代码逻辑

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

@Component
@Scope("prototype")
public class ShoppingCart {

    private final Map<String, Integer> items = new HashMap<>();

    public void addItem(String isbn) {
        items.put(isbn, items.getOrDefault(isbn, 0) + 1);
    }

    public Map<String, Integer> getItems() {
        return items;
    }

    // ... 购物车的其他方法 ...
}

@Service
public class ShoppingService {

    @Autowired
    private ApplicationContext context;

    public ShoppingCart createNewShoppingCart() {
        return context.getBean(ShoppingCart.class);
    }
}

在这个场景中,ShoppingService 使用Spring的应用上下文来为每个用户创建一个新的ShoppingCart实例。

每次调用createNewShoppingCart方法时,都会返回一个全新的购物车实例,保证了用户之间购物车的隔离。

Request作用域

场景: 在一个新闻网站应用中,你需要跟踪每个HTTP请求的访问信息,例如用户的地理位置和设备类型,以便为他们提供定制化的新闻内容。

代码逻辑

import org.springframework.web.context.annotation.RequestScope;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;

@Component
@RequestScope
public class UserContext {

    private final String location;
    private final String deviceType;

    public UserContext(HttpServletRequest request) {
        // 通过请求获取用户的位置和设备类型
        this.location = request.getHeader("Location");
        this.deviceType = request.getHeader("Device-Type");
    }

    public String getLocation() {
        return location;
    }

    public String getDeviceType() {
        return deviceType;
    }
}

@RestController
public class NewsController {

    @Autowired
    private UserContext userContext;

    @GetMapping("/news")
    public ResponseEntity<List<NewsItem>> getNews() {
        List<NewsItem> news = newsService.getNewsForLocation(userContext.getLocation());
        // 返回定制化的新闻内容
        return ResponseEntity.ok(news);
    }
}

在这个场景中,UserContext是一个request作用域的Bean,它为每个HTTP请求提供了一个新实例,

包含了请求特定的用户上下文信息,如位置和设备类型。

Session作用域

场景: 在一个在线考试平台上,每个用户的考试过程需要被追踪。用户可能会在考试中断后返回继续考试,所以考试状态需要在会话中保持。

代码逻辑

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

@Component
@SessionScope
public class ExamSession {

    private Exam currentExam;
    private int currentQuestionIndex;

    // 考试会话的方法,例如开始考试、回答问题等
}

@Controller
public class ExamController {

    @Autowired
    private ExamSession examSession;

    @PostMapping("/exam/start")
    public String startExam() {
        examSession.startNewExam();
        return "exam_started";
    }

    @PostMapping("/exam/answer")
    public String answerQuestion(Answer answer) {
        examSession.answerQuestion(answer);
        return "answer_recorded";
    }
}

在这个场景中,ExamSession 是一个session作用域的Bean,它记录了用户的当前考试状态。每个用户会话都有自己的ExamSession实例。

Session vs Global-Session vs WebSocket

  1. Session:
    • 作用域: 限定在一个用户的HTTP会话中。
    • 典型用例: 在用户登录到网站后,你可能需要跟踪该用户的特定状态(比如购物车、偏好设置等)。这种情况下,每个用户都有自己的会话,每个会话都有自己的Bean实例。
  2. Global-Session:
    • 作用域: 限定在Portlet环境的全局HTTP会话中。在Spring 5及之后的版本中已不再使用,因为Spring 5不再支持Portlet。
    • 典型用例: 在Portlet环境中,多个Portlet可能需要共享全局会话数据。例如,用户的语言偏好可能需要跨多个Portlet保持一致。
  3. WebSocket:
    • 作用域: 绑定到WebSocket的会话中。
    • 典型用例: 当用户通过WebSocket连接到你的服务器时,你可能需要为每个WebSocket会话保存状态,例如游戏中的玩家状态或聊天应用中的用户会话

Singleton vs Prototype

Singleton vs Prototype

  1. Singleton:
    • 描述: 在Spring IoC容器中只创建一个Bean实例。
    • 典型用例: 大多数服务层和数据访问层的组件都是无状态的,可以被应用中的所有其他Bean共享。例如,数据库连接池、业务服务等。
  2. Prototype:
    • 描述: 每次请求时,Spring IoC容器都会创建一个新的Bean实例。
    • 典型用例: 比如在一个应用中,你想要为每个文件上传创建一个新的处理器(FileUploadHandler)实例,以防不同用户上传的数据相互冲突。

两种作用域的使用主要取决于Bean的状态管理需求。

Singleton用于那些不需要维护状态信息的共享组件,而Prototype适用于每次使用都需要一个新状态的场景。

Singleton vs Prototype 对比两者的生命周期区别

单例Bean的生命周期:

  1. 实例化: 只有一个Bean实例被创建。
  2. 属性填充: 容器注入依赖的属性。
  3. 初始化: 如果Bean实现了InitializingBean接口或定义了自定义的初始化方法(如使用@PostConstruct注解或在XML配置中指定init-method),将会执行。
  4. 后处理: BeanPostProcessors在初始化前后执行。
  5. 使用: Bean现在可以被应用中的其他Bean使用。
  6. 销毁: 当容器关闭时,如果Bean实现了DisposableBean接口或定义了自定义的销毁方法(如使用@PreDestroy注解或在XML配置中指定destroy-method),将会执行。

多例Bean的生命周期:

  1. 实例化: 每次请求时都创建一个新的Bean实例。
  2. 属性填充: 容器注入依赖的属性。
  3. 初始化: 与单例Bean相同,如果有指定的初始化方法,将会执行。
  4. 后处理: 与单例Bean相同,BeanPostProcessors在初始化前后执行。
  5. 使用: Bean被客户端获取并使用。
  6. 销毁: 容器不会管理多例Bean的完整生命周期;销毁由客户端负责。

区别:

  • 实例化频率:
    • 单例: 只在Spring IoC容器创建时实例化一次。
    • 多例: 每次请求时实例化。
  • 依赖注入时机:
    • 单例: 依赖项在容器创建单例Bean时注入。
    • 多例: 依赖项在每次创建新实例时注入。
  • 生命周期管理:
    • 单例: 容器负责整个生命周期,包括销毁。
    • 多例: 容器启动后,不再管理Bean的生命周期;Bean的销毁不由Spring容器管理,需要用户手动管理。
  • 销毁回调:
    • 单例: 容器关闭时,可以调用销毁方法。
    • 多例: 容器不自动调用销毁方法,必须由获取Bean的客户端代码来处理。

在实际应用中,单例Bean通常用于无状态的服务,例如业务逻辑组件和数据访问对象。

而多例Bean则用于有明确状态的操作,这些状态不能共享给其他实例或线程,例如用户的会话或独立的任务处理器。

多例Bean 的销毁操作有兴趣可以了解一下: https://springdoc.cn/spring/core.html#beans-factory-scopes-prototype

为了让Spring容器释放由 prototype scopeBean 持有的资源,可以尝试使用自定义 Bean后处理器,它持有对需要清理的Bean的引用。


参考