Spring Scope Bean是Spring框架中用于管理Bean的作用域的機制,它定義了Bean的生命周期和實例化策略。通過合理地選擇Bean的作用域,可以優(yōu)化應(yīng)用的性能和資源利用率。
環(huán)境:Spring5.3.23
一. 簡介
Spring Scope Bean是Spring用于管理Bean的作用域的一種機制。它定義了容器中Bean的生命周期和實例化策略,即如何創(chuàng)建Bean實例。
在Spring中,Bean的作用域包括單例(singleton)、原型(prototype)、請求(request)、會話(session)等。每個作用域都有其特定的使用場景和行為:
- 單例(singleton):這是Spring默認(rèn)的作用域,表示在整個Spring容器中,只有一個Bean實例存在。無論你從哪個地方獲取這個Bean,都將返回同一個實例。
- 原型(prototype):每次從容器中請求Bean時,都會創(chuàng)建一個新的Bean實例。
- 請求(request):在一個HTTP請求的范圍內(nèi),Bean是單例的。這種作用域適用于與單個請求關(guān)聯(lián)的Bean。
- 會話(session):在一個HTTP會話的范圍內(nèi),Bean是單例的。這種作用域適用于與單個用戶會話關(guān)聯(lián)的Bean。
此外,Spring還提供了其他一些作用域應(yīng)用(Application)、WebSocket,以滿足不同場景的需求。
通過合理地選擇Bean的作用域,可以優(yōu)化應(yīng)用的性能和資源利用率。例如,對于需要頻繁創(chuàng)建和銷毀實例的Bean,使用原型作用域會更高效;而對于需要在多個請求或會話之間共享狀態(tài)的Bean,則可以選擇單例或會話作用域。附官方圖:
圖片
接下來將分別介紹每一種作用域bean。
二. 作用域應(yīng)用
基礎(chǔ)類
static class Person {
@Override
public String toString() {
return super.toString() + " - " + this.hashCode() + "" ;
}
}
2.1 單例(singleton)
默認(rèn)使用@Bean,@Service,@Controller注解標(biāo)注的注解都是單例的。也可以同@Scope注解指定作用域為單例
@Bean
// 不指定@Scope默認(rèn)就是單例
@Scope(value = "singleton")
public Person person() {
return new Person() ;
}
測試
try (AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext()) {
context.registerBean(Config.class) ;
context.refresh() ;
System.out.println(context.getBean(Person.class)) ;
System.out.println(context.getBean(Person.class)) ;
}
控制臺輸出
com.pack.mAIn.scope.ScopeMain5$Person@5e0e82ae - 1578009262
com.pack.main.scope.ScopeMain5$Person@5e0e82ae - 1578009262
每次獲取的都是同一個實例。
原理
public abstract class AbstractBeanFactory {
protected <T> T doGetBean(...) {
// ...
// 判斷是否是單例
if (mbd.isSingleton()) {
// 先從單例池中查找是否已經(jīng)存在,不存在則調(diào)用createBean創(chuàng)建,
// 然后存入單例池中
sharedInstance = getSingleton(beanName, () -> {
try {
return createBean(beanName, mbd, args);
}
});
}
// ...
}
}
2.2 原型(prototype)
每次從容器中請求Bean時,都會創(chuàng)建一個新的Bean實例。
@Bean
@Scope(value = "prototype")
public Person person() {
return new Person() ;
}
控制臺輸出
com.pack.main.scope.ScopeMain5$Person@fa4c865 - 262457445
com.pack.main.scope.ScopeMain5$Person@3bd82cf5 - 1004023029
每次獲取都是不同的對象。
原理
public abstract class AbstractBeanFactory {
protected <T> T doGetBean(...) {
// ...
// 判斷是否是單例
if (mbd.isSingleton()) {
// ...
}
// 判斷是否是原型
else if (mbd.isPrototype()) {
Object prototypeInstance = null;
try {
// 不存在什么緩存池,直接創(chuàng)建bean實例返回
prototypeInstance = createBean(beanName, mbd, args);
}
}
// ...
}
}
這里考慮一個問題,如何在單例bean中正確的注入原型bean?
2.3 請求(request)
接下來都是與web環(huán)境相關(guān)了,所以這里演示的示例會以SpringBoot3.0.5環(huán)境演示。
基礎(chǔ)類
@Component
@Scope(value = "request")
public class Person {
}
測試類
@RestController
@RequestMapping("/scopes")
public class ScopeController {
@Resource
private Person person ;
@Resource
private PersonService ps ;
@GetMapping("/request")
public Person request() {
System.out.println("ScopeController: " + person) ;
ps.query() ;
return person ;
}
}
Service
@Service
public class PersonService {
@Resource
private Person person ;
public void query() {
System.out.println("PersonService: " + person) ;
}
}
如果上面這樣配置,啟動服務(wù)將會報錯:
Caused by: JAVA.lang.IllegalStateException: No thread-bound request found: Are you referring to request attributes outside of an actual web request, or processing a request outside of the originally receiving thread? If you are actually operating within a web request and still receive this message, your code is probably running outside of DispatcherServlet: In this case, use RequestContextListener or RequestContextFilter to expose the current request.
at org.springframework.web.context.request.RequestContextHolder.currentRequestAttributes(RequestContextHolder.java:131) ~[spring-web-6.0.7.jar:6.0.7]
該錯誤的原因就是你在一個單例bean中注入一個request作用域的bean,而request作用域bean的生命周期是在一個web請求開始創(chuàng)建的,所以這里你當(dāng)然是沒法注入的。
解決辦法:
- @Scope設(shè)置代理模式
@Component
@Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class Person {}
測試結(jié)果
ScopeController: com.pack.scopes.Person@106a9684 - 275420804
PersonService: com.pack.scopes.Person@106a9684 - 275420804
ScopeController: com.pack.scopes.Person@64396678 - 1681483384
PersonService: com.pack.scopes.Person@64396678 - 1681483384
每次請求接口都獲取的不是同一個實例。并且在一個完整的請求中獲取的Person都是同一個。
- 使用@RequestScope
該注解原理與上面其實一致的
@Scope(WebApplicationContext.SCOPE_REQUEST)
public @interface RequestScope {
@AliasFor(annotation = Scope.class)
// 設(shè)置好了使用代理
ScopedProxyMode proxyMode() default ScopedProxyMode.TARGET_CLASS;
}
2.4 會話(session)
@Component
@Scope(value = "session", proxyMode = ScopedProxyMode.TARGET_CLASS)
// 與request一樣,必須設(shè)置代理模式或者使用下面這個注解
// @SessionScope
public class Person {}
測試
ScopeController: com.pack.scopes.Person@2b56038d - 727057293
PersonService: com.pack.scopes.Person@2b56038d - 727057293
ScopeController: com.pack.scopes.Person@2b56038d - 727057293
PersonService: com.pack.scopes.Person@2b56038d - 727057293
多次訪問都是同一個session;你再換個瀏覽器訪問
ScopeController: com.pack.scopes.Person@1aa201fd - 446824957
PersonService: com.pack.scopes.Person@1aa201fd - 446824957
ScopeController: com.pack.scopes.Person@1aa201fd - 446824957
PersonService: com.pack.scopes.Person@1aa201fd - 446824957
此時對象就是一個新的了,不同的瀏覽器訪問當(dāng)然不是同一個session了。
2.5 應(yīng)用(application)
@Scope(value = "application", proxyMode = ScopedProxyMode.TARGET_CLASS)
// @ApplicationScope
// 都是web環(huán)境,所以情況都一樣
public class Person {}
測試
360瀏覽器
ScopeController: com.pack.scopes.Person@6371b4b6 - 1668396214
PersonService: com.pack.scopes.Person@6371b4b6 - 1668396214
Chrome瀏覽器
ScopeController: com.pack.scopes.Person@6371b4b6 - 1668396214
PersonService: com.pack.scopes.Person@6371b4b6 - 1668396214
他們是同一個對象,application作用域生命周期與整個應(yīng)用一樣,只有你關(guān)閉了服務(wù)器,在啟動后才會是再重新創(chuàng)建的bean對象。
3. web作用域原理
3.1 注冊作用域
public abstract class AbstractApplicationContext {
public void refresh() {
postProcessBeanFactory(beanFactory);
}
}
public class AnnotationConfigServletWebServerApplicationContext {
protected void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) {
super.postProcessBeanFactory(beanFactory);
}
}
public class ServletWebServerApplicationContext {
protected void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) {
// ...
registerWebApplicationScopes();
}
private void registerWebApplicationScopes() {
WebApplicationContextUtils.registerWebApplicationScopes(getBeanFactory());
}
}
public abstract class WebApplicationContextUtils {
public static void registerWebApplicationScopes(ConfigurableListableBeanFactory beanFactory) {
registerWebApplicationScopes(beanFactory, null);
}
public static void registerWebApplicationScopes(ConfigurableListableBeanFactory beanFactory,
@Nullable ServletContext sc) {
// 注冊作用域
beanFactory.registerScope(WebApplicationContext.SCOPE_REQUEST, new RequestScope());
beanFactory.registerScope(WebApplicationContext.SCOPE_SESSION, new SessionScope());
if (sc != null) {
ServletContextScope appScope = new ServletContextScope(sc);
beanFactory.registerScope(WebApplicationContext.SCOPE_APPLICATION, appScope);
}
}
}
這里每一種web作用域都有一個對應(yīng)的Scope實現(xiàn)RequestScope,SessionScope,ServletContextScope。
3.2 查找web作用域bean
public abstract class AbstractBeanFactory {
protected <T> T doGetBean(...) {
// ...
// 判斷是否是單例
if (mbd.isSingleton()) {
// ...
}
// 判斷是否是原型
else if (mbd.isPrototype()) {
Object prototypeInstance = null;
try {
// 不存在什么緩存池,直接創(chuàng)建bean實例返回
prototypeInstance = createBean(beanName, mbd, args);
}
}
// 其它作用域bean,如上面的web作用域
else {
String scopeName = mbd.getScope();
Scope scope = this.scopes.get(scopeName);
if (scope == null) {
throw new IllegalStateException("No Scope registered for scope name '" + scopeName + "'");
}
try {
// 通過具體Scope的實現(xiàn)類獲取bean對象
Object scopedInstance = scope.get(beanName, () -> {
beforePrototypeCreation(beanName);
try {
// 首次都還是會創(chuàng)建
return createBean(beanName, mbd, args);
}
});
}
}
}
// ...
}
}
總結(jié):Spring Scope Bean是Spring框架中用于管理Bean的作用域的機制,它定義了Bean的生命周期和實例化策略。通過合理地選擇Bean的作用域,可以優(yōu)化應(yīng)用的性能和資源利用率。