システム開発で思うところ

Javaで主にシステム開発をしながら思うところをツラツラを綴る

BusyConversationException、NonexistentConversationExceptionからの画面遷移

vermeer.hatenablog.jp

に続けて

vermeer.hatenablog.jp

でも、一旦 保留とした BusyConversationExceptionが発生した場合の制御です。

実行時例外はエラー画面へ遷移させるというのが基本原則だとは思いますが、コミットした情報(例えば注文)について把握しないまま再操作をしてしまうと2重登録(もしくは注文)をしてしまうことに繋がり兼ねません。 考慮すべきことは、大体 2重サブミットと同じだと思います。そのあたりを鑑みながら仕様を考えます。

なお、連打による過剰スレッド発生そのものの課題については、JavaScriptによるモーダルとか、WAFとか そういうDDos攻撃的なものへの対処と合わせて対応する事も前提としておきます。特に JavaScriptによる制御を考慮すると、基本的に「ボタン連打」については、かなり確率的に低いとは思っています。だからといってサーバーサイドで対処を全くしないというわけにはいきませんので考えておくべきです。

最低限やりたいこと

  • BusyConversationException、NonexistentConversationException になったら、会話開始ページへ強制遷移させる
  • メッセージで どうして会話開始ページへ遷移させられたか分かるようにする

わかったこと・できること

分かったこと、留意しておくべきこと

  • Servlet Filter内でも DIは出来る(少なくともApplicationScopedであれば問題ない)。
  • Servlet FilerではFacesContext#getCurrentInstance()からはnullが返却される。
  • web.xmlCDI Conversation Filterを設定するとExceptionがFilterで扱われるので、CustomExceptionHandlerに制御が移らない。
  • Servlet Filterから別の画面へ遷移しながらデータを渡したい場合は、forwardでパラメータで渡さないといけない。
  • Servlet Filterでは 画面遷移の指定くらいはできるけど、それ以上の制御は出来ない。
  • Servlet Filterから forwardで JSFxhtmlページには遷移できない。*1
  • Servlet Filterから forwardで JSPには遷移できる。

実装

エラーをServletFilterで捕捉

/**
 * Conversationに関する例外を扱うフィルターです.
 *
 * @author Yamashita,Takahiro
 */
public class ConversationExceptionFilter implements Filter {

    /**
     * {@inheritDoc }
     */
    @Override
    public void init(FilterConfig filterConfig) {
    }

    /**
     * {@inheritDoc }
     */
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        try {
            chain.doFilter(request, response);
        } catch (BusyConversationException | NonexistentConversationException ex) {

            HttpServletRequest httpServletRequest = (HttpServletRequest) request;
            String servletPath = httpServletRequest.getServletPath();
            String indexRootPath = servletPath.substring(0, servletPath.lastIndexOf("/") + 1);
            String indexPage = indexRootPath + "index.xhtml";

            String exception = ex instanceof BusyConversationException
                               ? BusyConversationException.class.getCanonicalName()
                               : NonexistentConversationException.class.getCanonicalName();

            request.setAttribute(ConversationExceptionKey.START_PAGE, indexPage);
            request.setAttribute(ConversationExceptionKey.EXCEPTION, exception);
            httpServletRequest.getRequestDispatcher("/parts/conversation/forward-jsf.jsp").forward(request, response);

        }
    }

    /**
     * {@inheritDoc }
     */
    @Override
    public void destroy() {
    }

}

ポイントは、異常のトリガーとなったURLとExceptionの情報を渡すために forwardを使っているところです。
JSFへ遷移したらエラーになりました。
Servlet Filterは、まだJSFのライフサイクル前だからかな?と思います。
仕方が無いので JSPを経由させることにしました。
ついでにパラメータも渡します。

JSFのページの橋渡し

中身のない、ただリダイレクトするためだけのJSPです。

forward-jsf.jsp

<%@page contentType="text/html" pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<!DOCTYPE html>
<%@ page import="spec.scope.conversation.ConversationExceptionKey"%>

<%
    String conversationException = (String) request.getAttribute(ConversationExceptionKey.EXCEPTION);
    String conversationStartPage = (String) request.getAttribute(ConversationExceptionKey.START_PAGE);
%>
<c:redirect url="/parts/conversation/conversation-exception-handler.xhtml">
    <c:param name="conversation-start-page" value="<%=conversationStartPage%>"/>
    <c:param name="conversation-exception" value="<%=conversationException%>"/>
</c:redirect>

エラー制御をするためのJSF

会話開始ページへ遷移するためのJSFです。
これも、先のJSPと同じくリダイレクトすることを目的とした共通のJSFページです。
更に経由ページを設けている理由は

  • 遷移前のJSPからパラメータを受領したかった
  • viewActionを使って会話開始ページへ遷移をさせたかった

ためです。

conversation-exception-handler.xhtml

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:f="http://xmlns.jcp.org/jsf/core"
      >

    <f:metadata>
        <f:viewParam name="conversation-start-page" value="#{conversationExceptionHandler.startPage}"/>
        <f:viewParam name="conversation-exception" value="#{conversationExceptionHandler.exception}"/>
        <f:viewAction action="#{conversationExceptionHandler.forward()}"/>
    </f:metadata>

</html>

ManagedBeanでは、遷移後の画面でメッセージを出力するための処理も行っています。
今回は 簡単に情報を引き渡す手段として Flashを使いました。*2

画面遷移および 後述するテンプレートからのメッセージ出力処理も以下のクラスで実施しています。
(System.out.println・・はデバッグ用の記述です。本来は不要なものです。消し忘れていました)

/**
 * ConversationExceptionが発生した際に画面遷移とメッセージ出力を行う機能を提供します.
 *
 * @author Yamashita,Takahiro
 */
@Named
@RequestScoped
public class ConversationExceptionHandler {

    private String exception;
    private String startPage;
    private MessageConverter messageConverter;
    private MessageWriter messageWriter;

    public ConversationExceptionHandler() {
    }

    @Inject
    public ConversationExceptionHandler(MessageConverter messageConverter, MessageWriter messageWriter) {
        this.messageConverter = messageConverter;
        this.messageWriter = messageWriter;
    }

    public String forward() {
        FacesContext.getCurrentInstance().getExternalContext().getFlash()
                .put(ConversationExceptionKey.EXCEPTION, this.exception);
        return this.startPage + "?faces-redirect=true";
    }

    public void writeMessage() {
        Flash flash = FacesContext.getCurrentInstance().getExternalContext().getFlash();
        String value = (String) flash.get(ConversationExceptionKey.EXCEPTION);
        if (value == null || value.equals("")) {
            return;
        }
        System.err.println("ttttt");
        String message = messageConverter.toMessage(value);
        messageWriter.appendErrorMessage(message);

        // 一度だけメッセージ出力をするために 共通メソッドで trueにしている設定を falseで上書きします
        flash.setKeepMessages(false);

        flash.remove(ConversationExceptionKey.START_PAGE);
        flash.remove(ConversationExceptionKey.EXCEPTION);
    }

    public String getException() {
        return exception;
    }

    public void setException(String exception) {
        this.exception = exception;
    }

    public String getStartPage() {
        return startPage;
    }

    public void setStartPage(String startPage) {
        this.startPage = startPage;
    }

}

テンプレート

会話スコープを使用するxhtmlにおいて共通で使用するxhtmlに、メッセージ出力の画面開設処理(conversationExceptionHandler.writeMessage())を追加しました。
URLを示すxhtmlではないので、f:eventを使います。

<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html>

<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:ui="http://java.sun.com/jsf/facelets"
      xmlns:h="http://java.sun.com/jsf/html"
      xmlns:f="http://xmlns.jcp.org/jsf/core"
      >

    <h:head>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
        <h:outputStylesheet library="css" name="base.css" />
        <h:outputStylesheet library="css" name="validation.css" />
        <link rel="stylesheet" type="text/css" href="https://cdn.jsdelivr.net/semantic-ui/2.2.10/semantic.min.css" />

        <f:event type="postAddToView" listener="#{conversationLifecycleManager.startConversation()}" />
        <f:event type="preRenderView" listener="#{conversationExceptionHandler.writeMessage()}" />
    </h:head>

    <h:body>
        <div id="page-content">
            <ui:insert name="content">Content</ui:insert>
        </div>
        <div id="page-bottom">
            <ui:include src="./bottom.xhtml"/>
        </div>
    </h:body>

</html>

Javaコードは上述の通りです。

実行

更新処理を行うControllerの 一覧へ戻るリンクに対するアクションに対して タイマーを設けました。

@Controller
public class UserUpdateAction {

 (略)

    @EndConversation
    public String fwTop() {

        try {
            Thread.sleep(3000);
        } catch (InterruptedException ex) {
        }
        return "index.xhtml";
    }
}

ダブルクリックをすると、BusyConversationExceptionがスローされます。
また、urlのcid=1というConversationIdを 存在しない数値(例えば、3)にして遷移をしようとしたらNonexistentConversationExceptionがスローされます。

いずれに例外でも、メッセージとして それぞれの実行時例外に合わせたメッセージが 一覧に表示されます。

Code

タイマーを設けて、BusyConversationExceptionを発生させるようのもの

vermeer_etc / jsf-ddd / source / — Bitbucket

タイマーの実装を外したもの

vermeer_etc / jsf-ddd / source / — Bitbucket

参考

リダイレクトとフォワードの違いを知る:JavaTips 〜JSP/サーブレット編 - @IT

JSF勉強メモ - 気まぐれラボラトリィ

jsp - HttpServletRequest for jsf - Stack Overflow

https://community.liferay.com/ja/forums/-/message_boards/message/53411185

さいごに

結構、煮詰まった上での苦肉の策です。
なんとか、形になったので 正直 ほっとしています。

より良いやり方をご存知の方がいらっしゃったら、twitterでもコメントでもアドバイスなど頂けると非常に嬉しいです。


*1:厳密には、拡張子がxhtmlというだけだったら遷移できるし、ManagedBeanの値を参照するくらいならできる。でもアクションなどは出来ない

*2:共通部品であれば用途目的も明確なので Flashを使っても良いと思いますが、個人的には業務ロジックで Flashを使うのは好ましいと思っていません。
型クラスに相当する Scopedを持ったManagedBeanを使う方が良いと思っています。