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.TenantBean6. 結論
在本快速教程中,我們演示瞭如何在 Spring 中定義、註冊和使用自定義作用域。
您可以在 Spring Framework Reference 中找到更多關於自定義作用域的信息。 還可以查看 Spring 在 GitHub 上的各種 Scope 類實現。