1. Spring的內(nèi)置作用域
我們來看看Spring
內(nèi)置的作用域類型。在5.x
版本中,Spring
內(nèi)置了六種作用域:
-
singleton:在
IOC
容器中,對應(yīng)的Bean
只有一個實(shí)例,所有對它的引用都指向同一個對象。這種作用域非常適合對于無狀態(tài)的Bean
,比如工具類或服務(wù)類。 -
prototype:每次請求都會創(chuàng)建一個新的
Bean
實(shí)例,適合對于需要維護(hù)狀態(tài)的Bean
。 -
request:在
Web
應(yīng)用中,為每個HTTP
請求創(chuàng)建一個Bean
實(shí)例。適合在一個請求中需要維護(hù)狀態(tài)的場景,如跟蹤用戶行為信息。 -
session:在
Web
應(yīng)用中,為每個HTTP
會話創(chuàng)建一個Bean
實(shí)例。適合需要在多個請求之間維護(hù)狀態(tài)的場景,如用戶會話。 -
application:在整個
Web
應(yīng)用期間,創(chuàng)建一個Bean
實(shí)例。適合存儲全局的配置數(shù)據(jù)等。 -
websocket:在每個
WebSocket
會話中創(chuàng)建一個Bean
實(shí)例。適合WebSocket
通信場景。
我們需要重點(diǎn)學(xué)習(xí)兩種作用域:singleton
和prototype
。在大多數(shù)情況下singleton
和prototype
這兩種作用域已經(jīng)足夠滿足需求。
2. singleton作用域
2.1 singleton作用域的定義和用途
??Singleton
是Spring
的默認(rèn)作用域。在這個作用域中,Spring
容器只會創(chuàng)建一個實(shí)例,所有對該bean
的請求都將返回這個唯一的實(shí)例。
例如,我們定義一個名為Plaything
的類,并將其作為一個bean
:
@Component
public class Plaything {
public Plaything() {
System.out.println("Plaything constructor run ...");
}
}
??在這個例子中,Plaything
是一個singleton
作用域的bean
。無論我們在應(yīng)用中的哪個地方請求這個bean
,Spring
都會返回同一個Plaything
實(shí)例。
下面的例子展示了如何創(chuàng)建一個單實(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
,所以沒必要加。
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
容器的工作中,掃描過程只會創(chuàng)建bean
的定義,真正的bean
實(shí)例是在需要注入或者通過getBean
方法獲取時才會創(chuàng)建。這個過程被稱為bean
的初始化。
??這里運(yùn)行 ctx.getBeansOfType(Kid.class).forEach((name, kid) -> System.out.println(name + " : " + kid.getPlaything()));
時,Spring IoC
容器會查找所有的Kid
類型的bean
定義,然后為每一個找到的bean
定義創(chuàng)建實(shí)例(如果這個bean
定義還沒有對應(yīng)的實(shí)例),并注入相應(yīng)的依賴。
運(yùn)行結(jié)果:
??三個 Kid
的 Plaything bean
是相同的,說明默認(rèn)情況下 Plaything
是一個單例bean
,整個Spring
應(yīng)用中只有一個 Plaything bean
被創(chuàng)建。
為什么會有3
個kid
?
-
Kid: 這個是通過在
Kid
類上標(biāo)注的@Component
注解自動創(chuàng)建的。Spring
在掃描時發(fā)現(xiàn)這個注解,就會自動在IOC
容器中注冊這個bean
。這個Bean
的名字默認(rèn)是將類名的首字母小寫kid
。 -
kid1: 在
BeanScopeConfiguration
中定義,通過kid1(Plaything plaything1)
方法創(chuàng)建,并且注入了plaything1
。 -
kid2: 在
BeanScopeConfiguration
中定義,通過kid2(Plaything plaything2)
方法創(chuàng)建,并且注入了plaything2
。
2.2 singleton作用域線程安全問題
需要注意的是,雖然singleton Bean
只會有一個實(shí)例,但Spring
并不會解決其線程安全問題,開發(fā)者需要根據(jù)實(shí)際場景自行處理。
我們通過一個代碼示例來說明在多線程環(huán)境中出現(xiàn)singleton Bean
的線程安全問題。
首先,我們創(chuàng)建一個名為Counter
的singleton Bean
,這個Bean
有一個count
變量,提供increment
方法來增加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)建一個名為CounterService
的singleton Bean
,這個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();
}
}
??我們在多線程環(huán)境中調(diào)用counterService.increaseCount
方法時,就可能出現(xiàn)線程安全問題。因?yàn)?code>counter.increment方法并非線程安全,多個線程同時調(diào)用此方法可能會導(dǎo)致count
值出現(xiàn)預(yù)期外的結(jié)果。
??要解決這個問題,我們需要使counter.increment
方法線程安全。
??這里可以使用原子變量,在Counter
類中,我們可以使用AtomicInteger
來代替int
類型的count
,因?yàn)?code>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
時,我們應(yīng)該盡可能地減少可變狀態(tài)。這是因?yàn)榭勺儬顟B(tài)使得并發(fā)編程變得復(fù)雜,而無狀態(tài)的Bean
通常更容易理解和測試。
??什么是無狀態(tài)的Bean呢? 如果一個Bean
不持有任何狀態(tài)信息,也就是說,同樣的輸入總是會得到同樣的輸出,那么這個Bean
就是無狀態(tài)的。反之,則是有狀態(tài)的Bean
。
3. prototype作用域
3.1 prototype作用域的定義和用途
在prototype
作用域中,Spring
容器會為每個請求創(chuàng)建一個新的bean
實(shí)例。
例如,我們定義一個名為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 ...");
}
}
在這個例子中,Plaything
是一個prototype
作用域的bean
。每次我們請求這個bean
,Spring
都會創(chuàng)建一個新的Plaything
實(shí)例。
我們只需要修改上面的Plaything
類,其他的類不用動。
打印結(jié)果:
這個@Scope(BeanDefinition.SCOPE_PROTOTYPE)
可以寫成@Scope("prototype")
,按照規(guī)范,還是利用已有的常量比較好。

3.2 prototype作用域在開發(fā)中的例子
??以我個人來說,我在excel
多線程上傳的時候用到過這個,當(dāng)時是EasyExcel
框架,我給一部分關(guān)鍵代碼展示一下如何在Spring
中使用prototype
作用域來處理多線程環(huán)境下的任務(wù)(實(shí)際業(yè)務(wù)會更復(fù)雜),大家可以對比,如果用prototype
作用域和使用new
對象的形式在實(shí)際開發(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<UserDataUploadVO>(dataList ->{
......
// 多線程處理上傳excel數(shù)據(jù),重點(diǎn)只看這一句話
Future<?> future = es.submit(context.getBean(AsyncUploadHandler.class, user, dataList, errorCount));
......
})).sheet().doRead();
......
}
??有人可能會問這里為什么使用context.getBean
,而不是@Resource
或@Autowired
注解,@Resource
或@Autowired
注解只會在注入時創(chuàng)建一個新的實(shí)例,這里并不會反復(fù)注入。ApplicationContext.getBean()
方法是在每次調(diào)用時解析的,所以它會在每次調(diào)用時創(chuàng)建一個新的AsyncUploadHandler
實(shí)例。
AsyncUploadHandler.java
@Component
@Scope(BeanDefinition.SCOPE_PROTOTYPE)
public class AsyncUploadHandler implements Runnable {
private User user;
private List<UserDataUploadVO> dataList;
private AtomicInteger errorCount;
@Resource
private RedisService redisService;
......
@Resource
private CompanyManagementMapper companyManagementMapper;
public AsyncUploadHandler(user, List<UserDataUploadVO> dataList, AtomicInteger errorCount) {
this.user = user;
this.dataList = dataList;
this.errorCount = errorCount;
}
@Override
public void run() {
......
}
......
}
??AsyncUploadHandler
類是一個prototype
作用域的bean
,它被用來處理上傳的Excel
數(shù)據(jù)。由于并發(fā)上傳的每個任務(wù)可能需要處理不同的數(shù)據(jù),并且可能需要在不同的用戶上下文中執(zhí)行,因此每個任務(wù)都需要有自己的AsyncUploadHandler bean
。這就是為什么需要將AsyncUploadHandler
定義為prototype
作用域的原因。
??由于AsyncUploadHandler
是由Spring
管理的,我們可以直接使用@Resource
注解來注入其他的bean
,例如RedisService
和CompanyManagementMapper
。
??如果用單例作用域的AsyncUploadHandler bean
行不行?
??如果AsyncUploadHandler
對象被定義為一個單例作用域的Spring Bean
,那么所有的線程都會共享同一個AsyncUploadHandler
對象。這可能會導(dǎo)致線程安全問題,因?yàn)槎鄠€線程同時修改同一個對象的狀態(tài)可能會導(dǎo)致數(shù)據(jù)不一致,這里我們需要的是每個用戶的數(shù)據(jù)都要上傳校驗(yàn)處理等邏輯(用戶數(shù)據(jù)在dataList
里面),如果是單例AsyncUploadHandler
,AsyncUploadHandler
對象里面的dataList
屬性會因其他線程的影響而導(dǎo)致被修改。
??把AsyncUploadHandler
交給Spring
容器管理,里面依賴的容器對象可以直接用@Resource
注解注入。如果采用new
出來的對象,那么這些對象只能從外面注入好了再傳入進(jìn)去。
不使用prototype
作用域改用new
對象的例子
@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<UserDataUploadVO>(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<UserDataUploadVO> dataList;
private AtomicInteger errorCount;
private RedisService redisService;
private CompanyManagementMapper companyManagementMapper;
......
public AsyncUploadHandler(user, List<UserDataUploadVO> 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() {
......
}
......
}
??如果直接新建AsyncUploadHandler
對象,則需要手動傳入所有的依賴,這會使代碼變得更復(fù)雜更難以管理,而且還需要手動管理AsyncUploadHandler
的生命周期。
3.3 prototype作用域在bean之間相互依賴時存在的問題
??在后續(xù)寫文章時,評論區(qū)有提問:“如果Bean A
依賴Bean B
,那如果在配置類中先定義Bean B
再定義Bean A
,會不會有問題?還是說Spring
會自動處理這種依賴關(guān)系?”
這個問題我發(fā)現(xiàn)在原型作用域這個點(diǎn)還需要再補(bǔ)充講解一下:
??這種情況在沒有相互依賴的情況下不會有問題,Spring
會在先解析配置類和@Bean
方法,獲得所有Bean
的依賴信息,之后Spring
根據(jù)依賴關(guān)系決定Bean
的實(shí)例化順序,而不管配置類中定義的順序。
??如果A
依賴B
,B
依賴A
形成循環(huán)依賴,對于單例Bean
,Spring
通過三級緩存機(jī)制來解決。對于原型Bean
的循環(huán)依賴無法解決,會拋出BeanCurrentlyInCreationException
異常,原因是原型Bean
每次都會創(chuàng)建新實(shí)例,Spring
無法管理其完整生命周期。
??注意:Spring
解析配置類和@Bean
方法是在BeanDefinitionReader
進(jìn)行的,這是 refresh
過程的一個步驟。
4. request作用域(了解)
??request
作用域:Bean
在一個HTTP
請求內(nèi)有效。當(dāng)請求開始時,Spring
容器會為每個新的HTTP
請求創(chuàng)建一個新的Bean
實(shí)例,這個Bean
在當(dāng)前HTTP
請求內(nèi)是有效的,請求結(jié)束后,Bean
就會被銷毀。如果在同一個請求中多次獲取該Bean
,就會得到同一個實(shí)例,但是在不同的請求中獲取的實(shí)例將會不同。
@Component
@Scope(value = WebApplicationContext.SCOPE_REQUEST, proxyMode = ScopedProxyMode.TARGET_CLASS)
public class RequestScopedBean {
// 在一次Http請求內(nèi)共享的數(shù)據(jù)
private String requestData;
public void setRequestData(String requestData) {
this.requestData = requestData;
}
public String getRequestData() {
return this.requestData;
}
}
上述Bean
在一個HTTP
請求的生命周期內(nèi)是一個單例,每個新的HTTP
請求都會創(chuàng)建一個新的Bean
實(shí)例。
5. session作用域(了解)
??session
作用域:Bean
是在同一個HTTP
會話(Session
)中是單例的。也就是說,從用戶登錄開始,到用戶退出登錄(或者Session
超時)結(jié)束,這個過程中,不管用戶進(jìn)行了多少次HTTP
請求,只要是在同一個會話中,都會使用同一個Bean
實(shí)例。
@Component
@Scope(value = WebApplicationContext.SCOPE_SESSION, proxyMode = ScopedProxyMode.TARGET_CLASS)
public class SessionScopedBean {
// 在一個Http會話內(nèi)共享的數(shù)據(jù)
private String sessionData;
public void setSessionData(String sessionData) {
this.sessionData = sessionData;
}
public String getSessionData() {
return this.sessionData;
}
}
??這樣的設(shè)計(jì)對于存儲和管理會話級別的數(shù)據(jù)非常有用,例如用戶的登錄信息、購物車信息等。因?yàn)樗鼈兪窃谕粋€會話中保持一致的,所以使用session
作用域的Bean
可以很好地解決這個問題。
??但是實(shí)際開發(fā)中沒人這么干,會話id
都會存在數(shù)據(jù)庫,根據(jù)會話id
就能在各種表中獲取數(shù)據(jù),避免頻繁查庫也是把關(guān)鍵信息序列化后存在Redis
。
6. application作用域(了解)
??application
作用域:在整個Web
應(yīng)用的生命周期內(nèi),Spring
容器只會創(chuàng)建一個Bean
實(shí)例。這個Bean
在Web
應(yīng)用的生命周期內(nèi)都是有效的,當(dāng)Web
應(yīng)用停止后,Bean
就會被銷毀。
@Component
@Scope(value = WebApplicationContext.SCOPE_APPLICATION, proxyMode = ScopedProxyMode.TARGET_CLASS)
public class ApplicationScopedBean {
// 在整個Web應(yīng)用的生命周期內(nèi)共享的數(shù)據(jù)
private String applicationData;
public void setApplicationData(String applicationData) {
this.applicationData = applicationData;
}
public String getApplicationData() {
return this.applicationData;
}
}
??如果在一個application
作用域的Bean
上調(diào)用setter
方法,那么這個變更將對所有用戶和會話可見。后續(xù)對這個Bean
的所有調(diào)用(包括getter
和setter
)都將影響到同一個Bean
實(shí)例,后面的調(diào)用會覆蓋前面的狀態(tài)。
7. websocket作用域(了解)
??websocket
作用域:Bean
在每一個新的 WebSocket
會話中都會被創(chuàng)建一次,就像 session
作用域的 Bean
在每一個 HTTP
會話中都會被創(chuàng)建一次一樣。這個Bean
在整個WebSocket
會話內(nèi)都是有效的,當(dāng)WebSocket
會話結(jié)束后,Bean
就會被銷毀。
@Component
@Scope(value = "websocket", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class WebSocketScopedBean {
// 在一個WebSocket會話內(nèi)共享的數(shù)據(jù)
private String socketData;
public void setSocketData(String socketData) {
this.socketData = socketData;
}
public String getSocketData() {
return this.socketData;
}
}
上述Bean
在一個WebSocket
會話的生命周期內(nèi)是一個單例,每個新的WebSocket
會話都會創(chuàng)建一個新的Bean
實(shí)例。文章來源:http://www.zghlxwxcb.cn/news/detail-478672.html
這個作用域需要Spring Websocket
模塊支持,并且應(yīng)用需要配置為使用websocket
。文章來源地址http://www.zghlxwxcb.cn/news/detail-478672.html
歡迎一鍵三連~
有問題請留言,大家一起探討學(xué)習(xí)
----------------------Talk is cheap, show me the code-----------------------
到了這里,關(guān)于Spring高手之路4——深度解析Spring內(nèi)置作用域及其在實(shí)踐中的應(yīng)用的文章就介紹完了。如果您還想了解更多內(nèi)容,請?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!