【Spring】关于 Spring 中 bean 实例的作用域 Scope(单例中注入原型?)
本文已参与「新人创作礼」活动,一起开启掘金创作之路。
前言
在 Spring 中的使用过程中,Scope 作用域的概念一定程度上是透明、弱化的,因为绝大多数的场景我们都是默认使用 Singleton 即单例的,它拥有最 “长” 的生命周期,几乎伴随容器的全生命周期,但其他生命周期比如 Spring 内置的 Prototype 原型和 Spring MVC 拓展的 Request Session 也是不容忽视的
本章节两个方面入手,简单聊一下 Scope 的部分内容:
- 不同作用域实例间的互相依赖
- 自定义
Scope
这部分内容在 BeanFactory 和 Spring AOP 等内容的铺垫下更容易理解,可以移步对应专栏了解
不同作用域实例间的互相依赖
如果我们在 Singleton 实例中注入另一个 Singleton 实例,不难理解没有任何问题,但是如果在不同作用域实例间互相依赖,比如: Singleton 实例中注入另一个其他作用域实例如 Prototype,这在不做额外处理的情况下可能达不到我们预期的语义
因为单例的创建一般只有一次,这包括它的属性填充,依此其属性尽管是
一个 Prototype 实例,但对该实例的方法调用也永远作用于这一个实
例上,这可能达不到预期的目的
比如这段代码:
@Configuration
public class ScopeProxyDemo {
@Component
@Scope(
scopeName = ConfigurableBeanFactory.SCOPE_PROTOTYPE
)
public static class PortoTypeBean {
private double random = Math.random();
public void m() {
System.out.println(random);
}
}
@Component
public static class Main {
@Autowired
PortoTypeBean portoTypeBean;
public void portoTypeMethod() {
portoTypeBean.m();
}
}
@Test
public void test() {
AnnotationConfigApplicationContext context
= new AnnotationConfigApplicationContext(ScopeProxyDemo.class);
Main bean = context.getBean(Main.class);
// 此时尽管 bean 为 原型 实例,但每次调用方法都指向同一个实例
bean.portoTypeMethod();
bean.portoTypeMethod();
}
}
在单例 Main 中注入原型实例 PortoTypeBean,如果期望对 PortoTypeBean 的调用每次都作用于一个新的实例(符合原型实例的语义),则当前的类是做不到的
于是,我们有两种模式来解决这个问题
- 基于代理处理
- 基于
Method Lookup机制处理
基于代理处理
只需要给上述示例中 Prototype 实例的 Scope 注解指定 proxyMode 属性即可,如下:
@Configuration
public class ScopeProxyDemo {
@Component
@Scope(
scopeName = ConfigurableBeanFactory.SCOPE_PROTOTYPE
, proxyMode = ScopedProxyMode.TARGET_CLASS
)
public static class PortoTypeBean {
private double random = Math.random();
public void m() {
System.out.println(random);
}
}
@Component
public static class Main {
@Autowired
PortoTypeBean portoTypeBean;
public void portoTypeMethod() {
portoTypeBean.m();
}
}
@Test
public void test() {
AnnotationConfigApplicationContext context
= new AnnotationConfigApplicationContext(ScopeProxyDemo.class);
Main bean = context.getBean(Main.class);
// 此时将达到预期的语义
bean.portoTypeMethod();
bean.portoTypeMethod();
}
}
- 我们为其指定了属性
proxyMode = ScopedProxyMode.TARGET_CLASS,默认情况下该属性值为ScopedProxyMode.DEFAULT,不会对容器中获取的对应实例进行代理 - 指定
ScopedProxyMode.INTERFACES或ScopedProxyMode.TARGET_CLASS时,创建的是一个ScopedProxyFactoryBean实例,则对该代理对象的方法调用会指向对应的域对象(比如PROTOTYPE作用域下就是一个全新的实例)
关于为什么 ScopedProxyFactoryBean 代理的实例可以拥有上述能
力,这里面涉及到 Spring AOP Introduction 相关的内容,不做
深入,有兴趣可以从 DelegatingIntroductionInterceptor 入手
基于 Method Lookup 机制处理
这是 Spring 提供的一种机制,仅给出示例代码,不做深入了解
@Configuration
public class ScopeLookupDemo {
@Component
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public static class B {
public B() {
System.out.println("init ...");
}
}
@Component
public static class A {
@Lookup
public B b() {
return null;
}
}
@Test
public void test() {
AnnotationConfigApplicationContext context
= new AnnotationConfigApplicationContext(ScopeLookupDemo.class);
A bean = context.getBean(A.class);
System.out.println(bean.b());
System.out.println(bean.b());
}
}
- 关键就在于
@Lookup注解,基于此机制也可以达到我们预期的目的 - 该方法明显没有前者来的好用,所以仅供参考
自定义 Scope
Scope
public interface Scope {
// 核心方法,当前作用域找不到时会基于 ObjectFactory#getObject 方法获取
// 这个 ObjectFactory#getObject 在容器中会帮我们调用 AbstractBeanFactory#createBean
Object get(String name, ObjectFactory<?> objectFactory);
/**
* 从当前作用域移除,同时移除注册的销毁回调(但不执行)
*/
@Nullable
Object remove(String name);
// 销毁回调注册
void registerDestructionCallback(String name, Runnable callback);
@Nullable
Object resolveContextualObject(String key);
// 会话 id,通常为 null
@Nullable
String getConversationId();
}
- 顶层
Scope接口,我们要自定义Scope首先要提供对应的实现类 - 其中核心方法是
get,它的主要逻辑是先从当前作用域中找,找不到就依赖于ObjectFactory#getObject - 在容器中,
ObjectFactory#getObject自然是委托到AbstractBeanFactory#createBean方法来创建新的实例,具体可参考AbstractBeanFactory#doGetBean方法
SimpleMapScope
public class SimpleMapScope implements Scope, Serializable {
// 基于 Map 管理,这个模式就类似于单例了,除非手动清除该 map
private final Map<String, Object> map = new HashMap<>();
// ...
// 先从 map 获取,否则 objectFactory.getObject()
@Override
public Object get(String name, ObjectFactory<?> objectFactory) {
synchronized (this.map) {
Object scopedObject = this.map.get(name);
if (scopedObject == null) {
scopedObject = objectFactory.getObject();
this.map.put(name, scopedObject);
}
return scopedObject;
}
}
// ...
}
Spring提供的内置实现SimpleMapScope- 它基于
Map管理作用域实例,除非手动清除或者内存溢出,否则这类似于单例
SimpleThreadScope
public class SimpleThreadScope implements Scope {
// 基于 NamedThreadLocal 管理,即以线程为作用域
private final ThreadLocal<Map<String, Object>> threadScope =
new NamedThreadLocal<Map<String, Object>>("SimpleThreadScope") {
@Override
protected Map<String, Object> initialValue() {
return new HashMap<>();
}
};
// ...
}
Spring提供的内置实现SimpleThreadScope- 它基于
NamedThreadLocal管理作用域实例,即每个实例以线程为生命周期
AbstractRequestAttributesScope
public abstract class AbstractRequestAttributesScope implements Scope {
@Override
public Object get(String name, ObjectFactory<?> objectFactory) {
// 从 RequestAttributes 中获取,获取不到则 objectFactory.getObject()
RequestAttributes attributes = RequestContextHolder.currentRequestAttributes();
Object scopedObject = attributes.getAttribute(name, getScope());
if (scopedObject == null) {
scopedObject = objectFactory.getObject();
attributes.setAttribute(name, scopedObject, getScope());
Object retrievedObject = attributes.getAttribute(name, getScope());
if (retrievedObject != null) {
scopedObject = retrievedObject;
}
}
return scopedObject;
}
// ...
}
Spring web提供的抽象基类AbstractRequestAttributesScope- 它基于
RequestAttributes管理作用域实例,这里就不深入解读,可以理解为:对于Request它就是Request#getAttribute,对于Session它就是Session#getAttribute RequestScope和SessionScope就基于此实现
自定义 Scope 的注册
- 这里的注册值得自然是注册到
Spring IoC Container,让它可以基于此Scope来创建对于作用域的实例 - 注册的方法为
ConfigurableBeanFactory#registerScope,它在AbstractBeanFactory有实现:基于一个Map管理
demo
public class ScopeRegisterDemo {
// @Scope 可作为 元注解 使用
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Scope("thread")
public @interface ThreadScope {
@AliasFor(annotation = Scope.class, attribute = "proxyMode")
ScopedProxyMode proxyMode() default ScopedProxyMode.DEFAULT;
}
public static class ThreadBean {
public ThreadBean() {
System.out.println("ThreadBean init ...");
}
public void m() {}
}
public static class Main {
ThreadBean threadBean;
public Main(ThreadBean threadBean) {
this.threadBean = threadBean;
}
// 由不同线程调用,则会创建新的作用域实例
public void m() {
new Thread(threadBean::m).start();
}
}
@Configuration
public static class Config {
@Bean
// 代理以注入单例实现预期语义
@ThreadScope(proxyMode = ScopedProxyMode.TARGET_CLASS)
public ThreadBean threadBean() {
return new ThreadBean();
}
@Bean
public Main main() {
return new Main(threadBean());
}
}
@Test
public void test() {
AnnotationConfigApplicationContext context
= new AnnotationConfigApplicationContext();
// ConfigurableBeanFactory#registerScope 注册作用域
ConfigurableBeanFactory beanFactory = context.getBeanFactory();
beanFactory.registerScope("thread", new SimpleThreadScope());
context.register(Config.class);
// 先注册在手动 refresh
context.refresh();
// 达到预期语义
Main bean = context.getBean(Main.class);
bean.m();
bean.m();
}
}
这段 demo 元素较多:
@Scope注解可作为元注解,因此我们可以以示例中的模式定义自己的作用域注解,集合@AliasFor注解暴露属性- 单例中注入其他作用域实例可以代理以符合预期语义
- 使用
ConfigurableBeanFactory#registerScope方法注册对应的SimpleThreadScope,它会为每一个不同的线程创建对应的实例 - 测试结果符合预期语义
总结
本文基于部分源码及大量示例,讨论了:
Singleton实例下注入其他作用域实例并保持语义的两种方式- 自定义
Scope及其注册