【便利】Beanのスコープについてわかりやすく解説【Spring】

【便利】Beanのスコープについてわかりやすく解説【Spring】
Spring FrameworkのBeanのスコープについてよくわからない人「Spring FrameworkのBeanのスコープの指定方法を知りたいです。また、どういうときにどんなスコープを指定すればいいのか知りたいので教えて下さい。」


そんな方向けになります。

構成は以下です。

著者情報

ちなみにですが、私は5年以上IT系エンジニアとして働いており、主にJavaを主戦場にしています。Webアプリケーションと業務系のアプリケーションの経験を持つごく普通のエンジニアです。

Beanのスコープについてわかりやすく解説

Spring Frameworkの「Bean」とはDIコンテナに登録するコンポーネントのことを言います。
今回はこのBeanのスコープについて以下の順で解説していきます。

  1. Beanのスコープを管理できることのメリット
  2. Beanのスコープの管理方法


順に説明していきます。

【説明①】Beanのスコープを管理できることのメリット

DIコンテナを使用することで得られるメリットはいくつかありますが、その中の一つが「Beanのスコープを管理することができること」になります。

アプリケーション開発経験のある方ならわかるかと思いますが、特定のクラスのインスタンスを管理することは非常に重要なことであり、規模が大きくなればなるほど管理が難しくなる場合があります。
管理しきれないとデグレーションの原因になったりと、不具合のもとに繋がります。

管理しやすいように仕組みを作ることもできるかと思いますが、アプリケーションのメインとなる機能(ロジック)の実装以外で工数を費やすのはあまり効率のいい開発方法ではありません。

なので、アプリケーションのメインとなる機能(ロジック)の実装に集中するためにフレームワークを用いることになります。そして、Spring Frameworkには「Beanのスコープの管理ができるような仕組みを持っている」ので、余計な工数をかけずに開発を進めることができます。

以上がBeanのスコープを管理できることのメリットになります。

【説明②】Beanのスコープの管理方法

それでは実際にBeanのスコープの管理方法について解説していきます。

BeanのスコープとはDIコンテナに登録されたコンポーネント(インスタンス)の「生存期間」のことです。

Spring Frameworkでは3種類のBean定義方法があり、それぞれのスコープの記述方法は以下になります。

  • XMLベースのBean定義の場合
  • JavaベースのBean定義の場合
  • アノテーションベースのBean定義の場合


順に説明します。

【記述方法①】XMLベースのBean定義の場合
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="
        http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd
    <context:component-scan base-package="com.example.spring.demo"/>
    <!-- 
        <bean>要素のscope属性を追加しスコープを指定する
    -->
    <bean id="sample" class="com.example.spring.demo.Sample" scope="(スコープ)" />
</beans>
【記述方法②】JavaベースのBean定義の場合
package com.example.spring.demo;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Scope;
/**
 * @Scopeアノテーションを付与し、スコープを指定する
 * スコープを指定する場合は文字列で指定するか、定数で指定する。
 * 定数の場合はvalue属性で指定する。
 */
@Configuration
@Scope("(スコープ)")
public class AppConfig {
    @Bean
    Sample sample(){
        return new Sample();
    }
}
【記述方法③】アノテーションベースのBean定義の場合
package com.example.spring.demo;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;
/**
 * @Scopeアノテーションを付与し、スコープを指定する
 * スコープを指定する場合は文字列で指定するか、定数で指定する。
 * 定数の場合はvalue属性で指定する。
 */
@Component
@Scope("(スコープ)")
public class Sample {
    public Sample(){
    }
}


また、Spring Frameworkには以下のような管理することのできるスコープを指定できます。

  • singleton
  • prototype
  • session
  • request
  • globalSession
  • application
  • カスタムスコープ


それぞれについての順に説明します。

【スコープ①】singleton

singletonはSpring Frameworkのデフォルトの設定であり、DIコンテナ起動時にインスタンスを生成し、共有して利用するスコープです。

言い換えるならば、ApplicationContext単位のスコープになります。

指定する場合は文字列「singleton」か定数「ConfigurableBeanFactory.SCOPE_SINGLETON」を指定します。

【スコープ②】prototype

Bean取得時に毎回インスタンスを生成するスコープです。スレッドセーフではない場合利用します。

指定する場合は文字列「prototype」か定数「ConfigurableBeanFactory.SCOPE_PROTOTYPE」を指定します。

【スコープ③】session

HTTPセッション単位でBeanインスタンスを生成するスコープです。
また、WEBアプリケーションで使用できます。

指定する場合は文字列「session」か定数「WebApplicationContext.SCOPE_SESSION」を指定します。

【スコープ④】request

HTTPリクエスト単位でBeanインスタンスを生成するスコープです。
また、WEBアプリケーションで使用できます。

指定する場合は文字列「request」か定数「WebApplicationContext.SCOPE_REQUEST」を指定します。

【スコープ⑤】globalSession

指定する場合は文字列「globalSession」を指定します。

ポートレット環境におけるグローバルセッション単位でインスタンスを生成するスコープです。
また、ポートレットに対応したWEBアプリケーションで使用できます。サーブレット環境ではグローバルセッションスコープはセッションスコープに等しいみたいです。

グローバルセッションとは、複数のサーバ間で引き継いで使用できるセッションのことを言います。

ポートレットとは、ウェブポータルで管理・表示される着脱可能なユーザインタフェースコンポーネントのことを言います。つまり、ユーザ自身がカスタマイズできるWebサイトのことだと思われます。(ここはあまり詳しくないので分かる方は教えて下さい)

【スコープ⑥】application

サーブレットのコンテキスト単位でBeanを生成するスコープです。
また、WEBアプリケーションで使用できます。

指定する場合は文字列「application」か定数「WebApplicationContext.SCOPE_APPLICATION」を指定します。

サーブレットのコンテキストとは、アプリケーションサーバ(Tomcat等)で動くアプリケーションと言い換えてもいいかと思います。

singletonスコープとの区別が分かりづらいですが、singletonスコープは前述したとおりApplicationContext単位で、applicationスコープはサープレットコンテキスト単位になります。
言い換えるならば、サーブレットコンテキスト内で複数のApplicationContextを作成する場合があるので、application スコープは複数のApplicationContext内で同一のコンポーネントを使用することになります。

【スコープ⑦】カスタムスコープ

独自に定義したルールでBeanのインスタンスを生成するスコープです。

スコープを独自に定義する方法は以下になります。

  1. Scopeインターフェースの実装クラスを作成
  2. BeanFactoryPostProcessorインターフェースの実装クラスを作成し、作成したカスタムスコープを登録
  3. 作成したカスタムスコープ用のアノテーション作成
  4. (JavaベースのBean定義の場合)2.で作成したBeanFactoryPostProcessorインターフェースの実装クラスをBeanに登録し、カスタムスコープを付与したいBeanに3.で作成したカスタムスコープ用のアノテーションを付与


ソースコードの例を順に示します。

【手順①】Scopeインターフェースの実装クラスを作成
package com.example.spring.demo;

import java.util.Collections;
import java.util.HashMap;
import java.util.Map;

import org.springframework.beans.factory.ObjectFactory;
import org.springframework.beans.factory.config.Scope;

/**
 * Scopeインターフェースの実装クラスを作成
 */
public class CustomScope implements Scope {

    private Map<String, Object> scopedObjects
            = Collections.synchronizedMap(new HashMap<String, Object>());
    
    private Map<String, Runnable> destructionCallbacks
            = Collections.synchronizedMap(new HashMap<String, Runnable>());

    /**
     * 基になるスコープから指定された名前のオブジェクトを返します。
     * Scope の中心的な操作であり、絶対に必要な唯一の操作です。
     */
    @Override
    public Object get(String name, ObjectFactory<?> objectFactory) {
        Object scopeObject = scopedObjects.get(name);
        if(scopeObject == null){
            scopeObject = objectFactory.getObject();
            scopedObjects.put(name, scopeObject);
        }
        return null;
    }

    /**
     * 指定された name を持つオブジェクトを基になるスコープから削除します。
     */
    @Override
    public Object remove(String name) {
        Object scopeObject = scopedObjects.get(name);
        if(scopeObject != null){
            scopedObjects.remove(name);
            return scopeObject;
        } else {
            return null;
        }
    }

    /**
     * スコープ内の指定されたオブジェクトの破棄(またはスコープが個々のオブジェクトを破棄せず、その全体で終了する場合はスコープ全体の破棄)で実行されるコールバックを登録します。
     */
    @Override
    public void registerDestructionCallback(String name, Runnable callback) {
        destructionCallbacks.put(name, callback);
    }

    /**
     * 指定されたキーがある場合、そのコンテキストオブジェクトを解決します。
     */
    @Override
    public Object resolveContextualObject(String key) {
        return null;
    }

    /**
     * 存在する場合、現在の基礎となるスコープの会話 ID を返します。
     */
    @Override
    public String getConversationId() {
        return "custom";
    }

}
【手順②】BeanFactoryPostProcessorインターフェースの実装クラスを作成し、作成したカスタムスコープを登録
package com.example.spring.demo;

import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanFactoryPostProcessor;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;

/**
 * BeanFactoryPostProcessorインターフェースの実装クラスを作成
 */
public class CustomBeanFactoryPostProcessor implements BeanFactoryPostProcessor {
    /**
     * 作成したカスタムスコープを登録
     */
    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
        beanFactory.registerScope("customScope", new CustomScope());
    }
}
【手順③】作成したカスタムスコープ用のアノテーション作成
package com.example.spring.demo;

import org.springframework.context.annotation.Scope;

/**
 * 作成したカスタムスコープ用のアノテーション作成
 */
@Scope("customScope")
public @interface CustomScoped {
}
【手順④】(JavaベースのBean定義の場合)2.で作成したBeanFactoryPostProcessorインターフェースの実装クラスをBeanに登録し、カスタムスコープを付与したいBeanに3.で作成したカスタムスコープ用のアノテーションを付与
package com.example.spring.demo;

import org.springframework.beans.factory.config.BeanFactoryPostProcessor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * @Scopeアノテーションを付与し、スコープを指定する
 */
@Configuration
public class AppConfig {

    /**
     * 作成したカスタムスコープを付与したいBeanにカスタムスコープアノテーションを追加する
     * @return
     */
    @Bean("sample")
    @CustomScoped
    Sample sample(){
        return new Sample();
    }

    /**
     * BeanFactoryPostProcessorインターフェースの実装クラスをBeanに登録
     */
    @Bean
    public static BeanFactoryPostProcessor beanFactoryPostProcessor(){
        return new CustomBeanFactoryPostProcessor();
    }
}

異なるスコープのコンポーネントをインジェクションする方法を解説

異なるスコープのコンポーネントをインジェクションすると、Beanのスコープはインジェクション先のスコープになってしまいます。

なので、異なるスコープとしてインジェクションしたい場合は以下の2つの中でいずれか1つの方法をとります。

  • ルックアップメソッドインジェクションを使用する
  • プロキシモードを有効にする


順に説明します。

【方法①】ルックアップメソッドインジェクションを使用する

ルックアップとはDIコンテナからコンポーネントを取得することを言います。
つまり、以下のようにApplicationContextから直接コンポーネントを取得することです。

上記を踏まえた上で、ルックアップメソッドインジェクションとはスコープの異なるコンポーネントをApplicationContextから取得するメソッドを作成し、そのメソッドを通してコンポーネントの値を取得する方法です。

例えば以下のように記述します。

package com.example.spring.demo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;
@Component("sample2")
@Scope("prototype")
public class Sample2 {
    // ApplicationContextをDI
    @Autowired
    ApplicationContext context;
    public void echo(){
        // ルックアップメソッドから取得
        Sample sample = sample();
        sample.echo();
    }
    /**
     * ルックアップメソッド
     * @return
     */
    Sample sample(){
        // ApplicationContextからコンポーネントをルックアップしている
        return this.context.getBean("sample", Sample.class);
    }
}


また、上記の記述を以下の2つの方法で書き換えることができます。
※JavaベースのBean定義ではルックアップメソッドインジェクションは利用できません。

  • アノテーションベースのBean定義では@Lookupアノテーションを用いる
  • XMLベースのBean定義では<lookup-method>要素を追加する


書き換えたソースコードを順に下記に示します。

アノテーションベースのBean定義では@Lookupアノテーションを用いる
package com.example.spring.demo;
import org.springframework.beans.factory.annotation.Lookup;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;
@Component("sample2")
@Scope("prototype")
public class Sample2 {
    public void echo(){
        // ルックアップメソッドから取得
        Sample sample = sample();
        sample.echo();
    }
    /**
     * ルックアップメソッド
     * @Lookupアノテーションを付与することで、ApplicationContextからコンポーネントをルックアップして返します。
     * また、value属性でbean名を指定できます。
     * @return
     */
    @Lookup(value="sample")
    Sample sample(){
        // 内部でサブクラスが作成され、オーバーライドされるので処理は任意でいい
        return null;
    }
}
XMLベースのBean定義では<lookup-method>要素を追加する
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="
        http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd">
    <bean id="sample" class="com.example.spring.demo.Sample" scope="singleton" />
    <bean id="sample2" class="com.example.spring.demo.Sample2" scope="prototype">
        <!-- 
            <lookup-method>要素を追加することで、ルックアップメソッドインジェクションを利用可能にする
            name属性ではルックアップメソッド名、bean属性ではbean名を指定します。
        -->
        <lookup-method name="sample" bean="sample"/>
    </bean>
</beans>

XMLベースのBean定義でルックアップメソッドインジェクションを上記のように指定した場合のSample2クラスは以下になります。

package com.example.spring.demo;
public class Sample2 {
    public void echo(){
        // ルックアップメソッドから取得
        Sample sample = sample();
        sample.echo();
    }
    /**
     * ルックアップメソッド
     * @return
     */
    Sample sample(){
        // 内部でサブクラスが作成され、オーバーライドされるので処理は任意でいい
        return null;
    }
}

【方法②】プロキシモードを有効にする

次にプロキシモードを有効にする場合について説明します。

プロキシとは「代理」という意味であり、プロキシモードを有効にしたBeanをインジェクションし、インジェクションされたBeanのメソッドを呼ぶと、DIコンテナからルックアップしたBeanのメソッドの処理が呼ばれます。

プロキシモードを有効にする場合は以下2つのプロキシモードを選択可能です。

  • インターフェースベースのプロキシを使用する
  • サブクラスベースのプロキシを使用する。


順に説明します。

【プロキシモード①】インターフェースベースのプロキシを使用する

JDKのプロキシを用いた、インターフェースベースのプロキシを使用する場合を説明します。

JavaベースとアノテーションベースのBean定義の場合

Scopeアノテーションのproxy属性に「ScopedProxyMode.INTERFACES」を指定します。

参考例は以下です。

 
@Scope(value="prototype", proxyMode=ScopedProxyMode.INTERFACES)
 
XMLベースのBean定義の場合

<bean>要素の子要素に<aop:scoped-proxy>要素を追加し、<aop:scoped-proxy>要素の「proxy-target-class」属性にfalseを指定します。

参考例は以下です。

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:aop="http://www.springframework.org/schema/aop"
    xsi:schemaLocation="
        http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/aop
        http://www.springframework.org/schema/aop/spring-aop.xsd">
    <bean id="sample" class="com.example.spring.demo.Sample" scope="singleton">
        <!-- 
            インターフェースベースのプロキシを使用する
        -->
        <aop:scoped-proxy proxy-target-class="false"/>
    </bean>
</beans>


インターフェースのプロキシを使用する場合は、プロキシを使用したいBeanがインターフェースを持っていないと使用できません。インターフェースを持っていない場合は後述のサブクラスベースのプロキシを使用します。

【プロキシモード②】サブクラスベースのプロキシを使用する

Spring Frameworkに内蔵されているCGLIBを用いた、サブクラスベースのプロキシを使用する場合を説明します。

JavaベースとアノテーションベースのBean定義の場合

Scopeアノテーションのproxy属性に「ScopedProxyMode.TARGET_CLASS」を指定します。

参考例は以下です。

 
@Scope(value="prototype", proxyMode=ScopedProxyMode.TARGET_CLASS)
 
XMLベースのBean定義の場合

<bean>要素の子要素に<aop:scoped-proxy>要素を追加し、<aop:scoped-proxy>要素の「proxy-target-class」属性にtrueを指定します。

参考例は以下です。

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:aop="http://www.springframework.org/schema/aop"
    xsi:schemaLocation="
        http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/aop
        http://www.springframework.org/schema/aop/spring-aop.xsd">
    <bean id="sample" class="com.example.spring.demo.Sample" scope="singleton">
        <!-- 
            サブクラスベースのプロキシを使用する
        -->
        <aop:scoped-proxy proxy-target-class="true"/>
    </bean>
</beans>



長くなりましたが、Beanのスコープについての解説は以上になります。
Spring Frameworkアプリケーション開発の参考になれば幸いです。


人気記事①:
【厳選4冊+α】Spring Framework初心者におすすめな本

人気記事②:現役エンジニアがおすすめするプログラミングスクール5社:無料あり