在使用 Spring 开发应用时,Bean 的作用域(Scope) 决定了 Bean 实例的生命周期。Spring 提供了多种作用域,其中最常见的是 单例(singleton) 和 原型(prototype) 作用域。
- 单例作用域(singleton):容器中只有一个实例,整个应用上下文中的 Bean 共享同一个实例。
- 原型作用域(prototype):每次请求都会创建一个新的实例,每个请求返回一个不同的对象。
理论上,原型 Bean 的每次请求都应返回一个新的实例,而单例 Bean 应该只存在一个实例。问题发生在当 原型 Bean 注入到单例 Bean 中时,原型 Bean 的实例化行为可能并不像预期的那样,每次都返回新的实例。
一、问题描述
假设我们有一个 单例 Bean SingletonBean
,它需要使用一个 原型 Bean PrototypeBean
来完成某些功能。通常,我们期望每次调用 SingletonBean
中的 PrototypeBean
时,都能获得一个新的实例。然而,由于 Spring 的默认行为,PrototypeBean
在 SingletonBean
中可能会被共享,这与我们希望原型 Bean 每次创建新实例的设计相冲突。
示例代码
@Component
@Scope("singleton") // SingletonBean 是单例作用域
public class SingletonBean {
@Autowired
private PrototypeBean prototypeBean; // 注入原型 Bean
public void doSomething() {
prototypeBean.doSomething(); // 每次都会调用同一个实例
}
}
@Component
@Scope("prototype") // PrototypeBean 是原型作用域
public class PrototypeBean {
public void doSomething() {
System.out.println("PrototypeBean doing something");
}
}
在这个示例中:
SingletonBean
是单例作用域的,每次访问都会返回相同的实例。PrototypeBean
是原型作用域的,本应每次请求时创建新的实例,但由于它被注入到SingletonBean
中,Spring 只会在容器启动时实例化一次PrototypeBean
,并将该实例注入到SingletonBean
中。
问题的本质是:尽管 PrototypeBean
是原型作用域的,Spring 会在 SingletonBean
创建时注入它的实例,并且在 SingletonBean
的整个生命周期内使用同一个 PrototypeBean
实例,而不会每次都创建新的实例。
二、问题产生的原因
问题的根源在于 Spring 的依赖注入机制:
-
Spring 容器在启动时创建所有的 Bean。对于单例作用域的 Bean,Spring 会在容器初始化时创建实例,并且在整个应用生命周期内保持该实例。而原型作用域的 Bean 每次被请求时都会创建新实例。
-
单例 Bean 中注入原型 Bean 时,Spring 会在容器初始化时就实例化
PrototypeBean
,并将其注入到SingletonBean
中。即使PrototypeBean
是原型作用域的,它的实例也会在容器初始化时就被创建,并且这个实例会在SingletonBean
的整个生命周期内被共享。 -
没有延迟加载机制:单例 Bean 中的
PrototypeBean
注入后,Spring 并不会每次重新创建一个新的实例,而是使用已经注入的原型 Bean 实例。因此,每次访问SingletonBean
时,得到的PrototypeBean
实例是同一个,而不是一个新的实例。
三、解决方案
为了确保 单例 Bean 中注入的原型 Bean 每次都能返回新的实例,可以通过以下几种方法来解决这个问题。
1. 使用 ObjectProvider
ObjectProvider
是 Spring 提供的一种 延迟加载机制。通过 ObjectProvider
,我们可以在需要时动态地获取原型 Bean,确保每次调用时都返回一个新的实例。
@Component
public class SingletonBean {
@Autowired
private ObjectProvider<PrototypeBean> prototypeBeanProvider;
public void doSomething() {
// 每次调用 getObject() 时都会获取新的 PrototypeBean 实例
PrototypeBean prototypeBean = prototypeBeanProvider.getObject();
prototypeBean.doSomething();
}
}
ObjectProvider<PrototypeBean>
让PrototypeBean
以延迟加载的方式实例化,每次调用getObject()
时都会返回新的实例。- 这种方法可以确保每次调用时都获得一个新的原型 Bean 实例,而不是复用已注入的实例。
2. 使用 ApplicationContext
获取原型 Bean
ApplicationContext
提供了一个方法 getBean()
,可以动态获取原型作用域的 Bean。通过这种方式,我们可以确保每次获取原型 Bean 时,都会得到一个新的实例。
@Component
public class SingletonBean {
@Autowired
private ApplicationContext applicationContext;
public void doSomething() {
// 每次调用 getBean() 都会获取新的 PrototypeBean 实例
PrototypeBean prototypeBean = applicationContext.getBean(PrototypeBean.class);
prototypeBean.doSomething();
}
}
applicationContext.getBean(PrototypeBean.class)
每次调用时都会返回新的PrototypeBean
实例,而不是使用已经注入的实例。- 这种方法适用于需要通过
ApplicationContext
动态获取原型 Bean 的场景。
3. 结合 @Scope("prototype")
和 @Lazy
注解
@Lazy
注解可以延迟 Bean 的初始化,确保原型 Bean 只在第一次需要时才会被创建。这结合 @Scope("prototype")
注解可以确保每次请求时都会创建新的实例。
package com.nbsaas.boot.controller.web;
import com.nbsaas.boot.rest.response.ResponseObject;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.Resource;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.context.annotation.Lazy;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class StateController {
private Integer account=0;
@Resource
private ObjectProvider<StateHandle> stateHandles;
@Lazy
@Resource
private StateHandle stateHandle;
@RequestMapping("/state")
public ResponseObject<String> state(){
ResponseObject<String> result=new ResponseObject<>();
account++;
stateHandles.getObject().handle();
stateHandle.handle();
return result;
}
@RequestMapping("/account")
public String account(){
return "account:"+account;
}
@PostConstruct
public void init(){
System.out.println("init");
}
}
当我们在 Spring 中遇到 单例 Bean 注入原型 Bean 被共享的问题时,问题的根源在于 Spring 默认会在容器启动时实例化原型 Bean,并将同一个实例注入单例 Bean。为了解决这个问题,我们可以使用以下几种方法:
- 使用
ObjectProvider
:延迟加载原型 Bean,每次调用时返回新的实例。 - 使用
ApplicationContext
:通过getBean()
动态获取原型 Bean,每次获取新的实例。 - 结合
@Scope("prototype")
和@Lazy
注解:延迟初始化原型 Bean,确保每次请求时创建新的实例。
这些解决方案能确保单例 Bean 中的原型 Bean 每次都创建新的实例,而不是共享同一个实例,从而解决了原型 Bean 被共享的问题。