JSF2.0のエラーハンドリング
JSF2.0のエラーハンドリングについて調べてみたのでまとめる。
ここで言う『エラーハンドリング』とは、Struts1.xの<globa-exceptions>や、org.apache.struts.action.ExceptionHandlerを継承してユーザが作成するカスタム例外ハンドラを想定しており、Struts1.xと同じようなことがJSF2.0でも可能か確かめることが目的だ。
今回は、以下のようなバッキングBeanからランタイム例外が投げられた場合に、スタックトレースが表示されないようにしたい。
@Model public class BookController { @EJB private BookService service; @Inject private Book book; public String createBook() { service.persist(book); // ここから投げられるランタイム例外に対応したい return "nextpage.xhtml"; } }
何もエラーハンドリングしないと、以下のような画面が表示される。スタックトレースにはアプリケーションの内部構造を示す情報も含まれており、セキュリティという観点でもあまり好ましくない*1。
JSF2.0においてエラーハンドリングは大きくわけて2つの方法がある。
1. web.xmlに<error-page>を設定する
Servletに含まれる機能で、Struts1.xを使っている方にもおなじみだと思うが、web.xmlに例外クラス名やHTTPレスポンスコードを定義し、対応する遷移先を設定することが可能だ。例えば、なんでも良いので例外が投げられたらエラーページに遷移したい場合は以下のように設定する。
<error-page> <exception-type>java.lang.Exception</exception-type> <location>/error.xhtml</location> </error-page>
このやり方はシンプルで良いが、例えば遷移先画面に何かしら動的なメッセージを表示したい場合に困る。またJSF(mojjara2.1)の場合、バッキングBeanのアクションメソッドからスローされた例外はjavax.faces.el.EvaluationExceptionでラップされているため、アプリケーション例外に応じた遷移先を設定するのが難しい。*2
例えば、以下のような設定は、意図したように動かない。
<error-page> <!-- DataAccessExceptionがバッキングBeanから投げられても、 EvaluationExceptionでラップされるので、error.xhtmlには遷移しない。 --> <exception-type>sample.DataAccessException</exception-type> <location>/error.xhtml</location> </error-page>
このようなケースに対応するためには、次に紹介するJSF2.0のカスタム例外ハンドラを使う。
2. カスタム例外ハンドラを作成する
カスタム例外ハンドラはJSF2.0から導入された機能で、FacesMessagesの追加、画面遷移の指定、ロギングなど、柔軟なエラーハンドリングを実現している。前述したアプリケーション例外sample.DataAccessExceptionのハンドリングをカスタム例外ハンドラで行いたい。
カスタム例外ハンドラは、javax.faces.context.ExceptionHandlerWrapperクラスを継承して作る。
public class DataAccessExceptionHandler extends ExceptionHandlerWrapper { private ExceptionHandler wrapped; public DataAccessExceptionHandler (ExceptionHandler wrapped) { this.wrapped = wrapped; } @Override public void handle() { for (Iterator<ExceptionQueuedEvent> it = getUnhandledExceptionQueuedEvents().iterator(); it.hasNext() == true;) { ExceptionQueuedEventContext eventContext = it.next().getContext(); // 1. ハンドリング対象のアプリケーション例外を取得 Throwable th = getRootCause(eventContext.getException()).getCause(); if (th instanceof DataAccessException) { FacesContext facesContext = eventContext.getContext(); // メッセージを追加する facesContext.addMessage(null, new FacesMessage(FacesMessage.SEVERITY_ERROR, "システム障害", "システム障害が発生しました。")); // 2. リダイレクトしてもFacesMessageが消えないように設定 facesContext.getExternalContext().getFlash().setKeepMessages(true); try { // エラー画面に画面遷移させる String contextPath = facesContext.getExternalContext().getRequestContextPath(); facesContext.getExternalContext().redirect(contextPath + "/error.xhtml"); } catch (IOException e) { System.out.println("error.xhtmlがありません"); } finally { // 3. 未ハンドリングキューから削除する it.remove(); } } } wrapped.handle(); } @Override public ExceptionHandler getWrapped() { return wrapped; } }
例外ハンドリング処理はhandle()メソッドに実装する。今回の例ではfaceletのxhtml上で>h:messeages>でメッセージを表示できるようにすることと、遷移先のエラー画面をリダイレクト指定した。そのなかでも3つのポイントがある。
1. ハンドリング対象の例外クラスはラップされている
// 1. ハンドリング対象のアプリケーション例外を取得
Throwable th = getRootCause(eventContext.getException()).getCause();
上記のようなコードで例外を取得している理由は、バッキングBeanから投げられた例外がこのカスタム例外ハンドラに至るまでに、様々な例外でラップされて到達するためである。
手元のJBossAS7.1.1で試した場合は、FacesException → (getCause) → FacesException → (getCause) → EvaluationException → (getCause) → DataAccessExceptionのように、アプリケーション例外が2重のFacesExceptionと、さらにEvaluationExceptionでラップされた状態で取得された。上位クラスでJSF(mojjara)が実装しているExceptionHandlerWrapper#getRootCause()でまずFacesExceptionの部分を取り除き、さらにgetCause()することでアプリケーション例外を抽出している。
2. Flash.setKeepMessages(true) を設定する
// 2. リダイレクトしてもFacesMessageが消えないように設定 facesContext.getExternalContext().getFlash().setKeepMessages(true);
この設定を忘れると、リダイレクトによりリクエストスコープに格納されたFacesMessagesが消えてしまい、メッセージが表示できない。setKeepMessages(true)により、リダイレクトの間はメッセージを保持してくれるようになる。
3. ハンドリングした後は、未ハンドリングキューから削除する
// 3. 未ハンドリングキューから削除する
it.remove();
拡張forループを使わずにループをまわしていたのは、この処理が必要であったため。ハンドリングが終わった例外はgetUnhandledExceptionQueuedEvents()から取得できる未ハンドル例外キューから削除する必要がある。この設定を忘れると、例えば複数のカスタム例外ハンドラを設定した場合に、他の例外ハンドラで処理が終わっているのにも関わらず、getUnhandledExceptionQueueEvents()で取得できてしまう。運が悪いと、重複したロギングや、思いもしない画面遷移の不具合を盛り込むことになるだろう。
カスタム例外ハンドラを作成したら、合わせてカスタム例外ハンドラファクトリを作成する。
作成したファクトリをfaces-config.xmlに登録したら完成だ。
/** * カスタム例外ハンドラファクトリの実装 */ public class DataAccessExceptionHandlerFactory extends ExceptionHandlerFactory { private ExceptionHandlerFactory parent; public DataAccessExceptionHandlerFactory(ExceptionHandlerFactory parent) { this.parent = parent; } @Override public ExceptionHandler getExceptionHandler() { ExceptionHandler handler = new DataAccessExceptionHandler(parent.getExceptionHandler()); return handler; } }
faces-config.xmlには以下のようにファクトリを登録する。
<factory> <exception-handler-factory>handler.DataAccessExceptionHandlerFactory</exception-handler-factory> </factory>
参考 JBoss Seem Solder だと簡単にできる
SpringMVCなどもっと簡単に例外ハンドリングできるフレームワークと比較すると、前述したJSFカスタム例外ハンドラは非常に面倒に感じる。さらにプログラム中でinstanceOfで例外クラスごとに条件分岐するのもあんまりいけてない。こんなときにはJBoss Seemに含まれるSolderを使うと、アノテーションベースで例外ハンドラが作成できるらしい(まだ実際に動かしてはいない)。
@HandlesExceptions public class CustomHandlers { void handle(@Handles CaughtException<Throwable> evt) { // 例外ハンドリング処理 } }
こういう便利そうな仕様がもっとJava EEに盛り込まれると嬉しい。
*1:参考 : 安全なウェブサイトの作り方 1-(iii) エラーメッセージをそのままブラウザに表示しない
*2:参考 GlassFishのフォーラム JSF: user exception thrown in action won't show web.xml error page