1. 程式人生 > 其它 >【Spring】IoC容器 - Spring Bean作用域Scope(含SpringCloud中的RefreshScope )

【Spring】IoC容器 - Spring Bean作用域Scope(含SpringCloud中的RefreshScope )

前言

上一章學習了【依賴來源】,本章主要討論SpringBean的作用域,我們這裡討論的Bean的作用域,很大程度都是預設只討論依賴來源為【Spring BeanDefinition】的作用域,因為在我們的業務開發中,我們都是Spring框架的使用者,我們自定義的bean幾乎全部都是屬於【Spring BeanDefinition】的。後續文章以這個為預設前提。

作用域概覽

來源 說明
singleton 預設的spring bean作用域,一個BeanFactory有且僅有一個例項,重要
prototype 原型作用域,每一次的依賴查詢和依賴注入都會生成新的bean物件,重要
request 將SpringBean儲存在ServletRequest上下文中,不重要
session 將SpringBean儲存在HttpSession上下文中,不重要
application 將SpringBean儲存在ServletContext上下文中,不重要

由於目前的開發模式基本都是前後端分離,以前我們寫JSP的時候需要從後端的response物件中獲取部分資料進行展示,現在這些模板技術[JSP,Freemarker,Velocity等等]已經邊緣化,這裡不會重點討論後三者作用域

Singleton作用域

首先我們看一下Spring官方文件的描述:
Only one shared instance of a singleton bean is managed, and all requests for beans with an ID or IDs that match that bean definition result in that one specific bean instance being returned by the Spring container.

To put it another way, when you define a bean definition and it is scoped as a singleton, the Spring IoC container creates exactly one instance of the object defined by that bean definition. This single instance is stored in a cache of such singleton beans, and all subsequent requests and references for that named bean return the cached object. The following image shows how the singleton scope works:

Spring’s concept of a singleton bean differs from the singleton pattern as defined in the Gang of Four (GoF) patterns book. The GoF singleton hard-codes the scope of an object such that one and only one instance of a particular class is created per ClassLoader

. The scope of the Spring singleton is best described as being per-container and per-bean. This means that, if you define one bean for a particular class in a single Spring container, the Spring container creates one and only one instance of the class defined by that bean definition. The singleton scope is the default scope in Spring. To define a bean as a singleton in XML, you can define a bean as shown in the following example:

<bean id="accountService" class="com.something.DefaultAccountService"/>

<!-- the following is equivalent, though redundant (singleton scope is the default) -->
<bean id="accountService" class="com.something.DefaultAccountService" scope="singleton"/>

核心點已經高亮出來,對Spring來說,Singleton是指在一個容器中,一般是BeanFactory,只存在一個特定ID的Bean。
singleton作用域也是預設的作用域。

Prototype作用域

依然先看一下Spring官方文件的描述:
The non-singleton prototype scope of bean deployment results in the creation of a new bean instance every time a request for that specific bean is made. That is, the bean is injected into another bean or you request it through a getBean() method call on the container. As a rule, you should use the prototype scope for all stateful beans and the singleton scope for stateless beans.
我們應該讓所有的有狀態bean是prototype scope,讓所有的無狀態bean是singleton scope。

The following diagram illustrates the Spring prototype scope:

(A data access object (DAO) is not typically configured as a prototype, because a typical DAO does not hold any conversational state. It was easier for us to reuse the core of the singleton diagram.)
DAO一般不應該被設定為prototype作用域,因為常規的DAO不應該包含任何會話狀態。所以應該配置為singleton作用域。它這裡只是舉例說明。

The following example defines a bean as a prototype in XML:

<bean id="accountService" class="com.something.DefaultAccountService" scope="prototype"/>

In contrast to the other scopes, Spring does not manage the complete lifecycle of a prototype bean. The container instantiates, configures, and otherwise assembles a prototype object and hands it to the client, with no further record of that prototype instance. Thus, although initialization lifecycle callback methods are called on all objects regardless of scope, in the case of prototypes, configured destruction lifecycle callbacks are not called. The client code must clean up prototype-scoped objects and release expensive resources that the prototype beans hold. To get the Spring container to release resources held by prototype-scoped beans, try using a custom bean post-processor, which holds a reference to beans that need to be cleaned up.
與其他作用域不同,Spring並不管理prototype bean的整個生命週期。容器例項化、配置和以其他方式組裝prototype物件並將其交給客戶端,而不再記錄該prototype例項。

In some respects, the Spring container’s role in regard to a prototype-scoped bean is a replacement for the Java new operator. All lifecycle management past that point must be handled by the client. (For details on the lifecycle of a bean in the Spring container, see Lifecycle Callbacks.)
在某些方面,Spring的prototype作用域是Java裡new這種行為的一種替代,這種作用域的生命週期都被客戶端控制。生命週期管理請參考Lifecycle Callbacks章節。

點選檢視[示例程式碼]
public class BeanScopeDemo implements DisposableBean {

    @Bean
    // 預設 scope 就是 "singleton"
    public static User singletonUser() {
        return createUser();
    }

    @Bean
    @Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
    public static User prototypeUser() {
        return createUser();
    }

    private static User createUser() {
        User user = new User();
        user.setId(System.nanoTime());
        return user;
    }

    @Autowired
    @Qualifier("singletonUser")
    private User singletonUser;

    @Autowired
    @Qualifier("singletonUser")
    private User singletonUser1;

    @Autowired
    @Qualifier("prototypeUser")
    private User prototypeUser;

    @Autowired
    @Qualifier("prototypeUser")
    private User prototypeUser1;

    @Autowired
    @Qualifier("prototypeUser")
    private User prototypeUser2;

    @Autowired
    private Map<String, User> users;

    @Autowired
    private ConfigurableListableBeanFactory beanFactory; // Resolvable Dependency

    public static void main(String[] args) {

        // 建立 BeanFactory 容器
        AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext();
        // 註冊 Configuration Class(配置類) -> Spring Bean
        applicationContext.register(BeanScopeDemo.class);

        applicationContext.addBeanFactoryPostProcessor(beanFactory -> {
            beanFactory.addBeanPostProcessor(new BeanPostProcessor() {

                @Override
                public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
                    System.out.printf("%s Bean 名稱:%s 在初始化後回撥...%n", bean.getClass().getName(), beanName);
                    return bean;
                }
            });
        });

        // 啟動 Spring 應用上下文
        applicationContext.refresh();

        scopedBeansByLookup(applicationContext);

        scopedBeansByInjection(applicationContext);

        // 顯示地關閉 Spring 應用上下文
        applicationContext.close();
    }

    private static void scopedBeansByLookup(AnnotationConfigApplicationContext applicationContext) {

        for (int i = 0; i < 3; i++) {
            // singletonUser 是共享 Bean 物件
            User singletonUser = applicationContext.getBean("singletonUser", User.class);
            System.out.println("singletonUser = " + singletonUser);
            // prototypeUser 是每次依賴查詢均生成了新的 Bean 物件
            User prototypeUser = applicationContext.getBean("prototypeUser", User.class);
            System.out.println("prototypeUser = " + prototypeUser);
        }
    }

    private static void scopedBeansByInjection(AnnotationConfigApplicationContext applicationContext) {
        BeanScopeDemo beanScopeDemo = applicationContext.getBean(BeanScopeDemo.class);

        System.out.println("beanScopeDemo.singletonUser = " + beanScopeDemo.singletonUser);
        System.out.println("beanScopeDemo.singletonUser1 = " + beanScopeDemo.singletonUser1);

        System.out.println("beanScopeDemo.prototypeUser = " + beanScopeDemo.prototypeUser);
        System.out.println("beanScopeDemo.prototypeUser1 = " + beanScopeDemo.prototypeUser1);
        System.out.println("beanScopeDemo.prototypeUser2 = " + beanScopeDemo.prototypeUser2);

        System.out.println("beanScopeDemo.users = " + beanScopeDemo.users);
    }

    @Override
    public void destroy() throws Exception {

        System.out.println("當前 BeanScopeDemo Bean 正在銷燬中...");

        this.prototypeUser.destroy();
        this.prototypeUser1.destroy();
        this.prototypeUser1.destroy();
        this.prototypeUser2.destroy();
        // 獲取 BeanDefinition
        for (Map.Entry<String, User> entry : this.users.entrySet()) {
            String beanName = entry.getKey();
            BeanDefinition beanDefinition = beanFactory.getBeanDefinition(beanName);
            if (beanDefinition.isPrototype()) { // 如果當前 Bean 是 prototype scope
                User user = entry.getValue();
                user.destroy();
            }
        }

        System.out.println("當前 BeanScopeDemo Bean 銷燬完成");
    }
}
結論一:
Singleton Bean 無論依賴查詢還是依賴注入,均為同一個物件
Prototype Bean 無論依賴查詢還是依賴注入,均為新生成的物件

結論二:
如果依賴注入集合型別的物件,Singleton Bean 和 Prototype Bean 均會存在一個於你指定的集合中,
並且該集合裡的Prototype Bean和其他地方依賴注入的Prototype Bean是不一樣的物件。相當於又生成了一個。

結論三:
無論是 Singleton 還是 Prototype Bean 均會執行初始化方法回撥
不過僅 Singleton Bean 會執行銷燬方法回撥,所以prototype bean的銷燬需要客戶端自己控制。

未深入研究的作用域

1.Request作用域:作用域為同一個 Http Request。
2.Session作用域:作用域為同一個 Http Session。
3.Application作用域:作用域為同一個WEB容器,可以看做Web應用中的單例模式。
4.WebSocket作用域:作用域為同一個WebSocket應用。
前後端分離後,不重要,暫不記錄。

自定義Bean作用域

Spring官方文件有介紹自定義作用域,下例建立了一個作用域為當前執行緒的作用域:如果在不同的執行緒中,呼叫同一個spring容器的依賴查詢或者依賴注入某個bean,每個執行緒都會分別建立一個該bean的示例。

import org.springframework.beans.factory.ObjectFactory;
import org.springframework.beans.factory.config.Scope;
import org.springframework.core.NamedThreadLocal;
import org.springframework.lang.NonNull;
import org.springframework.lang.Nullable;

import java.util.HashMap;
import java.util.Map;

/**
 * ThreadLocal 級別 Scope
 */
public class ThreadLocalScope implements Scope {

    public static final String SCOPE_NAME = "thread-local";

    private final NamedThreadLocal<Map<String, Object>> threadLocal = new NamedThreadLocal("thread-local-scope") {

        public Map<String, Object> initialValue() {
            return new HashMap<>();
        }
    };

    @Override
    public Object get(String name, ObjectFactory<?> objectFactory) {

        // 非空
        Map<String, Object> context = getContext();

        Object object = context.get(name);

        if (object == null) {
            object = objectFactory.getObject();
            context.put(name, object);
        }

        return object;
    }

    @NonNull
    private Map<String, Object> getContext() {
        return threadLocal.get();
    }

    @Override
    public Object remove(String name) {
        Map<String, Object> context = getContext();
        return context.remove(name);
    }

    @Override
    public void registerDestructionCallback(String name, Runnable callback) {
        // TODO
    }

    @Override
    public Object resolveContextualObject(String key) {
        Map<String, Object> context = getContext();
        return context.get(key);
    }

    @Override
    public String getConversationId() {
        Thread thread = Thread.currentThread();
        return String.valueOf(thread.getId());
    }
}

Spring官方文件我們學習到要使用一個自定義作用域有2步:
1.建立一個自定義作用域(Creating a Custom Scope),如上面的程式碼示例,其實只是一個create
2.把你的作用域註冊到spring(Using a Custom Scope),讓spring容易知道存在這樣一個新的scope

將作用域註冊到spring容器中,基本的API是:ConfigurableBeanFactory#registerScope

package org.springframework.beans.factory.config;
public interface ConfigurableBeanFactory extends HierarchicalBeanFactory, SingletonBeanRegistry {
	/**
	 * Register the given scope, backed by the given Scope implementation.
	 * @param scopeName the scope identifier
	 * @param scope the backing Scope implementation
	 */
	void registerScope(String scopeName, Scope scope);
}

下例展示瞭如何將作用域註冊到spring容器中:

public class ThreadLocalScopeDemo {

    @Bean
    @Scope(ThreadLocalScope.SCOPE_NAME) //這裡將一個Bean的作用域設定為我們自己的作用域
    public User user() {
        return createUser();
    }

    private static User createUser() {
        User user = new User();
        user.setId(System.nanoTime());
        return user;
    }

    public static void main(String[] args) {

        // 建立 BeanFactory 容器
        AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext();
        // 註冊 Configuration Class(配置類) -> Spring Bean
        applicationContext.register(ThreadLocalScopeDemo.class);

        applicationContext.addBeanFactoryPostProcessor(beanFactory -> {
            // 註冊自定義 scope
            beanFactory.registerScope(ThreadLocalScope.SCOPE_NAME, new ThreadLocalScope());
        });
        // 啟動 Spring 應用上下文
        applicationContext.refresh();
        //依賴查詢
        scopedBeansByLookup(applicationContext);
        // 關閉 Spring 應用上下文
        applicationContext.close();
    }

    private static void scopedBeansByLookup(AnnotationConfigApplicationContext applicationContext) {

        for (int i = 0; i < 3; i++) {
            Thread thread = new Thread(() -> {
                // user在相同執行緒是共享Bean物件,在不同執行緒是不同的物件
                User user = applicationContext.getBean("user", User.class);
                System.out.printf("[Thread id :%d] user = %s%n", Thread.currentThread().getId(), user);
            });

            // 啟動執行緒
            thread.start();
            // 強制執行緒執行完成
            try {
                thread.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

這樣我們就實現了一個例子,把自定義scope的使用說清了,下面我們研究一下這個介面:org.springframework.beans.factory.config.Scope
這可是和BeanDefinition在同一個包中,相當於是SpringIOC的基礎核心包。
這個介面從Spring2.0就出現了,介面定義的方法如下:

public interface Scope {
	Object get(String name, ObjectFactory<?> objectFactory);

	@Nullable
	Object remove(String name);

	void registerDestructionCallback(String name, Runnable callback);

	@Nullable
	Object resolveContextualObject(String key);

	@Nullable
	String getConversationId();
}
方法名 說明
get(String name, ObjectFactory<?> objectFactory) 返回一個屬於當前自定義Scope的物件
remove(String name) 將一個指定name的物件從當前Scope中移除
registerDestructionCallback(String name, Runnable callback) 註冊一個回撥,當某個name的物件在當前Scope被銷燬時執行。
resolveContextualObject(String key) 解析給定鍵的上下文物件(如果有)。例如,HttpServletRequest物件的key:"request" ,不太好理解,但是查詢呼叫的地方,發現是BeanExpressionContext.java在使用,看起來是SpEL表示式(或理解為JSP頁面)使用時,用"request"表示servletRequest
getConversationId() 獲取一個當前scope的會話id,對上例ThreadLocalScope來說,就是當前執行緒的執行緒id

這裡需要注意的是:之前我們在學習依賴查詢時,比如我們呼叫applicationContext.getBean(String name)的時候,內部的程式碼會判斷一個bean是singleton還是prototype,也會和我們上述的Scope介面打交道,singleton與prototype是最最基礎的2個作用域,它們是通過在org.springframework.beans.factory.config.ConfigurableBeanFactory類中定義了2個常量字串來表示,而每一個BeanDefinition中的定義都是包含2個方法:boolean isPrototype();boolean isSingleton();
1.org.springframework.beans.factory.config.ConfigurableBeanFactory#SCOPE_SINGLETON
2.org.springframework.beans.factory.config.ConfigurableBeanFactory#SCOPE_PROTOTYPE

依賴查詢最核心的方法:
AbstractBeanFactory.doGetBean(final String name, final Class requiredType, final Object[] args, boolean typeCheckOnly)

	protected <T> T doGetBean(final String name, @Nullable final Class<T> requiredType,
			@Nullable final Object[] args, boolean typeCheckOnly) throws BeansException {

		final String beanName = transformedBeanName(name);
		Object bean;

		// Eagerly check singleton cache for manually registered singletons.
		Object sharedInstance = getSingleton(beanName);
		if (sharedInstance != null && args == null) {
			//省略...
		}
		else {
			// Fail if we're already creating this bean instance: We're assumably within a circular reference.
			if (isPrototypeCurrentlyInCreation(beanName)) {
				throw new BeanCurrentlyInCreationException(beanName);
			}

			// Check if bean definition exists in this factory.
			BeanFactory parentBeanFactory = getParentBeanFactory();
			if (parentBeanFactory != null && !containsBeanDefinition(beanName)) {
				// 省略。。。
			}

			if (!typeCheckOnly) {
				markBeanAsCreated(beanName);
			}

			try {
				final RootBeanDefinition mbd = getMergedLocalBeanDefinition(beanName);
				checkMergedBeanDefinition(mbd, beanName, args);

				// 省略...

				// 判斷BeanDefinition是不是單例
				if (mbd.isSingleton()) {
					//省略...單例bean的建立方式
				}

				else if (mbd.isPrototype()) {
                                        //判斷BeanDefinition是不是prototype
					//省略...prototype bean的建立方式
				}
				else {
//其他作用域的bean的建立,在這裡和Scope介面打交道的
					String scopeName = mbd.getScope();
					final Scope scope = this.scopes.get(scopeName);
					if (scope == null) {
						throw new IllegalStateException("No Scope registered for scope name '" + scopeName + "'");
					}
					try {
//這裡呼叫了Scope.get()
						Object scopedInstance = scope.get(beanName, () -> {
							beforePrototypeCreation(beanName);
							try {
								return createBean(beanName, mbd, args);
							}
							finally {
								afterPrototypeCreation(beanName);
							}
						});
						bean = getObjectForBeanInstance(scopedInstance, name, beanName, mbd);
					}
					catch (IllegalStateException ex) {
						throw new BeanCreationException(beanName,
								"Scope '" + scopeName + "' is not active for the current thread; consider " +
								"defining a scoped proxy for this bean if you intend to refer to it from a singleton",
								ex);
					}
				}
			}
			catch (BeansException ex) {
				cleanupAfterBeanCreationFailure(beanName);
				throw ex;
			}
		}
		// 省略...
	}

所以這裡的Scope介面的學習都是為了學習自定義Scope的使用,一般業務程式碼的編寫是不會使用到的,但是在我們新的SpringCloud生態圈中就出現了一個自定義Scope:RefreshScope。

SpringCloud中的RefreshScope

(a)RefreshScope簡介
在如下spring模組中引入了一個自定義Scope,叫RefreshScope。基於上面對Scope介面的學習,我們可以認為:@RefreshScope 是scopeName="refresh"的@Scope.

  <groupId>org.springframework.cloud</groupId>
  <artifactId>spring-cloud-context</artifactId>

SpringCloud context中存在2個RefreshScope.class

  1. org.springframework.cloud.context.config.annotation.RefreshScope 這是註解,是從Scope註解派生出來的。
  • @RefreshScope等於@Scope("refresh")
  1. org.springframework.cloud.context.scope.refresh.RefreshScope 這是Scope介面(上面介紹過)的實現類。
public class RefreshScope extends GenericScope implements ApplicationContextAware, ApplicationListener<ContextRefreshedEvent>, Ordered {
...
    public RefreshScope() {
        super.setName("refresh");
    }
...
}

(b)RefreshScope實現類的註冊
GenericScope註冊自己(實現類是RefreshScope)

public class GenericScope implements Scope, BeanFactoryPostProcessor...{
  @Override
  public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory)
      throws BeansException {
      beanFactory.registerScope(this.name, this); //name=refresh this=RefreshScope物件
      ...
  }
}

(c)@RefreshScope註解修飾的Bean
當Spring容器啟動的時候,我們自定義的bean會被註冊到IoC容器中,在該過程中:
org.springframework.context.annotation.AnnotatedBeanDefinitionReader#doRegisterBean方法中,會解析我們自定義Bean物件的Scope註解的屬性等。

	private <T> void doRegisterBean(Class<T> beanClass, @Nullable String name,
			@Nullable Class<? extends Annotation>[] qualifiers, @Nullable Supplier<T> supplier,
			@Nullable BeanDefinitionCustomizer[] customizers) {

		AnnotatedGenericBeanDefinition abd = new AnnotatedGenericBeanDefinition(beanClass);
		if (this.conditionEvaluator.shouldSkip(abd.getMetadata())) {
			return;
		}

		abd.setInstanceSupplier(supplier);
		ScopeMetadata scopeMetadata = this.scopeMetadataResolver.resolveScopeMetadata(abd); //這裡解析Scope註解元資訊
		abd.setScope(scopeMetadata.getScopeName());
		String beanName = (name != null ? name : this.beanNameGenerator.generateBeanName(abd, this.registry));

(d)Scope介面的實現類RefreshScope注入到容器的過程
到此,我們還有一個疑問沒有解決:
RefreshScope實現類並不是一個Bean,並未被容器管理到,它是怎麼被注入到IoC容器中的呢?
答案是:org.springframework.cloud.autoconfigure.RefreshAutoConfiguration
在SpringCloud中,RefreshAutoConfiguration自動裝配的時候,會初始化RefreshScope例項。

@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(RefreshScope.class)
@ConditionalOnProperty(name = RefreshAutoConfiguration.REFRESH_SCOPE_ENABLED, //spring.cloud.refresh.enabled
		matchIfMissing = true)
@AutoConfigureBefore(HibernateJpaAutoConfiguration.class)
public class RefreshAutoConfiguration {
	@Bean
	@ConditionalOnMissingBean(RefreshScope.class)
	public static RefreshScope refreshScope() {
		return new RefreshScope();
	}

配置spring.cloud.refresh.enabled預設值true配置在:
/Users/baitao/.m2/repository/org/springframework/cloud/spring-cloud-commons/2.2.0.RELEASE/spring-cloud-commons-2.2.0.RELEASE.jar!/META-INF/additional-spring-configuration-metadata.json

{
	"properties": [
		{
			"defaultValue": "true",
			"name": "spring.cloud.refresh.enabled",
			"description": "Enables autoconfiguration for the refresh scope and associated features.",
			"type": "java.lang.Boolean"
		}
                ...
        ]
}

總:
1.SpringCloud程式的存在一個自動裝配的類,這個類預設情況下會自動初始化一個RefreshScope例項,該例項是GenericScope的子類,然後註冊到容器中。(RefreshAutoConfiguration.java, )
2.當容器啟動的時候,GenericScope會自己把自己註冊到scope中(ConfigurableBeanFactory#registerScope)(GenericScope)
3.然後當自定義的Bean(被@RefreshScope修飾)註冊的時候,會被容器讀取到其作用域為refresh。(AnnotatedBeanDefinitionReader#doRegisterBean)
通過上面三步,一個帶有@RefreshScope的自定義Bean就被註冊到容器中來,其作用域為refresh。
4.當我們後續進行以來查詢的時候,會繞過Singleton和Prototype分支,進入最後一個分支,通過呼叫Scope介面的get()獲取到該refresh作用域的例項。(AbstractBeanFactory.doGetBean)

TODO

待完善的點
RefreshScope被觸發的場景,工作專案中的RefreshScope測試。
1.RefreshScope被觸發:Spring Boot Actuator Endpoint
2.Web hook(Git hook)
3.RefreshScope被觸發:Spring Cloud Bus