12
第 章
CDI ー新しいオブジェクトの作り方
CDI(Context and Dependency Injection)は「コンテキストと依存性注入」と訳され ていますが、要は、new演算子を使わずにオブジェクトを取得する仕組みのことです。
コンストラクタがどうなっているのか知らなくてもよく、生成や廃棄の手間も必要な いのでウェブシステム全体で広く使われています。このサービスを提供してくれるサ ブシステムをCDI実装とかCDIサービスといいます。この章では、CDIの使い方につい て解説します。
1 CDIとは ...
2 Conversation(会話)スコープ ...
3 複数ページをまとめるFlowスコープ ...
4 インジェクトできるようにする@Produces ...
5 CDIビーンの実装を使い分ける ...
6 まとめ ...
Jakarta
12
JavaEEを使うウェブアプリケーション開発では、必要なオブジェクトを
new
で作成する 代わりにCDI(Context and Dependency Injection)サービスから取得することができます。なぜそのような操作が必要なのか、最初にそのあたりから解説します。
まず、次は、
Game
クラスでDice
型のオブジェクトを作成する例です。public class Game {
Dice dice = new Dice(); // プログラマがnewでオブジェクトを作る
public void CreateGame(){
start(dice);
} ・・・
}
オブジェクト作成により、
Game
クラスとDice
クラスには依存関係が生じます。という のも、Game
クラスは実行のためにDice
クラスが必須となり、また、Dice
クラスは引数の ないコンストラクタを持つ、という実装を変えることができなくなるからです。一方、次はCDI(Context and Dependency Injection)サービスにより
Dice
型オブジェク トを取得する例です。public class Game { @Inject
Dice dice; // CDIからオブジェクトを受け取る
public void CreateGame(){
start(dice);
} ・・・
}
@Inject
は、「変数にオブジェクトを代入する」というCDIのアノテーションです。これ だけでDice
型のオブジェクトがdice
にセットされます。Game
クラスとDice
クラスの依 存関係がなくなるわけではありませんが、コンストラクタについての情報が含まれていな い分、依存関係は弱くなります。つまり、疎結合(loose coupling)になるわけです。Section Section Section Section Section S Sectition Se Sectctioionn Section S i
1
S tiCDIとは
Jakartaトルツメ
Jakarta Context and Dependency Injection で索引語にする
12
また、
@Inject
を書くことで依存関係が発生するので、これを依存性注入(Dependency Injection)といいます。用語が堅苦しいため、「インジェクトする」とか「インジェクション」などと簡略に表現することがあるので、覚えておいてください。
ただ、CDIの効用はこれだけではありません。それを知るにはコンテキスト(Context)に ついても理解する必要があります。コンテキストとは「プログラムの実行状況に関する情報 全体」を指す言葉ですが、CDIでのコンテキストとは
@RequestScoped
のような「スコープ アノテーションで指定するオブジェクトの存続状態」を指します。次の例題で具体例を示 します。1.1 CDIを使ってみる
この例題では、次のように[カウントアップボタン]を 押すと、そのたびに表示される値が 1 ずつ増えていきま す。Sample17-01プロジェクトを開いて実行してみてく ださい。
なお、このJSFページは次のような簡単なものです。
カウントアップ画面 sample17-01/index.xhtml 例題1
1 <h:body>
2 <h:form>
3 <h1>CDIによるカウントアップ</h1>
4 <h:commandButton action="#{bb.next()}" value="カウントアップ"/>
5 #{bb.value}
6 </h:form>
7 </h:body>
カウントアップボタンをクリックすると、バッキングビーンの
next
メソッドを実行しま す。5行目でバッキングビーンの変数value
を画面に表示しているので、next
メソッドを クリックしたとき、この値が 1 増やされる、ということがわかります。バッキングビーンを次に示します。
12
例題1(続き):CDIを使う sample17-01/beans/Bb.java 1 @Named
2 @RequestScoped
3 public class Bb{
4 private Integer value;
5 @Inject
6 private Counter counter; // カウントアップ機能を持つCounterオブジェクト 7 public String next(){
8 value=counter.countup(); // counterを+1して、その値を得る
9 return null; // 元の画面を再表示する
10 }
11 // セッターとゲッター(省略)
12 }
next
メソッドを見ると、value
の値を直接増やすのではなく、counter
という変数のcountup
メソッドを実行して、その戻り値をvalue
に代入しています(8行目)。この変数counter
は、6行目を見るとCounter
型のオブジェクトであることがわかります。実は、
Counter
型のオブジェクトは内部にカウンタを持っていて、countup
メソッドを 実行するたびに 1 増やした値を返します。その値をvalue
に代入していたので、画面で値 が 1 ずつ増えていたのです。しかし、
Counter
型のオブジェクトはいつ作成したのでしょうか。本来なら、次のよう な文がなければいけないところです。Counter counter = new Counter(); // Counter型のオブジェクトを作る
そこで5行目の
@Inject
に注目してください。@Inject
private Counter counter; // カウントアップ機能を持つCounterオブジェクト
@Inject
は、JavaEEシステム(CDIサービス)に依頼して、オブジェクトをインジェクト してもらう(作成して変数counter
に代入してもらう)という意味です。これでオブジェク トをインジェクトしているので、8行目のようにいきなり、オブジェクトのメソッドを使う ことができるのです。value=counter.countup(); // countup()は+1したカウンタの値(整数)を返す Jakarta EE
12
最初は不思議な感じがしますが、ウェブシステムでは、疎結合にしておいてシステムの 修正をやりやすくするために、インジェクトによる方法が広く使われています。
ここでインジェクトされた
Counter
クラスは、次のような簡単なクラスです。例題1(続き):インジェクトされるクラス sample17-01/beans/Counter.java
1 @SessionScoped
2 public class Counter implements Serializable{
3 private Integer n=0; // 初期値を0にしておく 4 public Integer countup() {
5 return ++n; // nの値を1増やして返す
6 }
7 // セッターとゲッター(省略)
8 }
フィールド変数
n
(初期値は0)があり、countup()
メソッドはその値を+1して返すだけ の機能です。ここで、
Counter
クラスにスコープアノテーション(@SessionScoped
)が付いている ことに注意してください。スコープアノテーションが付いているクラスは、CDIサービス に管理されるCDIビーンとなり、インジェクションできるクラスになります。条件がゆるいので、ほとんどのクラスは、スコープアノテーションを付けてCDIビーン にすることができます。
【CDIビーンにできるクラスの基本的な条件】
・具象クラスであること
・引数なしのデフォルトコンストラクタを持つこと
・static付のインナークラスではないこと
な お、
WEB-INF
フ ォ ル ダ にbeans.xml
フ ァ イ ル を 作 成 し、bean-discovery- mode="all"
と指定しておくと、上の条件を満たす全てのクラスが、スコープアノテー ションがなくても、自動的にCDIビーンとみなされるようになります。JETに含まれる
builder
プロジェクトやアーキタイプを使ってプロジェクトを作成する と、bean-discovery-mode="all"
と設定したweb.xml
ファイルが自動生成されます。12
独立したコンテキスト
Counterクラスのスコープは、
@SessionScoped
を指定しています。というのも、@RequestScoped
だと継続的なカウントアップができないからです。@RequestScoped
では、ウェブで[カウントアップ]ボタンが押されるたびにオブジェクトが生成され、ウェ ブを再表示した後で消去されるため n の値が毎回クリアされます。
一方、バッキングビーンのスコープアノテーションを見てください。
@RequestScoped
になっています。つまり、[カウントアップ]ボタンが押されるたびに、オブジェクトが再 作成される設定です。これでは
counter
も毎回クリアされてしまうのではと思うのですが、動かしてみてわかったように、そうはなりませんでした。
きちんとカウントアップされたことからわかるように、バッキングビーンは毎回消えて しまっても、
counter
変数は消えずに残っていたのです。これは、counter
のスコープ とバッキングビーンのスコープがそれぞれ独立していて、互いに影響されないからです。これを、互いに「独立したコンテキストを持つ」といいます。
1.2 CDIのスコープアノテーション
次の9つのスコープアノテーションが定義されています。
スコープアノテーション 関連 意 味
@RequestScoped HTTP 1回のrequest-responseの間
@SessionScoped HTTP 1回のセッションの間
@ConversationScoped HTTP 複数ページの遷移の間
@ApplicationScoped HTTP アプリケーションが実行されている間
@Dependent HTTP インジェクト先のスコープを引き継ぐ
@Singleton シングルトン(1つのオブジェクトしか生成されないクラス。
EJBにも同じアノテーションがあり、メソッドへの同時アクセス の排他制御機能が付加されている)
@ViewScoped JSF 同じJSFページにアクセスしている間
@FlowScoped JSF 1つのJSFページグループ(Flow)にアクセスしている間
@Transactional DB処理 1回のトランザクション処理
CDIビーンにはこれらのアノテーションを付けて、オブジェクトの状態や寿命(スコープ)
を設定できます。これをオブジェクトのコンテキストといいます。CDIビーンは、単にイ ンジェクトできるだけでなく、コンテキストを持つことができるという特徴があります。
なお、まだ、解説の済んでいないスコープアノテーションについては、この後のページ で解説しています。
12
1.3 FacesContextとServletContext
@Inject
を使って、JSFの関連オブジェクト(JSF アーティファクトという)をインジェ クトできます。また、EL式としてJSFページの中でも使用できます。後で解説するように、CDIには、どんなオブジェクトでもインジェクトできるようにする 仕組みがありますが、JSFアーティファクトは、最初から組み込みでインジェクトできるよ うになっているオブジェクトです。
代表的なものとして、FacesContextとServletContextがあります(注)。
FacesContextは、プログラム実行時点におけるJSFの実行パラメータとそれらを操作 するメソッドの集合体です。したがって、FacesContextを利用すると、JSFの実行パラ メータを取得・設定できます。また、JSFはサーブレットの上に構築されているので、
ServletContextを利用すると、サーブレットレベルでのいろいろな情報を取得・設定でき ます。
(1)ServletContext
絶対パスに変換する sample17-01A/beans/FileReader.java 例題2
1 @SessionScoped
2 public class FileReader implements Serializable {
3 @Inject
4 private ServletContext context;
5
6 // テキストファイルをすべて読み出して文字列にして返す 7 public String readAllText(String fpath) {
8 String path = context.getRealPath(fpath); // 絶対パスに変換する 9 // 読み出し処理
10 Path p = Paths.get(path);
11 String data = "";
12 try {
13 byte[] bytes = Files.readAllBytes(p); // バイト列として読み込む 14 data = new String(bytes, "UTF-8"); // 文字列に変換する 15 } catch (IOException ex) {
16 System.out.println(e.getMessage());
17 }
18 return data;
19 }
20 }
12
このクラスの
readAllText
メソッドは、テキストファイルのパス(fpath
)を受け取っ て全内容を読み出し、文字列として返します。ただし、ファイルはJSFアプリケーション内のフォルダである
resources
フォルダに格 納されているので、fpath
は、"/resources/data/my.txt"
のようなアプリケーショ ンルートを起点とする仮想パスです。しかし、ファイルにアクセスするにはファイルの絶 対パスが必要です。そこで、仮 想 パ スから 実 行 環 境 の 絶 対 パ スを 得 るた めに、
ServletContext.
getRealPath()
メソッドを使います。ServletContextオブジェクトはインジェクトする だけで使えるので、例題では3、4行目でそれを行っています。@Inject
private ServletContext context;
そして、8行目で次のように仮想パスから絶対パスを取得します。
String path = context.getRealPath(fpath); // 絶対パスに変換する
10行目以下の枠で囲った部分は、典型的なファイル一括読み出し処理です。
なお、ServletContextの他のメソッドはJakarta EEのAPIドキュメント(注)で調べること ができます。
(2)FacesContext
次は、JSFの
/resources/data/
フォルダ内にあるファイルを読み出して表示する処 理です。1.txt
と2.txt
という2つのファイルがあるので、ラジオボタンでどちらを表示す るか選択します。sample17-01Aプロジェクトを実行して、機能を確認してください。(注) https://jakarta.ee/specifications/platform/9/apidocs/index.html
12
この例題では、ラジオボタンを選択した直後に、「ファイルを表示しました」というメッ セージが表示されます。このメッセージは
h:messages
タグにより表示されています。h:messages
タグは、エラーメッセージを表示するために使ってきましたが、実は特定の メッセージのためのタグではなく、「JSFのメッセージキューに格納されている文字列を表 示する」というのが本来の機能です。そこで、例題では、「ファイルを表示しました」というメッセージを表示するために、JSF のメッセージキューに「ファイルを表示しました」という文字列をセットします。このテク ニックは、プログラム実行中での確認・応答などに広く使われていますが、この時使うの がFacesContextです。
例題2(続き)メッセージキューに入れる sample17-01A/beans/Bb.java 1 @Named
2 @RequestScoped
3 public class Bb {
4 private String text; // 表示するテキスト
5 private Integer num; // ファイル番号(ファイル名はno+".txt"となる)
6
7 @Inject
8 FileReader reader; // ファイル読み出しのユーティリティクラス 9 @Inject
10 FacesContext context;
11
12 public String next() {
13 String path = "/resources/data/"+num+".txt"; // 表示するファイル名 14 text = reader.readAllText(path); // ファイルの一括読み出し
15 FacesMessage msg = new FacesMessage("ファイルを表示しました");
16 context.addMessage(null, msg); // メッセージをキューに登録 17 }
// セッター・ゲッターなどを省略
}
このバッキングビーンは、ファイルデータを一括取得するために、FileReaderクラスの オブジェクトをインジェクトしています。FileReaderには、全ファイル内容を読み出して 文字列にして返す
readAllText()
メソッドがあります。ウェブで、コマンドボタンを押すと、バッキングビーンの
next()
メソッドが実行されま す。next()
メソッドでは、ラジオボタンの値(num
)から、表示するファイル名を決定し、readAllText
メソッドで内容を読み出し、text
に代入します。12
13 String path = "/resources/data/"+ num +".txt"; // ファイル名
14 text = reader.readAllText(path); // 一括読み出し
この時、「ファイルを表示しました」というメッセージを表示させるには、FacesContext を使って、JSFのメッセージキューにメッセージを登録します。15、16行目の処理がそれ です。
15 FacesMessage msg = new FacesMessage("ファイルを表示しました");
16 context.addMessage(null, msg);
JSFのメッセージキューに登録できるのは、FacesMessageオブジェクトなので、15行目 でそれを作成し、16行目で
addMessage
メソッドを使って登録します。ここで、context
は、FacesContextオブジェクトで、9、10行目でインジェクトしていることに注意してください。
では、最後に、
index.xhtml
を見ておきましょう。例題2(続き)メッセージを表示する sample17-01A/index.xhtml
1 <h:body>
2 <h1></h1>
3 <h:form id="formId">
4 <h:panelGrid columns="1">
5 <h:selectOneRadio value="#{bb.num}" styleClass="inp">
6 <f:selectItem itemValue="1" itemLabel="杜子春(その1)"/>
7 <f:selectItem itemValue="2" itemLabel="杜子春(その2)"/>
8 </h:selectOneRadio>
9 <h:inputTextarea value="#{bb.text}" cols="60" rows="10"/>
10 <h:commandButton action="#{bb.next()}" />
11 </h:panelGrid >
12 <h:messages layout="table" styleClass="msg"/>
13 </h:form>
14 </h:body>
index.xhtml
は、2つのラジオボタン、テキストエリア、コマンドボタンがある簡単な 構成です。そして、最後にh:messages
タグがあり、これでメッセージを表示します。つ まりindex.xhtml
には、特別な仕掛けは何もありません。JSFのメッセージキューにメッ セージを登録するだけで、h:messages
タグがそれを表示してくれるわけです。(3)表示したFacesMessageを消去する方法
Sample17-01Aを実行すると、表示したメッセージが消えずに残ってしまうことに気がつ
ラジオボタン
メッセージタグ FacesMessage#addMessageメソッド で索引語に登録
12
いたと思います。このメッセージを、例えば「2秒間表示したら消す」ようにするには、どう したらいいでしょう。これまでそういう機能はサードパーティのライブラリを利用するし かありませんでしたが、JSF2.3からは、AJAXに追加された機能を使って、同じことがで きるようになりました。
さて、AJAX機能で呼び出されたバッキングビーンのメソッドは、普通、renderパラメー タで要求されたデータだけをウェブ側に応答として返します。ウェブ側では、それを使っ て部分的な再描画を行います。
ところが、JSF2.3からは、要求されたデータに加えて、実行してほしいJavascriptプロ グラムを応答に含めることができるようになりました。ウェブ側ではデータを受け取った 直後に、送られてきたJavascriptプログラムを実行します。
この機能を使って、2秒表示した後にメッセージを消す例を見てみましょう。
例題2(続き)メッセージキューに入れる sample17-01A/beans/Bb.java
AJAXでJavascriptを実行する sample17-01B/index.xhtml 例題3
1 <h:body>
2 <script>
3 function erase() {
4 var msgtag = document.getElementById('formId:msg');
5 msgtag.innerHTML = "";
6 }
7 </script>
8 <h1>AJAXでJavascriptを実行する</h1>
9 <h:form id="formId">
10 <h:panelGrid columns="1">
11 <h:selectOneRadio value="#{bb.num}" styleClass="inp">
12 <f:selectItem itemValue="1" itemLabel="杜子春(その1)"/>
13 <f:selectItem itemValue="2" itemLabel="杜子春(その2)"/>
14 <f:ajax event="click" execute="@this"
15 listener="#{bb.next()}" render="txa msg"/>
16 </h:selectOneRadio>
17 <h:inputTextarea id="txa" value="#{bb.text}" cols="60" rows="10"/>
18 </h:panelGrid >
19 <h:messages layout="table" id="msg" styleClass="msg"/>
20 </h:form>
21 < /h:body>
表示したメッセージを消去するJavascript プログラム
トルツメ
12
※AJAX機能を使うのでコマンドボタンはありません
index.xhtml
には、<script>
タグを使ってerase()
というJavascriptプログラムが埋 め込まれていますが、それ以外はAJAXを使った普通のJSFページです。なお、JavaScript はh:outputScript
タグを使って、ファイルから読み込むこともできます(⇒3章)。
"function erase()"
は関数といい、java言語でのメソッドにあたります。erase()
関数の機能は、「
id
の値で該当するタグを検索し、その内容を消去する」というものです。function erase() {
var msgtag = document.getElementById('formId:msg');
msgtag.innerHTML = "";
}
ちなみに、検索するidの値は
"formId:msg"
となっています。9行目のh:form
タグのid
が"formId"
、またその子要素であるh:messages
タグのidが"msg"
ですから、これら を合成したformId:msg
で検索すると、h:messages
タグが検索されます。ただ、
index.xhtml
には、erase()
関数を呼び出して起動するプログラムが書かれて いないので、このままでは、何の働きもしません。起動するプログラムは、AJAX呼び出 しの戻り値として、バッキングビーンから送ります。そこで、
index.xhtml
のf:ajax
タグについても見ておきましょう。<f:ajax event="click" execute="@this"
listener="#{bb.next()}" render="txa msg"/>
f:ajax
はラジオボタンを表すh:selectOneRadio
タグの中に書かれていて、event=
"click"
となっているので、ラジオボタンをクリックした時に起動します。そして、12
h:selectOneRadio
タグの値をバッキングビーンに送り、next()
メソッドを起動します。また、
render="txa msg"
のtxa
とmsg
はそれぞれh:inputTextarea
、h:messages
タグのIDですから、これらのタグの値を受け取って再描画します。
例題3(続き)JavaScriptプログラムを送信する sample17-01B/beans/Bb.java 1 @Named
2 @RequestScoped
3 public class Bb {
4 private String text;
5 private Integer num;
6 @Inject
7 FileReader reader;
8 @Inject
9 FacesContext context;
10
11 private static final String SCRIPT="setTimeout(erase, 2000);";
12
13 public String next() {
14 String path = "/resources/data/"+num+".txt";
15 text = reader.readAllText(path);
16 FacesMessage msg = new FacesMessage("ファイルを表示しました");
17 context.addMessage(null, msg);
18
19 context.getPartialViewContext()
20 .getEvalScripts()
21 .add(SCRIPT);
22
23 return null;
24 }
25 // セッター、ゲッター等を省略 26 }
バッキングビーンには、JSFページに書いた
erase()
を起動するJavascriptプログラム を追加します。11行目の記述がそれで、setTimeout
というJavascript関数を使って、2秒 間アイドルした後にerase()
を起動するという命令です。Javascriptですから、単に文字 列として作成し、String
型の変数SCRIPT
に代入しおきます。AJAXからの応答にこのプログラムを含める書き方が、19 ~ 21行です。いつもこのよう に書きます。定型的な書き方として覚えておきましょう。なお、ここでも、インジェクト したFacesContextのオブジェクト
context
を使います。2000ミリ秒(=2秒)後にerase()を実行する、
というJavascriptプログラム
応答(response)に、javascriptを追加す る書き方
12
context.getPartialViewContext() .getEvalScripts() .add(SCRIPT);
これで、応答に
SCRIPT
の内容が追加され、ウェブ側で受け取った後、実行されます。setTimeout
は指定時間アイドルした後で、指示された関数を実行する命令ですから、実 行開始から2秒後に、erase()
関数が実行されます。つまり、メッセージは2秒間だけ表示され、その後、消去されます。smple17-01Bプロジェ クトを実行し、結果を確認してください。
1.4 インジェクションポイント
@Inject
を書く場所を、インジェクションポイントといいます。普通、インジェクションポイントはフィールド変数ですが、コンストラクタやセッター の引数をインジェクションポイントにすることもできます。
// フィールド変数
@Inject
Counter counter;
// コンストラクタ(引数)
@Inject
public Bb(Counter counter){
this.counter = counter;
}
// セッター(引数)
@Inject
public void setCounter(Counter counter) { this.counter = counter;
}
ただし、コンストラクタやセッターの引数にインジェクトした場合、プログラマはそれ らのコンストラクタやセッターを直接利用することはできません。オブジェクトをセット するのは、常にCDIサービスの役割だからです。
12
例えば、ウェブで買い物をする時は買い物かごに入れる操作をした後、送り先などの情 報入力、支払情報の入力、確認画面を経て一連の操作が終わります。この間、注文と利用 者情報を保持し続ける必要があります。それは、
@SessionScoped
でも可能ですが、も う少し短い期間だけにしたいという時、@ConversationScoped
が使えます。
@ConversationScoped
は、ブラウザとサーバーの間で何往復かする間だけ、オブ ジェクト(バッキングビーン)が存続します。そのため、開始と終了をプログラマが指示し なくてはなりません。@ConversationScoped
を指定しても、開始の指示をするまでは@RequestScoped
と同じ働きになります。また、終了の指示をした後も同じです。2.1 例題の画面推移
次は会話スコープによるシステムの例です。sample17-02を開いて実行しながら、解説 を読んでください。
sample17-02は[会話処理をスタートする]ボタンを押した時、会話スコープが開始しま す。画面1、画面2で注文情報を入力し、画面3で確認します。画面3で[購入する]ボタンを 押すまでは会話スコープが続いているので、前の画面に戻ってもデータが残っています(戻 るを押して確認してください)。
Section Section Section Section Section S Sectition Se Sectctioionn Section
S i
2
S tiConversation(会話)スコープ
12
2.2 会話処理の開始と終了
次は、会話スコープを使用するバッキングビーンのソースコードです。
会話スコープ sample17-02/beans/Bb.java 例題4
1 @Named
2 @ConversationScoped
3 public class Bb implements Serializable{
4 private String product; // 商品名 5 private Integer qty; // 数量 6 private String name; // お名前 7 private String address; // ご住所
8 @Inject // 会話スコープマネージャーをインジェクトする
9 Conversation conv;
10 public String goto_1(){
11 if(conv.isTransient()){ // 会話スコープがすでに開始していないか調べる 12 conv.begin(); // 開始していなければbeginで開始する 13 System.out.println("**第1画面 会話スコープ開始 **");
14 }else{
15 System.out.println("**第1画面**");
16 }
17 return "view_1.xhtml"; // 画面1(商品と数量の入力)を表示する 18 }
19 public String goto_2(){
20 System.out.println("** 第2画面 **");
21 return "view_2.xhtml"; // 画面2(氏名と住所の入力)を表示する 22 }
23 public String goto_3(){
24 System.out.println("** 第3画面 **");
25 return "view_3.xhtml"; // 画面3(確認表示)を表示する 26 }
27 public String goto_0(){
28 conv.end(); // 会話処理を終了する
29 System.out.println("** 開始画面 会話スコープ終了 **");
30 return "index.xhtml";
31 }
32 // セッターとゲッター(省略)
33 }
会話スコープでは、クラスに
@ConversationScoped
アノテーションを付けます。会 話スコープは、会話スコープマネージャーを使って、プログラムで開始と終了を行います。そのため、CDIサービスから会話スコープマネージャーをインジェクトで取得しておかね ばなりません。
12
@Inject // 会話スコープマネージャーをインジェクトする
Conversation conv;
トップページで[会話処理をスタートする]ボタンが押されると10行目の
goto_1
メソッド を実行しますが、これが会話スコープの開始です。if(conv.isTransient()){ // 会話スコープがすでに開始していないか調べる
conv.begin(); // 開始していなければbeginで開始する
System.out.println("** 第1画面 会話スコープ開始 **");
}else{
System.out.println("** 第1画面 **");
}
return "view_1.xhtml"; // 画面1(商品と数量の入力)を表示する
conv.begin()
で開始できますが、すぐにそうせず、conv.isTransient()
を使って すでに会話スコープが始まっていないことを確かめてからスタートします(第2画面から第1 画面に戻ってくる場合にもこのgoto_1
メソッドが呼ばれるからです)。会話スコープマネージャーの
isTransient
メソッドは、会話スコープがまだ開始され ていない場合にtrue
を返します。以上で、商品と個数の入力を行う第1画面を表示して会話スコープがスタートします。
第3画面までは会話スコープの中なので、入力したデータは継続して使用できます。そ のため、前の画面に戻ることも自由にできます。
そして第3画面の確認表示で[購入する]ボタンを押すと、会話スコープを終了します。
27行目の
goto_0
メソッドがそれで、次のような内容です。conv.end(); // 会話処理を終了する
System.out.println("** 開始画面 会話スコープ終了 **");
return "index.xhtml";
end
メソッドは、会話スコープを終了します。これにより次の画面(トップページ)を送 信した後でバッキングビーンは廃棄され、入力データも利用できなくなります。情報の表示
例題では、実行時の情報をGlassFishサーバーのサーバーログに表示します。サーバー ログを開いて監視しながら実行してください。
Conversation#begin() で索引語に
Conversation#isTransient() で索引語に
Conversation#end() で索引語に
12 2.3 JSFページの内容
これらのJSFページを次に示します。コマンドボタンを押したときバッキングビーンの どのメソッドを実行しているかを確認してください。
例題4(続き):会話スコープのトップページ sample17-02/index.xhtml 1 <h:body>
2 <h1>会話スコープ</h1>
3 <h:form>
4 <h:commandButton value="会話処理をスタートする" action="#{bb.goto_1}"/>
5 </h:form>
6 </h:body>
例題4(続き):会話スコープの画面1 sample17-02/view_1.xhtml
1 <h:body>
2 <h1>会話スコープ<画面1></h1>
3 <h:form>
4 <h:panelGrid columns="2">
5 商品名<h:inputText value="#{bb.product}"/>
6 数 量<h:inputText value="#{bb.qty}"/>
7 <h:commandButton value="キャンセル" action="#{bb.goto_0}"/>
8 <h:commandButton value="第2画面へ移る" action="#{bb.goto_2}"/>
9 </h:panelGrid>
10 </h:form>
11 </h:body>
12
例題4(続き):会話スコープの画面2 sample17-02/view_2.xhtml 1 <h:body>
2 <h1>会話スコープ<画面2></h1>
3 <h:form>
4 <h:panelGrid columns="2">
5 お名前<h:inputText value="#{bb.name}"/>
6 ご住所<h:inputText value="#{bb.address}"/>
7 <h:commandButton value="戻る" action="#{bb.goto_1}"/>
8 <h:commandButton value="第3画面へ移る" action="#{bb.goto_3}"/>
9 </h:panelGrid>
10 </h:form>
11 </h:body>
例題4(続き):会話スコープの画面3 sample17-02/view_3.xhtml
1 <h:body>
2 <h1>会話スコープ<画面3></h1>
3 <h:form>
4 <h:panelGrid columns="2">
5 お名前:<h:outputText value="#{bb.name}" />
6 ご住所:<h:outputText value="#{bb.address}" />
7 商品名:<h:outputText value="#{bb.product}" />
8 数 量:<h:outputText value="#{bb.qty}" />
9 <h:commandButton value="戻る" action="#{bb.goto_2}" />
10 <h:commandButton value="購入する" action="#{bb.goto_0}" />
11 </h:panelGrid>
12 </h:form>
13 </h:body>
12
Flowスコープは、会話スコープとは違った方法で、複数ページにわたるスコープを実現 します。Flowスコープでは、複数のJSFページをまとめて同じフォルダにいれておき、1つ のFlowスコープとします。それらのJSFページを行き来している間は同じスコープ内なの で、ページを移動しても同じバッキングビーンが有効です。
Flowスコープを作成するには、最初に、フォルダを作成して、その中に関連する複数の JSFページを入れます。そして、作成したフォルダ名がFlowの定義名になります。
ここでは前節と同じ例を使うので、
order
という名前のフォルダを作成して、関連する JSFページを入れますが、最初にアクセスするJSFページは、フォルダ名と同じ名前(=定 義名)を持つ必要があります。つまり、order.xhtmlとします。最後に、order Flowの定義ファイルを作成します。Flow定義を、プログラムで行う方法 もありますが、定義ファイルの方が簡単です。
3.1 orderフォルダとJSFページ
左図のように、[Webページ]の直下に、orderフォルダを 作成して、その中に関連する3つのJSFページを入れます。
最初にアクセスされるJSFページは、
order.xhtml
とする 必要がありますが、それ以外のページには、任意の名前を 付けることができます。※フォルダを作成するには、[Webページ]を右クリックして、[新規]
⇒[フォルダ]と選択します。ダイアログが開くので、フォルダ名に orderと入力して[終了]を押します
Section Section Section Section Section S Sectition Se Sectctioionn Section S i
3
S ti複数ページをまとめるFlowスコープ
3.2 Flow定義ファイルの作成 見出しを
つける
<ここ以下に、P.342 の印をつけた部分を移動してくる>
FLOWスコープ用のフォルダの作成 次に
12
JSFページ sample17-021/order/order.xhtml 例題5
<h:body>
<h1>Flowスコープ<画面1></h1>
<h:form>
<h:panelGrid columns="2">
商品名<h:inputText value="#{bb.product}" />
数 量<h:inputText value="#{bb.qty}" />
<h:commandButton value="キャンセル" action="order-exit" />
<h:commandButton value="第2画面へ移る" action="order2" />
</h:panelGrid>
</h:form>
</h:body>
sample17-021/order/order2.xhtml <h:body>
<h1>Flowスコープ<画面2></h1>
<h:form>
<h:panelGrid columns="2">
お名前<h:inputText value="#{bb.name}" />
ご住所<h:inputText value="#{bb.address}" />
<h:commandButton value="第3画面へ移る" action="order3" />
<h:commandButton value="戻る" action="order" />
</h:panelGrid>
</h:form>
</h:body>
sample17-021/order/order3.xhtml <h:body>
<h1>Flowスコープ<画面3></h1>
<h:form>
<h:panelGrid columns="2">
商品名:<h:outputText value="#{bb.product}" />
数 量:<h:outputText value="#{bb.qty}" />
お名前:<h:outputText value="#{bb.name}" />
ご住所:<h:outputText value="#{bb.address}" />
<h:commandButton value="購入する" action="order-exit" />
<h:commandButton value="戻る" action="order2" />
</h:panelGrid>
</h:form>
</h:body>
これらのJSFページは、commandボタンで前後のJSFページを遷移するだけの構成です が、次のリンク先として指定されている
"order-exit"
だけが、JSFページの名前ではな 3.3 JSFページの書き方つける (続き)
続きの スタイルで
12
"order-exit"
はFlowスコープからの脱出先で、order Flowの定義ファイルで定義され た名前です。index.xhtml
ファイルの呼び名として定義されています。Flowスコープでは、脱出先だけは、JSFページ名(
index.xhtml
)で指定できないことになっているのです。なお、
index.xhtml
はorderディレクトの外にあり、次のようになっています。<h:body>
<h1>Flowスコープを始める</h1>
<h:form>
<h:commandLink value="会話処理をスタートする" action="order"/>
</h:form>
</h:body>
※CommandLinkを使っていますが、CommandButtunを使っても同じです 全体の動作は次のようです。
index.xhtml
order.xhtml order2.xhtml order3.xhtml
次に、定義ファイルです。先ほど作成したorderフォ ルダにJSFファイルと一緒に置くこともできますが、
外部からアクセスできないように、別にフォルダを作 成して、その中に置くのがいいでしょう。
それには、図のように[WEB-INF]フォルダの中に、もう1つ[order]というフォルダを作 成して、その中に置きます(フォルダ名は、Flowの名前と同じでなければいけません)。定 義ファイル名の名前は、order-flow.xmlです。つまり、Flowの名前に、
"-flow.xml"
を付 けたものになります。定義ファイルの内容は、次のようです。
ここから P.340 に移動
12
例題5(続き) Flow定義ファイル sample17-021/WEB-INF/order/order-flow.xml
<?xml version='1.0' encoding='UTF-8'?>
<faces-config ・・・ 以下省略 ・・・ >
<flow-definition id = "order">
<flow-return id = "order-exit">
<from-outcome>/index</from-outcome>
</flow-return>
</flow-definition>
</faces-config>
このファイルの形式は、
faces-config.xml
と同じものです。NetBeansで自動生成す るには、次のようにします。①orderフォルダを右クリックする
②[新規][その他]と選択する ⇒ [新規ファイル]ダイアログが開く
③カテゴリ欄で[JavaServer Faces]、ファイル・タイプ欄で[JSF Faces構成]を選んで [次>]を押す ⇒ [New JSF Faces構成]ダイアログが開く
④[ファイル名]にorder-flow.xmlと入力して[終了]を押す
order-flow.xml
ファイルの内容は、Flowの定義名、脱出先の呼び名、脱出先のJSF ファイルURLを書くだけの単純なものです。他のFlowを定義する時も、青字の部分だけを 変更すればいいでしょう。なお、必要な場合は、2つ以上の脱出先を定義できます。最後にバッキングビーンです。バッキングビーンには、@FlowScopedアノテーションを 付け、さらに、Flow名を指定します。後は、普通のバッキングビーンと同じです。
例題5(続き)バッキングビーン sample17-021/beans/Bb.java
@Named
@FlowScoped("order")
public class Bb implements Serializable { private String product; // 商品名
private Integer qty; // 数量
private String name; // 名前
private String address; // 住所
// セッター、ゲッターなどの記載を省略
}
例題を実行して動作を確認してください。
Flowの定義名 Flowの脱出先の呼び名 Flowの脱出先のJSFファイル
例題の スタイルで
移動 ここまで
3.4 バッキングビーンの書き方 見出しを
つける
12
会話スコープよりも簡単です。
なお、複数のFlowスコープを作成しておいて、あるFlowスコープから他のFlowスコー プを呼び出すような使い方もできますが、本書では割愛します。詳細を知りたい場合は、
"JakartEE9 Tutorial"(https://eclipse-ee4j.github.io/jakartaee-tutorial/toc.html)などを参 照してください。
12
4.1 @Producesの使い方
既存のクラスとして次のようなMyBeanクラスがあります。
public class MyBean { private String text;
public MyBean(String text) { this.text = text;
}
// セッター・ゲッターを省略
}
MyBeanクラスは引数のないコンストラクタを持ちません。したがって、MyBeanクラス をCDIビーンとみなすことはできず、残念ながら、次のようにインジェクトする形式で使 うことはできません。
@Inject
private MyBean bean; // コンパイルエラー
しかし、どこかのCDIビーンの中に、MyBeanのインスタンスを作成して返すメソッドを 作成しておくと、それだけでMyBeanをインジェクトできるようになります。この時、使う のが
@Produces
アノテーションです。次の例を見てください。インジェクトできるようにする sample17-03/beans/MyFactory.java 例題6
1 @Dependent
2 public class MyFactory { 3 @Produces
4 public MyBean producer() {
5 return new MyBean("こんにちは!");
6 } 7 } Section Section Section Section Section S Sectition Se Sectctioionn Section
S i
4
S tiインジェクトできるようにする@Produces
12
このMyBeanFactoryは、
@Dependent
アノテーションが付いていて、デフォルトコンス トラクタもあるので、明らかにCDIビーンです。このようなCDIビーンのクラスの中に、MyBeanオブジェクトを返すメソッドを作成しま す。そして、そのメソッドには@Producesアノテーションを付けておきます。これだけで、
他のクラスで、MyBeanをインジェクトできるようになります。
実際、次のバッキングビーンでは、MyBeanをインジェクトしています。
例題6(続き) MyBeanをインジェクトする sample17-03/beans/Bb.java 1 @Named
2 @RequestScoped
3 public class Bb implements Serializable { 4 @Inject
5 private MyBean bean;
6 private String data;
7
8 public void next(){
9 data = bean.getText();
10 }
// セッター・ゲッターを省略
}
sample17-03/index.xhtml
<h:body>
<h1>@Producesで@Injectできるようにする</h1>
<h:form>
<h:commandButton action="#{bb.next()}" value="表示"/>
<h:outputText value="#{bb.data}"/>
</h:form>
</h:body>
バッキングビーンBb.
java
は、4、5行目でMyBean
オブジェクトをbean
にインジェクト しています。そして、ウェブでコマンドボタンが押されたとき、next()
メソッドが起動し、bean
の持つ文字列「こんにちは!」が表示されます。sample17-03プロジェクトを実行し、結果を確認してください。
4.2 List<Book>型を返すメソッドに@Producesを付ける
例えば、CDIビーンの中に、書籍のリストを返すメソッドあったとします。これは
List<Book>
型のオブジェクトを返すので、次のように@Produces
を付けることができ ます。トルツメ
115 に差替
12
List<Book>型を返す sample17-03B/beans/MyBean.java 例題6B
@Dependent
public class MyBean { @Produces
public List<Book> getBookList(){
return Arrays.asList( new Book("杜子春", "芥川龍之介"), new Book("高瀬舟", "森鴎外") );
} }
すると、次のようにList<Book>型のオブジェクトを@Injectできます。
例題6B(続き)List<Book>をインジェクトする sample17-03B/beans/Bb.java @Named
@RequestScoped
public class Bb implements Serializable { @Inject
private List<Book> books;
// 以下省略
}
Sample17-03Bプロジェクトは、インジェクトされた このbooksオブジェクトをブラウザに表示します。実 行して、結果を確認してください。
4.3 フィールドに@Producersを付ける
データベースを操作するために使うEntityManager(詳細は15章)は、
@Inject
で取り込 むことはできず、次のように、@PersistenceContext
を使って取り込みます。// データベース
@PersistenceContext(unitName = "defaultUnit") private EntityManager defaultEm;
しかし、これに
@Produces
を付けると、オブジェクトの生成に@Produces
を付けるの 青字にする120 を挿入
トルツメ
12
と同じですから、他のクラスでは
@Inject
で取り込むことができるようになります。@Produces
@PersistenceContext(unitName = "default") private EntityManager defaultEm;
4.4 インジェクトできるロガーを作る
これまでプログラムの実行状況を見るために
System.out.println
を使ってきまし たが、本来、java.util.logging
パッケージ(モジュールはjava.logging
です)のLogger
(ロガー)を使ってログとして記録すべきものです。ただ、
Logger
クラスはCDIビーンではないのでインジェクトできません。そこで、@Produces
を使ってインジェクトできるようにしてみます。※ Loggerの使い方は、コラム「Loggerの使い方」を参照してください
すでに見たように、CDIビーンのクラスの中に
Logger
オブジェクトを作成して返すメ ソッドを作って、@Produces
アノテーションを付けます。全体は次のような形のクラスに なるでしょう。@Dependent
public class LoggerProducer { @Produces
public Logger createLogger(){
Logger lg = ・・・ // ロガーを作成
return lg; // ロガーを返す
} }
Logger
を作成する方法ですが、Logger
オブジェクトは、Logger
クラスのgetLogger
メソッドを使って次のように取得します。
Logger lg = Logger.getLogger( ロガーの名前 );
getLogger
メソッドの引数はロガーに付ける名前文字列ですが、任意の名前ではなく、ロガーを使用するクラスの名前(パッケージ名を含む)を使うことが強く推奨されています。
すると、ロガーを使用する(インジェクト先の)クラス名をどうやって取得するかという 問題がありますが、これについては
jakarta.enterprise.inject.spi
パッケージにトルツメ
12
ある
InjectionPoint
が役に立ちます。実は、インジェクトされるCDIビーンは、インジェクト先のスコープを知る必要がある ので、インジェクト先の情報を持つオブジェクトをCDIサービスから受け取れるようになっ ています。これが、
InjectionPoint
型のオブジェクトで、@Inject
を使って簡単に受 け取れます。@Dependent
public class LoggerProducer { @Inject
InjectionPoint point;
・・・
}
この
InjectionPoint
オブジェクトを使うと、インジェクト先のクラス名がわかるので それを引数にしてロガーを作成すればよいわけです。
InjectionPoint
からインジェクト先のクラス名を得るには、次のようにします。point.getMember().getDeclaringClass().getName();
getMember()
は イ ン ジ ェ クト さ れ た オ ブ ジ ェ クト( こ の 場 合Logger
)を 返 し、getDeclaringClass()
はインジェクト先のクラスを返します。最終的に、getName()
でそのクラス名を取得できます。
以上から、目的のCDIビーンクラスは次のようになります。
インジェクトできるロガー sample17-04/beans/LoggerProducer.java 例題7
1 @Dependent
2 public class LoggerProducer {
3 @Inject
4 InjectionPoint point;
5 @Produces
6 public Logger getLogger() {
7 String className = point.getMember().getDeclaringClass().getName();
8 Logger logger = Logger.getLogger(className);
9 return logger;
10 }
11 }
※ 3、4行目の@Inject InjectionPoint point;は、Loggerをインジェクトした時に実行されるので、
12
この
LoggerProducer
クラスを作っておくと、次のように、目的のクラスにスマートにLogger
をインジェクトできます。@Inject Logger log;
ただし、
Logger
はSerializable
インタフェースを実装していないのでシリアライズ できません。そこで、インジェクト先が@SessionScoped
や@ConversationScoped
である場合は、シリアライズ処理から除外するように
transient
修飾子を付けます。transient
はシリアライズしないという意味です。@Inject
transient Logger log;
ロガーの使い方は、コラム「ロガーの使い方」を見てください。
コラム ロガーの使い方
ロガーでのメッセージ出力は、
info
(情報)、warning
(警告)、severe
(エラー)な どに分かれています。どの種類のメッセージでも出力できますが、setLevel
メソッ ドでどのレベル以上のものを実際に出力するか指定します。例えば、次のようにすると "警告メッセージ" と "エラーメッセージ" だけが出力さ れます。
@Inject Logger log;
log.setLeveL(LEVEL_WARNING); // 出力メッセージレベルを「警告」レベルに設定
log.info("情報メッセージ"); // infoレベルメッセージを出力
log.warning("警告メッセージ"); // warningレベルメッセージを出力
log.severe("エラーメッセージ"); // severeレベルメッセージを出力
Logger
には、以下に示すようなログレベルがあり、それに応じて記録に使うメソッドが違います。デフォルトは
LEVEL.INFO
になっているのでINFO
、WARNINIG
、SEVERE
のログが出力されます。12
ログレベル定数 意 味 記録メソッド
LEVEL.ALL すべてのログを取る LEVEL.OFF ログを出力しない LEVEL.CONFIG 設定情報に関するログ
LEVEL.FINEST 大量のトレース情報 finest(〜) LEVEL.FINER 特定の処理についての開始、終了情報など finer(〜)
LEVEL.FINE トレース情報ログ fine(〜)
LEVEL.INFO 処理確認のための情報出力のログ info(〜) LEVEL.WARNING 警告レベルのメッセージ。処理は継続可能 warning(〜) LEVEL.SEVERE 重大な障害を示すメッセージ。継続不能 severe(〜)
4.5 後始末をするためのディスポーザー
@Produces
でインジェクトできるようにしたオブジェクトのうち、使用後にclose処理 が必要なものがあります。例えば、ファイルやデータベース処理に関係するオブジェクト です。そこで、@Produces
でインジェクトしたオブジェクトの終了処理を行うのが、ディ スポーザーです。ここでは、ファイル出力するロガーのケースを使って、解説します。ログをファイルにも出力するようにする
ログをファイルに出力するのは簡単です。ロガーがログをファイルに送れるようにする ファイルハンドラーを作成し、ファイルハンドラーに、出力形式を指定するためのフォー マッターを指定します。次のようにします。
① Handler handler
=new FileHandler("w:/temp/log.txt", 1000000, 1, true); // ハンドラーを作成
② handler.setFormatter(new SimpleFormatter()); // フォーマッタを指定
③ logger.addHandler(handler); // ロガーにセットする
①FileHandlerの作成
ファイルパス、最大ファイルサイズ、ローテーションするファイルの数、追記モードに するかどうか(
true
で追記になる)を指定します。ここでは、wドライブのtempフォルダ にlog.txt
という名前のファイルを作ることとし、最大ファイルサイズは1MB、1つの ログファイルに追記モードで記録するようにしています。ログファイルを入れるフォルダ(temp)はすでに存在していなければいけません。また、
絶対パスで指定する必要があります。Macでは、"
/user/<home>/temp/log.txt
" な12
どと指定するといいでしょう。なお、最大ファイルサイズを超えると、新しいファイル が開かれます。
②フォーマッタの作成
フォーマッタは、出力データを整形するためのパーツです。指定しないと、XML形式 になってしまうので、JDKが最初から持っているSimpleFormatterを指定します(フォー マッタは自作できますが、ここでは省略します)。
③ロガーにセットする
最後に、作成したハンドラーをaddHandlerメソッドでロガーに追加します。
ファイルハンドラーを閉じる
以上で、ログがファイルに出力されるようになりますが、Loggerをインジェクトしたオ ブジェクトが破棄される時、ファイルハンドラーも閉じておく必要があります。そうしな いと、ログファイルが永遠に書き込み保護の状態のままになり、新しくログを書き込むた びに、新しいログファイルが生成されます。何十ものログファイルが生成されて、期待し たようなログは得られません。
ファイルハンドラーを閉じるには、ロガーからすべてのハンドラー(コンソールハンドラ、
ファイルハンドラなど)を取得し、それらをすべて閉じます。次のようにします。
Handler[] hs = log.getHandlers(); // ロガーからハンドラの配列を得る
for (Handler h : hs) { // すべてのハンドラをクローズする
h.close();
}
この部分を、Disposerとして作成します。
すべてを組み込んだ