メタデータの末尾に移動
メタデータの先頭に移動

MyFaces Core 2.0 およびそれ以降のバージョンのエラーハンドリング

JSF 2.0 以降では、独自の javax.faces.context.ExceptionHandler または javax.faces.context.ExceptionHandlerWrapper を実装して例外を処理することができるようになっています。それには、これらをラップ/オーバーライドするファクトリとなる独自のクラスを作成し、次の行を faces-config.xml に追加します。

faces-config.xml
<faces-config xmlns="http://java.sun.com/xml/ns/javaee" 
              xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
              xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-facesconfig_2_0.xsd" 
              version="2.0">

    <!-- ... -->
    <factory>
        <!-- ... -->
        <exception-handler-factory>org.apache.myfaces.context.ExceptionHandlerFactoryImpl</exception-handler-factory>
        <!-- ... -->
    </factory>
    <!-- ... -->
</faces-config>

次に示すのは、MyFaces コードからとった ExceptionHandlerFactory の例です。

ExceptionHandlerFactoryImpl.java
public class ExceptionHandlerFactoryImpl extends ExceptionHandlerFactory
{

    @Override
    public ExceptionHandler getExceptionHandler()
    {
        return new SwitchAjaxExceptionHandlerWrapperImpl(
                new MyFacesExceptionHandlerWrapperImpl(new ExceptionHandlerImpl()) , 
                new AjaxExceptionHandlerImpl());
    }
}

ラッパーが必要な場合は、次のようにします。

ExceptionHandlerFactoryImpl.java
public class ExceptionHandlerFactoryImpl extends ExceptionHandlerFactory
{

    @Override
    public ExceptionHandler getExceptionHandler()
    {
        return new CustomExceptionHandlerWrapper(getWrapped().getExceptionHandler());
    }
}

オーバーライドする最も重要なメソッドは、ExceptionHandler.handle() です。

MyFacesExceptionHandlerWrapperImpl.java
public class MyFacesExceptionHandlerWrapperImpl extends ExceptionHandlerWrapper
{
    //...
    public void handle() throws FacesException
    {
       //... 独自のコードをここに記述します ...
    }
}

ExceptionHandler の実装の動作について細かく知るには、MyFaces Core のソースコードを覗いてみてください。

MyFaces の ExceptionHandler

MyFaces Core では、例外を処理し、例外に関する詳細な情報を提供するための独自の ExceptionHandler が用意されています。この機能は 2.0.8/2.1.2 以降、web.xml ファイルで有効にしない限り、実稼働環境では無効になっています。有効にする場合は、必要以上の情報が表示されないよう、独自のエラーページも忘れずに用意しておきます。

  <context-param>
    <param-name>org.apache.myfaces.ERROR_HANDLING</param-name>
    <param-value>true</param-value>
  </context-param>

  <!-- <s16>"META-INF/rsc/myfaces-dev-error.xml"</s16> とは別の 
       "META-INF/rsc/myfaces-dev-error.xml" this param let you configure
       す (1.2.4-SNAPSHOT および 1.1.6-SNAPSHOT 以降) -->
  <context-param>
    <param-name>org.apache.myfaces.ERROR_TEMPLATE_RESOURCE</param-name>
    <param-value>META-INF/rsc/custom-dev-error.xml</param-value>
  </context-param>

エラーページの用意

実稼働環境すなわち MyFaces のエラーハンドリングが無効になっている場合、デフォルトの ExceptionHandler は単に例外をスローします。したがって、web.xml ファイルに次のように記述すれば、エラーページをセットアップすることができます。

	<error-page>
		<error-code>500</error-code>
		<location>/somepage.jsp</location>
	</error-page>

MyFaces Core 1.2 およびそれ以前のバージョンのエラーハンドリング

MyFaces では、バージョン 1.2.1 と 1.1.6 以降、JSF の全ライフサイクルを対象とした自動エラーハンドリングが含まれています (ほとんどは Facelets からとったもので、一部修正と追加を行っています)。このため、開発中のほとんどのプロジェクトでは、この新しいエラーハンドリングを使うことで目的の機能を実現できます。

こうした機能を使用したくない場合は、次のパラメータを設定することでエラーハンドリングを無効化または修正できます。

  <!-- 動作を完全に無効にする場合 -->
  <context-param>
    <param-name>org.apache.myfaces.ERROR_HANDLING</param-name>
    <param-value>false</param-value>
  </context-param>
  <!-- MyFaces + Facelets を使っている場合は次の設定も忘れないでください  -->
  <context-param>
    <param-name>facelets.DEVELOPMENT</param-name>
    <param-value>false</param-value>
  </context-param>

  <!-- "META-INF/rsc/myfaces-dev-error.xml" とは別の 
       リソーステンプレートファイルを使う場合は、次のパラメータを設定しま
       す (1.2.4-SNAPSHOT および 1.1.6-SNAPSHOT 以降) -->
  <context-param>
    <param-name>org.apache.myfaces.ERROR_TEMPLATE_RESOURCE</param-name>
    <param-value>META-INF/rsc/custom-dev-error.xml</param-value>
  </context-param>

  <!-- 例外処理に別のクラスを選択する場合、エラーハンドラーにはメソッド handleException(FacesContext fc, Exception ex) が含まれている必要があります -->
  <context-param>
    <param-name>org.apache.myfaces.ERROR_HANDLER</param-name>
    <param-value>my.project.ErrorHandler</param-value>
  </context-param> 

上の設定を行った場合は、一般的なサーバーエラーのハンドリングについて説明した部分に進んでください。

HTTP 500 などのサーバーエラーは、キャッチされなかった例外、JSF や Backing Bean の不足、不適切な URL など、さまざまな原因で起こります。こうしたエラーは開発中に発生するだけなら心配はいりませんが、複数のユーザーを対象に実際にアプリケーションを稼働している場合でも、これらのエラーをキャッチして適切に処理できるようにしておくことが重要です。

メーリングリストでは、以下に取り上げるようなさまざまなアプローチが議論されました。

デフォルトハンドラーを使用する

MyFaces には、JSP テンプレートファイル (META-INF/rsc/myfaces-dev-error.xml と META-INF/rsc/myfaces-dev-debug.xml) を使ってエラー処理を行うデフォルトエラーハンドラー (class javax.faces.webapp._ErrorPageWriter) があります。

独自のテンプレートファイルを定義するには、次のようにします。

  <context-param>
    <param-name>org.apache.myfaces.ERROR_HANDLING</param-name>
    <param-value>true</param-value>
  </context-param>

  <context-param>
    <param-name>org.apache.myfaces.ERROR_TEMPLATE_RESOURCE</param-name>
    <param-value>META-INF/rsc/mycustom-template-error.xml</param-value>
  </context-param>

Sandbox の org.apache.myfaces.tomahawk.util.ErrorRedirectJSFPageHandler を使用する

このハンドラーは、MyFaces のエラーハンドリング機能を使って、エラー発生時に JSF ページへのリダイレクトを行います。

リダイレクト用 JSF ページの例は、http://issues.apache.org/jira/browse/TOMAHAWK-1297 にあります。

このクラスは、MyFaces Core JSF で利用可能な
構成パラメータ org.apache.myfaces.ERROR_HANDLER として設定します(リファレンス実装では動作しません)。

アイデアとしては、MyFaces のエラーハンドリング機能を拡張し、エラー発生時には、
ナビゲーションルールを使って JSF ページへのリダイレクトを行えるようにするというものです。

このハンドラーがエラーを処理することができない場合、別のエラーハンドラーを
構成パラメータ org.apache.myfaces.ERROR_REDIRECT_ALTERNATE_HANDLER で設定できます。

JSF ページのエラーに関する情報は、次のようにして見つけることができます。

#{exceptionContext.cause} : 例外から取得された原因
#{exceptionContext.stackTrace} : 例外のスタックトレース
#{exceptionContext.exception} : このページによって処理された例外 
#{exceptionContext.tree} : エラーが発生したページのコンポーネントツリーの表示
#{exceptionContext.vars} : リクエストの環境変数

サーブレットを使用する

Mert Caliskan (http://www.jroller.com/page/mert?entry=handling_errors_with_an_errror) が説明しているアプローチでは、JSF サーブレットを新しいサーブレットでラップし、この新しいサーブレットが Faces サーブレットへの委譲を行いつつ、キャッチされない例外については自分で処理して、開発者が独自のエラーページへリダイレクトさせることができるようにします。

Andrea Paternesi は MyFaces 用にこのテクニックをさらに洗練させて、次のところで説明しています。
[http://patton-prog-tips.blogspot.com/2008/10/myfaces-handling-viewexpiredexception.html]

JSF を使用する

書籍の『Core Server Faces』には、サーブレットエンジンを使ってエラーをキャッチし、JSF エラーページに委譲する別の方法の解説があります。最初のアプローチほどエレガントではありませんが、この方法は一部のユーザーにとって次のような利点があります。

[1] 標準的な JSP のアプローチとフレームワークを使用している。
[2] 独自のサーブレットが不要である。
[3] web.xml でエラーハンドラーの定義を変更するだけで、簡単にカスタマイズ/修正できる。

この方法を実際に使うには、次の手順を実行します。

[1] web.xml でエラーハンドリング Web ページを定義する。

	<error-page>
		<error-code>500</error-code>
		<location>/ErrorDisplay.jsf</location>
	</error-page>

[2] エラーハンドラーの画面を作成します。JSF 1.1 の仕様の問題で、エラーハンドリングページでは <f:view> を使用することができず、subview を使用しなければなりません。

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<f:subview id="error"
    xmlns:f="http://java.sun.com/jsf/core"
    xmlns:t="http://myfaces.apache.org/tomahawk"
    xmlns:h="http://java.sun.com/jsf/html">

<html>
<head>
	<meta content="no-cache" http-equiv="Cache-Control" />
	<meta content="no-cache" http-equiv="Pragma" />
	<title>CMS - Error</title>
        <t:stylesheet path="#{SessionBean.styleSheet}" />
	</head>
	<body>
	<h:form>
           :
           : 通常のビューのセットアップ
           :
	   <h:outputText styleClass="infoMessage" escape="false" value="#{ErrorDisplay.infoMessage}" />
	   <t:htmlTag value="br" />
	   <h:inputTextarea style="width: 99%;" rows="10" readonly="true" value="#{ErrorDisplay.stackTrace}" />
           :
           : その他のビュー要素
           :
        </h:form>
    </body>
</html>
</f:subview>

[3] エラーを処理できるリクエストスコープの Backing Bean を作成します (faces-config.xml への登録も忘れないでください)。

ErrorDisplay.java

import cms.beans.framework.AbstractUIBean;

import com.c2gl.jsf.framework.ApplicationResource;

import java.io.PrintWriter;
import java.io.StringWriter;

import java.util.Map;

import javax.faces.context.FacesContext;

import javax.servlet.ServletException;

public class ErrorDisplay extends AbstractUIBean {
    private static final long serialVersionUID = 3123969847287207137L;
    private static final String BEAN_NAME = ErrorDisplay.class.getName();

    public String getInfoMessage() {
        return "An unexpected processing error has occurred. Please cut and paste the following information" + " into an email and send it to <b>" +
        some email address + "</b>. If this error " + "continues to occur please contact our technical support staff at <b>" +
        some phone number etc + "</b>.";
    }

    public String getStackTrace() {
        FacesContext context = FacesContext.getCurrentInstance();
        Map requestMap = context.getExternalContext().getRequestMap();
        Throwable ex = (Throwable) requestMap.get("javax.servlet.error.exception");

        StringWriter writer = new StringWriter();
        PrintWriter pw = new PrintWriter(writer);
        fillStackTrace(ex, pw);

        return writer.toString();
    }

    private void fillStackTrace(Throwable ex, PrintWriter pw) {
        if (null == ex) {
            return;
        }

        ex.printStackTrace(pw);

        if (ex instanceof ServletException) {
            Throwable cause = ((ServletException) ex).getRootCause();

            if (null != cause) {
                pw.println("Root Cause:");
                fillStackTrace(cause, pw);
            }
        } else {
            Throwable cause = ex.getCause();

            if (null != cause) {
                pw.println("Cause:");
                fillStackTrace(cause, pw);
            }
        }
    }
}

ExceptionUtils クラスも見てください。このクラスは実際の根本的原因の取得方法をカプセル化します。

	[1] List exceptions = ExceptionUtils.getExceptions(exception);
	[2] Throwable throwable = (Throwable) exceptions.get(exceptions.size()-1);
	[3] String exceptionMessage = ExceptionUtils.getExceptionMessage(exceptions);

[1] getRootCause が利用可能な場合は getRootCause、そうでなければ getCause を使って、すべての例外のリストを取得します。
[2] 初回例外を取得します。
[3] 初回例外からスタートしてメッセージのある最初の例外を取得します。

したがって、新しい fillStackTrace は次のようになります。

    private void fillStackTrace(Throwable ex, PrintWriter pw)
    {
        if (null == ex) {
            return;
        }

	List exceptions = ExceptionUtils.getExceptions(exception);
	Throwable throwable = (Throwable) exceptions.get(exceptions.size()-1);

        for (int i = 0; i<exceptions.size(); i++)
        {
            if (i > 0)
            {
                pw.println("Cause:");
            }
            throwable.printStackTrace(pw);
        }
    }

Backing Bean では、想定外の事態が起こったことをユーザーに知らせ、誰に連絡してどうすればよいかなどの指示を含むメッセージを作成しています。ユーザーが実際にエラーをカット&ペーストしてメールで送信するかどうかは、実際には問題ではありません。いずれにしても Tomcat のログには記録されるからです。ただ、ユーザー側で行えることを知らせ、問題の解決へ向けて協力してもらいたいという姿勢を示すことは、常に有効です

ViewExpiredException: No saved view state could be found for the view identifier

構成によっては、上に示したエラーハンドリング方法を使った場合に、この例外が発生することがあります。この問題は、エラーの結果がリダイレクトではなく、転送になる場合に起こります。''ViewHandler'' は、エラーが発生すると ''response.sendError()'' を呼び出し、''response.sendError()'' は ''web.xml'' の ''<error-page>'' 宣言を参照して、エラー URL への転送 (forward) を行います。エラー URL が ''FacesServlet'' によって拾われると (すなわち JSF URL の場合)、新しい JSF ライフサイクルがスタートします。しかし、これは転送なので、リクエストオブジェクトには、 ''"javax.faces.ViewState"'' など、リクエストパラメータのすべてが依然として含まれています。このため、このリクエストは ''ViewHandler'' にとってポストバックのように見えます。ポストバックである以上、 ''ViewHandler'' は保存されたビューを期待しますが、言うまでもなくこれは存在しません。 ''viewId'' は今はエラーページを参照しているからです。

この問題は、 ''ViewHandler'' をカスタマイズして、 ''response.sendError()'' の代わりに ''response.sendRedirect()'' を使うようにすれば解決できますが、この場合、エラーページのマッピングを指定するのに ''web.xml'' は使用することはできなくなります。

問題を解決する別の方法として、中間的なステップを使用し、エラーページとして JSF ではない URL を指定して、何らかの方法で JSF エラーページにリダイレクトするやり方があります。たとえば、404 エラーコードの場合、次のような内容の ''/error/404_redirect.html'' を指定します。

<html><head><meta http-equiv="Refresh" content="0; URL=/DPSV4/error/404.jsf"></head></html>

この方法で目的は達成できますが、コンテキストパスをハードコードしなければなりません。

私が最終的に採用した解決策は、 ''RedirectServlet'' を使うものです。

RedirectServlet.java
public class RedirectServlet extends HttpServlet {
	private static final String URL_PREFIX = "url=";

	@Override
	protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
		String query = req.getQueryString();
		if ( query.contains(URL_PREFIX) ) {
			String url = query.replace(URL_PREFIX, "");
			if ( !url.startsWith(req.getContextPath()) ) {
				url = req.getContextPath() + url;
			}
			
       		resp.sendRedirect(url);
		}
	}

	@Override
	protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
		doGet(req, resp);
	}

}

これを ''web.xml'' で次のように使います。

	<servlet>
	    <servlet-name>Redirect Servlet</servlet-name>
	    <servlet-class>ca.gc.agr.ops.web.jsf.redirect.RedirectServlet</servlet-class>
	</servlet>    
	<servlet-mapping>
	    <servlet-name>Redirect Servlet</servlet-name>
	    <url-pattern>/redirect</url-pattern>
	</servlet-mapping>

	<error-page>
		<error-code>403</error-code>
		<location>/redirect?url=/error/403.jsf</location>
	</error-page>
	<error-page>
		<error-code>404</error-code>
		<location>/redirect?url=/error/404.jsf</location>
	</error-page>
	<error-page>
		<error-code>500</error-code>
		<location>/redirect?url=/error/500.jsf</location>
	</error-page>

プレーン JSP を使用する

JSP ページで JSF の機能をまったく必要としない場合は、次の error.jsp を使う方法も十分検討に値します。

[http://svn.apache.org/viewvc/myfaces/tomahawk/trunk/examples/simple/src/main/webapp/error.jsp?view=markup]

web.xml の構成は次のようにします。

	<error-page>
		<error-code>500</error-code>
		<location>/error.jsp</location>
	</error-page>

この方法では、エラーページの表示に必要なテクノロジの数を減らすことができるので、エラーページの利用可能性を高めることができます。

Apache Geronimo JavaEE サーバーでの独自のエラーハンドラー

Apache Geronimo で独自のエラーハンドラー (org.apache.myfaces.ERROR_REDIRECT_ALTERNATE_HANDLER) を使う場合は、デプロイメントプラン (通常は geronimo-web.xml) を適切に設定しなければなりません。さもないと、クラスのクラスロードで問題が生じます。デフォルトでは、MyFaces のクラスは、org.apache.geronimo.framework.jee-specs/CAR の依存関係を通じてクラスパスにロードされます。これは一般的なケースでは問題になりませんが、独自のエラーハンドラークラスを使用するよう MyFace に指示した場合はエラーになります。なぜなら、MyFaces は class.forName() を呼び出して目的のクラスを見つけることができないからです。この問題を回避するのは簡単です。デプロイメントプランで myfaces-api と myfaces-impl に依存していることを明示的に指定し、hidden-classes 設定でクラスロードを修正します。

デプロイメントプランの該当箇所は、およそ次のようになるでしょう。

<dep:dependencies>
.
.
	<dep:dependency>
		<dep:groupId>org.apache.myfaces.core</dep:groupId>
		<dep:artifactId>myfaces-api</dep:artifactId>
		<dep:type>jar</dep:type>
	</dep:dependency>
	<dep:dependency>
		<dep:groupId>org.apache.myfaces.core</dep:groupId>
		<dep:artifactId>myfaces-impl</dep:artifactId>
		<dep:type>jar</dep:type>
	</dep:dependency>
.
.
</dep:dependencies>

<dep:hidden-classes>
	<dep:filter>javax.faces</dep:filter>
	<dep:filter>org.apache.myfaces</dep:filter>
</dep:hidden-classes>

この解決策は Apache Geronimo 2.1.3 でテストされましたが、ほかのバージョンでも対処の方法はほぼ同じでしょう

ラベル

jsf jsf Delete
myfaces myfaces Delete
exceptionhandler exceptionhandler Delete
error error Delete
handling handling Delete
exception exception Delete
Enter labels to add to this page:
Please wait 
Looking for a label?Just start typing.