【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