[spring-core]作用域

Xianhuii 發表於 2021-11-28
Spring

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

從底層實現上看,這些作用域可以分成兩類:

  1. 內建作用域:singletonprototype
  2. 自定義作用域:requestsessionapplicationwebsocket以及我們自定義的作用域。

所有Spring IoC容器中都具備singletonprototype作用域功能,而只有實現Web功能的容器(如org.springframework.web.context.support.GenericWebApplicationContext介面)中才具備requestsession等作用域功能。這是因為它們底層的實現機制不同。

spring-core官方文件中說了這麼一段話:

image-20211116220207985

我個人這麼理解作用域機制的擴充套件性:

  1. 內建的singletonprototype作用域——不可擴充套件。
  2. 可以複寫requestsession等預定義作用域的規則——可擴充套件。
  3. 可以自定義作用域——可擴充套件。

以上簡要概括了Spring IoC容器作用域的基本概念,希望能夠引起大家思考以下幾個問題(本文後續部分會一一探討):

  1. 什麼是作用域?如何使用作用域?
  2. 作用域的底層原理?
  3. 內建作用域和自定義作用域的區別?
  4. 如何自定義作用域?

1 什麼是作用域?如何使用作用域?

1.1 什麼是作用域?

作用域是個很寬泛的概念,本文討論的特指是Spring IoC容器中Bean物件的作用域,簡單可以理解成:bean物件的存活範圍。

為了便於深入理解,我們先要大概瞭解一下Spring IoC容器的工作原理。Spring IoC容器的使用流程大概可以分為以下3個步驟:

flowchart LR 配置檔案 --> 建立容器 --> 獲取bean
  1. 配置Bean

    @Configuration
    public class AppConfiguration {
        @Bean
        public A a() {
            return new A();
        }
    }
    
    class A {
    
    }
    
  2. 建立Spring IoC容器,並讀取配置資訊:

    ApplicationContext context = new AnnotationConfigApplicationContext(AppConfiguration.class);
    
  3. 從容器中獲取bean,並使用:

    A a = context.getBean("a", A.class);
    System.out.println(a);	// [email protected]
    a = context.getBean("a", A.class);
    System.out.println(a);	// [email protected]
    

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);	// [email protected]
a = context.getBean("a", A.class);
System.out.println(a);	// [email protected]

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);	// [email protected]
a = context.getBean("a", A.class);
System.out.println(a);	// [email protected]

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);	// [email protected]
a = context.getBean("a", A.class);
System.out.println(a);	// [email protected]

2 作用域的底層原理?

Bean作用域本質上指的是多次呼叫context.getBean()方法獲取到的是否是同一個bean物件。

所以,作用域底層執行原理在context.getBean()方法中,其中與作用域有關的執行流程如下:

flowchart TD A[獲取BeanDefinition] --> B{scope?}; B -->|singleton| C[singleton建立規則]; B -->|prototype| D[prototype建立規則]; B -->|自定義作用域| E[自定義建立規則];
  1. BeanFactory中獲取已載入的BeanDefinition,判斷該Bean的作用域。
  2. 如果是singleton作用域,則執行單例建立規則。
  3. 如果是prototype作用域,則執行原型建立規則。
  4. 如果是自定義作用域,則執行自定義建立規則。

相關核心原始碼如下(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物件的建立過程分為三個步驟:

  1. 判斷是否為singleton作用域。
  2. 根據singleton規則建立bean物件。
  3. 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作用域:

  1. 預設情況(即不顯示指定作用域),會預設宣告為SCOPE_DEFAULT作用域,而SCOPE_DEFAULT實際上就是singleton作用域。

    @Bean
    public A a() {
        return new A();
    }
    
  2. 顯示指定為singleton作用域,通過@Scope("singleton")等方式。

    @Bean
    @Scope("singleton")
    public A a() {
        return new A();
    }
    
  3. 顯示指定為預設作用域,通過@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的規則(模板方法模式):

  1. singletonObjects物件(單例物件快取)加鎖:一次只能建立一個單例物件。
  2. singletonObjects中獲取當前beanName的物件。
  3. 如果存在,說明已經建立,直接返回。
  4. 如果不存在,說明還沒有建立,則進行建立物件:
    1. 預處理。
    2. 建立物件。
    3. 後處理。
    4. 新增到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()方法是外部傳入的,即呼叫該方法實際上會執行外部傳入的匿名物件中定義的方法:

  1. 呼叫createBean(beanName, mbd, args)會使用反射機制建立bean物件。
  2. 如果建立失敗,則呼叫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功能的入口。其內部會進行如下判斷:

  1. 容器內部使用。
  2. 是否為普通bean:直接返回。
  3. 是否為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物件的建立過程分為三個步驟:

  1. 判斷是否為prototype作用域。
  2. 根據prototype規則建立bean物件。
  3. 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,那麼就會執行自定義作用域的建立規則:

  1. 獲取BeanDefinitionscope屬性值。
  2. BeanFactory的作用域快取scopes中獲取對應的作用域。
  3. 呼叫scope.get()方法,執行自定義建立規則。
  4. 後處理:Spring AOP功能入口,與singletonprototype相同。
// 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);
}

從以上原始碼中我們可以得出自定義作用域的三個步驟:

  1. 建立作用域實現類:實現org.springframework.beans.factory.config.Scope介面,實現其get()方法。

    classDiagram class Scope Scope : +get(String name, ObjectFactory<?> objectFactory)
  2. 將作用域實現類註冊到BeanFactoryscopes快取中:key為作用域名,value為自定義作用域物件(org.springframework.beans.factory.config.ConfigurableBeanFactory#registerScope)。

  3. 配置Bean時,指定對應的作用域名。

3 內建作用域和自定義作用域的區別?

通過上述的講解,想必大家對內建作用域(singletonprototype)和自定義作用域的區別有了本質上的理解。

內建作用域的作用域名和bean建立規則已經寫死到Spring IoC容器中。

自定義作用域通過自定義的作用域名從BeanFactoryscopes快取中找到自定義作用域實現類,根據其中實現的get()方法建立bean。同時,自定義作用域的bean物件存放於自定義的快取中。

4 自定義作用域案例:request

接下來,我們以request作用域為例,展示如何自定義作用域。

4.1 RequestScope實現類

該實現類全限定類名為org.springframework.web.context.request.RequestScope。類圖如下:

classDiagram class Scope Scope : +get(String name, ObjectFactory<?> objectFactory) Scope <|-- AbstractRequestAttributesScope AbstractRequestAttributesScope <|-- RequestScope

其中,最核心的的部分在於org.springframework.web.context.request.AbstractRequestAttributesScope#get

  1. 獲取當前請求的RequestAttributes物件。
  2. 從快取中獲取bean
  3. 如果快取中不存在,則需要重新建立:
    1. 使用objectFactory匿名物件建立bean
    2. bean放到快取中。
    3. 重新從快取中獲取。
  4. 如果快取中存在,直接返回。
@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等自定義作用域註冊到BeanFactoryscopes快取中:

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作用域:

  1. 第一次呼叫context.getBean()方法時建立新bean
  2. 之後每次呼叫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;
   }
}

輸出結果為:

[email protected]
key: a, value: [email protected]
[email protected]
key: a, value: [email protected]

相關文章