本文試圖從原理上講解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