404エラーのハンドリング方法から派生して、Wicket
内での例外ハンドリングの拡張ポイントについて調べてみました。
なかなか拡張ポイントが結構多く、勉強になりました。
目次
この記事の目次になります。
調べた限り以下のような実装が可能でした。
各頁は基本的に内容が独立していますので、必要なところだけ読んで頂ければと思います。
Web.xml を使う
Servlet
の標準機能に任せる方法です。
Error Pages and Feedback Messages - Apache Wicket - Apache Software Foundationに全部書いてありますが、下記の記述を追加することで、カスタム404ぺージを表示することができます。
-
web.xml
タグ<error-page>
を追加します。
<filter-mapping> <filter-name>WicketFilter</filter-name> <url-pattern>/*</url-pattern> <dispatcher>REQUEST</dispatcher> <dispatcher>ERROR</dispatcher> </filter-mapping> <error-page> <error-code>404</error-code> <location>/404</location> </error-page>
-
Application.java
web.xml
で追加した404の URL に対応する Page を mount する実装を追加します。
上記は、404エラーのハンドリング方法 にも書いた内容ですが、 この実装方法だと、「// 404 mountPage("/404", ErrorPage.class);
Wicket
に登録していないURL
に対してのハンドリング」となります。
アプリケーション内から明示的に404
ページに飛ばすには、下記のようにAbortWithHttpErrorCodeException
をスローしたり、
下記のように、throw new AbortWithHttpErrorCodeException(404, "Festival is not Exist...");
RestartResponseException
に遷移したいページを指定することで、明示的なページ表示ができます。
個人的には、Java の Web フレームワークは使っている状況なので、生 Servlet の機能を使用するのはとあまりお勧めできないかと考えます。// ここではErrorPage.classが遷移したいページ throw new RestartResponseException(ErrorPage.class);
ApplicationSettings#set…Page() を使う
Web.xml を使うと同じくError Pages and Feedback Messages - Apache Wicket - Apache Software Foundation に記載のある内容になります。
Application#set...Page()
という、末尾が Page()
で終了する API を使用することでステータスエラーに対する細かなハンドリングが可能です。
以下末尾がPage()
の API について説明します。
ApplicationSettings#setPageExpiredErrorPage()
PageExpiredException
に発生した場合、遷移するエラーページを設定します。
ApplicationSettings#setInternalErrorPage()
予期しないエラーが発生した場合、遷移するエラーページを設定します。
ApplicationSettings#setAccessDeniedPage()
AuthorizationException
、ListenerInvocationNotAllowedException
が発生した場合(認証エラーが発生した場合)、遷移するエラーページを設定します。
補足
-
AuthenticatedWebApplication
を継承したApplication
クラスを作成している場合、getSignInPageClass
でLoginページを指定する必要があります。 -
ページアクセス時に認証が通っていない場合、
getSignInPageClass
での指定ページ、またはPageに制御がうつってからの権限エラーという状況の場合では、
AuthorizationException
が発生することを許容します。 -
ApplicationSettings
で指定した設定は、DefaultExceptionMapper#mapExpectedExceptions()
と、DefaultExceptionMapper#mapUnexpectedExceptions
で、使用されています。
以下、DefaultExceptionMapper
の実装抜粋になります。
DefaultExceptionMapper.java
/**
* Maps expected exceptions (i.e. those internally used by Wicket) to their corresponding
* {@link IRequestHandler}.
*
* @param e
* the current exception
* @param application
* the current application object
* @return the {@link IRequestHandler} for the current exception
*/
protected IRequestHandler mapExpectedExceptions(Exception e, final Application application)
{
if (e instanceof StalePageException)
{
// If the page was stale, just re-render it
// (the url should always be updated by an redirect in that case)
return new RenderPageRequestHandler(new PageProvider(((StalePageException)e).getPage()));
}
else if (e instanceof PageExpiredException)
{
return createPageRequestHandler(new PageProvider(Application.get()
.getApplicationSettings()
.getPageExpiredErrorPage()));
}
else if (e instanceof AuthorizationException ||
e instanceof ListenerInvocationNotAllowedException)
{
return createPageRequestHandler(new PageProvider(Application.get()
.getApplicationSettings()
.getAccessDeniedPage()));
}
else if (e instanceof ResponseIOException)
{
logger.error("Connection lost, give up responding.", e);
return new EmptyRequestHandler();
}
else if (e instanceof PackageResource.PackageResourceBlockedException && application.usesDeploymentConfig())
{
logger.debug(e.getMessage(), e);
return new ErrorCodeRequestHandler(404);
}
return null;
}
/**
* Maps unexpected exceptions to their corresponding {@link IRequestHandler}.
*
* @param e
* the current exception
* @param application
* the current application object
* @return the {@link IRequestHandler} for the current exception
*/
protected IRequestHandler mapUnexpectedExceptions(Exception e, final Application application)
{
final ExceptionSettings.UnexpectedExceptionDisplay unexpectedExceptionDisplay = application.getExceptionSettings()
.getUnexpectedExceptionDisplay();
logger.error("Unexpected error occurred", e);
if (ExceptionSettings.SHOW_EXCEPTION_PAGE.equals(unexpectedExceptionDisplay))
{
Page currentPage = extractCurrentPage();
return createPageRequestHandler(new PageProvider(new ExceptionErrorPage(e,
currentPage)));
}
else if (ExceptionSettings.SHOW_INTERNAL_ERROR_PAGE.equals(unexpectedExceptionDisplay))
{
return createPageRequestHandler(new PageProvider(
application.getApplicationSettings().getInternalErrorPage()));
}
// IExceptionSettings.SHOW_NO_EXCEPTION_PAGE
return new ErrorCodeRequestHandler(500);
}
DefaultExceptionMapper
では、下記のExceptionSettings
に設定を元にした制御も実装されています。
ExceptionSettings#set…() を使う
Application
クラスのinit
メソッドで以下のような記述をすることで、Exception
発生時の、ふるまい制御できます。
getExceptionSettings().setAjaxErrorHandlingStrategy(ExceptionSettings.AjaxErrorStrategy.REDIRECT_TO_ERROR_PAGE);
getExceptionSettings().setThreadDumpStrategy(ExceptionSettings.ThreadDumpStrategy.THREAD_HOLDING_LOCK);
getExceptionSettings().setUnexpectedExceptionDisplay(ExceptionSettings.SHOW_NO_EXCEPTION_PAGE);
ExceptionSettings#setAjaxErrorHandlingStrategy()
Ajax
リクエストでError
になった場合の振る舞いをコントロールします。
設定値について説明します。
-
AjaxErrorStrategy.REDIRECT_TO_ERROR_PAGE
通常のリクエストと同様にApplicationSettings#setInternalErrorPage()
で設定した
エラーページに遷移させます。 -
AjaxErrorStrategy.INVOKE_FAILURE_HANDLER
Javascript
側のcallback
メソッドに制御を任せて、
AjaxCallListener#onFailure
メソッドを呼びだします。
ExceptionSettings#setThreadDumpStrategy()
スレッドダンプの出力方法をコントロールします。
-
ThreadDumpStrategy.NO_THREADS
出力しない。 -
ThreadDumpStrategy.THREAD_HOLDING_LOCK
Lock
を取得しているThread
のStackTrace
を出力する -
ThreadDumpStrategy.ALL_THREADS
Application
の全てのThread
のStackTrace
を出力する
ExceptionSettings#setUnexpectedExceptionDisplay()
予期しない例外の発生時に、どのように振る舞うかを制御します。
-
ExceptionSettings.SHOW_EXCEPTION_PAGE
ExceptionErrorPage
に遷移します。 -
ExceptionSettings.SHOW_INTERNAL_ERROR_PAGE
ApplicationSettings#setInternalErrorPage()
で指定したページに遷移します。 -
ExceptionSettings.SHOW_NO_EXCEPTION_PAGE
500エラーを返して、Wicket
ではなく、Web.xml
側で指定した
エラーページに遷移します。
IExceptionMapper を使う
DefaultExceptionMapper
の継承クラスを作成して、エラーハンドリングのロジックを拡張することができます。
以下、Github
で example
を作っている人がいたので、そちらへのリンクです。
上記を参考に ExceptionMapper
の継承クラスを作成してみました。
継承クラスだけでなく、Application#getExceptionMapperProvider()
をオーバーライド、更にExceptionMapper
の継承クラス の継承クラスを返すカスタムExceptionMapperProvider
を作成する必要があります。
なかなかわかりにくいですが、JAVA8 で作成すると、以下のような実装になります。
- WicketApplication.java の対象実装の抜粋
/** * the exceptionMapper */ private final AppExceptionMapper exceptionMapper = new AppExceptionMapper(); /** * IProvider exceptionMapperProvider */ private final IProvider<IExceptionMapper> exceptionMapperProvider; public WicketApplication() { this.exceptionMapperProvider = () -> exceptionMapper; } /* (non-Javadoc) * @see org.apache.wicket.Application#getExceptionMapperProvider() */ @Override public IProvider<IExceptionMapper> getExceptionMapperProvider() { return exceptionMapperProvider; }
IRequestCycleListener#onException() を使う
以下にサンプルコードの記載があります。
* How to use a different Wicket expiration/error page for popups and other special pages? - Stack Overflow
Application
クラス内で、IRequestCycleListener
を登録することで、
エラーハンドリング処理を追加することが可能です。
エラーハンドリングが実装可能なAPIが複数ありわかりにくいので、以下説明します。
IExceptionMapper
とIRequestCycleListener#onException()
の挙動の違いについて
以下のコードで検証しました。
少なくとも、ハンドリングが行われるタイミングに違いがあります。
-
WicketApplication.java
@Log4j2 public class WicketApplication extends WebApplication { /** * the exceptionMapper */ private final AppExceptionMapper exceptionMapper = new AppExceptionMapper(); /** * IProvider exceptionMapperProvider */ private final IProvider<IExceptionMapper> exceptionMapperProvider; public WicketApplication() { this.exceptionMapperProvider = () -> exceptionMapper; } /** * @see org.apache.wicket.Application#init() */ @Override public void init() { super.init(); // Add RequestCycleListener getRequestCycleListeners().add(new AbstractRequestCycleListener() { @Override public IRequestHandler onException(RequestCycle cycle, Exception ex) { // Log log.info("============================================================"); log.info("Raise Exception", ex); log.info("============================================================"); return super.onException(cycle, ex); } }); // Do Somethings.. } /* (non-Javadoc) * @see org.apache.wicket.Application#getExceptionMapperProvider() */ @Override public IProvider<IExceptionMapper> getExceptionMapperProvider() { return exceptionMapperProvider; }
-
AppExceptionMapper.java
import lombok.extern.log4j.Log4j2; import org.apache.wicket.DefaultExceptionMapper; import org.apache.wicket.request.IRequestHandler; /** * AppExceptionMapper * * @author Kem */ @Log4j2 public class AppExceptionMapper extends DefaultExceptionMapper { @Override public IRequestHandler map(Exception e) { log.info("---------------------------------------"); log.info("Raise Exception>>>", e); log.info("--------------------"); return super.map(e); }
ExceptionMapper
と、RequestCycleListener#onException
の双方を実装して、どちらが優先でハンドリングされるのかを確認してみました。
エラー発生時にログは以下の通り出力されました。
20:10:11.728 [dw-51] INFO xyz.monotalk.festivals4partypeople.web.WicketApplication - ============================================================ 20:10:11.729 [dw-51] INFO xyz.monotalk.festivals4partypeople.web.WicketApplication - Raise Exception java.lang.NullPointerException 20:10:11.730 [dw-51] INFO xyz.monotalk.festivals4partypeople.web.WicketApplication - ============================================================ 20:10:11.730 [dw-51] INFO xyz.monotalk.festivals4partypeople.web.AppExceptionMapper - --------------------------------------- 20:10:11.730 [dw-51] INFO xyz.monotalk.festivals4partypeople.web.AppExceptionMapper - Raise Exception>>> java.lang.NullPointerException 20:10:11.731 [dw-51] INFO xyz.monotalk.festivals4partypeople.web.AppExceptionMapper - --------------------
ハンドリングの優先順位について
ハンドリングの順序は、
1. IRequestCycleListener#onException()
2. IExceptionMapper#map()
となります。
どちらを使用すべきか
IRequestCycleListener#onException()
はパラメータで、RequestCycle
を保持しており、結構何でもやりたい放題できそうです。
それに対して、IExceptionMapper#map()
はException
のみが引数になるため、発生元のページ情報などはあらかじめ例外に積んでおく必要があります。
IRequestCycleListener#onException()
のほうが柔軟な制御ができそうですが、そこで全てを実装すると煩雑な実装になりそうです。
個人的には、共通処理で例外レベルに依りアプリケーション全画面遷移をコントロールしたい場合は、IExceptionMapper#map()
。
遷移元ページ等、ReuqestCycle
の情報で共通で判断したい場合、IRequestCycleListener#onException()
にハンドリング処理を実装するのがよいかと思いました。
まとめ
以下まとめます。
Wicket は、例外ハンドリングのための拡張ポイントが多数実装されている。
拡張ポイントが多数実装されているので、正直使い方は迷う(学習コストが高い)。
考えた限り以下のような判断で使うのがよさそう。
-
Web.xml
では、Wicket
フレームワーク外のエラーページを指定する。これはセーフティーネットとしての設定を行うスタンス。 -
ApplicationSettings
、ExceptionSettings
は主に開発用で、これらを使って開発環境でのエラー調査しやすくする。 -
IExceptionMapper#map()
でアプリケーション全体のカスタムエラーハンドリングする。
これは、認証エラー的なものをハンドリングするイメージです。 -
IRequestCycleListener#onException()
ではReuqestCycle
の情報が取得できる。画面の状態に依る共通のハンドリング処理を実装する。
大規模なアプリケーションだとどのポイントでも実装が必要なケースが出てきそうに思います。
一箇所にまとまっていて欲しい気もしますが、それだけフレームワークの歴史が長いってことだと理解しました。
以上です。
コメント