に続けて
でも、一旦 保留とした BusyConversationException
が発生した場合の制御です。
実行時例外はエラー画面へ遷移させるというのが基本原則だとは思いますが、コミットした情報(例えば注文)について把握しないまま再操作をしてしまうと2重登録(もしくは注文)をしてしまうことに繋がり兼ねません。 考慮すべきことは、大体 2重サブミットと同じだと思います。そのあたりを鑑みながら仕様を考えます。
なお、連打による過剰スレッド発生そのものの課題については、JavaScriptによるモーダルとか、WAFとか そういうDDos攻撃的なものへの対処と合わせて対応する事も前提としておきます。特に JavaScriptによる制御を考慮すると、基本的に「ボタン連打」については、かなり確率的に低いとは思っています。だからといってサーバーサイドで対処を全くしないというわけにはいきませんので考えておくべきです。
最低限やりたいこと
- BusyConversationException、NonexistentConversationException になったら、会話開始ページへ強制遷移させる
- メッセージで どうして会話開始ページへ遷移させられたか分かるようにする
わかったこと・できること
分かったこと、留意しておくべきこと
- Servlet Filter内でも DIは出来る(少なくともApplicationScopedであれば問題ない)。
- Servlet Filerでは
FacesContext#getCurrentInstance()
からはnull
が返却される。 - web.xmlに
CDI Conversation Filter
を設定するとExceptionがFilterで扱われるので、CustomExceptionHandler
に制御が移らない。 - Servlet Filterから別の画面へ遷移しながらデータを渡したい場合は、forwardでパラメータで渡さないといけない。
- Servlet Filterでは 画面遷移の指定くらいはできるけど、それ以上の制御は出来ない。
- Servlet Filterから forwardで JSFのxhtmlページには遷移できない。*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です。
<%@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
jsp - HttpServletRequest for jsf - Stack Overflow
https://community.liferay.com/ja/forums/-/message_boards/message/53411185
さいごに
結構、煮詰まった上での苦肉の策です。
なんとか、形になったので 正直 ほっとしています。
より良いやり方をご存知の方がいらっしゃったら、twitterでもコメントでもアドバイスなど頂けると非常に嬉しいです。