自定义Scope
Bean 的生命周期
所谓生命周期,即是Bean何时创建,何时生存,何时销毁。也即Bean 存在的范围。更直白点儿就是Bean的作用范围,有点儿变量的意味。
Spring 内置了singleton、prototype两种Scope,Bean 默认为singleton,在Spring IOC 容器中,只会创建一个,并将其缓存起来。
prototype作用域部署的bean,每一次请求(将其注入到另一个bean中,或者以程序的方式调用容器的getBean()方法)都会产生一个新的bean实例,相当与一个new的操作。对于prototype作用域的bean,有一点非常重要,那就是Spring不能对一个prototype bean的整个生命周期负责,容器在初始化、配置、装饰或者是装配完一个prototype实例后,将它交给客户端,随后就对该prototype实例不闻不问了。不管何种作用域,容器都会调用所有对象的初始化生命周期回调方法,而对prototype而言,任何配置好的析构回调方法都将不会被调用(destory-method不会被调用),因为在注册为DisposableBean时将prototype排除在外。 清除prototype作用域的对象并释放任何prototype bean所持有的昂贵资源,都是客户端代码的职责。让Spring 容器释放被singleton作用域 bean 占用资源的一种可行方式是,通过使用 bean的后置处理器,该处理器持有要被清除的bean的引用。
针对Web环境,Spring又增加了session、request、global session三种专用于Web应用程序上下文的Scope。
Scope接口
有时候我们还需要特殊的作用域,这时我们就可以实现该接口来达到我们的目的。上面提到的web环境中的三种也是基于此接口来实现的。
此接口是ConfigurableBeanFactory使用的策略接口,表示用于保存bean实例的作用范围。可以用来扩展BeanFactory的标准作用域singleton和prototype实现自定义作用域。
虽然该 SPI 接口主要用于扩展web环境中的Bean 作用范围,它也是完全通用的:提供了从任何底层存储机制(如HTTP会话或自定义会话机制)获取和设置对象的能力。传递到该类的get和remove方法中的名称将标识当前作用域中的目标对象。
Scope实现类应该是线程安全的。如果需要的话,一个Scope实例可以供多个bean工厂同时使用(除非显式地希望知道所包含的bean工厂),并且任意数量的线程可以从任意数量的工厂并发地访问该作用域。
方法
- Object get(String name, ObjectFactory<?> objectFactory):从此 - Scope返回具有给定名称的对象,如果没有找到,则通过- ObjectFactory#getObject()创建。- 这是 - Scope的核心操作,也是惟一绝对必需的操作。
- Object remove(String name):从 - Scope中删除该名称的对象。如果没有找到对象,则返回- null;否则返回已删除的对象。- 注意:实现类还应该删除指定对象已注册的销毁回调(如果有的话)。实际执行回调并销毁移除的对象是调用者的责任。 - 注意:这是一个可选操作。如果实现类不支持显式删除对象,则可能引发 - UnsupportedOperationException。
- void registerDestructionCallback(String name, Runnable callback):注册一个回调函数,在范围内指定对象被销毁时执行(或者在整个范围被销毁时,如果该范围没有销毁单个对象,而只是全部终止)。 - 注意:这是一个可选操作。此方法将仅对具有实际销毁配置的作用域bean调用(dispose - bean、destroy-method、DestructionAwareBeanPostProcessor)。实现类应该尽量在适当的时候执行给定的回调。如果底层运行时环境根本不支持这样的回调,则必须忽略回调并记录相应的警告。 - 请注意,“销毁”指的是作为 - Scope自身生命周期的一部分的对象的自动销毁,而不是指应用程序显式删除的单个作用域对象。如果一个作用域对象通过- remove(String)方法被删除,那么任何已注册的销毁回调也应该被删除,假设被删除的对象将被重用或手动销毁。
- Object resolveContextualObject(String key):解析给定键的上下文对象(如果有的话)。如果此Scope支持多个上下文对象,则将每个对象与一个键值相关联,并返回与提供的键参数相对应的对象。否则,约定将返回null。例如: - request对应的- HttpServletRequest对象。- 此方法提供了通过key来获取对象的功能。在Spring中有一个应用的地方,在自定义Scope的bean中通过 - @Value注解注入属性时,会通过此方法解析出对应的属性。
- String getConversationId():返回当前底层范围的会话ID(如果有的话)。 - 会话ID的确切含义取决于底层存储机制。对于 - session范围的对象,会话ID通常等于(或源自)session ID。- 注意:这不是必须的。如果底层存储机制没有明显的ID时,则可以在此方法的实现中返回 - null。
自定义Scope
在AbstractBeanFactory的doGetBean方法中,会判断是否是自定义Scope,并调用get方法。在我们的自定义Scope的get方法中,需要根据我们的场景来返回bean。比如我们要实现线程级别共享的bean,则需要判断当前线程是否存在,不存在就调用ObjectFactory的getObject方法创建bean,否则就返回存在的对象。ObjectFactory负责去创建bean,这个创建的过程跟其他的Scope一致,Scope要做的就是控制何时创建就OK了。
同时,我们还必须确保实现是线程安全的,因为Scope可以同时由多个bean工厂使用。
| 1 | String scopeName = mbd.getScope(); | 
下面就以线程级别共享的Bean来创建自定义ThreadScope。
自定义Scope类:ThreadScope
实现Scope接口
实现自定义Scope,我们需要实现Scope接口。
| 1 | public class ThreadScope implements Scope { | 
管理Scope中的对象和回调
实现自定义Scope类时要考虑的第一件事是如何存储和管理作用域对象和销毁回调。
在此例中,使用ThreadLocal来保存对象。
| 1 | /** | 
实现get方法
当Spring 容器遇到我们定义的Scope时,会从Scope中获取bean。因此我们需要实现get方法,当要获取的对象不在当前Scope中时,我们需要创建该对象并返回。
在这个例子中,则是判断当前线程中是否有该对象。
| 1 | Map<String, Object> map = Optional.ofNullable(objectThreadLocal.get()).orElse(new HashMap<>()); | 
在Scope接口定义的五个方法中,仅get方法才需要具有所描述行为的完整实现。其他四个方法是可选的,如果它们不需要或不支持功能,则可能引发UnsupportedOperationException。
注册销毁回调
我们还需要registerDestructionCallback方法。此方法提供了一个回调,当命名对象被销毁或者Scope本身被销毁时执行此回调。
| 1 | public void registerDestructionCallback(String name, Runnable callback) { | 
从Scope中删除对象
接下来实现remove方法,该方法从Scope中删除命名对象,并删除其注册的销毁回调,并返回删除的对象:
| 1 | Map<String, Object> map = objectThreadLocal.get(); | 
请注意,Spring 并不会帮我们调用remove和执行回调方法,实际执行回调并销毁移除的对象是调用者的责任,因为Spring也不知道何时该remove掉对象。
获取会话ID
现在实现getConversationId方法。如果Scope支持会话ID的概念,则可以在此处将其返回。否则,约定将返回null:
此例中不需要会话ID,直接返回null。
解析上下文对象
最后实现resolveContextualObject方法。如果范围支持多个上下文对象,则将每个对象与一个键值相关联,并返回与提供的键相对应的对象。否则,约定将返回null。此例中不需要。
注册自定义Scope
为了使Spring容器知道这个新作用域,可以通过ConfigurableBeanFactory实例上的registerScope方法对其进行注册。让我们看一下该方法的定义:
| 1 | void registerScope(String scopeName, Scope scope); | 
第一个参数scopeName用于指定唯一标识,第二个参数scope指定具体的实例。
要拿到ConfigurableBeanFactory我们可以实现BeanFactoryPostProcessor:
| 1 | public class ThreadScopeBeanFactoryPostProcessor implements BeanFactoryPostProcessor { | 
将此ThreadScopeBeanFactoryPostProcessor 注册到Spring容器中:
| 1 | context.addBeanFactoryPostProcessor(new ThreadScopeBeanFactoryPostProcessor()); | 
Spring 为我们提供了一个更方便的类:org.springframework.beans.factory.config.CustomScopeConfigurer。
该类也实现了BeanFactoryPostProcessor接口,增加了两个方法:setScopes(Map<String, Object> scopes)和addScope(String scopeName, Scope scope),可以更方便注册Scope。
同样也通过context.addBeanFactoryPostProcessor()方法将CustomScopeConfigurer注册到容器中。
这里手动注册的原因是可以更清楚的知道是如何使用这个BeanFactoryPostProcessor,在平时使用的过程中,只需要在xml中配置,或者使用注解@Bean配置即可。
使用自定义Scope
现在已经注册了自定义Scope,可以将其应用于我们的任何bean,通过使用`@Scope注解并指定我们的自定义Scope名称。
定义Bean
让我们创建一个简单的ScopeBean类,稍后我们将声明这种类型的ThreadScope的bean
| 1 | public class ScopeBean { | 
请注意,我们没有在此类上使用类级别的@Component和@Scope批注。
注册Bean
注册Bean有很多种方式:xml,@Bean注解。这里展示另一种方式:实现BeanDefinitionRegistryPostProcessor接口。该接口继承自BeanFactoryPostProcessor,在该接口的基础上添加了postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry)方法,用来注册Bean。当你不想写配置文件或者配置类的时候,用编程的方式来注册Bean,也不愧是一种有效的方式。
从这里我们也看到BeanFactoryPostProcessor接口是我们想要扩展Spring时的突破口。
| 1 | public class ScopeBeanDefinitionRegistryPostProcessor implements BeanDefinitionRegistryPostProcessor { | 
在上面的代码中手动设置Scope为ThreadScope。
测试自定义Scope
让我们编写一个测试类,通过加载ApplicationContext,并检索我们的ThreadScope的bean :
| 1 | ConfigurableApplicationContext context = new ClassPathXmlApplicationContext(); | 
分别打印在同一个线程和不同线程中获取的Bean:
| 1 | 同一线程:main:cn.sexycode.spring.study.chapter4.ScopeBean@e70f13a | 
从结果中可以看出,在同一线程中获取的Bean 是同一个,不同线程中获取的Bean不同。
结论
在本文中,我们了解了Scope是什么,以及如何在Spring中定义,注册和使用自定义Scope。
