本文試圖從原理上講解Spring IoC容器的作用域機制,建議對着源碼閱讀,事半功倍。
0 引入問題
當我們談到Spring作用域的時候,自然而然會想到如下作用域(來自spring-core官方文檔):
作用域 | 描述 |
---|---|
singleton | (Default) Scopes a single bean definition to a single object instance for each Spring IoC container. |
prototype | Scopes a single bean definition to any number of object instances. |
request | Scopes a single bean definition to the lifecycle of a single HTTP request. That is, each HTTP request has its own instance of a bean created off the back of a single bean definition. Only valid in the context of a web-aware Spring ApplicationContext. |
session | Scopes a single bean definition to the lifecycle of an HTTP Session. Only valid in the context of a web-aware Spring ApplicationContext. |
application | Scopes a single bean definition to the lifecycle of a ServletContext. Only valid in the context of a web-aware Spring ApplicationContext. |
websocket | Scopes a single bean definition to the lifecycle of a WebSocket. Only valid in the context of a web-aware Spring ApplicationContext. |
從功能上看,這些作用域分別定義了調用org.springframework.beans.factory.BeanFactory#getBean()
方法時,容器根據bean definition
實例化bean object
的規則。
從底層實現上看,這些作用域可以分成兩類:
- 內置作用域:
singleton
和prototype
。 - 自定義作用域:
request
、session
、application
、websocket
以及我們自定義的作用域。
所有Spring IoC容器中都具備singleton
和prototype
作用域功能,而只有實現Web功能的容器(如org.springframework.web.context.support.GenericWebApplicationContext
接口)中才具備request
和session
等作用域功能。這是因為它們底層的實現機制不同。
spring-core官方文檔中說了這么一段話:
我個人這么理解作用域機制的擴展性:
- 內置的
singleton
和prototype
作用域——不可擴展。 - 可以復寫
request
和session
等預定義作用域的規則——可擴展。 - 可以自定義作用域——可擴展。
以上簡要概括了Spring IoC容器作用域的基本概念,希望能夠引起大家思考以下幾個問題(本文后續部分會一一探討):
- 什么是作用域?如何使用作用域?
- 作用域的底層原理?
- 內置作用域和自定義作用域的區別?
- 如何自定義作用域?
1 什么是作用域?如何使用作用域?
1.1 什么是作用域?
作用域是個很寬泛的概念,本文討論的特指是Spring IoC容器中Bean
對象的作用域,簡單可以理解成:bean
對象的存活范圍。
為了便於深入理解,我們先要大概了解一下Spring IoC容器的工作原理。Spring IoC容器的使用流程大概可以分為以下3個步驟:
-
配置
Bean
:@Configuration public class AppConfiguration { @Bean public A a() { return new A(); } } class A { }
-
創建Spring IoC容器,並讀取配置信息:
ApplicationContext context = new AnnotationConfigApplicationContext(AppConfiguration.class);
-
從容器中獲取
bean
,並使用:A a = context.getBean("a", A.class); System.out.println(a); // com.xianhuii.spring.beanscopes.demo01.A@16267862 a = context.getBean("a", A.class); System.out.println(a); // com.xianhuii.spring.beanscopes.demo01.A@16267862
Bean
作用域本質上指的是多次調用context.getBean()
方法獲取到的是否是同一個bean
對象。
上面例子中,默認指定作用域為singleton
,所以兩次調用context.getBean()
方法獲取到同一個對象。
如果指定作用域為prototype
:
@Bean
@Scope("prototype")
public A a() {
return new A();
}
此時,兩次調用context.getBean()
方法獲取到就是兩個對象了:
A a = context.getBean("a", A.class);
System.out.println(a); // com.xianhuii.spring.beanscopes.demo01.A@49070868
a = context.getBean("a", A.class);
System.out.println(a); // com.xianhuii.spring.beanscopes.demo01.A@6385cb26
1.2 如何使用作用域?
作用域的使用比較簡單,只需要在配置Bean
時使用指定作用域即可。我們使用作用域的重點其實在於不同作用域下context.getBean()
的規則。
1.2.1 singleton
singleton
作用域下,多次調用context.getBean()
方法獲取到同一個對象。
- 配置:
@Configuration
public class AppConfiguration {
@Bean
public A a() {
return new A();
}
}
class A {}
- 使用:
ApplicationContext context = new AnnotationConfigApplicationContext(AppConfiguration.class);
A a = context.getBean("a", A.class);
System.out.println(a); // com.xianhuii.spring.beanscopes.demo01.A@16267862
a = context.getBean("a", A.class);
System.out.println(a); // com.xianhuii.spring.beanscopes.demo01.A@16267862
1.2.2 prototype
prototype
作用域下,每次調用context.getBean()
方法獲取到新對象。
- 配置:
@Configuration
public class AppConfiguration {
@Bean
@Scope("prototype")
public A a() {
return new A();
}
}
class A {}
- 使用:
ApplicationContext context = new AnnotationConfigApplicationContext(AppConfiguration.class);
A a = context.getBean("a", A.class);
System.out.println(a); // com.xianhuii.spring.beanscopes.demo01.A@49070868
a = context.getBean("a", A.class);
System.out.println(a); // com.xianhuii.spring.beanscopes.demo01.A@6385cb26
2 作用域的底層原理?
Bean
作用域本質上指的是多次調用context.getBean()
方法獲取到的是否是同一個bean
對象。
所以,作用域底層執行原理在context.getBean()
方法中,其中與作用域有關的執行流程如下:
- 從
BeanFactory
中獲取已加載的BeanDefinition
,判斷該Bean
的作用域。 - 如果是
singleton
作用域,則執行單例創建規則。 - 如果是
prototype
作用域,則執行原型創建規則。 - 如果是自定義作用域,則執行自定義創建規則。
相關核心源碼如下(org.springframework.beans.factory.support.AbstractBeanFactory#doGetBean()
):
// 獲取BeanDefinition
RootBeanDefinition mbd = getMergedLocalBeanDefinition(beanName);
// 1、如果作用域是singleton,執行對應創建規則:創建bean,並放到容器中
if (mbd.isSingleton()) {
sharedInstance = getSingleton(beanName, () -> {
try {
return createBean(beanName, mbd, args);
}
catch (BeansException ex) {
destroySingleton(beanName);
throw ex;
}
});
beanInstance = getObjectForBeanInstance(sharedInstance, name, beanName, mbd);
}
// 2、如果作用域是prototype,執行對應創建規則:創建bean,但不會放到容器中
else if (mbd.isPrototype()) {
Object prototypeInstance = null;
try {
beforePrototypeCreation(beanName);
prototypeInstance = createBean(beanName, mbd, args);
}
finally {
afterPrototypeCreation(beanName);
}
beanInstance = getObjectForBeanInstance(prototypeInstance, name, beanName, mbd);
}
// 3、如果是自定義作用域,執行對應創建規則:自定義規則
else {
String scopeName = mbd.getScope();
Scope scope = this.scopes.get(scopeName);
try {
Object scopedInstance = scope.get(beanName, () -> {
beforePrototypeCreation(beanName);
try {
return createBean(beanName, mbd, args);
}
finally {
afterPrototypeCreation(beanName);
}
});
beanInstance = getObjectForBeanInstance(scopedInstance, name, beanName, mbd);
}
catch (IllegalStateException ex) {
throw new ScopeNotActiveException(beanName, scopeName, ex);
}
}
2.1 singleton
作用域
singleton
作用域bean
對象的創建過程分為三個步驟:
- 判斷是否為
singleton
作用域。 - 根據
singleton
規則創建bean
對象。 - 對
bean
對象進行后處理。
1、判斷
mbd.isSingleton()
方法(org.springframework.beans.factory.support.AbstractBeanDefinition#isSingleton
)的源碼如下:
public boolean isSingleton() {
return SCOPE_SINGLETON.equals(this.scope) || SCOPE_DEFAULT.equals(this.scope);
}
其中兩個靜態變量分別為:
String SCOPE_SINGLETON = ConfigurableBeanFactory.SCOPE_SINGLETON; // "singleton"
public static final String SCOPE_DEFAULT = "";
所以,我們在聲明Bean
時,以下情況會聲明為singleton
作用域:
-
默認情況(即不顯示指定作用域),會默認聲明為
SCOPE_DEFAULT
作用域,而SCOPE_DEFAULT
實際上就是singleton
作用域。@Bean public A a() { return new A(); }
-
顯示指定為
singleton
作用域,通過@Scope("singleton")
等方式。@Bean @Scope("singleton") public A a() { return new A(); }
-
顯示指定為默認作用域,通過
@Scope
等方式。@Bean @Scope public A a() { return new A(); }
2、創建單例bean
創建單例bean
的源碼如下:
sharedInstance = getSingleton(beanName, () -> {
try {
// 創建bean
return createBean(beanName, mbd, args);
}
catch (BeansException ex) {
// 創建失敗,刪除緩存
destroySingleton(beanName);
throw ex;
}
});
其核心在於org.springframework.beans.factory.support.DefaultSingletonBeanRegistry#getSingleton(java.lang.String, org.springframework.beans.factory.ObjectFactory<?>)
方法,其中定義了大部分創建單例bean
的規則(模板方法模式):
- 為
singletonObjects
對象(單例對象緩存)加鎖:一次只能創建一個單例對象。 - 從
singletonObjects
中獲取當前beanName
的對象。 - 如果存在,說明已經創建,直接返回。
- 如果不存在,說明還沒有創建,則進行創建對象:
- 預處理。
- 創建對象。
- 后處理。
- 添加到singletonObjects緩存。
public Object getSingleton(String beanName, ObjectFactory<?> singletonFactory) {
// 1、singletonObjects緩存加鎖
synchronized (this.singletonObjects) {
// 2、從singletonObjects緩存中獲取singletonObject
Object singletonObject = this.singletonObjects.get(beanName);
// 3、如果不存在,則創建新對象
if (singletonObject == null) {
// 3.1、預處理
beforeSingletonCreation(beanName);
boolean newSingleton = false;
try {
// 3.2、創建對象
singletonObject = singletonFactory.getObject();
newSingleton = true;
}
catch (IllegalStateException ex) {
}
catch (BeanCreationException ex) {
}
finally {
if (recordSuppressedExceptions) {
this.suppressedExceptions = null;
}
// 3.3、后處理
afterSingletonCreation(beanName);
}
if (newSingleton) {
// 3.4、添加到singletonObjects緩存
addSingleton(beanName, singletonObject);
}
}
// 4、如果存在,直接返回
return singletonObject;
}
}
其中,預處理和后處理都是單例對象創建過程中的回調,可以通過重寫自定義回調規則。默認情況下,預處理和后處理會分別標記/清除單例對象“創建中”的標記。
addSingleton(beanName, singletonObject)
方法會將該對象添加到singletonObjects
單例對象緩存和registeredSingletons
已注冊單例對象緩存中,並將該對象從singletonFactories
單例工廠緩存和earlySingletonObjects
早期單例對象緩存中移除:
protected void addSingleton(String beanName, Object singletonObject) {
synchronized (this.singletonObjects) {
this.singletonObjects.put(beanName, singletonObject);
this.singletonFactories.remove(beanName);
this.earlySingletonObjects.remove(beanName);
this.registeredSingletons.add(beanName);
}
}
最重要的singletonFactory.getObject()
方法是外部傳入的,即調用該方法實際上會執行外部傳入的匿名對象中定義的方法:
- 調用
createBean(beanName, mbd, args)
會使用反射機制創建bean
對象。 - 如果創建失敗,則調用
destroySingleton(beanName)
方法刪除相關緩存信息。
try {
return createBean(beanName, mbd, args);
}
catch (BeansException ex) {
// Explicitly remove instance from singleton cache: It might have been put there
// eagerly by the creation process, to allow for circular reference resolution.
// Also remove any beans that received a temporary reference to the bean.
destroySingleton(beanName);
throw ex;
}
3、后處理
org.springframework.beans.factory.support.AbstractBeanFactory#getObjectForBeanInstance
方法會對上述根據BeanDefinition
創建處理bean
進行后處理,該方法其實就是Spring AOP功能的入口。其內部會進行如下判斷:
- 容器內部使用。
- 是否為普通
bean
:直接返回。 - 是否為
org.springframework.beans.factory.FactoryBean
實現類:Spring AOP核心類。
protected Object getObjectForBeanInstance(
Object beanInstance, String name, String beanName, @Nullable RootBeanDefinition mbd) {
// 1、pring容器內部bean,不必太過關注
if (BeanFactoryUtils.isFactoryDereference(name)) {}
// 2、普通bean,直接返回
if (!(beanInstance instanceof FactoryBean)) {
return beanInstance;
}
// 3、FactoryBean對象,通過AOP功能獲取其真正代理的對象
Object object = null;
if (mbd != null) {
mbd.isFactoryBean = true;
} else {
object = getCachedObjectForFactoryBean(beanName);
}
if (object == null) {
// Return bean instance from factory.
FactoryBean<?> factory = (FactoryBean<?>) beanInstance;
// Caches object obtained from FactoryBean if it is a singleton.
if (mbd == null && containsBeanDefinition(beanName)) {
mbd = getMergedLocalBeanDefinition(beanName);
}
boolean synthetic = (mbd != null && mbd.isSynthetic());
object = getObjectFromFactoryBean(factory, beanName, !synthetic);
}
return object;
}
2.2 prototype
作用域
prototype
作用域bean
對象的創建過程分為三個步驟:
- 判斷是否為
prototype
作用域。 - 根據
prototype
規則創建bean
對象。 - 對
prototype
對象進行后處理。
1、判斷
mbd.isPrototype()
方法(org.springframework.beans.factory.support.AbstractBeanDefinition#isPrototype
)的源碼如下:
public boolean isPrototype() {
return SCOPE_PROTOTYPE.equals(this.scope);
}
其中靜態變量為:
String SCOPE_PROTOTYPE = ConfigurableBeanFactory.SCOPE_PROTOTYPE; // "prototype"
所以,我們在聲明Bean
時,可以通過@Scope("prototype")
等方式顯示指定為prototype
作用域:
@Bean
@Scope("prototype")
public A a() {
return new A();
}
2、創建原型bean
有了以上經驗,我們就能很容易理解創建原型bean
的源碼:
Object prototypeInstance = null;
try {
// 1、預處理,默認標記為“創建中”狀態
beforePrototypeCreation(beanName);
// 2、通過反射機制創建對象
prototypeInstance = createBean(beanName, mbd, args);
}
finally {
// 3、后處理,默認移除“創建中”標記
afterPrototypeCreation(beanName);
}
3、后處理
該后處理過程與singleton
作用域完全相同。
2.3 自定義作用域
如果BeanDefinition
既不是singleton
,也不是prototype
,那么就會執行自定義作用域的創建規則:
- 獲取
BeanDefinition
的scope
屬性值。 - 從
BeanFactory
的作用域緩存scopes
中獲取對應的作用域。 - 調用
scope.get()
方法,執行自定義創建規則。 - 后處理:Spring AOP功能入口,與
singleton
和prototype
相同。
// 1、獲取BeanDefinition的scope屬性值
String scopeName = mbd.getScope();
// 2、從BeanFactory的作用域緩存scopes中獲取對應的作用域
Scope scope = this.scopes.get(scopeName);
try {
// 3、執行自定義創建規則
Object scopedInstance = scope.get(beanName, () -> {
beforePrototypeCreation(beanName);
try {
return createBean(beanName, mbd, args);
}
finally {
afterPrototypeCreation(beanName);
}
});
// 4、后處理
beanInstance = getObjectForBeanInstance(scopedInstance, name, beanName, mbd);
}
catch (IllegalStateException ex) {
throw new ScopeNotActiveException(beanName, scopeName, ex);
}
從以上源碼中我們可以得出自定義作用域的三個步驟:
-
創建作用域實現類:實現
org.springframework.beans.factory.config.Scope
接口,實現其get()
方法。classDiagram class Scope Scope : +get(String name, ObjectFactory<?> objectFactory) -
將作用域實現類注冊到
BeanFactory
的scopes
緩存中:key為作用域名,value為自定義作用域對象(org.springframework.beans.factory.config.ConfigurableBeanFactory#registerScope
)。 -
配置
Bean
時,指定對應的作用域名。
3 內置作用域和自定義作用域的區別?
通過上述的講解,想必大家對內置作用域(singleton
和prototype
)和自定義作用域的區別有了本質上的理解。
內置作用域的作用域名和bean
創建規則已經寫死到Spring IoC容器中。
自定義作用域通過自定義的作用域名從BeanFactory
的scopes
緩存中找到自定義作用域實現類,根據其中實現的get()
方法創建bean
。同時,自定義作用域的bean
對象存放於自定義的緩存中。
4 自定義作用域案例:request
接下來,我們以request
作用域為例,展示如何自定義作用域。
4.1 RequestScope
實現類
該實現類全限定類名為org.springframework.web.context.request.RequestScope
。類圖如下:
其中,最核心的的部分在於org.springframework.web.context.request.AbstractRequestAttributesScope#get
:
- 獲取當前請求的
RequestAttributes
對象。 - 從緩存中獲取
bean
。 - 如果緩存中不存在,則需要重新創建:
- 使用
objectFactory
匿名對象創建bean
。 - 將
bean
放到緩存中。 - 重新從緩存中獲取。
- 使用
- 如果緩存中存在,直接返回。
@Override
public Object get(String name, ObjectFactory<?> objectFactory) {
// 1、獲取當前請求的RequestAttributes緩存對象。
RequestAttributes attributes = RequestContextHolder.currentRequestAttributes();
// 2、從緩存中獲取bean
Object scopedObject = attributes.getAttribute(name, getScope());
// 3、如果緩存中不存在,則需要重新創建
if (scopedObject == null) {
// 3.1、使用objectFactory匿名對象創建bean
scopedObject = objectFactory.getObject();
// 3.2、將bean存放到緩存中
attributes.setAttribute(name, scopedObject, getScope());
// 3.3、重新從緩存中獲取
// Retrieve object again, registering it for implicit session attribute updates.
// As a bonus, we also allow for potential decoration at the getAttribute level.
Object retrievedObject = attributes.getAttribute(name, getScope());
if (retrievedObject != null) {
// Only proceed with retrieved object if still present (the expected case).
// If it disappeared concurrently, we return our locally created instance.
scopedObject = retrievedObject;
}
}
// 4、如果緩存中存在,直接返回
return scopedObject;
}
4.2 注冊作用域
在實現Web功能的容器(如org.springframework.web.context.support.GenericWebApplicationContext
接口)中,會自動將request
等自定義作用域注冊到BeanFactory
的scopes
緩存中:
protected void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) {
if (this.servletContext != null) {
beanFactory.addBeanPostProcessor(new ServletContextAwareProcessor(this.servletContext));
beanFactory.ignoreDependencyInterface(ServletContextAware.class);
}
WebApplicationContextUtils.registerWebApplicationScopes(beanFactory, this.servletContext);
WebApplicationContextUtils.registerEnvironmentBeans(beanFactory, this.servletContext);
}
在這里會調用封裝好的WebApplicationContextUtils.registerWebApplicationScopes(beanFactory, this.servletContext);
方法進行注冊:
public static void registerWebApplicationScopes(ConfigurableListableBeanFactory beanFactory,
@Nullable ServletContext sc) {
// 1、注冊request作用域:key為request,value為RequestScope實現類
beanFactory.registerScope(WebApplicationContext.SCOPE_REQUEST, new RequestScope());
// 2、注冊session作用域:key為session,value為SessionScope實現類
beanFactory.registerScope(WebApplicationContext.SCOPE_SESSION, new SessionScope());
if (sc != null) {
// 3、注冊application作用域:key為application,value為ServletContextScope實現類
ServletContextScope appScope = new ServletContextScope(sc);
beanFactory.registerScope(WebApplicationContext.SCOPE_APPLICATION, appScope);
// Register as ServletContext attribute, for ContextCleanupListener to detect it.
sc.setAttribute(ServletContextScope.class.getName(), appScope);
}
beanFactory.registerResolvableDependency(ServletRequest.class, new RequestObjectFactory());
beanFactory.registerResolvableDependency(ServletResponse.class, new ResponseObjectFactory());
beanFactory.registerResolvableDependency(HttpSession.class, new SessionObjectFactory());
beanFactory.registerResolvableDependency(WebRequest.class, new WebRequestObjectFactory());
if (jsfPresent) {
FacesDependencyRegistrar.registerFacesDependencies(beanFactory);
}
}
4.3 配置Bean
在定義Bean
時,通過指定作用域為request
即可:
@Bean
@Scope("request")
public A a() {
return new A();
}
為了方便,Spring還實現了@RequestScope
注解,使用方式如下:
@Bean
@RequestScope
public A a() {
return new A();
}
@RequestScope
本質上和@Scope("request")
沒有任何區別(不過我們在自定義作用域時可以采用類似的方式來炫技):
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Scope(WebApplicationContext.SCOPE_REQUEST) // 相當於@Scope("request")
public @interface RequestScope {
/**
* Alias for {@link Scope#proxyMode}.
* <p>Defaults to {@link ScopedProxyMode#TARGET_CLASS}.
*/
@AliasFor(annotation = Scope.class)
ScopedProxyMode proxyMode() default ScopedProxyMode.TARGET_CLASS;
}
5 自定義作用域實戰
在文章的最后,我們通過實戰來自定義作用域myScope
,用來簡單模擬singleton
作用域:
- 第一次調用
context.getBean()
方法時創建新bean
。 - 之后每次調用
context.getBean()
方法都會獲取同一個bean
。
5.1 MyScope
實現類
該實現類核心在於get()
方法,其他方法都使用默認實現,因此省略:
public class MyScope implements Scope {
// bean緩存
private static Map<String, Object> beanMap = new ConcurrentHashMap<>();
@Override
public Object get(String name, ObjectFactory<?> objectFactory) {
// 1、從緩存中獲取bean
Object bean = beanMap.get(name);
// 2、如果緩存中不存在,則新建
if (bean != null) {
// 2.1創建bean實例
bean = objectFactory.getObject();
// 2.2、放到緩存中
beanMap.put(name, bean);
}
// 3、如果緩存中存在,則直接返回
return bean;
}
}
5.2 注冊作用域
注冊作用域的方法定義為org.springframework.beans.factory.config.ConfigurableBeanFactory#registerScope
:
@Configuration
public class AppConfiguration implements BeanFactoryAware {
private BeanFactory beanFactory;
@Override
public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
this.beanFactory = beanFactory;
}
@PostConstruct
public void registerScope() {
if (beanFactory instanceof ConfigurableBeanFactory) {
ConfigurableBeanFactory configurableBeanFactory = (ConfigurableBeanFactory) beanFactory;
configurableBeanFactory.registerScope("myScope", new MyScope());
}
}
}
5.3 配置Bean
@Bean
@Scope("myScope")
public A a() {
return new A();
}
5.4 測試
@SpringBootApplication
public class Application implements ApplicationContextAware {
@Bean
public ApplicationRunner runner() {
return new ApplicationRunner() {
@Override
public void run(ApplicationArguments args) throws Exception {
A a = applicationContext.getBean("a", A.class);
System.out.println(a);
MyScope.getBeanMap().forEach((key, value) -> {
System.out.println("key: " + key + ", value: " + value);
});
a = applicationContext.getBean("a", A.class);
System.out.println(a);
MyScope.getBeanMap().forEach((key, value) -> {
System.out.println("key: " + key + ", value: " + value);
});
}
};
}
private ApplicationContext applicationContext;
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}
}
輸出結果為:
com.xianhuii.springboot.demo.A@757194dc
key: a, value: com.xianhuii.springboot.demo.A@757194dc
com.xianhuii.springboot.demo.A@757194dc
key: a, value: com.xianhuii.springboot.demo.A@757194dc