R e i - D r e a m
for Laravel
Guest
login

最終投稿日:2026年08月23日

エラーハンドリング

エラーハンドリングとは?

先ほどの章でユーザーエージェントで弾かれた時の画面どうでしたか?
『もう見れたもんじゃありませんでしたよね』
画面いっぱいにフレームワークのエラーが画面を埋め尽くしてたと思います。
これ単純に見栄えが悪いだけではありません。
エラーの内容って実はとても重要な事が書かれています。
例えば以下の様なリスクがあります。
・システムの内部構造が筒抜け
・使用しているフレームワーク情報が筒抜け
・データベースの種類やSQLの内容が筒抜け
・最悪個人情報すら筒抜け
悪意のあるハッカーから見たら、まさに『お宝』ですよ。
結論としては『エラー画面』を作成しましょうと言う事です。

お手軽エラー画面設定

では『404エラー』の場合の画面を作成してみましょう。
    ※404 = そんなURLないぞってやつ
『src/main/webapp/error/404.xhtml』を作成します。
error/404.xhtml

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
        xmlns:h="jakarta.faces.html">
<h:head>
    <title>404.error</title>
</h:head>
<h:body>
    <h2>404</h2>
    <p>ページが見つかりません。</p>
    <p>URLをご確認の上、再度アクセスして下さい。</p>
    
    <h:link outcome="/sample" value="トップページへ戻る" />
</h:body>
</html>

更にこのエラー画面に遷移する設定を『web.xml』に記述します。
src/main/webapp/WEB-INF/web.xml

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="https://jakarta.ee/xml/ns/jakartaee"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="https://jakarta.ee/xml/ns/jakartaee https://jakarta.ee/xml/ns/jakartaee/web-app_6_0.xsd"
        version="6.0">
    <context-param>
        <param-name>jakarta.faces.FACELETS_ENCODING</param-name>
        <param-value>UTF-8</param-value>
    </context-param>
    <servlet>
        <servlet-name>Faces Servlet</servlet-name>
        <servlet-class>jakarta.faces.webapp.FacesServlet</servlet-class>
        <load-on-startup>1</load-on-startup>
    </servlet>
    
    <servlet-mapping>
        <servlet-name>Faces Servlet</servlet-name>
        <url-pattern>*.faces</url-pattern>
        <url-pattern>*.xhtml</url-pattern>
    </servlet-mapping>
    
    <welcome-file-list>
        <welcome-file>index.xhtml</welcome-file>
    </welcome-file-list>

    <error-page>
        <error-code>404</error-code>
        <location>/error/404.xhtml</location>
    </error-page>
    
    <error-page>
        <error-code>500</error-code>
        <location>/error/500.xhtml</location>
    </error-page>
    
    <error-page>
        <exception-type>java.lang.Throwable</exception-type>
        <location>/error/500.xhtml</location>
    </error-page>
</web-app>

たったこれだけです。
500エラーも定義したので『error/500.xhtml』も作成して下さいね。
どうでしょう。
ブラウザ判定で弾かれた場合は『500』エラー
存在しないURLを叩いた場合は『404』エラーになるハズです。

エラーハンドリングクラスを作成する

実はこちらが本命です。
『お手軽』の方は、本当にお手軽なのですが、ちょっと待ってください。
開発中の障害に対してこの画面に遷移して、エラーの情報が一切見れないと言われたらどうですか?
ちょっと暴れますよねw
ですので、環境によって柔軟にエラーの挙動を切り替えたいですよね。
というわけで作ってみましょう。
『src/main/java/exception/MyExceptionHandler.java』を作成します。
exception/MyExceptionHandler.java

package exception;

import java.util.Iterator;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import config.AppConfig;
import jakarta.faces.FacesException;
import jakarta.faces.application.NavigationHandler;
import jakarta.faces.context.ExceptionHandler;
import jakarta.faces.context.ExceptionHandlerWrapper;
import jakarta.faces.context.FacesContext;
import jakarta.faces.event.ExceptionQueuedEvent;
import jakarta.faces.event.ExceptionQueuedEventContext;

public class MyExceptionHandler extends ExceptionHandlerWrapper {
    private static final Logger logger = LoggerFactory.getLogger(MyExceptionHandler.class);
    private final ExceptionHandler wrapped;
    
    @SuppressWarnings("deprecation")
    public MyExceptionHandler(ExceptionHandler wrapped) {
        this.wrapped = wrapped;
    }
    
    @Override
    public ExceptionHandler getWrapped() {
        return this.wrapped;
    }
    
    @Override
    public void handle() throws FacesException {
        Iterator<ExceptionQueuedEvent> i = getUnhandledExceptionQueuedEvents().iterator();
        while (i.hasNext()) {
            ExceptionQueuedEvent event = i.next();
            ExceptionQueuedEventContext context = (ExceptionQueuedEventContext) event.getSource();
            Throwable t = context.getException();
            // 根本の原因(Root Cause)を取得
            Throwable rootCause = getRootCause(t);
            try {
                // ① サーバーログにエラーを出力(ここでAppLog等を使ってログ出力!)
                logger.error("予期せぬ例外が発生しました", rootCause);
                // ② 画面側に必要なコンテキスト(FacesContext)を取得
                FacesContext fc = FacesContext.getCurrentInstance();
                // ③ エラーモードを判別して処理を分岐する
                if (AppConfig.get("ERROR_MODE", "").equals("DEBUG")) {
                    getWrapped().handle();
                    i.remove();
                    return;
                }
                // ④ 本番環境扱いなら「500.xhtml」へリダイレクト/画面遷移
                NavigationHandler nav = fc.getApplication().getNavigationHandler();
                nav.handleNavigation(fc, null, "/error/500.xhtml?faces-redirect=true");
                fc.renderResponse();
            } finally {
                // 番環境扱いの場合キューから削除
                if (i.hasNext() || !(AppConfig.get("ERROR_MODE", "").equals("DEBUG"))) {
                    i.remove();
                }
            }
        }
        // 残りの例外処理を親クラスに渡す
        getWrapped().handle();
    }
    // 例外の根本原因を探るユーティリティ
    @Override
    public Throwable getRootCause(Throwable t) {
        Throwable cause = t.getCause();
        if (cause != null) {
            return getRootCause(cause);
        }
        return t;
    }
}

流れは以下となります。
① 以下関数(コンストラクタ含む)
『MyExceptionHandler』『ExceptionHandler』『getRootCause』は割愛します。お約束と思って下さい。
② エラーイベントの内容を『Iterator』型で取得しループ処理します。
③ ループ処理はしていますが、ループは1回で終了させます。
④ 最初のエラーの内容が『rootCause』に格納されるのでログとして出力します。
⑤ エラーのハンドリングモードを参照します。
アプリの動的設定となるので、当然『appinfo.properties』ですね。
    ※『動的環境設定』参照
⑥『ERROR_MODE=DEBUG』の時は、エラーハンドリングはせずに画面いっぱいにエラーを出力します(開発用)
⑦ ⑥以外の場合は本番用として『/error/500.xhtml』にリダイレクトさせます。
これだけでは発火しません。
次にフレームワークのライフサイクルの中で『MyExceptionHandler』を生成するためのファクトリを作成します。
exception/MyExceptionHandlerFactory.java

package exception;

import jakarta.faces.context.ExceptionHandler;
import jakarta.faces.context.ExceptionHandlerFactory;

public class MyExceptionHandlerFactory extends ExceptionHandlerFactory{
    public MyExceptionHandlerFactory(ExceptionHandlerFactory wrapped) {
        super(wrapped);
    }
    
    @Override
    public ExceptionHandler getExceptionHandler() {
        return new MyExceptionHandler(getWrapped().getExceptionHandler());
    }
}

最後に作成したファクトリを新規作成した『src/main/webapp/WEB-INF/faces-config.xml』に登録します。
WEB-INF/faces-config.xml

<?xml version="1.0" encoding="UTF-8"?>
<faces-config
    xmlns="https://jakarta.ee/xml/ns/jakartaee"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="https://jakarta.ee/xml/ns/jakartaee https://jakarta.ee/xml/ns/jakartaee/web-facesconfig_4_0.xsd"
    version="4.0">
    
    <factory>
        <exception-handler-factory>exception.MyExceptionHandlerFactory</exception-handler-factory>
    </factory>
</faces-config>

それでは動作確認してみましょう。 まずは先ほど『web.xml』に設定したエラー遷移設定を削除します。
これはカスタムエラーハンドリングより、処理の優先順位が高いためです。
次に開発側の『appinfo.properties』に以下設定をします。
    ERROR_MODE=DEBUG
この状態で『開発』『本番』環境での挙動を確認しましょう。
    ※ 先ほどの『ユーザーエージェントを判別するインターセプタ』ではファイヤーフォックスでアクセスするとエラーになります
本番環境では『500.xhtml』へ遷移し、開発環境では画面一杯にエラーが表示されます。

独自の例外クラスを作成する

折角エラーハンドラーを作成したので、もう少し柔軟なハンドリングしたいと思います。
そこで独自の『例外』を作成しましょう。
『src/main/java/exception/MyException.java』を作成します。
exception/MyException.java

package exception;

public class MyException extends RuntimeException {
    private static final long serialVersionUID = 1L;
    // メッセージだけ渡すコンストラクタ
    public MyException(String message) {
        super(message);
    }
    // メッセージと「元のエラー(cause)」をセットで渡すコンストラクタ
    public MyException(String message, Throwable cause) {
        super(message, cause);
    }
}

ポイント

『RuntimeException』を継承してるのは《非チェック例外》でお手軽だからです。
    ※チェック例外の場合は「try ~ catch」を強要されます

それではこの例外『MyException』を使ってエラー画面をリッチにしてみましょう。

MyExceptionHandler.java

package exception;

import java.io.IOException;
import java.util.Iterator;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import config.AppConfig;
import jakarta.faces.FacesException;
import jakarta.faces.application.FacesMessage;
import jakarta.faces.context.ExceptionHandler;
import jakarta.faces.context.ExceptionHandlerWrapper;
import jakarta.faces.context.FacesContext;
import jakarta.faces.event.ExceptionQueuedEvent;
import jakarta.faces.event.ExceptionQueuedEventContext;

public class MyExceptionHandler extends ExceptionHandlerWrapper {
    private static final Logger logger = LoggerFactory.getLogger(MyExceptionHandler.class);
    private final ExceptionHandler wrapped;
    
    @SuppressWarnings("deprecation")
    public MyExceptionHandler(ExceptionHandler wrapped) {
        this.wrapped = wrapped;
    }
    
    @Override
    public ExceptionHandler getWrapped() {
        return this.wrapped;
    }
    
    @Override
    public void handle() throws FacesException {
        Iterator<ExceptionQueuedEvent> i = getUnhandledExceptionQueuedEvents().iterator();
        while (i.hasNext()) {
            ExceptionQueuedEvent event = i.next();
            ExceptionQueuedEventContext context = (ExceptionQueuedEventContext) event.getSource();
            Throwable t = context.getException();
            // 根本の原因(Root Cause)を取得
            Throwable rootCause = getRootCause(t);
            try {
                // ① サーバーログにエラーを出力(ここでAppLog等を使ってログ出力!)
                logger.error("予期せぬ例外が発生しました", rootCause);
                // ② 画面側に必要なコンテキスト(FacesContext)を取得
                FacesContext fc = FacesContext.getCurrentInstance();
                // ③ エラーモードを判別して処理を分岐する
                if (AppConfig.get("ERROR_MODE", "").equals("DEBUG")) {
                    getWrapped().handle();
                    i.remove();
                    return;
                }
                // 独自エラーからの遷移の場合

                if (rootCause instanceof MyException) {
                    fc.getExternalContext().getSessionMap().put("ERROR_MSG", rootCause.getMessage());
                }
                // ④ 本番環境扱いなら「500.xhtml」へリダイレクト/画面遷移
                String contextPath = fc.getExternalContext().getRequestContextPath();
                fc.getExternalContext().redirect(contextPath + "/error/500.xhtml");
                fc.responseComplete();
            } catch (IOException e) {
                e.printStackTrace();
                throw new RuntimeException("リダイレクト処理に失敗しました", e);
            } finally {
                // 番環境扱いの場合キューから削除
                if (i.hasNext() || !(AppConfig.get("ERROR_MODE", "").equals("DEBUG"))) {
                    i.remove();
                }
            }
        }
        // 残りの例外処理を親クラスに渡す
        getWrapped().handle();
    }
    // 例外の根本原因を探るユーティリティ
    @Override
    public Throwable getRootCause(Throwable t) {
        Throwable cause = t.getCause();
        if (cause != null) {
            return getRootCause(cause);
        }
        return t;
    }
}

独自エラーの分岐を作成しセッションマップに『KEY = ERROR_MSG』で、例外送出時に引数で渡したメッセージを追加します。
ポイント

『500.xhtml』へのリダイレクト処理周りのコードが少し変わってる事に気が付きましたでしょうか?
実は『NavigationHandler』クラスを使用するのを止めています。
理由はこのクラスが『エラーハンドリング』だからです。

具体的に説明すると、フレームワーク処理のスコープ外から呼び出された場合にも対応するためです。
『NavigationHandler』クラスは、スコープ内からの呼び出しを想定したものなので無視される可能性があるのです。
ただ『スコープ外からの呼び出しって何処を想定してるの?』って疑問に思うかもですが、
そもそもどんなエラーでも呼ばれれば出来るだけ交通整理をしたいのが目的のクラスですので、同じ結果が得られるなら安全な処理に変えた方が良いですよね。
そして確実にスコープ外となるのは『フィルター処理』となります。
ここから強引に『エラーハンドラ』を呼び出した場合『NavigationHandler』クラスは無効化されます。

これで何が起きる?
そうです。エラー画面にメッセージが渡せるのです。
例えばエラーコードを仕込んで『管理者にコード【xxxx-xxx】をお伝えください。』等の内容を画面に表示する事が可能となります。
ビュー側には以下コードを追加するだけです。
500.xhtml

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
        xmlns:h="jakarta.faces.html"
        xmlns:c="jakarta.tags.core">
<h:head>
    <title>500.error</title>
</h:head>
<h:body>
    <h2>500</h2>
    <c:set var="msg" value="#{sessionScope.remove('ERROR_MSG')}" scope="request" />
    <p style="color: red;">#{msg}</p>
    
    <p>内部サーバエラーです。</p>
    
    <h:link outcome="/sample" value="トップページへ戻る" />
</h:body>
</html>

少し複雑な処理ですが内容は以下となります。
変数『msg』にセッションマップに格納した『KEY = ERROR_MSG』の内容をセットし、同時にセッションを削除します。
そして、変数『msg』の内容を改めて表示します。
ポイント

エラー画面に値を送る方法ですが、実は何通りかの方法があります。
今回の方法は、わざわざセッションマップに値を格納しましたが、フラッシュセッションを利用する方法もあります。
本来はその方がよりシンプルな処理となりますが、やはり便利の裏には罠もひそんでいます。

今回動作確認をしている中で『インターセプタ』から例外を送出したらなんと、値が渡らない事が判明しました。
この挙動を解決するため、より単純なセッションマップでの値の渡し方になったというわけです。

ログインしてコメントを残そう!!


きっぷる