他のExceptionHandlerを実装している中で 以前の記事
で扱っていなかったタイムアウト周りの制御を やってみました。
ベースとするプロジェクトは、以前のサンプルのものではなく 以下のものに実装を加えて行いました。
やりたいこと
- ConversationScopedがタイムアウトしたら「会話スコープの開始位置」へ遷移
- もし 遷移先の画面描画でエラーがあったら、その時はどうしようもないのでエラー画面に遷移
会話スコープが終わった状態で操作をしたら NonexistentConversationException
がスローされます。
この際、他の例外と同じ扱いをしてエラー画面へ強制遷移させるというやり方もあると思います。
セッションタイムアウトでも無い「一定操作をしていなかったから その操作を無効にします」というだけで
エラー画面から、ときには再ログインを誘導するというのは ちょっと違うのではないかな?という理由です。
実装とポイント
ExceptionHandlerWrapper
JSFでの例外処理は ExceptionHandlerWrapper
を拡張したクラスで制御します
public class CustomExceptionHandler extends ExceptionHandlerWrapper { private final ExceptionHandler wrapped; private final ThrowableHandlerFactory throwableHandlerFactory; private final ErrorPageNavigator errorPageNavigator; CustomExceptionHandler(ExceptionHandler exception, ThrowableHandlerFactory throwableHandlerFactory, ErrorPageNavigator errorPageNavigator) { this.wrapped = exception; this.throwableHandlerFactory = throwableHandlerFactory; this.errorPageNavigator = errorPageNavigator; } @Override public ExceptionHandler getWrapped() { return this.wrapped; } @Override public void handle() { final Iterator<ExceptionQueuedEvent> it = getUnhandledExceptionQueuedEvents().iterator(); while (it.hasNext()) { ExceptionQueuedEventContext eventContext = (ExceptionQueuedEventContext) it.next().getSource(); Throwable throwable = getRootCause(eventContext.getException()).getCause(); ThrowableHandler throwableHandler = this.throwableHandlerFactory.createThrowableHandler(throwable, eventContext); try { throwableHandler.execute(); } catch (Exception ex) { this.errorPageNavigator.navigate(ex); } finally { // 未ハンドリングキューから削除する it.remove(); } getWrapped().handle(); } } }
コードは省略しますが
で紹介したように、DIもできます。
例外を制御
もし会話スコープの開始が任意のタイミングだったら結構面倒なことになったと思うのですが、方式として会話スコープの開始を常に index.xhtml
になるようにしているので、やることは単純です。
NonexistentConversationException
を受けたら index.xhtml
に遷移させるだけです。
画面遷移には ExternalContext
を使用します。
NavigationHandler
を使って画面遷移をしたのですが、フェーズのタイミングが違うのか 上手く行きませんでした。*1
なお、エラー画面への遷移は、個別の例外処理で行うのではなくて ThrowableHandler の具象クラスにおいて 実行時例外としてThrowさせて共通化を図っています。
あとメッセージ出力をさせるために、状態を持ちまわします。
FacesMessageへ出力する実装もしたのですが、それだとメッセージが出力されなかったための処置です。*2
/** * NonexistentConversationException の捕捉後の処理を行う機能を提供します. * <p> * 会話スコープが既に終わっている場合の実行時例外なので、会話のスタートに位置する{@code index.xhtml}へ遷移させる。 * 同時に会話スコープも終了(終了しているが念のため終了)させてから、再開させる。 * * @author Yamashita,Takahiro */ public class NonexistentConversationExceptionHandler implements ThrowableHandler { private final ConversationLifecycleManager conversationLifecycleManager; private final FacesContext facesContext; private final NonexistentConversationExceptionMessage nonexistentConversationExceptionMessage; public NonexistentConversationExceptionHandler(ConversationLifecycleManager conversationLifecycleManager, NonexistentConversationExceptionMessage nonexistentConversationExceptionMessage, FacesContext facesContext) { this.conversationLifecycleManager = conversationLifecycleManager; this.facesContext = facesContext; this.nonexistentConversationExceptionMessage = nonexistentConversationExceptionMessage; } /** * {@inheritDoc } * <p> * {@code NavigationHandler} では正しく画面遷移が実現しなかったので、{@code ExternalContext} で遷移させます. */ @Override public void execute() { conversationLifecycleManager.endConversation(); String contextPath = facesContext.getExternalContext().getRequestContextPath(); String currentPage = facesContext.getViewRoot().getViewId(); String indexRootPath = currentPage.substring(0, currentPage.lastIndexOf("/") + 1); String indexPage = indexRootPath + "index.xhtml"; String forwardPage = contextPath + indexPage; ExternalContext externalContext = facesContext.getExternalContext(); try { ServletContext servletContext = (ServletContext) facesContext.getExternalContext().getContext(); if (servletContext.getRealPath(indexPage) == null) { throw new ThrowableHandlerException("Target context file could not find."); } nonexistentConversationExceptionMessage.setException(); externalContext.redirect(forwardPage); } catch (IOException ex) { throw new ThrowableHandlerException(ex); } } }
会話開始画面へ遷移して会話を再開するときにメッセージを出力します。
/** * 会話スコープのライフサイクルを操作するクラスです. * * @author Yamashita,Takahiro */ @Named @ApplicationScoped public class ConversationLifecycleManager { (略) /** * 会話スコープの開始します。 * <P> * 会話スコープが未開始にもかかわらず、indexページ以外を遷移先として指定していた場合は、強制的にindexページへ遷移させます. * * @return 会話スコープ開始済みの場合は指定のページ、未開始の場合はindexページ */ public String startAndForwardIndexPage() { // NonexistentConversationException があった場合にメッセージを出力します. if (nonexistentConversationExceptionMessage.state() == NonexistentConversationExceptionMessage.State.HAS_EXCEPTION) { String message = this.messageConverter.toMessage(nonexistentConversationExceptionMessage.message()); this.messageWriter.appendErrorMessage(message); } String currentViewId = context.currentViewId(); if (this.conversation.isTransient() == false) { return currentViewId; } this.startConversation(); if (currentViewId.equals("index.xhtml") == false) { return (String) context.responseViewId("index.xhtml"); } return (String) context.responseViewId(currentViewId); } (略)
エラー画面への遷移をするクラス
これは、このクラスが読みだされる場合、必ずしも会話スコープを使っているとは限らないので、 念のため会話スコープを終了させつつ、エラー画面へ遷移させます。
@ApplicationScoped public class ErrorPageNavigator { private ConversationLifecycleManager conversationLifecycleManager; public ErrorPageNavigator() { } @Inject public ErrorPageNavigator(ConversationLifecycleManager conversationLifecycleManager) { this.conversationLifecycleManager = conversationLifecycleManager; } /** * エラー画面へ遷移させます. */ public void navigate(Exception ex) { System.err.println(ex.getMessage()); System.err.println(Arrays.toString(ex.getStackTrace())); conversationLifecycleManager.endConversation(); FacesContext facesContext = FacesContext.getCurrentInstance(); NavigationHandler navigationHandler = facesContext.getApplication().getNavigationHandler(); String forwardPage = "/error.xhtml?faces-redirect=true"; navigationHandler.handleNavigation(facesContext, null, forwardPage); facesContext.renderResponse(); } }
Code
vermeer_etc / jsf-ddd / source / — Bitbucket
参考情報
jjug-jsf/MyExceptionHandler.java at master · MasatoshiTada/jjug-jsf · GitHub
JSF2.0のエラーハンドリング - 見習いプログラミング日記
さいごに
NavigationHandler
で画面遷移が思ったように出来なかったのは想定外でしたが、こういうのも色々と学びにつながって良いですね。
ちなみに、まだ BusyConversationException
については対応していません。
どっかでやんないとなぁ。
追記(2018/12/6)
このやり方はスレッドアンセーフな気がします。
コードの記述を省略しているNonexistentConversationExceptionMessage
はスコープをSessionScopedにしており、複数の会話スコープが並列に存在すると適切に処理できない可能性が大きい気がします。
別対応に関連して、方式自体を見直しているため検証はしませんが、参考にされる方は ご自身でテストをするなどしてください。
補足として、メッセージを出力後 ステータスは初期化しているので「操作でエラーが出て、画面に表示されるまでのステータス」となっているので 概ね問題は発生しないとは思いますが、複数タブを開いて それぞれで操作をしていると 想定していない挙動になる可能性は あるにはあると思います。