はじめに
この記事は、Java EE/ Jakarta EE で Web アプリケーションを開発しているが「画面遷移すると管理 Bean の値がリセットされてしまう」という悩みを持つ中級者を対象にしています。
記事を読み終えると、CDI(Contexts and Dependency Injection)が提供する @ConversationScoped を使って、複数画面にまたがるワンセットの処理(ワザード形式や入力確認画面など)で値を保持しながら遷移する方法が身に付きます。特に、FacesContext や Flash スコープを使わずにタイプセーフにデータを受け渡す実装パターンを丸ごと習得できます。
前提知識
- Java の基本的な文法(クラス、インターフェース、アノテーション)
- JSF(JavaServer Faces)2.3 以降の基礎知識(画面と BackingBean の紐付け方)
- CDI のビルトインスコープ(
@RequestScoped、@SessionScoped)を使ったことがあること - Maven もしくは Gradle での依存管理ができること
なぜ「画面遷移時のデータ保持」が面倒なのか
HTTP はステートレスなプロトコルです。Java EE ではリクエスト単位で @RequestScoped な Bean が生成されるため、次の画面に遷移するときに値が消えてしまいます。
@SessionScoped を使えば値は保持されますが、ブラウザタブを複数開いたときに値が混ざる恐れがあり、また不用意にメモリを消費します。
@ViewScoped は JSF 専用で便利ですが、URL を直接叩かれたり F5 更新すると状態がリセットされます。
そこで登場するのが @ConversationScoped です。明示的に開始・終了させることで、複数画面にまたがる「会話(Conversation)」を実現します。
@ConversationScoped で管理 Bean を実装する
以下、手順を追って説明します。サンプルは Maven プロジェクトで、 Jakarta EE 10 + JSF 3.0 として動作確認しています。
ステップ1 依存を pom.xml に追加
@ConversationScoped は CDI 標準なので、すでに Jakarta EE プラットフォームに含まれています。
ただし、実装には Weld を使う場合、明示的にバージョンを固定しておくとトラブルが減ります。
Xml<dependency> <groupId>jakarta.platform</groupId> <artifactId>jakarta.jakartaee-api</artifactId> <version>10.0.0</version> <scope>provided</scope> </dependency> <!-- ビルド時に Weld を使う場合 --> <dependency> <groupId>org.jboss.weld.servlet</groupId> <artifactId>weld-servlet-core</artifactId> <version>5.1.0.Final</version> </dependency>
ステップ2 Conversation Bean を作成
Javapackage com.example.wizard; import jakarta.inject.Named; import jakarta.enterprise.context.Conversation; import jakarta.enterprise.context.ConversationScoped; import jakarta.inject.Inject; import java.io.Serializable; @Named @ConversationScoped public class WizardBean implements Serializable { @Inject private Conversation conversation; private String userName; private String email; private String address; // 会話を開始 public void start() { if (conversation.isTransient()) { conversation.begin(); // 必要に応じてタイムアウトを長めに設定(秒) conversation.setTimeout(1_800L); // 30分 } } // 会話を終了 public String finish() { if (!conversation.isTransient()) { conversation.end(); } return "complete?faces-redirect=true"; } // 次画面へ public String next() { return "step2?faces-redirect=true"; } // 前画面へ public String back() { return "step1?faces-redirect=true"; } // Getter/Setter は省略 }
ポイント
1. @ConversationScoped を付けると、CDI が「Conversation」コンテキストを管理してくれます。
2. Conversation#begin() で cid(Conversation ID)が発行され、以降同一 cid がリクエストパラメータに含まれている限り同じ Bean インスタンスが使われます。
3. end() すると cid は破棄され、以降は新規インスタンスになります。
ステップ3 画面側で cid を引き継ぐ
JSF の h:button / h:link タグを使うと、自動的に ?cid=xx が付与されます。
あるいは、従来通り h:commandButton でアクション経由で遷移しても、FacesContext 内に cid が維持されるため、意識しなくて OK です。
step1.xhtml(抜粋)
Xml<h:form> <h:outputLabel value="名前"/> <h:inputText value="#{wizardBean.userName}"/> <h:commandButton value="次へ" action="#{wizardBean.next()}"/> </h:form>
step2.xhtml
Xml<h:form> <h:outputLabel value="メール"/> <h:inputText value="#{wizardBean.email}"/> <h:commandButton value="戻る" action="#{wizardBean.back()}"/> <h:commandButton value="完了" action="#{wizardBean.finish()}"/> </h:form>
ハマりどころ1 cid が付かない=新しい Bean が作られる
状況:step1 → step2 へ遷移したのに値が null。
原因:start() を呼んでいない、あるいは begin() した cid がリクエストに含まれていない。
解決:遷移先が faces-redirect=true の場合、手動で cid を引き継ぐ必要がある。
h:link / h:button を使うか、ExternalContext#redirect() 時に conversation.getId() で cid を取得してクエリパラメータに付与する。
ハマりどころ2 タイムアウト値とセッションタイムアウトの兼ね合い
状況:30 分放置後に遷移すると値が空。
原因:Conversation のタイムアウトは設定したが、HttpSession のタイムアウトが 15 分だった。
解決:セッションタイムアウト >= 会話タイムアウトになるよう web.xml で調整するか、逆に会話タイムアウトを短くする。
Xml<session-config> <session-timeout>60</session-timeout><!-- 60分 --> </session-config>
ハマりどころ3 ブラウザ バックで二重送信
状況:step2 で完了ボタンを 2 回押すと、同じ会話で二重処理が走る。
解決:POST-REDIRECT-GET パターンを徹底し、完了後は必ず @ConversationScoped を終了させて、次に使えないようにする。
上記コードでは finish() メソッド内で conversation.end() を呼んでいるため、再リロードすると新しい会話が始まり二重処理を回避できる。
まとめ
本記事では、Java EE/Jakarta EE が提供する @ConversationScoped を使って、画面遷移しても値を保持する管理 Bean の実装方法を解説しました。
@ConversationScopedは、明示的に begin/end することで複数画面にまたがる会話を実現- cid をリクエストに付与し続けることで、同じ Bean インスタンスが使われる
- タイムアウトやセッション管理に注意すれば、メモリ効率もよく安全に利用可能
この記事を通して、Session スコープほど重くなく、Request スコープより寿命を制御できる @ConversationScoped の有用さを実感してもらえれば幸いです。
次回は、同じ考え方を活用して「複数タブで会話を独立させる」方法や、Ajax による部分更新と組み合わせた際の落とし穴について掘り下げていきます。
参考資料
- Jakarta EE 公式 CDI ガイド
- Weld リファレンス
- 『Java EE 7 徹底ガイド』(秀和システム)
