ぺんぎんらぼ

お笑いとマンガ好きなしょぼしょぼWeb系エンジニアの日記です。たまに絵を描きます。

お笑いとマンガ好きなしょぼしょぼWeb系エンジニアの日記です

よくあるDIの勘違い① - 異なるスコープを持つBeanの親子関係

Spring FrameworkのDIや、Java EECDIの対象となるBeanには「スコープ」が定義されてます。
この「スコープ」の定義によって、Beanのライフサイクルが変わることは広く知られていると思います。

例えば、アプリケーションスコープのBeanは、アプリケーション内で1つだけインスタンスが作成され、そのインスタンスが使いまわされます。 リクエストスコープのBeanは、Webのリクエスト単位で1つのインスタンスが作成され、リクエスト内ではそのインスタンスが使いまわされ、別リクエストであれば別のインスタンスが使われます。

異なるスコープを持つBeanのインジェクトの勘違い

以下のようなリクエストスコープのBeanがあったとします。

@RequestScope
public class ReqScope {
   ・
   ・
   ・
}

このリクエストスコープのBeanをアプリケーションスコープのBeanがインジェクトするとします。

@ApplicationScope
public class AppScope {
    @Autowired
    private ReqScope reqScope;
   ・
   ・
   ・
}

アプリケーションスコープのインスタンスは1つなので、アプリケーションスコープのBeanでインジェクトしたリクエストスコープも1つになると思ってる人もいると思います。
なぜなら、このサンプルソースのAppScopeクラスが持つフィールド「reqScope」も1つだから、アプリケーションスコープ1つに対して、リクエストスコープを複数持てないからです。

次の図のようになると思っている人もいると思います。

しかし、これは勘違いなんです。

異なるスコープを持つBeanのインジェクトの実際

では、実際には各スコープのインスタンスはどのようになるのでしょうか。
おそらく、次の図のようなスコープの構成を期待していると思います。そして、実際に期待通りのスコープの構成となります。

アプリケーションスコープのインジェクトフィールドに複数のリクエストスコープのインスタンスが結びついてます。
配列でもない1つのJavaの変数に複数のオブジェクトは格納できません。しかし、実際にリクエストごとに別のインスタンスが結びつきます。この仕組みについては後程解説します。

まずは、アプリケーションスコープ1つに対して、複数のリクエストスコープが結びついているのかを確認します。

それぞれのスコープのメソッドでオブジェクトIDを出力します。

@ApplicationScope
public class AppScope {
    @Autowired
    private ReqScope reqScope;

    public String getMessage(String request) {
        System.out.println("Start Request " + request + " AppScope Object ID : " + ObjectUtils.getIdentityHexString(this));

        String message = reqScope.getMessage(request);

        System.out.println("End Request " + request + " AppScope Object ID : " + ObjectUtils.getIdentityHexString(this));

        return message;
    }
}
@RequestScope
public class ReqScope {
    public String getMessage(String request) {
        System.out.println("Start Request " + request + " ReqScope Object ID : " + ObjectUtils.getIdentityHexString(this));

        try {
            TimeUnit.SECONDS.sleep(10);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }

        System.out.println("End Request " + request + " ReqScope Object ID : " + ObjectUtils.getIdentityHexString(this));

        return request;
    }
}

ほぼ同じタイミングで2つのリクエストを実行してみます。

Start Request A AppScope Object ID : 7aaa1b39
Start Request A ReqScope Object ID : 468bf1a6
Start Request B AppScope Object ID : 7aaa1b39
Start Request B ReqScope Object ID : 475926a2
End Request A ReqScope Object ID : 468bf1a6
End Request A AppScope Object ID : 7aaa1b39
End Request B ReqScope Object ID : 475926a2
End Request B AppScope Object ID : 7aaa1b39

AppScopeのオブジェクトIDは、すべて7aaa1b39と同じことから、アプリケーションスコープのインスタンスは1つであることがわかります。

それに対して、ReqScopeのオブジェクトIDは、リクエストAでは468bf1a6、リクエストBでは475926a2であることから、リクエストスコープのインスタンスは1リクエストにつき1つであることがわかります。

異なるスコープを持つBeanのインジェクトの仕組み

検証により、アプリケーションスコープのBeanにリクエストスコープのBeanをインジェクトしても、正しいスコープで動作していることがわかりました。
Spring FrameworkのDI、Java EECDIともに、広いスコープのBeanに、より狭いスコープのBeanをインジェクトすることができます。

このようなことを可能とする仕組みとして、インジェクトされるインスタンスはプロキシクラスであるためです。
実際のインスタンスの構成は次のようになります。

アプリケーションスコープのフィールドにインジェクトされるインスタンスは、対象クラスのプロキシになります。プロキシクラスは1つであり、1つしかないアプリケーションスコープのフィールドに格納されます。
そして、リクエストスコープのメソッドが呼び出されると、プロキシクラスのメソッドが呼び出され、そこでリクエストごとの振り分け処理が実行され、リクエストに応じたBeanのメソッドが呼び出されます。