知識庫 / Spring RSS 訂閱

Spring 中的自定義作用域

Spring
HongKong
4
02:37 PM · Dec 06 ,2025

1. 概述

Spring 提供兩種標準 Bean 作用域 (“singleton”“prototype”),可用於任何 Spring 應用,以及三種額外的 Bean 作用域 (“request”, “session”, 和 “globalSession”),僅用於 Web 應用程序。

標準 Bean 作用域不能被覆蓋,並且覆蓋 Web 作用域通常被認為是不好的做法。但是,您可能有一個應用程序需要超出提供的作用域的功能或額外的能力。

例如,如果您正在開發一個多租户系統,您可能希望為每個租户提供一個特定的 Bean 或 Bean 集合的獨立實例。Spring 提供了創建自定義作用域的機制,以應對此類場景。

在本快速教程中,我們將演示 如何創建、註冊和使用自定義作用域在 Spring 應用程序中的方法

2. 創建自定義作用域類

為了創建自定義作用域,我們必須實現 Scope 接口。 在實現過程中,必須確保實現是線程安全的,因為作用域可以同時被多個 BeanFactory 使用。

2.1. 管理範圍對象和回調函數

在實現自定義 Scope 類時,首先要考慮如何存儲和管理範圍對象以及銷燬回調函數。這可以使用映射或專用類等方式來實現。

對於本文,我們將以線程安全的模式使用同步映射來實現。

讓我們開始定義我們的自定義範圍類:

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 方法。正如 JavaDoc 聲明的,如果名稱對象不存在於作用域中,此方法必須創建並返回一個新的對象

在我們的實現中,我們檢查名稱對象是否存在於我們的 map 中。如果存在,則返回它。如果不存在,則使用 ObjectFactory 創建一個新的對象,將其添加到我們的 map 中,然後返回它:

@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. 解決上下文對象

最後,讓我們實現 <em>resolveContextualObject</em> 方法。如果你的作用域支持多個上下文對象,你將會將每個對象與一個鍵值對關聯起來,並返回與提供的 <em>key</em> 參數對應的對象。否則,約定是返回 <em>null</em>

@Override
public Object resolveContextualObject(String key) {
    return null;
}

3. 註冊自定義作用域

為了使 Spring 容器意識到你的新作用域,你需要通過在 ConfigurableBeanFactory 實例的 registerScope 方法中進行註冊。

void registerScope(String scopeName, Scope scope);

第一個參數,scopeName,用於通過其唯一名稱標識/指定一個範圍。第二個參數,scope,是您希望註冊和使用的自定義 Scope 實現的實際實例。

讓我們創建一個自定義 BeanFactoryPostProcessor 並使用 ConfigurableListableBeanFactory 註冊我們的自定義範圍:

public class TenantBeanFactoryPostProcessor implements BeanFactoryPostProcessor {

    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory factory) throws BeansException {
        factory.registerScope("tenant", new TenantScope());
    }
}

現在,讓我們編寫一個 Spring 配置類,加載我們的 BeanFactoryPostProcessor 實現:

@Configuration
public class TenantScopeConfig {

    @Bean
    public static BeanFactoryPostProcessor beanFactoryPostProcessor() {
        return new TenantBeanFactoryPostProcessor();
    }
}

4. 使用自定義作用域

現在我們已經註冊了自定義作用域,我們可以像使用任何其他作用域(除了 singleton 作用域,即默認作用域)的 bean 一樣,將其應用於我們的任何 bean — 通過使用 @Scope 註解並指定自定義作用域的名稱。

讓我們創建一個簡單的 TenantBean 類 — 我們稍後將聲明此類型的 tenant-scoped 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()));
    }
}

請注意,我們沒有為該類使用@Component@Scope類級別的註解。

現在,讓我們在配置類中定義一些租户範圍的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. 測試自定義範圍

讓我們編寫一個測試,通過加載一個 ApplicationContext,註冊我們的 Configuration 類,並檢索租户範圍的 Bean,來測試我們的自定義範圍配置。

@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 Framework Reference 中找到更多關於自定義作用域的信息。 還可以查看 Spring 在 GitHub 上的各種 Scope 類實現。

user avatar
0 位用戶收藏了這個故事!
收藏

發佈 評論

Some HTML is okay.