JavaEE7をはじめよう(12) - CDI Beanのインジェクション
前回は、CDIの定義方法を解説した。 今回は、CDI Beanをインジェクションする方法を紹介する。
インジェクションが可能なクラスの種類
インジェクションを行うには、@Inject
アノテーションを使用する。
@Inject
が使用可能なクラス、すなわちCDI Beanをプロパティなどに注入できるクラスには以下のものがある。
- 自作のCDI Bean - CDIコンテナによって生成されたものに限る
- EJB
- Servlet, Filter, Listener, JSP
- JSFのバッキングBean, View, コンバータ, バリデータ(最後の2つは Java EE 7から)
- JAX-RSのRESTリソースクラス
- Bean Validationのカスタムバリデータ(Java EE 7から)
基本的に、Java EEの開発で使用するほとんどのクラスに適用可能と考えてよいだろう。
ただし、自分でコンストラクタを呼んで生成したオブジェクトに対してはインジェクションが行われないことに注意が必要だ。 このため、自作したCDI Beanについてはコンストラクタを呼ばす、CDIコンテナに生成を任せる必要がある。 その他のJava EE関連のクラスについては、Java EEのバージョンに応じて対象範囲が異なることに注意する必要がある。
また、@RequestScoped
, @SessionScoped
, @ConversaionScopoed
はサーブレットコンテナがHTTPリクエストを処理中の場合のみ有効である。そのため、リモート呼び出しのEJBなどでは、これらのスコープのCDI Beanはインジェクションできない。
CDIコンテナの起動時にインジェクション不可能なプロパティが見つかった場合、CDIコンテナはアプリケーションのデプロイを失敗させる。
インジェクションの指定方法
インジェクションの指定方法は3種類ある。
方法1:フィールドインジェクション
フィールドに@Inject
を指定する方法である。対象のフィールドはprivate
でもよい。
@Dependent public class HogeBean { @Inject private Bean bean; /// }
方法2:コンストラクタインジェクション
コンストラクタの引数にCDI Beanを注入する方法である。
@Dependent public class HogeBean { private Bean bean; @Inject public HogeBean(Bean bean) { // BeanもCDI Beanである必要がある this.bean = bean; } }
前回の記事で説明したように、基本的にCDI Beanはデフォルトコンストラクタを持つ必要がある。しかし、上のHogeBean
クラスのように、コンストラクタの引数がインジェクション対象になっている場合は、CDI Beanにすることができる。
方法3:setter インジェクション
setterに@Inject
を指定する方法である。
@Dependent public class HogeBean { private Bean bean; @Inject public setBean(Bean bean) { this.bean = bean; } }
インジェクションの指定には3つの方法があるが、どの場合でもインジェクションを行うタイミングは、注入する側のクラス(上の例ではHogeBean
)のコンストラクタを呼び出すタイミングになる。
特に理由がないなら、フィールドを見ればどのフィールドがインジェクション対象かがわかるため、フィールドインジェクションを使用するのがよいだろう。
インジェクションの実行タイミング
実際にインジェクションが行われるタイミング、すなわちCDIコンテナがCDI Beanのコンストラクタを呼び出すタイミングは、指定したスコープによって異なる。
例えば、アプリケーションスコープではアプリケーションの起動時、セッションスコープではセッションの開始時、リクエストスコープではリクエストの開始時となる。
いずれの方法でもインジェクションが終わった後に@PostConstruct
アノテーションが付与された初期処理メソッドが呼び出される。CDI Beanに対して何らかの初期処理を行いたいなら、@PostConstruct
を付与したメソッドを使用するのが適切なやり方である。
なお、実際にインジェクションされるのは、CDI Beanそのものではなく、CDI Beanを継承したクライアントプロキシと呼ぶオブジェクトである。詳細については、本記事の最後の節で解説する。
異なるスコープのCDI Beanの多段注入
ここまで述べてきたように、CDI Beanに対してもインジェクションを行うこと、すなわち多段注入が可能となっている。その際にはスコープが異なっていても問題なく動作する。
コード例を使って説明しよう。
前々回の記事で取り上げたサーブレットのCDIのサンプルを修正し、サーブレットにインジェクションするBeanに対して、さらにインジェクションを行うようにする。
その際、@RequestScoped
のBeanからは@SessionScoped
のBeanを、@SessionScoped
のBeanからは@RequestScoped
のBeanをインジェクションするようにする。ついでに、@Dependent
のBeanをそれぞれのBeanにインジェクションする。
@Dependent
のBeanのコードは次の通りである。
@Dependent public class DependentBean implements Serializable { public String getHashCode() { return Integer.toHexString(this.hashCode()); } }
@RequestScoped
と@SessionScoped
のBeanのコードは次の通り。
それぞれのBeanでお互いを@Inject
することに加えて、上記の@Dependent
なBeanも@Inject
している。
@RequestScoped public class RequestBean { @Inject private SessionBean sbean; @Inject private DependentBean depBean; public String getHashCode() { return Integer.toHexString(this.hashCode()); } public String format() { return "this(Request):" + getHashCode() + " Session:" + sbean.getHashCode() + " Dependent:" + depBean.getHashCode(); } }
@SessionScoped public class SessionBean implements Serializable { @Inject private RequestBean rbean; @Inject private DependentBean depBean; public String getHashCode() { return Integer.toHexString(this.hashCode()); } public String format() { return "this(Session):" + getHashCode() + " Request:" + rbean.getHashCode() + " Dependent:" + depBean.getHashCode(); } }
そして、3つのBeanをサーブレットにインジェクションする。
@WebServlet(name = "scopeExample", urlPatterns = {"/scopeExample"}) public class ScopeExampleServlet extends HttpServlet { // CDI Beanのインジェクション @Inject private RequestBean rbean; @Inject private SessionBean sbean; @Inject private DependentBean depBean; protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { try (PrintWriter out = response.getWriter()) { out.println(rbean.format()); out.println(sbean.format()); out.println("DependentBean on Servlet:" + depBean.getHashCode()); } } }
実行結果は、以下のようになる。
1回目
this(Request):77ceecf6 Session:1436e3b5 Dependent:44c26299 this(Session):1436e3b5 Request:77ceecf6 Dependent:814d2cb DependentBean on Servlet:14e3719e
2回目
this(Request):6554df8b Session:1436e3b5 Dependent:616b47f9 this(Session):1436e3b5 Request:6554df8b Dependent:814d2cb DependentBean on Servlet:14e3719e
リクエストスコープのBean内のセッションスコープのBeanは、セッション単位で保持されているため、2回のハッシュ値は同じになっている。逆に、セッションスコープのBean内のリクエストスコープのBeanはリクエストごとにハッシュ値が違うので毎回生成されている。
また、@Dependent
スコープのBeanは、インジェクションしたBeanのライフサイクルにしたがっていることがわかる。すなわち、サーブレットはアプリケーション内でインスタンスが1つのため、サーブレットに@Dependent
スコープのBeanをインジェクションすると、アプリケーションスコープとして振舞う。
実行サンプルは以下で確認可能だ。
http://java-ee-example.herokuapp.com/java_ee_example/
インジェクションされるオブジェクトの正体
インジェクション時に実際にプロパティに設定されるのは、CDI Bean自身のオブジェクトではなく、クライアントプロキシと呼ぶ、動的に生成したオブジェクトである。
クライアントプロキシはインジェクション対象のCDI BeanをCDIコンテナが継承して作成したクラスである。
クライアントプロキシは実際のメソッドの実行をフックし、実行スレッドごとに適切なBeanをCDIコンテナから探し出して、本来のメソッドを実行する。クライアントプロキシを作成する都合上、CDI Beanのクラス定義にはfinal
修飾子を指定できない。
クライアントプロキシを使う理由は、スコープの異なるオブジェクトを効率的にかつ安全にインジェクションするためだ。
例えば、アプリケーションスコープのCDI BeanにリクエストスコープのCDI Beanをインジェクションする場合、アプリケーションスコープのBean はアプリケーション内で1つだけだが、そのプロパティであるリクエストスコープのBeanはリクエストごとに異なるインスタンスを作成する必要がある。 リクエストのたびにインジェクションを行うと、効率が落ちるだけでなく、スレッドセーフに関する考慮も必要になってしまう。
クライアントプロキシを導入することで、インジェクションの回数を減らし、かつスコープに応じた動的なCDI Beanの取得をCDIコンテナに任せることができるようになる。 CDIコンテナは、スコープに応じた Bean の生成やキャッシュの管理、スレッドセーフの考慮などをしているので、異なるスコープのインジェクションを効率的かつ安全に行える。
ただし、クライアントプロキシを注入するのは@Dependent
以外のスコープを指定した場合のみである。@Dependent
の場合はクライアントプロキシではなく、当該クラスのオブジェクトが直接設定される。
これは@Dependent
を指定した場合はスコープが同一になり、実行効率やスレッドセーフの考慮をする必要がないため、クライアントプロキシ方式を採用するメリットがないからである。
前節の動作例の中のセッションスコープのBean内のリクエストスコープのBeanはリクエストのたびにハッシュコードの値が変わるため、新しいオブジェクトを生成しているように見える。 しかし、実際にはクライアントプロキシによって、そのような振る舞いを行うように見せかけているだけである。
またSessionBean
クラスの中ではrbean.getHashCode()
を実行しているが、このときrbean
にインジェクションされているのはRequestBean
のクライアントプロキシである。
クライアントプロキシのgetHashCode
メソッドは、CDIコンテナを使用してスコープの内容に応じてRequestBean
インスタンスの取得または生成を行い、元のgetHashCode
メソッドを呼び出すように動作する。
SessionBean
とRequestBean
はお互いのクラスのプロパティを持ち合っている。
インジェクションが再帰的に行われて、無限にインジェクションが起きると思われるかもしれないが、そのような心配はない。
インスタンス生成時にCDIコンテナが行うのは、プロパティにクライアントプロキシを設定するだけで、プロパティの内部には触れないためである。
詳細はこちらなどを参照。
CDIを普通に使う分には実装の詳細を知る必要はあまりないが、CDIを使用したアプリケーションで例外発生時のスタックトレースに、HogeBean_$$WeldProxy.someMethod()
のような自分で作ったクラスでないものが現れるのはこのような実装に基づくことを知っておくとよいだろう。
まとめ
前回と今回の2回に分けて、インジェクションの基本的な部分について説明した。
スコープ指定を利用することで、セッション領域の管理を自動的かつ型安全に行える。 ただし、やみくもに色々なクラスにスコープを設定すると収集がつかなく恐れがある。
CDIを利用するなら、クラスの役割分担やスコープの設定の仕方など、設計の時点でどのようにCDIを利用するかを検討しておく必要があるだろう。
次回は、継承や実装が絡む場合や、インジェクションするオブジェクトを切り替える方法について解説する。
[前多 賢太郎]