1. 概述

Spring 开箱即用提供了两种标准 bean 作用域(singletonprototype),适用于任何 Spring 应用。此外还有三种仅适用于 Web 应用的作用域(requestsessionglobalSession)。

标准作用域不可覆盖,覆盖 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 容器识别新作用域,需通过 ConfigurableBeanFactoryregisterScope 方法注册:

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 中:

  1. 定义自定义作用域类
  2. 注册到 Spring 容器
  3. 应用到具体 bean
  4. 验证作用域行为

📚 延伸阅读

完整代码示例见 GitHub 项目


原始标题:Custom Scope in Spring