在 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
实例,因为注册上述示例的 BeanDefinition
时会基于它们的注解元数据判断是否需要代理,具体细节可见下文:【源码】Spring —— BeanDefinition 解读2
关于为什么 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
注解,基于此机制也可以达到我们预期的目的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
方法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
管理作用域实例,除非手动清除或者内存溢出,否则这类似于单例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
管理作用域实例,即每个实例以线程为生命周期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
就基于此实现Spring IoC Container
,让它可以基于此 Scope
来创建对于作用域的实例ConfigurableBeanFactory#registerScope
,它在 AbstractBeanFactory
有实现:基于一个 Map
管理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
及其注册