1. 概述
Spring 开箱即用提供了两种标准 bean 作用域(singleton
和 prototype
),适用于任何 Spring 应用。此外还有三种仅适用于 Web 应用的作用域(request
、session
和 globalSession
)。
标准作用域不可覆盖,覆盖 Web 相关作用域也被认为是不良实践。但你的应用可能需要超出内置作用域的特殊能力。
⚠️ 典型场景:开发多租户系统时,需要为每个租户提供特定 bean 的独立实例。Spring 提供了创建自定义作用域的机制满足这类需求。
本文将演示 如何在 Spring 应用中创建、注册和使用自定义作用域。
2. 创建自定义 Scope 类
要实现自定义作用域,必须实现 Scope
接口。关键点:实现必须线程安全,因为作用域可能被多个 bean 工厂同时使用。
2.1 管理作用域对象和回调
实现自定义 Scope
类时,首先要考虑如何存储和管理作用域对象及销毁回调。常见方案包括:
- 使用
Map
存储对象 - 专用管理类存储回调
本文采用线程安全的 synchronizedMap
实现:
public class TenantScope implements Scope {
private Map<String, Object> scopedObjects
= Collections.synchronizedMap(new HashMap<String, Object>());
private Map<String, Runnable> destructionCallbacks
= Collections.synchronizedMap(new HashMap<String, Runnable>());
// ...
}
2.2 从作用域获取对象
实现 getObject
方法从作用域获取对象。核心规则:如果命名对象不存在,必须创建并返回新对象。
@Override
public Object get(String name, ObjectFactory<?> objectFactory) {
if(!scopedObjects.containsKey(name)) {
scopedObjects.put(name, objectFactory.getObject());
}
return scopedObjects.get(name);
}
✅ 关键点:Scope
接口的五个方法中,只有 get
方法必须完整实现,其他四个方法可选,不支持时可抛 UnsupportedOperationException
。
2.3 注册销毁回调
实现 registerDestructionCallback
方法,用于在对象销毁或作用域销毁时执行回调:
@Override
public void registerDestructionCallback(String name, Runnable callback) {
destructionCallbacks.put(name, callback);
}
2.4 从作用域移除对象
实现 remove
方法,移除对象及其销毁回调:
@Override
public Object remove(String name) {
destructionCallbacks.remove(name);
return scopedObjects.remove(name);
}
⚠️ 注意:调用者负责执行回调并销毁对象,该方法仅移除引用。
2.5 获取会话 ID
实现 getConversationId
方法:
- 支持会话概念时返回 ID
- 否则返回
null
@Override
public String getConversationId() {
return "tenant"; // 返回固定值表示租户作用域
}
2.6 解析上下文对象
实现 resolveContextualObject
方法:
- 支持多上下文对象时返回与 key 对应的对象
- 否则返回
null
@Override
public Object resolveContextualObject(String key) {
return null; // 本实现不支持上下文对象
}
3. 注册自定义 Scope
让 Spring 容器识别新作用域,需通过 ConfigurableBeanFactory
的 registerScope
方法注册:
void registerScope(String scopeName, Scope scope);
参数说明:
scopeName
:作用域唯一标识名scope
:自定义Scope
实现实例
创建自定义 BeanFactoryPostProcessor
注册作用域:
public class TenantBeanFactoryPostProcessor implements BeanFactoryPostProcessor {
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory factory) throws BeansException {
factory.registerScope("tenant", new TenantScope());
}
}
通过配置类加载后处理器:
@Configuration
public class TenantScopeConfig {
@Bean
public static BeanFactoryPostProcessor beanFactoryPostProcessor() {
return new TenantBeanFactoryPostProcessor();
}
}
4. 使用自定义 Scope
注册后,像使用其他作用域一样应用自定义作用域——通过 @Scope
注解指定名称。
创建测试 bean 类(注意:未使用类级别注解):
public class TenantBean {
private final String name;
public TenantBean(String name) {
this.name = name;
}
public void sayHello() {
System.out.println(
String.format("Hello from %s of type %s",
this.name,
this.getClass().getName()));
}
}
在配置类中定义租户作用域 bean:
@Configuration
public class TenantBeansConfig {
@Scope(scopeName = "tenant")
@Bean
public TenantBean foo() {
return new TenantBean("foo");
}
@Scope(scopeName = "tenant")
@Bean
public TenantBean bar() {
return new TenantBean("bar");
}
}
5. 测试自定义 Scope
编写测试验证作用域行为:
@Test
public final void whenRegisterScopeAndBeans_thenContextContainsFooAndBar() {
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
try{
ctx.register(TenantScopeConfig.class);
ctx.register(TenantBeansConfig.class);
ctx.refresh();
TenantBean foo = (TenantBean) ctx.getBean("foo", TenantBean.class);
foo.sayHello();
TenantBean bar = (TenantBean) ctx.getBean("bar", TenantBean.class);
bar.sayHello();
Map<String, TenantBean> foos = ctx.getBeansOfType(TenantBean.class);
assertThat(foo, not(equalTo(bar)));
assertThat(foos.size(), equalTo(2));
assertTrue(foos.containsValue(foo));
assertTrue(foos.containsValue(bar));
BeanDefinition fooDefinition = ctx.getBeanDefinition("foo");
BeanDefinition barDefinition = ctx.getBeanDefinition("bar");
assertThat(fooDefinition.getScope(), equalTo("tenant"));
assertThat(barDefinition.getScope(), equalTo("tenant"));
}
finally {
ctx.close();
}
}
测试输出:
Hello from foo of type org.baeldung.customscope.TenantBean
Hello from bar of type org.baeldung.customscope.TenantBean
6. 总结
本文完整演示了在 Spring 中:
- 定义自定义作用域类
- 注册到 Spring 容器
- 应用到具体 bean
- 验证作用域行为
📚 延伸阅读:
完整代码示例见 GitHub 项目。