【Jakarta EE】ApplicationScopedのインスタンスにRequestScopedのクラスをインジェクトしたらどうなる?
はじめに
自己紹介と検証に至る経緯
私はモバイルアプリ~DBまでの開発を行っており、バックエンドはJakarta EEで作成しております。
過去Spring Bootで開発してきた経験からDI(Dependency Injection)は、なんとなく使いこなせていると思っていました。
今回、開発にあたりまじまじと考えたらDIの仕組みをあまり理解できていないことを認識したので、検証してみることにしました。
そもそもCDIとは
Jakarta EEではCDI(Contexts and Dependency Injection)という仕様が定義されています。
Red Hatによると
Jakarta Contexts and Dependency Injection には以下の利点があります。
- 多くのコードをアノテーションに置き換えることにより、コードベースが単純化および削減されます。
- 柔軟であり、インジェクションおよびイベントを無効または有効にしたり、代替の Bean を使用したり、非 Contexts and Dependency Injection オブジェクトを簡単にインジェクトしたりできます。
- デフォルトとは異なる設定をカスタマイズする必要がある場合、任意で beans.xml ファイルを META-INF/ または WEB-INF/ ディレクトリーに含めることができます。ファイルは空にすることができます。
- パッケージ化とデプロイメントが簡略化され、デプロイメントに追加する必要がある XML の量が減少します。
- コンテキストを使用したライフサイクル管理が提供されます。インジェクションを要求、セッション、会話、またはカスタムコンテキストに割り当てることができます。
- 文字列ベースのインジェクションよりも安全かつ簡単にデバッグを行える、タイプセーフな依存関係の注入が提供されます。
- インターセプターと Bean が切り離されます。
- 複雑なイベント通知が提供されます。[1]
非常に難しい。。。
ざっくり伝えたい情報だけ抽出すると、
- アノテーションで印をつけたオブジェクトのライフサイクル(生成〜破棄)をフレームワークで管理してくれる。
- あるオブジェクトから別のオブジェクトを参照する際に、直接ソースコード上にインスタンス化の処理を書かなくてもよくなり、柔軟に参照するクラスを切り替えることができる。
以下のサイトの説明がわかりやすいのでここを読んでもらえるとざっくりわかります。
気になったこと
@ApplicationScopedのインスタンスに @RequestScoped のインスタンスをDIした際に、本当にRequest単位でインスタンスが切り替わるのか?
CDIの説明を見たり、過去Spring Bootで開発していた経験から流石に切り替わらないことはないよなぁと思いつつ、自分の理解度ではもしかしたら切り替わらないのかも...?と思ってしまいました。
疑問が生まれた思考プロセスは以下です。
- @ApplicationScopedのオブジェクトは、アプリ起動時に生成される。
その際、必要な依存性は注入される。 - なので@ApplicationScopedのオブジェクトが@RequestScopedのオブジェクトを参照するような実装にしていると、アプリ起動時に依存性が注入される。
- @ApplicationScopedのオブジェクトは基本的にアプリ起動時に生成され、再生成されることはないので、@RequestScopedのオブジェクトも起動時に生成されたものが使われ続けてしまうのでは...?
検証
結論
先に結論から。
@ApplicationScopedのオブジェクトから参照されていたとしても @RequestScopedのオブジェクトはリクエスト単位に切り替わる
(さすがにそうだよね。よかったよかった。)
以下、検証の流れです。
検証環境
バージョンが古いのですが、手元に検証できる環境があった以下のバージョンで試しました。
- OpenJDK :
1.8.0_372 - WildFly :
23.0.0
まずは調べる
同じようなことを調べている人がいないかググってみました。
Java EE 7で同じようなことを調査している人の記事を発見!
この記事を確認する限りは問題なしと読み取れました。
とはいえ自分が使っているのはJakarta EEのバージョンとは異なるし、人の記事を見て問題ないと納得しきることができなかったため、念のため検証を始めました。
検証方法はかなり参考にさせていただきました。
準備
注意事項:以降、掲載したソースコードなどはサンプルになります。本ソースコードを使用することで発生するいかなる損害や不利益について、当社は一切の責任を負いませんので自己の責任においてご利用ください。
- RequestScopedのクラスを作成
@RequestScoped
public class RequestBean {
}
- ApplicationScopedのクラスを作成
- RequestScopedのクラスをインジェクト
- 以下のハッシュコードを出力するメソッドを作成
- ①ApplicationScopedのオブジェクトのハッシュコード
- ②ApplicationScopedのオブジェクトが参照しているRequestScopedのオブジェクトのハッシュコード
@ApplicationScoped
public class ApplicationBean {
@Inject
private RequestBean requestBean;
public void printHashCodes() {
System.out.println("***@ApplicationScopedのクラスのハッシュコード***");
System.out.println("this:" + this.hashCode());
System.out.println("***@RequestScopedのハッシュコード***");
System.out.println("requestBean:" + requestBean.hashCode());
}
}
- 適当なコントローラーでインジェクトして適当なメソッドで
applicationBean.printHashCodes()を呼び出す
@Controller
@Path("/appBean")
public class SampleController {
private ApplicationBean appBean;
public SampleController() {
}
@Inject
public SampleController(ApplicationBean appBean) {
this.appBean = appBean;
}
@GET
public void appBean() {
appBean.printHashCodes();
}
}
いざ検証!
一回目の結果
[stdout] ***@ApplicationScopedのオブジェクトのハッシュコード***
[stdout] this:449896816
[stdout] *@RequestScopedのクラスを注入した変数のハッシュコード***
[stdout] requestBean:1701862750
二回目の結果
[stdout] ***@ApplicationScopedのクラスのハッシュコード***
[stdout] this:449896816
[stdout] ***@RequestScopedのクラスを注入した変数のハッシュコード***
[stdout] requestBean:1701862750
ん?あっれれ〜?
だめじゃん、@ApplicationScopedのオブジェクトから参照してる@RequestScopedのオブジェクト変わってないじゃん。。。
お勉強しました。
上記でうまくいかないのもそのはず。
Red Hatによると、、、
通常、インジェクトされた bean のクライアントは bean インスタンスへの直接参照を保持しません。bean が依存オブジェクト (スコープ @Dependent) でない場合、コンテナーはプロキシーオブジェクトを使用して、インジェクトされたすべての参照を bean にリダイレクトする必要があります。
この bean プロキシーはクライアントプロキシと呼ばれ、メソッド呼び出しを受け取る bean インスタンスが、現在のコンテキストと関連するインスタンスになるようにします。[1:1]
各 bean のライフサイクルが異なる場合に、インジェクションにプロキシーが使用されます。プロキシーは起動時に作成されたbean のサブクラスで、bean クラスのプライベートメソッド以外のメソッドをすべて上書きします。プロキシーは実際の bean インスタンスへ呼び出しを転送します。[1:2]
つまり今回のサンプルでは以下のような挙動となっているようです。
- @ApplicationScopedのオブジェクトは、@RequestScopedのオブジェクトを直接参照するのではなく、プロキシーオブジェクトという起動時に自動生成されたBeanのサブクラスを参照している。
- プロキシーオブジェクトが現在のコンテキストに応じた適切なインスタンスを参照する。
たしかに間にプロキシーを挟んでおけば、スコープ(ライフサイクル)の差をうまく埋めることができるんだなと納得しました。
ということでやり方を変えて再検証!
再準備
- RequestScopedのクラスを修正
@RequestScoped
public class RequestBean {
// RequestBean側で自分のハッシュコードを出力できるように変更
public void printHash() {
System.out.println("requestBean:" + this.hashCode());
}
}
- ApplicationScopedのクラスを修正
@ApplicationScoped
public class ApplicationBean {
@Inject
private RequestBean requestBean;
public void printHashCodes() {
System.out.println("***@ApplicationScopedのオブジェクトのハッシュコード***");
System.out.println("this:" + this.hashCode());
System.out.println("***@RequestScopedのプロキシオブジェクトのハッシュコード***");
System.out.println("requestBean:" + requestBean.hashCode());
// 先ほどのRequestBeanのメソッドを呼び出すように変更
System.out.println("***@RequestScopedのオブジェクト自体のハッシュコード***");
requestBean.printHash();
}
}
- Controllerは変更なしです。
これでうまくいってくれ!再検証
- 1回目
[stdout] ***@ApplicationScopedのオブジェクトのハッシュコード***
[stdout] this:1718364564
[stdout] ***@RequestScopedのプロキシオブジェクトのハッシュコード***
[stdout] requestBean:2049301131
[stdout] ***@RequestScopedのオブジェクト自体のハッシュコード***
[stdout] requestBean:1892209311
- 2回目
[stdout] ***@ApplicationScopedのオブジェクトのハッシュコード***
[stdout] this:1718364564
[stdout] ***@RequestScopedのプロキシオブジェクトのハッシュコード***
[stdout] requestBean:2049301131
[stdout] ***@RequestScopedのオブジェクト自体のハッシュコード***
[stdout] requestBean:1921706211
- ちゃんと変わることを確認できました!
あとがき
- CDIの実際の挙動について理解できていなかった部分を検証したことにより納得ができました。
- 一方で公式のドキュメントにしっかりと書かれていたことに気づけていなかったので、なんとなく使ってきてしまった機能について改めて体系的に学び直さなければならないと思いました。
参考
-
Red Hat. "第7章 Jakarta Contexts and Dependency Injection". Red Hat Documentation. 更新日付不明, https://docs.redhat.com/ja/documentation/red_hat_jboss_enterprise_application_platform/7.4-beta/html/development_guide/contexts_and_dependency_injection, (参照 2025-09-26) ↩︎ ↩︎ ↩︎
Discussion