エラーハンドリング
エラーハンドリングとは?
『もう見れたもんじゃありませんでしたよね』 画面いっぱいにフレームワークのエラーが画面を埋め尽くしてたと思います。
これ単純に見栄えが悪いだけではありません。
エラーの内容って実はとても重要な事が書かれています。
例えば以下の様なリスクがあります。
・使用しているフレームワーク情報が筒抜け
・データベースの種類やSQLの内容が筒抜け
・最悪個人情報すら筒抜け
お手軽エラー画面設定
※404 = そんなURLないぞってやつ
『src/main/webapp/error/404.xhtml』を作成します。
<?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』を作成します。
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;
}
}
※『動的環境設定』参照
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());
}
}
<?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>
これはカスタムエラーハンドリングより、処理の優先順位が高いためです。 次に開発側の『appinfo.properties』に以下設定をします。
ERROR_MODE=DEBUG この状態で『開発』『本番』環境での挙動を確認しましょう。
※ 先ほどの『ユーザーエージェントを判別するインターセプタ』ではファイヤーフォックスでアクセスするとエラーになります 本番環境では『500.xhtml』へ遷移し、開発環境では画面一杯にエラーが表示されます。
独自の例外クラスを作成する
そこで独自の『例外』を作成しましょう。
『src/main/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」を強要されます
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;
}
}
『500.xhtml』へのリダイレクト処理周りのコードが少し変わってる事に気が付きましたでしょうか?
実は『NavigationHandler』クラスを使用するのを止めています。
理由はこのクラスが『エラーハンドリング』だからです。
『NavigationHandler』クラスは、スコープ内からの呼び出しを想定したものなので無視される可能性があるのです。 ただ『スコープ外からの呼び出しって何処を想定してるの?』って疑問に思うかもですが、
そもそもどんなエラーでも呼ばれれば出来るだけ交通整理をしたいのが目的のクラスですので、同じ結果が得られるなら安全な処理に変えた方が良いですよね。 そして確実にスコープ外となるのは『フィルター処理』となります。
ここから強引に『エラーハンドラ』を呼び出した場合『NavigationHandler』クラスは無効化されます。
例えばエラーコードを仕込んで『管理者にコード【xxxx-xxx】をお伝えください。』等の内容を画面に表示する事が可能となります。 ビュー側には以下コードを追加するだけです。
<!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』の内容を改めて表示します。
エラー画面に値を送る方法ですが、実は何通りかの方法があります。
今回の方法は、わざわざセッションマップに値を格納しましたが、フラッシュセッションを利用する方法もあります。
本来はその方がよりシンプルな処理となりますが、やはり便利の裏には罠もひそんでいます。
この挙動を解決するため、より単純なセッションマップでの値の渡し方になったというわけです。





<!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>