本文詳細(xì)解析了 Spring 的內(nèi)置作用域,包括 Singleton、Prototype、Request、Session、Application 和 WebSocket 作用域,并通過(guò)實(shí)例講解了它們?cè)趯?shí)際開(kāi)發(fā)中的應(yīng)用。特別是 Singleton 和 Prototype 作用域,我們深入討論了它們的定義、用途以及如何處理相關(guān)的線程安全問(wèn)題。通過(guò)閱讀本文,讀者可以更深入地理解 Spring 作用域,并在實(shí)際開(kāi)發(fā)中更有效地使用
1. Spring 的內(nèi)置作用域
我們來(lái)看看 Spring 內(nèi)置的作用域類型。在 5.x 版本中,Spring 內(nèi)置了六種作用域:
singleton:在 IOC 容器中,對(duì)應(yīng)的 Bean 只有一個(gè)實(shí)例,所有對(duì)它的引用都指向同一個(gè)對(duì)象。這種作用域非常適合對(duì)于無(wú)狀態(tài)的 Bean,比如工具類或服務(wù)類。
prototype:每次請(qǐng)求都會(huì)創(chuàng)建一個(gè)新的 Bean 實(shí)例,適合對(duì)于需要維護(hù)狀態(tài)的 Bean。
request:在 Web 應(yīng)用中,為每個(gè) HTTP 請(qǐng)求創(chuàng)建一個(gè) Bean 實(shí)例。適合在一個(gè)請(qǐng)求中需要維護(hù)狀態(tài)的場(chǎng)景,如跟蹤用戶行為信息。
session:在 Web 應(yīng)用中,為每個(gè) HTTP 會(huì)話創(chuàng)建一個(gè) Bean 實(shí)例。適合需要在多個(gè)請(qǐng)求之間維護(hù)狀態(tài)的場(chǎng)景,如用戶會(huì)話。
application:在整個(gè) Web 應(yīng)用期間,創(chuàng)建一個(gè) Bean 實(shí)例。適合存儲(chǔ)全局的配置數(shù)據(jù)等。
websocket:在每個(gè) WebSocket 會(huì)話中創(chuàng)建一個(gè) Bean 實(shí)例。適合 WebSocket 通信場(chǎng)景。
我們需要重點(diǎn)學(xué)習(xí)兩種作用域:singleton 和 prototype。在大多數(shù)情況下 singleton 和 prototype 這兩種作用域已經(jīng)足夠滿足需求。
2. singleton 作用域
2.1 singleton 作用域的定義和用途
Singleton 是 Spring 的默認(rèn)作用域。在這個(gè)作用域中,Spring 容器只會(huì)創(chuàng)建一個(gè)實(shí)例,所有對(duì)該 bean 的請(qǐng)求都將返回這個(gè)唯一的實(shí)例。 例如,我們定義一個(gè)名為 Plaything 的類,并將其作為一個(gè) bean:
@Component public class Plaything { public Plaything() { System.out.println("Plaything constructor run ..."); } }
在這個(gè)例子中,Plaything 是一個(gè) singleton 作用域的 bean。無(wú)論我們?cè)趹?yīng)用中的哪個(gè)地方請(qǐng)求這個(gè) bean,Spring 都會(huì)返回同一個(gè) Plaything 實(shí)例。
下面的例子展示了如何創(chuàng)建一個(gè)單實(shí)例的 Bean:
package com.example.demo.bean; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; @Component public class Kid { private Plaything plaything; @Autowired public void setPlaything(Plaything plaything) { this.plaything = plaything; } public Plaything getPlaything() { return plaything; } } package com.example.demo.bean; import org.springframework.stereotype.Component; @Component public class Plaything { public Plaything() { System.out.println("Plaything constructor run ..."); } }
這里可以在 Plaything 類加上 @Scope (BeanDefinition.SCOPE_SINGLETON),但是因?yàn)槭悄J(rèn)作用域是 Singleton,所以沒(méi)必要加。
package com.example.demo.configuration; import com.example.demo.bean.Kid; import com.example.demo.bean.Plaything; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class BeanScopeConfiguration { @Bean public Kid kid1(Plaything plaything1) { Kid kid = new Kid(); kid.setPlaything(plaything1); return kid; } @Bean public Kid kid2(Plaything plaything2) { Kid kid = new Kid(); kid.setPlaything(plaything2); return kid; } } package com.example.demo.application; import com.example.demo.bean.Kid; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.context.annotation.ComponentScan; @SpringBootApplication @ComponentScan("com.example") public class DemoApplication { public static void main(String[] args) { ApplicationContext context = new AnnotationConfigApplicationContext(DemoApplication.class); context.getBeansOfType(Kid.class).forEach((name, kid) -> { System.out.println(name + " : " + kid.getPlaything()); }); } } 在 Spring IoC 容器的工作中,掃描過(guò)程只會(huì)創(chuàng)建 bean 的定義,真正的 bean 實(shí)例是在需要注入或者通過(guò) getBean 方法獲取時(shí)才會(huì)創(chuàng)建。這個(gè)過(guò)程被稱為 bean 的初始化。
這里運(yùn)行 ctx.getBeansOfType (Kid.class).forEach ((name, kid) -> System.out.println (name + ":" + kid.getPlaything ())); 時(shí),Spring IoC 容器會(huì)查找所有的 Kid 類型的 bean 定義,然后為每一個(gè)找到的 bean 定義創(chuàng)建實(shí)例(如果這個(gè) bean 定義還沒(méi)有對(duì)應(yīng)的實(shí)例),并注入相應(yīng)的依賴。
運(yùn)行結(jié)果: 三個(gè) Kid 的 Plaything bean 是相同的,說(shuō)明默認(rèn)情況下 Plaything 是一個(gè)單例 bean,整個(gè) Spring 應(yīng)用中只有一個(gè) Plaything bean 被創(chuàng)建。 為什么會(huì)有 3 個(gè) kid?
Kid: 這個(gè)是通過(guò)在 Kid 類上標(biāo)注的 @Component 注解自動(dòng)創(chuàng)建的。Spring 在掃描時(shí)發(fā)現(xiàn)這個(gè)注解,就會(huì)自動(dòng)在 IOC 容器中注冊(cè)這個(gè) bean。這個(gè) Bean 的名字默認(rèn)是將類名的首字母小寫 kid。
kid1: 在 BeanScopeConfiguration 中定義,通過(guò) kid1 (Plaything plaything1) 方法創(chuàng)建,并且注入了 plaything1。
kid2: 在 BeanScopeConfiguration 中定義,通過(guò) kid2 (Plaything plaything2) 方法創(chuàng)建,并且注入了 plaything2。
2.2 singleton 作用域線程安全問(wèn)題
需要注意的是,雖然 singleton Bean 只會(huì)有一個(gè)實(shí)例,但 Spring 并不會(huì)解決其線程安全問(wèn)題,開(kāi)發(fā)者需要根據(jù)實(shí)際場(chǎng)景自行處理。 我們通過(guò)一個(gè)代碼示例來(lái)說(shuō)明在多線程環(huán)境中出現(xiàn) singleton Bean 的線程安全問(wèn)題。 首先,我們創(chuàng)建一個(gè)名為 Counter 的 singleton Bean,這個(gè) Bean 有一個(gè) count 變量,提供 increment 方法來(lái)增加 count 的值:
package com.example.demo.bean; import org.springframework.stereotype.Component; @Component public class Counter { private int count = 0; public int increment() { return ++count; } }
然后,我們創(chuàng)建一個(gè)名為 CounterService 的 singleton Bean,這個(gè) Bean 依賴于 Counter,在 increaseCount 方法中,我們調(diào)用 counter.increment 方法:
package com.example.demo.service; import com.example.demo.bean.Counter; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @Service public class CounterService { @Autowired private final Counter counter; public void increaseCount() { counter.increment(); } }我們?cè)诙嗑€程環(huán)境中調(diào)用 counterService.increaseCount 方法時(shí),就可能出現(xiàn)線程安全問(wèn)題。因?yàn)?counter.increment 方法并非線程安全,多個(gè)線程同時(shí)調(diào)用此方法可能會(huì)導(dǎo)致 count 值出現(xiàn)預(yù)期外的結(jié)果。
要解決這個(gè)問(wèn)題,我們需要使 counter.increment 方法線程安全。
這里可以使用原子變量,在 Counter 類中,我們可以使用 AtomicInteger 來(lái)代替 int 類型的 count,因?yàn)?AtomicInteger 類中的方法是線程安全的,且其性能通常優(yōu)于 synchronized 關(guān)鍵字。
?
package com.example.demo.bean; import org.springframework.stereotype.Component; import java.util.concurrent.atomic.AtomicInteger; @Component public class Counter { private AtomicInteger count = new AtomicInteger(0); public int increment() { return count.incrementAndGet(); } }盡管優(yōu)化后已經(jīng)使 Counter 類線程安全,但在設(shè)計(jì) Bean 時(shí),我們應(yīng)該盡可能地減少可變狀態(tài)。這是因?yàn)榭勺儬顟B(tài)使得并發(fā)編程變得復(fù)雜,而無(wú)狀態(tài)的 Bean 通常更容易理解和測(cè)試。 什么是無(wú)狀態(tài)的 Bean 呢??如果一個(gè) Bean 不持有任何狀態(tài)信息,也就是說(shuō),同樣的輸入總是會(huì)得到同樣的輸出,那么這個(gè) Bean 就是無(wú)狀態(tài)的。反之,則是有狀態(tài)的 Bean。
?
3. prototype 作用域
3.1 prototype 作用域的定義和用途
在 prototype 作用域中,Spring 容器會(huì)為每個(gè)請(qǐng)求創(chuàng)建一個(gè)新的 bean 實(shí)例。 例如,我們定義一個(gè)名為 Plaything 的類,并將其作用域設(shè)置為 prototype:
?
package com.example.demo.bean; import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.context.annotation.Scope; import org.springframework.stereotype.Component; @Component @Scope(BeanDefinition.SCOPE_PROTOTYPE) public class Plaything { public Plaything() { System.out.println("Plaything constructor run ..."); } }在這個(gè)例子中,Plaything 是一個(gè) prototype 作用域的 bean。每次我們請(qǐng)求這個(gè) bean,Spring 都會(huì)創(chuàng)建一個(gè)新的 Plaything 實(shí)例。 我們只需要修改上面的 Plaything 類,其他的類不用動(dòng)。 打印結(jié)果:


?
3.2 prototype 作用域在開(kāi)發(fā)中的例子
以我個(gè)人來(lái)說(shuō),我在 excel 多線程上傳的時(shí)候用到過(guò)這個(gè),當(dāng)時(shí)是 EasyExcel 框架,我給一部分關(guān)鍵代碼展示一下如何在 Spring 中使用 prototype 作用域來(lái)處理多線程環(huán)境下的任務(wù)(實(shí)際業(yè)務(wù)會(huì)更復(fù)雜),大家可以對(duì)比,如果用 prototype 作用域和使用 new 對(duì)象的形式在實(shí)際開(kāi)發(fā)中有什么區(qū)別。 使用 prototype 作用域的例子
?
@Resource private ApplicationContext context; @PostMapping("/user/upload") public ResultModel upload(@RequestParam("multipartFile") MultipartFile multipartFile) { ...... ExecutorService es = new ThreadPoolExceutor(10, 16, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<>(2000), new ThreadPoolExecutor.CallerRunPolicy()); ...... EasyExcel.read(multipartFile.getInputStream(), UserDataUploadVO.class, new PageReadListener(dataList ->{ ...... // 多線程處理上傳excel數(shù)據(jù) Future> future = es.submit(context.getBean(AsyncUploadHandler.class, user, dataList, errorCount)); ...... })).sheet().doRead(); ...... }
?
AsyncUploadHandler.java
?
@Component @Scope(BeanDefinition.SCOPE_PROTOTYPE) public class AsyncUploadHandler implements Runnable { private User user; private ListAsyncUploadHandler 類是一個(gè) prototype 作用域的 bean,它被用來(lái)處理上傳的 Excel 數(shù)據(jù)。由于并發(fā)上傳的每個(gè)任務(wù)可能需要處理不同的數(shù)據(jù),并且可能需要在不同的用戶上下文中執(zhí)行,因此每個(gè)任務(wù)都需要有自己的 AsyncUploadHandler bean。這就是為什么需要將 AsyncUploadHandler 定義為 prototype 作用域的原因。 由于 AsyncUploadHandler 是由 Spring 管理的,我們可以直接使用 @Resource 注解來(lái)注入其他的 bean,例如 RedisService 和 CompanyManagementMapper。 把 AsyncUploadHandler 交給 Spring 容器管理,里面依賴的容器對(duì)象可以直接用 @Resource 注解注入。如果采用 new 出來(lái)的對(duì)象,那么這些對(duì)象只能從外面注入好了再傳入進(jìn)去。 不使用 prototype 作用域改用 new 對(duì)象的例子dataList; private AtomicInteger errorCount; @Resource private RedisService redisService; ...... @Resource private CompanyManagementMapper companyManagementMapper; public AsyncUploadHandler(user, List dataList, AtomicInteger errorCount) { this.user = user; this.dataList = dataList; this.errorCount = errorCount; } @Override public void run() { ...... } ...... }
@PostMapping("/user/upload") public ResultModel upload(@RequestParam("multipartFile") MultipartFile multipartFile) { ...... ExecutorService es = new ThreadPoolExceutor(10, 16, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<>(2000), new ThreadPoolExecutor.CallerRunPolicy()); ...... EasyExcel.read(multipartFile.getInputStream(), UserDataUploadVO.class, new PageReadListener(dataList ->{ ...... // 多線程處理上傳excel數(shù)據(jù) Future> future = es.submit(new AsyncUploadHandler(user, dataList, errorCount, redisService, companyManagementMapper)); ...... })).sheet().doRead(); ...... }
AsyncUploadHandler.java
public class AsyncUploadHandler implements Runnable { private User user; private List如果直接新建 AsyncUploadHandler 對(duì)象,則需要手動(dòng)傳入所有的依賴,這會(huì)使代碼變得更復(fù)雜更難以管理,而且還需要手動(dòng)管理 AsyncUploadHandler 的生命周期。dataList; private AtomicInteger errorCount; private RedisService redisService; private CompanyManagementMapper companyManagementMapper; ...... public AsyncUploadHandler(user, List dataList, AtomicInteger errorCount, RedisService redisService, CompanyManagementMapper companyManagementMapper) { this.user = user; this.dataList = dataList; this.errorCount = errorCount; this.redisService = redisService; this.companyManagementMapper = companyManagementMapper; } @Override public void run() { ...... } ...... }
4. request 作用域(了解)
request 作用域:Bean 在一個(gè) HTTP 請(qǐng)求內(nèi)有效。當(dāng)請(qǐng)求開(kāi)始時(shí),Spring 容器會(huì)為每個(gè)新的 HTTP 請(qǐng)求創(chuàng)建一個(gè)新的 Bean 實(shí)例,這個(gè) Bean 在當(dāng)前 HTTP 請(qǐng)求內(nèi)是有效的,請(qǐng)求結(jié)束后,Bean 就會(huì)被銷毀。如果在同一個(gè)請(qǐng)求中多次獲取該 Bean,就會(huì)得到同一個(gè)實(shí)例,但是在不同的請(qǐng)求中獲取的實(shí)例將會(huì)不同。
?
@Component @Scope(value = WebApplicationContext.SCOPE_REQUEST, proxyMode = ScopedProxyMode.TARGET_CLASS) public class RequestScopedBean { // 在一次Http請(qǐng)求內(nèi)共享的數(shù)據(jù) private String requestData; public void setRequestData(String requestData) { this.requestData = requestData; } public String getRequestData() { return this.requestData; } }上述 Bean 在一個(gè) HTTP 請(qǐng)求的生命周期內(nèi)是一個(gè)單例,每個(gè)新的 HTTP 請(qǐng)求都會(huì)創(chuàng)建一個(gè)新的 Bean 實(shí)例。
5. session 作用域(了解)
session 作用域:Bean 是在同一個(gè) HTTP 會(huì)話(Session)中是單例的。也就是說(shuō),從用戶登錄開(kāi)始,到用戶退出登錄(或者 Session 超時(shí))結(jié)束,這個(gè)過(guò)程中,不管用戶進(jìn)行了多少次 HTTP 請(qǐng)求,只要是在同一個(gè)會(huì)話中,都會(huì)使用同一個(gè) Bean 實(shí)例。
@Component @Scope(value = WebApplicationContext.SCOPE_SESSION, proxyMode = ScopedProxyMode.TARGET_CLASS) public class SessionScopedBean { // 在一個(gè)Http會(huì)話內(nèi)共享的數(shù)據(jù) private String sessionData; public void setSessionData(String sessionData) { this.sessionData = sessionData; } public String getSessionData() { return this.sessionData; } }這樣的設(shè)計(jì)對(duì)于存儲(chǔ)和管理會(huì)話級(jí)別的數(shù)據(jù)非常有用,例如用戶的登錄信息、購(gòu)物車信息等。因?yàn)樗鼈兪窃谕粋€(gè)會(huì)話中保持一致的,所以使用 session 作用域的 Bean 可以很好地解決這個(gè)問(wèn)題。 但是實(shí)際開(kāi)發(fā)中沒(méi)人這么干,會(huì)話 id 都會(huì)存在數(shù)據(jù)庫(kù),根據(jù)會(huì)話 id 就能在各種表中獲取數(shù)據(jù),避免頻繁查庫(kù)也是把關(guān)鍵信息序列化后存在 Redis。
?
6. application 作用域(了解)
application 作用域:在整個(gè) Web 應(yīng)用的生命周期內(nèi),Spring 容器只會(huì)創(chuàng)建一個(gè) Bean 實(shí)例。這個(gè) Bean 在 Web 應(yīng)用的生命周期內(nèi)都是有效的,當(dāng) Web 應(yīng)用停止后,Bean 就會(huì)被銷毀。
?
@Component @Scope(value = WebApplicationContext.SCOPE_APPLICATION, proxyMode = ScopedProxyMode.TARGET_CLASS) public class ApplicationScopedBean { // 在整個(gè)Web應(yīng)用的生命周期內(nèi)共享的數(shù)據(jù) private String applicationData; public void setApplicationData(String applicationData) { this.applicationData = applicationData; } public String getApplicationData() { return this.applicationData; } }如果在一個(gè) application 作用域的 Bean 上調(diào)用 setter 方法,那么這個(gè)變更將對(duì)所有用戶和會(huì)話可見(jiàn)。后續(xù)對(duì)這個(gè) Bean 的所有調(diào)用(包括 getter 和 setter)都將影響到同一個(gè) Bean 實(shí)例,后面的調(diào)用會(huì)覆蓋前面的狀態(tài)。
7. websocket 作用域(了解)
websocket 作用域:Bean 在每一個(gè)新的 WebSocket 會(huì)話中都會(huì)被創(chuàng)建一次,就像 session 作用域的 Bean 在每一個(gè) HTTP 會(huì)話中都會(huì)被創(chuàng)建一次一樣。這個(gè) Bean 在整個(gè) WebSocket 會(huì)話內(nèi)都是有效的,當(dāng) WebSocket 會(huì)話結(jié)束后,Bean 就會(huì)被銷毀。
@Component @Scope(value = "websocket", proxyMode = ScopedProxyMode.TARGET_CLASS) public class WebSocketScopedBean { // 在一個(gè)WebSocket會(huì)話內(nèi)共享的數(shù)據(jù) private String socketData; public void setSocketData(String socketData) { this.socketData = socketData; } public String getSocketData() { return this.socketData; } }上述 Bean 在一個(gè) WebSocket 會(huì)話的生命周期內(nèi)是一個(gè)單例,每個(gè)新的 WebSocket 會(huì)話都會(huì)創(chuàng)建一個(gè)新的 Bean 實(shí)例。 這個(gè)作用域需要 Spring Websocket 模塊支持,并且應(yīng)用需要配置為使用 websocket。
編輯:黃飛
?
?
?
?
評(píng)論