你好,這里是codetrend專欄“Spring6全攻略”。
在 Spring 框架中,Bean 的作用域(Scope)定義了 Bean 實(shí)例在容器中如何創(chuàng)建、管理和銷毀的策略。
Spring 提供了多種 Bean 作用域,每種作用域都有其特定的生命周期和適用場景。
先試試不同的 Bean Scope
下面通過一個(gè)簡單的 Spring MVC Controller 示例來感受下 Bean 的作用域。
例子代碼是這樣的:
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Scope;
import org.springframework.web.context.WebApplicationContext;
import java.util.UUID;
@Configuration
public class AppConfig {
@Bean
@Scope(ConfigurableBeanFactory.SCOPE_SINGLETON)
public SingletonBean singletonBean() {
return new SingletonBean();
}
@Bean
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public PrototypeBean prototypeBean() {
return new PrototypeBean();
}
@Bean
@Scope(WebApplicationContext.SCOPE_SESSION)
public SessionBean sessionBean() {
return new SessionBean();
}
}
class SingletonBean {
private int count = 0;
public void increment() {
count++;
}
public int getCount() {
return count;
}
}
class PrototypeBean {
private String id;
public PrototypeBean() {
this.id = UUID.randomUUID().toString();
}
public String getId() {
return id;
}
}
class SessionBean {
private String id;
public SessionBean() {
this.id = UUID.randomUUID().toString();
}
public String getId() {
return id;
}
}
controller 代碼:
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class ScopeController {
@Autowired
private SingletonBean singletonBean;
@Autowired
private ApplicationContext context;
@GetMapping("/singleton")
public String singletonCount() {
singletonBean.increment();
return "Singleton Count: " + singletonBean.getCount();
}
@GetMapping("/prototype")
public String prototypeGet() {
PrototypeBean prototypeBean = context.getBean(PrototypeBean.class);
return "Prototype ID: " + prototypeBean.getId();
}
@GetMapping("/session")
public String sessionGet() {
SessionBean prototypeBean = context.getBean(SessionBean.class);
return "Session ID: " + prototypeBean.getId();
}
}
- Singleton(單例)的屬性持續(xù)增加,也就是說訪問到的SingletonBean每次都是同一個(gè)對象。訪問
/singleton接口的返回是這樣的:
1
2
3
- Prototype(原型)的屬性每次都是不一樣的,也就是說明id每次都是調(diào)用構(gòu)造器新創(chuàng)建的。訪問
/prototype接口的返回是這樣的:
Prototype ID: 3ea5af10-ddce-4a89-ad3c-3f07a764f179
Prototype ID: 7e6e9fe8-c0dc-423e-b282-96b7f8087dac
Prototype ID: 7aca1000-484d-46e8-80f7-d444a5a04f49
- Session(會話)的屬性同一窗口是一樣的,開啟無痕窗口或者其他瀏覽器就不一樣。訪問
/session接口的返回是這樣的:
Prototype ID: 7aca1000-484d-46e8-80f7-d444a5a04f49
Prototype ID: 7aca1000-484d-46e8-80f7-d444a5a04f49
# 開啟新的窗口后
Session ID: bd22d310-29e5-4004-8555-9678c08275f0
Session ID: bd22d310-29e5-4004-8555-9678c08275f0
可以直接把樣例代碼復(fù)制到例子里面驗(yàn)證測試。這樣我們就對BeanScope作用域有個(gè)直觀的感受。
自定義一個(gè) Bean Scope
接下來通過實(shí)現(xiàn)一個(gè)自定義作用域來感受下Bean的作用域原理。
在 Spring 框架中,除了預(yù)定義的幾種作用域(如 singleton、prototype 等)外,用戶還可以自定義作用域以滿足特定的業(yè)務(wù)需求。
自定義作用域允許控制 Bean 的創(chuàng)建、緩存和銷毀邏輯,以適應(yīng)特定的場景,如基于特定條件的實(shí)例化策略、自定義生命周期管理等。
自定義步驟:
-
定義作用域接口:首先,需要實(shí)現(xiàn)
org.springframework.beans.factory.config.Scope接口,該接口定義了 Bean 作用域的基本行為。 -
實(shí)現(xiàn)邏輯:在自定義的 Scope 接口實(shí)現(xiàn)中,需要覆蓋
get、remove和registerDestructionCallback方法,分別用于獲取 Bean 實(shí)例、移除 Bean 實(shí)例以及注冊銷毀回調(diào)。 - 注冊作用域:在 Spring 配置中注冊的自定義作用域,使其可被容器識別和使用。
-
使用自定義作用域:在 Bean 定義中通過
@Scope注解指定使用自定義的作用域名稱。
自定義作用域?qū)崿F(xiàn)
首先自定義作用域?qū)崿F(xiàn),也就是實(shí)現(xiàn)接口org.springframework.beans.factory.config.Scope。
import org.springframework.beans.factory.ObjectFactory;
import org.springframework.beans.factory.config.Scope;
import java.util.concurrent.ConcurrentHashMap;
import java.util.Map;
import java.util.UUID;
public class CustomScope implements Scope {
public final static String CUSTOM_SCOPE_NAME = "custom";
private final Map<String, Object> scopedObjects = new ConcurrentHashMap<>();
private final Map<String, Runnable> destructionCallbacks = new ConcurrentHashMap<>();
@Override
public Object get(String name, ObjectFactory<?> objectFactory) {
Object scopedObject = scopedObjects.get(name);
if (scopedObject == null) {
scopedObject = objectFactory.getObject();
scopedObjects.put(name, scopedObject);
}
return scopedObject;
}
@Override
public Object remove(String name) {
scopedObjects.remove(name);
Runnable callback = destructionCallbacks.remove(name);
if (callback != null) {
callback.run();
}
return null;
}
@Override
public void registerDestructionCallback(String name, Runnable callback) {
destructionCallbacks.put(name, callback);
}
@Override
public Object resolveContextualObject(String key) {
// 可以根據(jù)需要實(shí)現(xiàn)上下文對象解析邏輯
return null;
}
@Override
public String getConversationId() {
// 返回一個(gè)唯一的標(biāo)識,用于區(qū)分作用域上下文
return UUID.randomUUID().toString();
}
}
可以看到Scope接口其實(shí)是對Bean的全生命周期進(jìn)行管理,包括獲取get、緩存和銷毀remove和銷毀回調(diào)等邏輯。這也是作用域的核心原理。
Spring6怎么實(shí)現(xiàn)的Scope
這里以org.springframework.web.context.request.RequestScope 為例子來理解Spring6怎么實(shí)現(xiàn)BeanScope的。
得益于Spring框架的抽象和封裝,這個(gè)類的實(shí)現(xiàn)代碼并沒有多少。
RequestScope extends AbstractRequestAttributesScope核心實(shí)現(xiàn)在這個(gè)類AbstractRequestAttributesScope。get獲取對象方法,其中對象的存儲放在了ThreadLocal中,也就是RequestContextHolder這個(gè)類的核心。
/**
* 根據(jù)名稱獲取對象,如果當(dāng)前請求屬性中沒有該對象,則使用對象工廠創(chuàng)建一個(gè)對象,并將其設(shè)置到請求屬性中
* 然后再次獲取該對象,以便進(jìn)行隱式會話屬性更新。作為額外的好處,我們還允許在獲取屬性級別進(jìn)行潛在的裝飾。
* 如果再次獲取到的對象不為空(預(yù)期情況),則只使用該對象。如果它同時(shí)消失了,我們則返回本地創(chuàng)建的實(shí)例。
*/
public Object get(String name, ObjectFactory<?> objectFactory) {
// 獲取當(dāng)前請求的屬性
RequestAttributes attributes = RequestContextHolder.currentRequestAttributes();
// 根據(jù)名稱和作用域獲取對象
Object scopedObject = attributes.getAttribute(name, getScope());
if (scopedObject == null) {
// 使用對象工廠創(chuàng)建對象
scopedObject = objectFactory.getObject();
// 將創(chuàng)建的對象設(shè)置到請求屬性中
attributes.setAttribute(name, scopedObject, getScope());
// 再次獲取對象,進(jìn)行隱式會話屬性更新
// 并允許進(jìn)行潛在的裝飾
Object retrievedObject = attributes.getAttribute(name, getScope());
if (retrievedObject!= null) {
// 只使用再次獲取到的對象(如果仍然存在,這是預(yù)期情況)
// 如果它同時(shí)消失了,我們則返回本地創(chuàng)建的實(shí)例
scopedObject = retrievedObject;
}
}
// 返回獲取到的對象
return scopedObject;
}
-
remove方法也是差不多的。借助工具類RequestContextHolder將緩存在ThreadLocal中的對象移除。
/**
* 移除指定名稱的對象,如果當(dāng)前請求屬性中存在該對象,則將其從請求屬性中移除并返回該對象;否則返回 null
*/
public Object remove(String name) {
// 獲取當(dāng)前請求的屬性
RequestAttributes attributes = RequestContextHolder.currentRequestAttributes();
// 根據(jù)名稱和作用域獲取對象
Object scopedObject = attributes.getAttribute(name, getScope());
if (scopedObject!= null) {
// 將該對象從請求屬性中移除
attributes.removeAttribute(name, getScope());
// 返回移除的對象
return scopedObject;
} else {
// 返回 null
return null;
}
}
注冊自定義作用域
注冊作用域,需要通過BeanFactory的registerScope方法進(jìn)行注冊。
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanFactoryPostProcessor;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.stereotype.Component;
@Component
public class ScopeBeanFactoryPostProcessor implements BeanFactoryPostProcessor {
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
beanFactory.registerScope(CustomScope.CUSTOM_SCOPE_NAME, new CustomScope());
}
}
驗(yàn)證自定義作用域效果
將Bean注冊到Spring容器中,并使用自定義作用域。
public class MyScopeBean {
private String id;
public MyScopeBean() {
this.id = UUID.randomUUID().toString();
}
public String getId() {
return id;
}
}
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Scope;
@Configuration
public class AppScopeConfig {
@Bean
@Scope(CustomScope.CUSTOM_SCOPE_NAME)
public MyScopeBean myBean() {
return new MyScopeBean();
}
}
新建一個(gè)Controller,訪問/customScope接口,返回自定義作用域的Bean實(shí)例。
@RestController
public class CustomScopeController {
@Autowired
private ApplicationContext context;
@GetMapping("/customScope")
public String customScope() {
MyScopeBean prototypeBean = context.getBean(MyScopeBean.class);
return "Prototype ID: " + prototypeBean.getId();
}
}
訪問的結(jié)果輸出如下:
Session ID: bd22d310-29e5-4004-8555-9678c08275f0
Session ID: bd22d310-29e5-4004-8555-9678c08275f0
Session ID: bd22d310-29e5-4004-8555-9678c08275f0
因?yàn)閷ο笕志彺娴搅艘粋€(gè)MapscopedObjects,所以可以看到這個(gè)自定義作用域效果和單例模式基本一致的。
Bean Scope 的分類
| Scope | 描述 |
|---|---|
| singleton | (Default) 將單個(gè) bean 定義作用域限定為 Spring IoC 容器中的單個(gè)對象實(shí)例。 |
| prototype | 將單個(gè) bean 定義作用域限定為任意數(shù)量的對象實(shí)例。 |
| request | 將單個(gè) bean 定義作用域限定為單個(gè) HTTP 請求的生命周期。也就是說,每個(gè) HTTP 請求都有自己的一個(gè)基于單個(gè) bean 定義創(chuàng)建的 bean 實(shí)例。僅在 Web-aware Spring ApplicationContext 上下文中有效。 |
| session | 將單個(gè) bean 定義作用域限定為 HTTP Session 的生命周期。僅在 Web-aware Spring ApplicationContext 上下文中有效。 |
| application | 將單個(gè) bean 定義作用域限定為 ServletContext 的生命周期。僅在 Web-aware Spring ApplicationContext 上下文中有效。 |
| websocket | 將單個(gè) bean 定義作用域限定為 WebSocket 的生命周期。僅在 Web-aware Spring ApplicationContext 上下文中有效。 |
其中singleton、prototype是比較常用的數(shù)據(jù)。
Bean Scope 的使用
可以通過在Spring的配置文件(如XML配置文件或Java注解)中指定@Scope注解或<bean>元素的scope屬性來定義Bean的Scope。
其中@Scope注解可以是自定義的值或者如下常量:
- ConfigurableBeanFactory.SCOPE_PROTOTYPE
- ConfigurableBeanFactory.SCOPE_SINGLETON
- org.springframework.web.context.WebApplicationContext.SCOPE_REQUEST
- org.springframework.web.context.WebApplicationContext.SCOPE_SESSION
其中ConfigurableBeanFactory.SCOPE_PROTOTYPE是默認(rèn)值。
例如:
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
@Component
@Scope(ConfigurableBeanFactory.SCOPE_SINGLETON)
public class MyPrototypeBean {
// Bean內(nèi)容
}
或者使用XML配置:
<bean id="myBean" class="com.example.MyBean" scope="prototype">
<!-- Bean的其他配置 -->
</bean>
選擇合適的Bean Scope取決于應(yīng)用程序的需求。
為什么設(shè)計(jì) Bean Scope
Spring 框架設(shè)計(jì) Bean 作用域(Scope)的原因主要是為了提供靈活性和資源管理能力,以適應(yīng)不同應(yīng)用場景的需求。
不同的 Bean 作用域會影響 Bean 的生命周期、創(chuàng)建方式和在容器中的共享程度,從而影響應(yīng)用的性能、內(nèi)存占用和并發(fā)處理能力。
以下是 Spring 提供 Bean 作用域設(shè)計(jì)背后的主要原因:
- 資源優(yōu)化:通過作用域設(shè)計(jì),Spring 能夠根據(jù)業(yè)務(wù)場景高效管理 Bean 的創(chuàng)建與銷毀。例如,單例(Singleton)模式可以減少頻繁創(chuàng)建實(shí)例的開銷,原型(Prototype)模式則確保每次請求都得到新的實(shí)例,避免了共享狀態(tài)問題。
- 并發(fā)處理:對于 Web 應(yīng)用,特定作用域如請求(Request)和會話(Session)使得每個(gè)用戶請求或會話都有獨(dú)立的 Bean 實(shí)例,解決了并發(fā)用戶數(shù)據(jù)隔離的問題,提高了應(yīng)用的線程安全。
- 生命周期管理:不同的作用域允許開發(fā)者控制 Bean 的生命周期,比如通過自定義作用域?qū)崿F(xiàn)復(fù)雜的生命周期管理邏輯。Spring 容器在 Bean 的創(chuàng)建、初始化、銷毀等關(guān)鍵時(shí)刻調(diào)用生命周期回調(diào)方法,增加了靈活性。
- 可測試性:通過作用域的設(shè)計(jì),特別是原型模式,可以更容易地創(chuàng)建獨(dú)立的測試環(huán)境,因?yàn)槊看螠y試都能得到全新的實(shí)例,減少了測試間狀態(tài)干擾。
- 擴(kuò)展性:Spring 允許開發(fā)者自定義作用域,為特定的業(yè)務(wù)需求或架構(gòu)設(shè)計(jì)提供定制化的 Bean 管理方式,增強(qiáng)了框架的擴(kuò)展性和適應(yīng)性。
- 內(nèi)存管理:合理使用作用域可以減少內(nèi)存消耗,例如,原型模式避免了單例 Bean 累積大量狀態(tài)導(dǎo)致的內(nèi)存泄漏風(fēng)險(xiǎn),而請求作用域則確保請求結(jié)束后自動清理資源。
單例 bean 里面注入了原型 bean
當(dāng)單例 Bean 中注入原型(Prototype)Bean 時(shí),會出現(xiàn)一個(gè)問題:
- 單例 Bean 在整個(gè)應(yīng)用生命周期中只創(chuàng)建一次。
- 而原型 Bean 本應(yīng)每次請求時(shí)創(chuàng)建新實(shí)例。
- 但直接注入到單例 Bean 中時(shí),實(shí)際上只會注入一次原型 Bean 的實(shí)例。
- 后續(xù)對該原型 Bean 的使用都將復(fù)用首次注入的同一個(gè)實(shí)例,這可能并不符合預(yù)期。
以下demo可以復(fù)現(xiàn)這種情況。
SpringBean的配置:
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Scope;
@Configuration
public class FaultAppConfig {
@Bean
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public PrototypeInjectBean prototypeInjectBean() {
return new PrototypeInjectBean();
}
}
單例SpringBean:
import java.util.UUID;
public class PrototypeInjectBean {
private String id;
public PrototypeInjectBean() {
this.id = UUID.randomUUID().toString();
}
public String getId() {
return id;
}
}
測試代碼如下:
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Lookup;
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Scope;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@Scope(ConfigurableBeanFactory.SCOPE_SINGLETON)
public class PrototypeFaultController {
final private PrototypeInjectBean prototypeInjectBean;
protected PrototypeFaultController(PrototypeInjectBean prototypeInjectBean) {
this.prototypeInjectBean = prototypeInjectBean;
}
/**
* 原型作用域失效,每次返回同一個(gè)id
* @return
*/
@GetMapping("/prototypeDemo1")
public String prototypeDemo1() {
return "Prototype ID: " + prototypeInjectBean.getId();
}
}
在不重啟應(yīng)用或者垃圾回收的情況下,訪問接口 /prototypeDemo1 原型 Bean 的id值始終是相同的。
Prototype ID: 11893ed8-608c-452b-b2e6-82bc70b5cf97
Prototype ID: 11893ed8-608c-452b-b2e6-82bc70b5cf97
Prototype ID: 11893ed8-608c-452b-b2e6-82bc70b5cf97
那這種常用的使用場景遇到了該怎么解決呢?別急,Spring早已經(jīng)給出了幾種解決辦法。
通過完善上面的測試代碼給出3中解決方法。
修改完善后的代碼如下:
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Lookup;
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Scope;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@Scope(ConfigurableBeanFactory.SCOPE_SINGLETON)
public abstract class PrototypeFaultController {
@Autowired
private ApplicationContext context;
@Autowired
private ObjectProvider<PrototypeInjectBean> prototypeBeanProvider;
final private PrototypeInjectBean prototypeInjectBean;
protected PrototypeFaultController(PrototypeInjectBean prototypeInjectBean) {
this.prototypeInjectBean = prototypeInjectBean;
}
/**
* 原型作用域失效,每次返回同一個(gè)id
* @return
*/
@GetMapping("/prototypeDemo1")
public String prototypeDemo1() {
return "Prototype ID: " + prototypeInjectBean.getId();
}
/**
* 使用實(shí)例工廠方法注入獲取原型Bean,每次返回不同id
* @return
*/
@GetMapping("/prototypeDemo2")
public String prototypeDemo2() {
PrototypeInjectBean prototypeBean = context.getBean(PrototypeInjectBean.class);
return "Prototype ID: " + prototypeBean.getId();
}
/**
* Spring 提供了`ObjectProvider`接口(繼承自`Provider`接口),它允許延遲查找和實(shí)例化 Bean,非常適合在單例 Bean 中按需獲取原型 Bean 的新實(shí)例。
* @return
*/
@GetMapping("/prototypeDemo4")
public String prototypeDemo4() {
return "Prototype ID: " + prototypeBeanProvider.getObject().getId();
}
/**
* 使用`@Lookup`注解獲取原型Bean,每次返回不同id
* @return
*/
@GetMapping("/prototypeDemo5")
public String prototypeDemo5() {
return "Prototype ID: " + getPrototypeBean().getId();
}
@Lookup
public abstract PrototypeInjectBean getPrototypeBean();
}
-
解決辦法1: Spring 提供了
ObjectProvider接口(繼承自Provider接口),它允許延遲查找和實(shí)例化 Bean,非常適合在單例 Bean 中按需獲取原型 Bean 的新實(shí)例。
通過訪問接口/prototypeDemo4可以發(fā)現(xiàn)每次返回的id值是不同的。
- 解決辦法2: 可以通過定義一個(gè)工廠方法來創(chuàng)建原型 Bean 的實(shí)例,然后在單例 Bean 中注入這個(gè)工廠方法,每次需要時(shí)調(diào)用工廠方法獲取新實(shí)例。
通過訪問接口/prototypeDemo2可以發(fā)現(xiàn)每次返回的id值是不同的。
-
解決辦法3: 通過
@Lookup注解,@Lookup注解是Spring框架中的一個(gè)特殊注解,用于在Spring容器中查找另一個(gè)Bean,并將其注入到當(dāng)前Bean中。注意使用@Lookup注解的方法必須是抽象的(abstract)。
通過訪問接口/prototypeDemo5可以發(fā)現(xiàn)每次返回的id值是不同的。
關(guān)于作者
來自一線全棧程序員nine的探索與實(shí)踐,持續(xù)迭代中。
歡迎關(guān)注或者點(diǎn)個(gè)小紅心~