1. 概要

箱から出して、Springは任意のSpringアプリケーションで使用できる2つの標準Beanスコープ(「シングルトン」および「プロトタイプ」)と3つの追加のBeanスコープ()を提供します。 「request」「session」、および「globalSession」)は、Web対応アプリケーションでのみ使用します。

標準のBeanスコープをオーバーライドすることはできません。一般に、Web対応スコープをオーバーライドすることは悪い習慣と見なされています。 ただし、提供されているスコープにある機能とは異なる機能または追加の機能を必要とするアプリケーションがある場合があります。

たとえば、マルチテナントシステムを開発している場合は、テナントごとに特定のBeanまたはBeanのセットの個別のインスタンスを提供することができます。 Springは、このようなシナリオのカスタムスコープを作成するためのメカニズムを提供します。

このクイックチュートリアルでは、 Springアプリケーションでカスタムスコープを作成、登録、および使用する方法を示します。

2. カスタムスコープクラスの作成

カスタムスコープを作成するには、スコープインターフェイスを実装する必要があります。 その際、スコープは複数のBeanファクトリで同時に使用できるため、実装がスレッドセーフであることを確認する必要があります

2.1. スコープオブジェクトとコールバックの管理

カスタムScopeクラスを実装するときに最初に考慮すべきことの1つは、スコープオブジェクトと破棄コールバックを格納および管理する方法です。 これは、たとえば、マップまたは専用クラスを使用して実行できます。

この記事では、同期マップを使用してスレッドセーフな方法でこれを行います。

カスタムスコープクラスの定義を始めましょう。

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が述べているように、指定されたオブジェクトがスコープに存在しない場合、このメソッドは新しいオブジェクトを作成して返す必要があります。

この実装では、名前付きオブジェクトがマップにあるかどうかを確認します。 そうである場合はそれを返し、そうでない場合は ObjectFactory を使用して新しいオブジェクトを作成し、マップに追加して返します。

@Override
public Object get(String name, ObjectFactory<?> objectFactory) {
    if(!scopedObjects.containsKey(name)) {
        scopedObjects.put(name, objectFactory.getObject());
    }
    return scopedObjects.get(name);
}

Scope インターフェイスで定義された5つのメソッドのうち、 getメソッドのみが、説明されている動作の完全な実装を持つために必要です。 他の4つのメソッドはオプションであり、機能をサポートする必要がない、またはサポートできない場合は、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. カスタムスコープの登録

Springコンテナに新しいスコープを認識させるには、ConfigurableBeanFactoryインスタンスのregisterScopeメソッドを介して登録する必要があります。 このメソッドの定義を見てみましょう。

void registerScope(String scopeName, Scope scope);

最初のパラメーターscopeNameは、スコープを一意の名前で識別/指定するために使用されます。 2番目のパラメーターscopeは、登録して使用するカスタムScope実装の実際のインスタンスです。

カスタムBeanFactoryPostProcessorを作成し、ConfigurableListableBeanFactoryを使用してカスタムスコープを登録しましょう。

public class TenantBeanFactoryPostProcessor implements BeanFactoryPostProcessor {

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

それでは、BeanFactoryPostProcessor実装をロードするSpring構成クラスを作成しましょう。

@Configuration
public class TenantScopeConfig {

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

4. カスタムスコープの使用

カスタムスコープを登録したので、シングルトン(デフォルトのスコープ)以外のスコープを使用する他のBeanと同じように、を使用して任意のBeanに適用できます。 ]@Scopeアノテーションとカスタムスコープを名前で指定します。

単純なTenantBeanクラスを作成しましょう—このタイプのテナントスコープの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 FrameworkReferenceを参照してください。 また、GitHubSpringFrameworkリポジトリにあるさまざまなScopeクラスのSpringの実装を確認することもできます。

いつものように、この記事で使用されているコードサンプルはGitHubプロジェクトにあります。