エンタープライズギークス (Enterprise Geeks)

企業システムの企画・開発に携わる技術者集団のブログです。開発言語やフレームワークなどアプリケーション開発に関する各種情報を発信しています。ウルシステムズのエンジニア有志が運営しています。

JavaEE7をはじめよう(15) - CDI 会話スコープ

前回までで、CDIの基本的な仕組みを紹介した。今回は、CDIのスコープの中でも特殊なスコープである、会話スコープ(@ConversationScoped) について解説する。

@RequestScoped, @SessionScopde, @ApplicationScopedServletのスコープにもあるので、多くの方にとって馴染み深いだろう。これらのスコープの場合、インスタンスの生成や破棄はCDIが自動で行う。

一方、会話スコープはセッションスコープの範囲内で開始と終了を明示する必要がある。 業務アプリケーションでは、セッションを論理的に分割したい場合がよくある。例えば、アプリケーションのある機能を開始してから終了するまでの間だけ有効なセッションが欲しい場合や、ブラウザのウィンドウを複数起動してウィンドウごとに独立したセッションデータが欲しい場合などだ。 会話スコープはそのような場合に使用する。

Bean の定義

会話スコープのCDI Bean を定義するには、クラス宣言に@ConversationScopedスコープを付与し、フィールドにConversation型の変数を定義する。

簡単な例として、カウントを保持する会話スコープのBeanの例を掲載する。

(なお、サンプルプログラムの全体はこちらの example.cdi.conversation パッケージを参照いただきたい。)

@ConversationScoped
@Named // JSPで参照するため設定
public class ConvBean implements Serializable {
    /** 会話スコープごとのカウント */
    private int count = 0;
    
    @Inject 
    private Conversation conv;
    
    /** 会話の開始 */
    public void begin() {
        if (conv.isTransient()) {
            conv.begin();
        }
    }
    /** 会話の終了 */
    public void end() {
        if (!conv.isTransient()) {
            System.out.println(conv.getId() + " close.");
            conv.end();
        }
    }

    /**
     * 会話のIDを取得する。
     * Conversation#beginを実行すると、ユニークIDが生成される。
     * 会話スコープを継続するときは、このIDをリクエストパラメータに含める。
     * @return CID
     */
    public String getCid() {
        return conv.getId();
    }
    
    public void countUp() {
        count++;
    }
    
    public int getCount() {
        return count;
    }
    
    @PreDestroy
    public void destroy() {
        System.out.println("Convesation destoryed.");
    }
}

会話を開始するサーブレットのプログラムを次に示す。

@WebServlet(urlPatterns = {"/newConversaion"})
public class NewConvServlet extends HttpServlet {

    // CDI Beanのインジェクション
    @Inject
    private ConvBean cbean;
    
    protected void doGet(HttpServletRequest request, 
                         HttpServletResponse response)
            throws ServletException, IOException {
        request.getSession();
        // 会話の開始を明示的に指示する。
        cbean.begin();
        
        request.getRequestDispatcher("conv.jsp")
               .forward(request, response);
    }
}

上記のサーブレットからフォワードするJSPの抜粋を以下に示す。 このJSPは、会話スコープで保持するIDとカウントを表示する。 hidden で設定しているパラメータのcidについては後述する。

    <body>
        
        <ul>
            <li>Conversation ID : ${convBean.cid}</li>
            <li>Counter : ${convBean.count}</li>
        </ul>
        <form method="GET" action="conversationCountup">
      <!-- リクエストにCIDを必ず含める -->
            <input id="cid" type="hidden" value="${convBean.cid}" name="cid"/>
            <input type="submit" value="CountUp"/>
        </form>
        <input id="end" type="button" value="End Conversion"/>
       
    </body>

次に示すのは、カウントアップ処理を行うサーブレットのコードである(サンプルではCountUpボタンが押されたときに実行するようにしている)。
このサーブレットは、会話スコープのオブジェクトをカウントアップし、上記のJSPを再表示させる。

@WebServlet(urlPatterns = {"/conversationCountup"})
public class CountUpServlet extends HttpServlet {

    // CDI Beanのインジェクション
    @Inject
    private ConvBean cbean;
    
    protected void doGet(HttpServletRequest request, 
                         HttpServletResponse response)
            throws ServletException, IOException {
        cbean.countUp();
        
        request.getRequestDispatcher("conv.jsp")
               .forward(request, response);
    }
}

最後に示すのは会話を終了させるサーブレットである(サンプルではEnd Conversationボタンが押されたときに呼び出される)。
終了後には会話スコープのオブジェクトは破棄されるので、再度会話スコープを開始するとカウントはリセットされる。

@WebServlet(urlPatterns = {"/closeConversation"})
public class CloseConvServlet extends HttpServlet {

    // CDI Beanのインジェクション
    @Inject
    private ConvBean cbean;
    
    protected void doGet(HttpServletRequest request, 
                         HttpServletResponse response)
            throws ServletException, IOException {
        // 会話の終了
        cbean.end();
        
        response.getWriter().close();
    }
}

Conversationクラス

Conversationクラス は会話の状態を管理するクラスである。
Bean側でConversationインスタンスを保持し、会話を明示的に開始することで、複数リクエストをまたいだBeanの状態が保持されるようになる。

会話の状態

会話の状態には、 transient(一時的)と、long-run(長期的)の2つがあり、デフォルトはtransient状態である。 どちらの状態にあるかは、isTransientメソッドの戻り値で判別できる(trueならtransient)。

transientモードの場合、CDI Bean はリクエストスコープと同じ動作をする。つまり、リクエストごとにBeanが生成される。

long-runモードの場合、CDI Bean はセッションスコープのようにリクエストをまたいで保持される。

transientモードからlong-runモードに切り替える(会話スコープを開始する)には、 begin メソッドを呼び出す。

beginメソッドには引数が無いものと、引数にIDを取るものがある。
long-runモードでは一意なIDが必要となるため、前者を呼んだ場合には内部でIDが生成される。そのため、通常は前者の方を使用すればよい。後者のbeginメソッドを使えば自前のIDにできるが、呼び出し側で一意なIDを生成する必要がある。

また、long-runモードになっている場合、当該会話スコープのIDは、getIdメソッドで取得できる。 会話スコープを正しく動作させるためには、このIDの値を受け渡す必要がある。

long-runモードからtransientモードに切り替える(会話スコープを終了する)には、endメソッドを呼び出す。

先ほどのConvBeanクラスのコードでは、会話スコープの開始・終了を外側(サーブレットなど、このBeanをインジェクションする側)に明示するため、begin, end, getId の呼び出しを委譲するメソッドとしてbegin, end, getCid を用意した。

タイムアウトの設定

明示的にendが呼ばれない状況(ブラウザを閉じた場合など)に対応するため、セッションのタイムアウトとは個別に、会話スコープにタイムアウトの設定を行うことができる。 そのためには、setTimeoutメソッドを呼び出し、タイムアウトまでの時間を設定する。

会話スコープの使用

前述したサーブレットプログラムにあるとおり、会話スコープを使うには、サーブレット等のプログラムでの開始(begin)と終了(end)の呼び出しが必要である。また会話スコープを継続するために、JSP や VIEWから会話スコープのIDをHTTPリクエストに載せる必要もある。

前述の conv.jsp では Form に cid という名前のINPUT要素を定義し、値に${convBean.cid} というEL式を記述した。

このEL式は、 @Namedアノテーションを設定した前述のCDI Beanの ConvBean#getCid の戻り値を指定することを指す。 会話スコープを持続させるためには、cidという名前でConversationのIDを設定して、HTTPリクエストに乗せる必要があるため、このような記述が必要となる。

cidを設定しない場合は、会話スコープがtransientモードとして扱われるので、CDI Beanが毎回生成される。

なお、JSFの場合は、会話スコープを使用すると自動でcidがHTTPリクエストに乗るようになっているため、開発者が明示的にcidを設定する必要はない。

複数ウィンドウ、タブの対応

会話スコープの用途は、範囲を限定したセッションスコープだけに限らない。
cidごとに独立した内容を保持できるため、新規ウィンドウの開設と同時に会話スコープを開始するようにしておけば、複数のウィンドウやタブで同じ処理を実行しても、ウィンドウやタブごとに独立した内容を保持できる。

以下のように、今回のサンプルでは、会話スコープを開始するサーブレットを 新しいタブやウィンドウで起動するようにしている。

<a href="newConversaion" target="_blank">CDIサンプル3(conversation)</a>

こうすることで、CIDがタブ・ウィンドウごとに別々になるため、各タブ・ウィンドウで個別のカウントを保持できる。

まとめ

今回は会話スコープについて解説した。

会話スコープは、セッションに格納するデータを論理的に分割できるため、セッション制御に便利な仕組みである。 とはいえ、開始・終了を明示したり、CIDを連携したりする必要がある。 特に終了処理を忘れないようにしないと、セッションデータが残存するので注意する必要がある。

また、タブ・ウィンドウごとに独立したデータを保持できるので、複数ウィンドウを使用するアプリケーションにも有効に使用できる。

[前多 賢太郎]