JavaEEで個人的に鬼門がCDIとスコープだと思っています。
ここが少しずつ分かりかけてきたんでメモします。
まずJavaEEでは変数の生存期間をアノテーションで定義します。
これがなかなか分かりにくく、慣れるまで時間がかかりました。
ちょっと簡単な例で見ていきましょう。
Contents
スコープアノテーション
サンプル
上記のようなボタンを押したらカウントアップするようなプログラムを作るとします。
ファイル構成は単純化するために画面側(counter.xhtml)、プログラム側(SampleBean.java)にともに1つとします。
画面側のファイルcounter.xhtmlについては常に下記のものとします。
プロパティの表示とカウントアップのメソッドのみにします。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
<?xml version='1.0' encoding='UTF-8' ?> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml" xmlns:h="http://xmlns.jcp.org/jsf/html"> <head> <title>TODO supply a title</title> <meta name="viewport" content="width=device-width, initial-scale=1.0"/> </head> <body> <h:form> カウンター<h:outputText value="#{sampleBean.value}" /><br /> <h:commandButton action="#{sampleBean.upCount()}" value="カウントアップ" /> </h:form> </body> </html> |
外部ファイルは使わず変数のみでこれを実装したいと思います。
@RequestScoped
次にプログラムのほうを見ていきましょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
package com.sampleweb; import javax.enterprise.context.RequestScoped; import javax.inject.Named; import lombok.Getter; import lombok.Setter; /** * * @author matsumoto */ @Named @RequestScoped public class SampleBean{ @Getter @Setter private Integer value=0; public String upCount(){ value++; return null; } } |
リクエストスコープの場合、1回の通信の間のみ変数を保存します。
そのためボタンを押して表示するまでしか変数(正確にはSampleBean)は生きていません。
- ボタンを押す
- SampleBeanが生成される
- ロジックが処理される
- 値がレンダリングされる
- SampleBeanが破棄される
という流れになるのでカウンタは常に1のままです。(上のサイクルは完全に正確ではないと思いますが、おおまかなイメージです。)
@SessionScoped
次にセッションビーンです。SampleBeanを若干書き換えます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
package com.sampleweb; import java.io.Serializable; import javax.enterprise.context.SessionScoped; import javax.inject.Named; import lombok.Getter; import lombok.Setter; /** * * @author matsumoto */ @Named @SessionScoped public class SampleBean implements Serializable { @Getter @Setter private Integer value=0; public String upCount(){ value++; return null; } } |
この場合、ボタンを押すたびにカウンターが上がります。
この場合、流れは下記のようになります。
- すでにSampleBeanを生成されている
- ボタンを押す
- ロジックが処理される
- 値がレンダリングされる
- セッションが切れた時点でSampleBeanが破棄される
リクエストスコープとちがい、セッションスコープはglassfishがあらかじめ、オブジェクトを生成します。
なので、一定期間(セッションが有効な間は)、値が生存します。
スコープアノテーションで注意すること
importの種類
スコープアノテーションですがimportが複数あるようなので正確なものを選びましょう。そうしないと動きません。下記がそれぞれ正常なスコープアノテーションのimoportです。
javax.enterprise.context.RequestScoped
javax.enterprise.context.SessionScoped
Serializableインターフェイス
なお、リクエストではなくセッションのようにデータを保存する場合、シリアライズという処理を行います。これを忘れるとそもそもビルドに失敗するので注意しましょう。
参考リンク
ちなみにスコープはリクエストとセッションではなく、下記のように数種類あります。
viewScoped
リクエストにより表示されたJSFが他の画面に切り替わるまでは値が生存します。今回のカウンターの例でいうと値が残ります。
ApplicationScoped
ウェブアプリケーションが実行されている間は値が残ります。
ConversationScoped
1回以上のリクエストの間で開始と終了をプログラマが明確に定義できます。
参考リンク
なお、当然のことながら不要に値の生存期間を増やすことは
- メモリの圧迫
- バグの温床
になりますので、できる限り狭くすることが大切です。
CDIという考え方
ManagedBeanについて説明した後で同時にCDIについても理解しておきたいと思います。
まずサンプルから見ていきましょう。
上記のようなカウンターですが、プログラムをSampleBean.javaのみでなく、
SampleBean.java
Counter.javaの2種類にし、
カウントアップに関してはCounter.javaで行うこととします。
ソース
SampleBean.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
package com.sampleweb; import javax.enterprise.context.RequestScoped; import javax.inject.Inject; import javax.inject.Named; import lombok.Getter; import lombok.Setter; /** * * @author matsumoto */ @Named @RequestScoped public class SampleBean { @Getter @Setter private Integer value = 0; @Inject private Counter counter; public String upCount() { value = counter.countUp(); return null; } } |
Counter.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
package com.sampleweb; import java.io.Serializable; import javax.enterprise.context.SessionScoped; import lombok.Getter; import lombok.Setter; /** * * @author matsumoto */ @SessionScoped public class Counter implements Serializable { @Getter @Setter private Integer n = 0; public Integer countUp() { n++; return n; } } |
このプログラムを動かすとクリックするごとにカウントアップします。
これはSampleBean.javaがリクエストスコープですが、Counter.javaがセッションスコープなので、値が保存されているためです。
CDI(Context and Dependency Injection)の定義
CDIの定義ですがいろんなところで見聞きしたのですがようわかりませんでした・・・
今では簡単に下記のような解釈をしております。
- new演算子を使わずにオブジェクトを取得する仕組みであり、生成や破棄の手間がいらない(=正確にはコンテナ(この場合はglassfish)が管理するのでプログラマが管理しなくてよい。)
- 他のオブジェクトを使用する場合は@Injectというアノテーションを付与するだけでそのオブジェクトが使用できる。
上記の例などがそうですが、Counterオブジェクトに関してnewを使用せずとも、@Injectを使用するだけで使えてしまいます。
インスタンスの生成、破棄などはプログラマ側がしてしなくてよいのです。
ちなみにこのようにして管理されたオブジェクトをCDIビーンと呼びます。
今になってようやくメリットが理解でき始めました(爆)
ちなみにManagedBeanもオブジェクトの生成や破棄をしていないのに、使用できたのはこのCDIの特性を持っているからです。
CDIの条件
このように便利なCDIですが、どんなクラスでもなれるというわけではありません。
CDIとはコンテナがインスタンスを生成することです。
なので、インスタンスが生成できなければCDIビーンにはなれません。これはいわゆる通常のクラスといっしょなので
- 具象クラスであること
- 引数なしのディフォルトコンストラクタをもつこと
- static付きのインナークラスでないこと
という条件が必要になってきます。
CDIのスコープ
CDIのスコープもアノテーションを付与して判断します。アノテーションの種類はさきほど紹介したものといっしょですが、ViewScopedはJSFで定義されているものなので、通常のCDIビーンで定義はできないようです。
また呼び出し側のスコープ(この場合でいうとSampleBean.java)に依存するという@Dependentというスコープも存在します。
CDIって聞いたときはわけがわからんかったんですが、アプリを実際に作りつつようやくメリットが理解できてきました。
ちなみに今回は下記書籍のサンプルを参考にしました。
結構前に読んだ時はよくわからなかったんですが、今読んでみると大変すっきりしました。
参考書籍