開拓の例外ハンドリング (Exception Handling)
LastaFluteの特徴の一つです。
- 概要
- システム例外ハンドリング
- クライアント例外ハンドリング
- 業務例外ハンドリング (アプリ例外)
- UserMessagesの使い方
- アプリでマイ例外クラス作る?
- 一件検索でデータ無かったとき
- 通知ログの監視って?
- 例外ハンドリングこそ負債になりやすい?
概要
基本、自分でcatchする必要なし
システム例外も、クライアント例外も、業務例外 (アプリ例外) も、フレームワークが catch して処理 (Handling) をするので、基本的にActionなどで自分でcatchする必要はありません。 (アプリ内で特別に処理が必要なときだけcatch)
したら、だれがcatchするの?
Actionクラスであれば RequestLoggingFilter, Jobクラスであれば LaJobRunner, AsyncManagerの非同期スレッド内であれば AsyncManager が catch します。
- Actionクラス (システム例外)
- RequestLoggingFilter (HTTPステータス: 500)
- Actionクラス (クライアント例外)
- RequestLoggingFilter (HTTPステータス: 400 or 404)
- Actionクラス (業務例外)
- ApplicationExceptionResolver (HTTPステータス: 400 or ...)
- Jobクラス
- LaJobRunner
- 非同期スレッド
- AsyncManager (AsyncManagerの非同期スレッドなら)
システム例外ハンドリング
そもそもシステム例外とは?
システムを継続することができないような状況、システムエラーでthrowする例外です。
NullPointerException とか、バグやデータ不備など、発生したら何かしらディベロッパーが対応をしないといけない状況のものです。 エラーログに出力され、場合によっては緊急通知メールが送られるようなものです。
だれがthrowする?
みんなです。
DBに接続できない、SQLが間違っている、NullPointerやっちゃった、フレームワークがバグっている、メモリが足らない、などなど、幾らでも。 普通に例外を投げたら基本的にシステム例外です。
どうやってthrowする?
アプリで throw するときは、"クライアント例外や業務例外ではない例外" を投げればすべてシステム例外になります。LastaFlute に LaSystemException という例外クラスがありますが、これは LastaFlute 内で明示的に throw しているシステム例外を示すスーパークラスというだけなので、これを継承する必要はありません。
だれがcatchする?
Actionクラスであれば、RequestLoggingFilter が catch します。ServletFilterなので、(ほぼすっぽり)フレームワークを囲ってcatchします。そこでエラーログ (ERRORレベルのログ) が出力されます(なので、throwしたシステム例外を自分でログ出す必要はありません)。 HTTPステータスは500で、web.xml にて定義されたHTML(or JSON)が戻ります。
Jobクラスであれば、LaJobRunner が catch します。同様に、エラーログが出力されます。 ちなみに、Job内で発生した例外は、例外の種類に関わらずすべてシステム例外として処理されます(ユーザーがいない世界なので業務例外のリカバリを自分でやらないといけないため)。
AsyncManagerを使った非同期スレッドであれば、AsyncManager (のサブクラス) 自身が catch します。同様に、エラーログが出力され、すべてシステム例外になります。 (自分で起動したスレッド、つまり管理外スレッドだと、エラーログには出力されません。基本、AsyncManagerを使いましょう)
エラーログは、どこに出力される?
エラーログは、logback.xml の設定次第ですが、error_[app].log に出力されます。
システム例外のHTTPステータスは?
500 になります。RequestLoggingFilterにて、そのように制御されます。
システム例外のレスポンスは?
web.xml に定義されている、500 に対応するリソースがレスポンスになります。 500.html だったらエラー画面、500.json であればエラー用のJSONが戻ります。
通常、システムエラーのときは、ユーザー(人)に対して詳しい情報を教える必要はありません。 とういかセキュリティ上、教えてはいけません。JSON APIであっても、BtoC なら誰でも叩けますし、内部向けでもサーバーのエラーログに出力されていればクライアント(プログラム)に教える必要はないでしょう。 なので、固定のレスポンスで問題ないと想定されます(例外の種類ごとにレスポンスを変更する必要はない)。
本当にエラーのものだけエラーログに
エラーログでいちばん大切なこと。
対応しなくても良いような例外 (業務例外などシステム的には正常な例外) がエラーログに出力されると... エラーログが発生して "ざわっ" となって緊急対応しようとすると実は重要ではない例外だった "はぁ" の繰り返し が発生し、だんだんエラーログを見なくなります。つまり "ざわざわ" が減ります。すると、本当に重要なエラーを見過ごしてしまう可能性があるのです。 最悪のケース、エラーログは大量に吐かれているけど誰も見ず垂れ流しの状態で運用しているなんてことも(考えたくもない状況)。
ゆえに、エラーログに出力されるものは、出たからにはディベロッパーが何かしら対応をして、適切に 出ないように処置をする、というブランドをキープするのがポイントです。
ですが、システム例外なのか業務例外なのか、なかなか微妙で曖昧なものもあります。 なので、業務例外の方のログ出力の仕組みも合わせることで、エラーログのブランドを守ります。
クライアント例外ハンドリング
そもそもクライアント例外とは?
(サーバー)システムの不具合ではなく、リクエストを投げたクライアントの不備によって処理が継続できないときに発生させる例外です。
ここで言うクライアントとは、ユーザー(人)ではなく、基本的にプログラムを指しています。例えば、JSON API のときに、パースできないJSONのリクエストや、必須の項目が入ってないリクエストなどが来たとき。 サーバーサイドHTMLでも、ユーザー(人)が入力しない制御用の項目などがおかしい値になっているリクエストなどが来たときは同じです。
サーバーサイドのリカバリは不要ですが、クライアントサイドのリカバリ(バグ修正など)が必要な状況です。 "いたずら" であれば、人の心をリカバリする必要があります。ゆえに、クライアント例外は、クライアントにとっては基本的にはシステム例外と言えます。 リクエストの投げ方が良くないということで、呼び出しプログラムではそのように翻訳されることが期待されます。 (ただ、ユーザーの挙動などで間接的な発生しているケースでは、業務例外と言える場面もあるかもしれません。その辺はクライアントサイドでうまく制御する必要があるでしょう)
ちなみに、クライアント例外なのにサーバーサイドの修正が必要になったら、例外ハンドリング自体がバグっているので、サーバーでの対応が必要です。 (例外ハンドリング自体がバグってない前提で概念の話をしないとややこしくなります)
Actionクラスだけの概念で、Jobとか非同期スレッドでは関係ありません。 厳密には、クライアントの不備でJobや非同期スレッドが落ちることもあるかもしれませんが、突き返す相手がいないので自分で制御します。
だれがthrowする?
基本的に、フレームワークですが、アプリも明示的にthrowすることもあるでしょう。
LastaFluteでは、RequestClientErrorException を継承した例外クラスがthrowされるとクライアント例外になります。 リクエストを受け付けるフレームワーク内の部品で、throwすることがあります。代表的な例を以下に挙げます。
- RequestJsonParseFailureException
- JSONリクエストのパースエラー (400)
- RequestPropertyMappingFailureException
- リクエストパラメーターの型変換エラー (400)
- CrossSiteRequestForgeriesForbiddenEx...
- CSRFエラー (403)
- Forced404NotFoundException
- Pathパラメーターの型変換エラー (404)
※Pathパラメーターの型変換エラーは、後で例外クラスを新しく作るかも...スーパークラスを直接投げちゃってるなぁ
どうやってthrowする?
throw responseManager.new400()
アプリで throw するときは、ResponseManager の new400(), new404() などを使って例外を生成して throw すると良いでしょう。
e.g. アプリで、とある条件のときに400を戻す @Java
if (currentDate.isAfter(endDate)) {
throw responseManager.new400("mystic is over: endDate=" + endDate);
}
e.g. アプリで、検索したデータがなかったときに404を戻す @Java
Integer memberId = ...;
memberBhv.selectEntity(cb -> {
cb.query().setMemberId_Equal(memberId);
}).orElseTranslatingThrow(cause -> {
throw responseManager.new404("memberId=" + memberId, op -> op.cause(cause));
});
引数のメッセージは、ディベロッパー向けのデバッグメッセージです。 もしかしたらデバッグが必要になるかもしれないので、念のため原因追求のための情報を入れてログに出力しておます(後述)。 元の例外を引き継いでいる (orElseTranslatingThrow()を使っている) のも同じ理由です。
クライアント例外でマイ例外を作る必要はあまりないだろうということで例外クラスは固定です。
groups=ClientError.class
間接的に throw することもできます。FormやBodyの Validator Annotation で、groups に ClietError を指定すると、引っかかったときにエラーメッセージを戻すのではなく、クライアントエラーになります。 ユーザー入力項目ではない必須項目 (制御用のシステム項目) などに使うと良いでしょう。
e.g. Required annotation in Form class @Java
public class SeaLandForm {
@Required(groups=ClientError.class)
public Integer memberId;
@Required
@Length(max = 10)
public String memberName;
...
@Required(groups=ClientError.class)
public Long versionNo;
}
verifyOrClientError()
Actionクラスであれば、verifyOrClientError() が利用できます。 この状況はクライアントエラーだと思ったら、その判定を引数に指定して呼び出すと、デフォルトでは 404 になります(後で変わるかもしれません...オーバーライドで変更や固定化できます)。
e.g. アプリで、判定次第でクライアントエラー @Java
verifyOrClientError("...", currentDate.isAfter(endDate));
だれがcatchする?
システム例外と同様に、Actionクラスであれば、RequestLoggingFilter が catch します。
catchされた後は、どう処理される?
HTTPステータスは 400, 403, 404 で通知ログが出力され、レスポンスは色々なので後述。
あれっ、クライアント例外にログって必要? (通知ログ:INFO)
クライアントに状況を詳しく情報を戻すのは、セキュリティ的にも良くないですし、単純にプログラム的にも手間がかかるので、サーバーサイドで残しておくことで対処ができるようにしておく必要があるでしょう。 そもそも、サーバーの例外ハンドリング自体のバグで発生している可能性もあるので、特にインクリメンタル開発のサービス運用であれば、念のため情報は残しておきたいところです。
ただ、厳密には業務例外なのか?いたずらなのか?などなど曖昧なケースもあり得るので、LastaFluteではエラーログに出力するのではなく通知ログ(INFOレベル)で出力します。
通知ログは、logback.xml の設定次第ですが、app_[app].log に出力されます。
クライアント例外のレスポンスは?
基本的には、web.xml に定義されている、400 や 404 に対応するリソースがレスポンスになります。 400.html だったらエラー画面、400.json であればエラー用のJSONが戻ります。
サーバーサイドHTMLであれば、システム例外と同様、固定のレスポンスで問題ないと想定されます。クライアントに "どうダメだったのか?" を教えてあげた方が良さそうに思えますが、公開されているシステムだと、セキュリティ上あまり詳しくは教えないほうが良いので、やはりサーバーのログを確認してもらうのが良いでしょう。
JSON APIのときも、固定的で良いとは考えられますが、業務例外の 400 と合わせた JSON を戻すほうがクライアントは扱いやすいので、ApiFailureHook でレスポンスを差し替えてあげると良いでしょう。 LastaFluteのExampleデフォルトでは、すでにそのように実装されています。
400, 404の違いは?
ひとつは、URLは合っていてもパラメーターに不備あるようなものは 400 (Bad Request) にして、URLが合っていないようなものは 404 (Not Found) にする、という考え方を基本としています。通常、URLが間違っていれば自然とアプリケーションサーバーの機能などで 404 になりますが、Pathパラメーターが絡むと、一度フレームワークの中で入っての判断になるので専用の制御で 404 を表現しています。
もうひとつは、ダメなリクエストだってことを教えてあげたほうが良いのか?教えずに "そんなリクエストを受け付けるActionはない" と隠蔽したほうが良いのか?そこの違いです。URLは合っていてActionクラスまで動いていたとしても、400 を戻すと "そのパラメーターがDBに存在している" ということが判明してしまうと良くないです。
例えば、URLにユーザー名を入れて画面を表示するような場合、ユーザーが存在する場合とユーザーが存在しない場合で挙動が同じだと、そのユーザーの存在がバレてしまいます。 それはセキュリティ的に良くありません。そういう場合は、プログラムで明示的に 404 を戻すと良いでしょう。 その観点の応用で、LastaFluteでは明らかに "いたずら" (攻撃的なリクエスト) と言えるようなリクエストが来たときは 404 にしています。
業務例外ハンドリング (アプリ例外)
そもそも業務例外とは?
システムの挙動しては問題はなく、業務上の都合で処理を継続できないような "正常なレアケース" で発生させる例外です。アプリケーション例外、アプリ例外、ビジネス例外とも呼ばれます。(ここでは基本的に業務例外と呼びます)
なかなか曖昧ではありますが、発生しても特にディベロッパーが何か対応をしないといけないわけではない状況ではなく、 ユーザーが自分でリカバリできる状況のものです。 例えば、すれ違い更新 (による排他制御例外) など、業務上仕方なく発生するようなもので、メッセージさえ戻せばユーザーがやり直したりすることができるもの。
業務例外はなかなか曖昧です
例えば、サーバーサイドHTMLで一覧画面から詳細画面へ遷移するときに、リクエストのGETパラメーターの値を "いたずら" で存在しない値 (-999999 とか) に変更されたりすると、DBに値が存在せず詳細画面が開けません。 ただ、"いたずら" じゃなくても "すれ違い" でそれが発生する可能性もあります。
"いたずら" だからクライアントエラーなのか?"すれ違い" だから業務例外なのか? システム的に区別ができない以上は、対処としてどちらも問題のない方に寄せます。 "いたずら" をやった人に対して業務例外として戻しても、処理は中断されているわけなので、最低限の防御はできていると考えれます。 他にも、バグなのかすれ違いなのか?バグなのかいたずらなのか?区別が付かない状況がよくあります。 このときも同じような発想で、対処としてどちらも問題のない方に寄せます。 (ゆえに業務例外も通知ログとして本番でも情報を残します: 後述)
また、しっかりテストされて正常に動作していれば、その例外は業務例外といい切れるし、まだ開発中やそもそもバグっていれば単なるバグの例外とも言えます。 そもそも例外ハンドリング自体のバグがあるかもしれない。議論するときは、その辺を踏まえて話をしないと空中戦になりやすいです。
だれがthrowする?
基本的には、アプリケーションが独自のタイミングで投げるものです。
ただ、LastaFluteでは、業務的な機能をフレームワークとして組み込んでいるものがあるので、その機能の中で業務例外が投げられることがあります。 アプリがその機能のメソッドを明示的に呼び出すこともあります。
- ログイン失敗例外
- LoginFailureException (loginAssist.login()など)
- ログイン必須例外
- LoginRequiredException (フレームワーク内の処理にて発生)
- 不正な画面遷移例外
- RequestIllegalTransitionEx... (verifyOrIllegalTransition()など)
- ダブルサブミット例外
- DoubleSubmittedRequestEx... (verifyToken()など)
また、dbfluteがthrowする例外の中に、アプリとしては業務例外として扱った方が良いだろうという例外もあります。 LastaFluteでは、それらも組み込まれて業務例外として扱われます。
- 一件検索データなし例外
- EntityAlreadyDeletedException (selectEntity()など)
- 排他制御例外
- EntityAlreadyUpdatedException (update()など)
- ユニーク制約違反
- EntityAlreadyExistsException (insert()など)
どうやってthrowする?
業務例外を作成して、普通に throw new すればOKです。LaApplicationException を継承した例外は業務例外として認識されます。
MessagingApplicationException
必ずユーザー向けメッセージが必要な業務例外であれば、MessagingApplicationException を継承すると良いでしょう。(継承ではなくそのままnewでもOKです。マイ例外のポリシー次第で)
e.g. ユーザー向けメッセージを必要とする業務例外の定義 @Java
public class SeaLandPiariException extends MessagingApplicationException {
public SeaLandPiariException(String debugMsg, UserMessages messages) {
super(debugMsg, messages);
}
...
}
debugMsgはあくまでディベロッパー用のデバッグメッセージです。業務例外でもディベロッパー対応が必要かもなので、状況を表す値などを埋め込みましょう。 一方で、messages は、[app]_message.properties に定義された、ユーザー(人)向けのメッセージを指定しましょう。
MessageResponseApplicationException
レスポンスも独自に差し替えたい場合は、MessageResponseApplicationException の方を継承すると良いでしょう。newするときに自分で ActionResponse を指定できます。
RequestIllegalTransitionException
単純に "不正な画面遷移ですよ" と固定のメッセージで突き返したいだけであれば、Actionクラスの verifyOrIllegalTransition() で判定すると良いでしょう。(主にサーバーサイドHTML向け)
その他、フレームワークメソッド経由でthrow
他にも、ログイン認証の loginAssist.login(), 二度押し防止の verifyToken(), dbfluteの selectEntity() など、フレームワークのメソッドを呼ぶことで発生させられる業務例外があります。
だれがcatchする?
Actionクラスであれば、ApplicationExceptionResolver が catch します。
Jobクラスや非同期スレッドの場合は、リカバリーするユーザーがいないので、(アプリが実装する)その処理のトップの部分で自分で処理する必要があります。
catchされた後は、どう処理される?
ひとまず通知ログ(INFO)が出力され、どういうレスポンスが戻るかは例外によって変わります。 (厳密には、通知ログが出力されるかどうか自体も、例外によって変わります)
HTMLレスポンスなら
HTTPステータスは 200 で、[app]_message.properties のキーに対応します。
- LoginFailureException
- ログイン画面へリダイレクト (errors.login.failure)
- LoginRequiredException
- ログイン画面へリダイレクト (メッセージなし)
- RequestIllegalTransitionEx...
- show_errors.html (errors.app.illegal.transition)
- DoubleSubmittedRequestEx...
- verifyToken()で指定 (errors.app.double.submit.request)
- EntityAlreadyDeletedEx...
- show_errors.html (errors.app.db.already.deleted)
- EntityAlreadyUpdatedEx...
- show_errors.html (errors.app.db.already.updated)
- EntityAlreadyExistsEx...
- show_errors.html (errors.app.db.already.exists)
- MessagingApplicationEx...継承
- show_errors.html (newのときに指定されたメッセージ)
- MessageResponseAppli...継承
- 指定されたレスポンス (newのときに指定されたメッセージ)
- それ以外のLaAppicationEx...
- show_errors.html (例外に登録されていればそのメッセージ)
アプリの独自例外も、メッセージを表示するだけなら MessagingApplicationException, レスポンスも独自に変更したいのであれば MessageResponseApplicationException を使うと良いでしょう。
それ以外の LaAppicationException で、例外の種類によって恒久的に独自に処理を変更したい場合は、 Actionクラスにて、handleApplicationException() をオーバーライドすると良いでしょう。 [App]BaseAction にてオーバーライド実装すれば共通的な処理となります。
JSONレスポンスなら
JSONで戻す場合は、リダイレクトしたり show_errors.html を戻してもしょうがないので、ApiFailureHook で Hook して JSON を戻すようにします。HTTPステータスやJSONの形などは、そのアプリのポリシー次第です。 Failure統一パターンなのか、共通ヘッダーパターンなのか、それ以外なのか、よく検討しましょう。
あれっ、業務例外にログって必要? (通知ログ:INFO)
通常は不要ですが、厳密にはクライアント例外なのか?いたずらなのか?などなど曖昧なケースもあり得るので、LastaFluteではエラーログに出力するのではなく通知ログ(INFOレベル)で出力します。 そもそも、サーバーの例外ハンドリング自体のバグで発生している可能性もあるので、特にインクリメンタル開発のサービス運用であれば、念のため情報は残しておきたいところです。
ただ、Login関連の例外など、明らかに不要と言えるものに関しては、通知ログは抑制されています。 アプリで throw する例外も、例外を new した後に withoutInfo() を呼べば、通知ログを抑制することができます。 また、ActionAdjustmentProvider にて、統一的に通知ログを出すか出さないかの判定を調整することもできます。
通知ログは、logback.xml の設定次第ですが、app_[app].log に出力されます。
UserMessagesの使い方
まず、propertyの概念から
ユーザーメッセージには、どの項目に対するメッセージなのか? という概念が必ず存在します。UserMessages では、property という名前でその項目を表します。HTMLレスポンスであればHTML上の項目名、JSONレスポンスであればJSON上のキー名がそれに相当します。
特にどの項目に属するわけじゃないユーザーメッセージも存在します。業務例外のメッセージなんかはそうです。 イメージとしては、画面の上部に表示されるだけのメッセージを思い浮かべると良いでしょう。 その場合は、"グローバルなメッセージ" という扱いで、LastaFluteでは property が _global という値で表現します。UserMessages.GLOBAL にて定数として定義されています。
[App]Messages を new して add...()
[App]Messages を new して、FreeGenで自動生成されたタイプセーフな add...() メソッドを使いましょう。
e.g. ActionでUserMessagesを使った業務例外のthrow @Java
throw new MessagingApplicationException("..."
, new HarborMessages().addErrorsSignup...(UserMessages.GLOBAL, ...));
第一引数のプロパティですが、バリデーションエラーの場合、該当する項目のプロパティ名を指定しましょう。 業務例外であれば、ほとんどの場合GLOBAL固定で良いでしょう(フロント側との取り決め次第ですが)。
このやり方は、どのクラスでも利用できますが、あまりLogicクラスとかでユーザーメッセージを意識した業務例外を投げるのは役割的に微妙なので、 基本的に Action や Assist クラスでのやり方と言えるでしょう。
一方で、Assistクラスで業務例外を投げるよりも、Actionクラスで投げるほうがフローコントロールが分かりやすくなるので、 できるだけ戻り値や別の例外などでActionクラスに状況を伝えて、Actionクラスで投げてもらうほうが良いでしょう。 業務を止めるって大きな話なので、verify...()のメソッドも同様ですが、Actionクラスで表現したいものです。 (実装の都合上難しい場合は、しょうがないです。努力目標で)
アプリでマイ例外クラス作る?
マイ例外、どのくらい作る?
これは悩ましいです。
- 全部作る
- 大変な覚悟が必要
- 一部作る
- 基準は?って話になる (区別が必要なものだけ作るとか、重要なものだけ作るとか!?)
- 作らない
- まあ楽ではあるが... (区別が必要な場合は作らざるを得ないので、これはあり得ない!?)
システム例外と業務例外でポリシーを分けるというのもアリです。 システム例外で都度作っていたらそれこそ大変なので、業務例外だけ "全部作る" or "一部作る" というのも現実的かと思います。
また、アプリの例外であることがわかるように、[App]SystemException とか [App]AppException とかを用意して、それを使うというのもアリです。
いずれにせよ、最初に決めましょう。
マイ例外、どこに作る?
再利用しない例外
その場限りの再利用しない例外 (もしくは、他のパッケージからは使わない例外) であれば、その throw するクラスのパッケージに exception パッケージを作って、そこに入れると良いでしょう。
e.g. Action や Assist でthrowする例外の置き場所 @Directory
app.web
|-product
| |-exception
| | |-TooMiracleProductException.java
| | |-TooWonderfulProductException.java
| | |-...
| |-ProductListAction.java
| |-ProductListAssist.java
| |-...
もちろん自由ではあるのですが、例外クラスはあまりコード的に重要ではないので、Actionクラスと横並びに作るとどんどんファイルが探しづらくなるので、例外だけをまとめるのがオススメです。 (LastaFluteでは、webの下はクラスの種別ではなく業務単位でパッケージを作るのが強い慣習ですが、ピンポイントで小さな世界で効果的に種別ごとにパッケージを作るのは問題ありません)
再利用する例外
どのくらいの範囲で再利用したいかに寄ります。
- webパッケージ全体で再利用
- app.web.base.exception
- logicパッケージ全体で再利用
- app.logic.exception
- appパッケージ全体で再利用
- app.exception
- プロジェクト全体で再利用
- bizfw.exception (HotDeploy効かない領域)
作ってるときは再利用できるのかどうかわからないケースが多いと思うので、まずは近いところ (再利用しないケースの場所) に置いておくでも良いかとは思います。再利用するときに移動すればと。
一件検索でデータ無かったとき
データが見つからないってどういうこと?
データベースに検索してデータが見つからないってどういうことでしょうか? HTMLレスポンスなら詳細画面で "すれ違い" やGETパラメーターの "いたずら" であり得ます。JSONレスポンスなら一件データ取得として (クライアント次第で様々でしょうが) 似たような感じであり得るかと思います。
基本的には業務例外として
いずれにせよ、システム例外(サーバーサイドの不具合)ではないので、単に普通の例外を投げれば良いというわけではないですし、メッセージも必要かもしれません。 ということで、基本的には業務例外として扱う のが自然でしょう。
メッセージと例外は固定でOK
ただ、データの種別 (会員なのか商品なのか) によって、メッセージを変更する必要はあまりない と考えらます。"すれ違い" であれば排他制御を示す統一的なメッセージを戻せばユーザーはわかりますし、"いたずら" であればそこまで気を使う必要はありません。例外クラスについても、つどつど違う例外を投げる必要はあまりない と考えれます。 一回のリクエストで要求される一件データは多くのケースで単一種類であり、クライアントサイドもデータの種類を識別する必要はなく、逆に区別してしまうとクライアントサイドの統一的な処理の妨げになる可能性もあります。 なによりも、いちいち区別しているとサーバーサイドの実装が大変です(不要なことはやらない)。
JSON API で戻すJSONのイメージはこちら
LastaFluteなら組み込み
すでにこのページをしっかり読んでいれば、LastaFluteの思想がなんとなくわかるかと思います。EntityAlreadyDeletedException が業務例外として組み込まれていますから、ここで書いた思想で仕組みが整備されており、Actionクラスの中でも dbflute で検索してデータが無かったときに発生する例外をそのままthrowすればOKです。
そして、"(統一の)メッセージ" や "Failure統一のJSON" を調整するなら...
- (統一の)メッセージを調整
- [app]_message.propertiesを修正
- (Failure統一パターンの) failureType
- [App]ApiFailureHookを修正
排他制御例外やユニーク制約違反も
排他制御例外 (EntityAlreadyUpdatedException) や、ユニーク制約違反 (EntityAlreadyExistsException) もほぼ同じような話になり、LastaFluteとしてもその二つも加えて組み込んでいます。
通知ログの監視って?
エラーログを常日頃から監視しないのは論外なのでここでは話題にしませんが、通知ログもウォッチすることも大切だと考えています。
基本的に、通知ログの中で発生している例外は "正常なレアケース" ばかりなので、あまり目くじらを立てる必要はありませんが...
- なんだかんだ曖昧なケースもあり得る
- そもそも例外ハンドリング自体がうまくいってるの?
...などの状況が想定されるため、通知ログも重要な関心事です。
また、あまりに "正常なレアケース" が多いと、そもそも業務的な問題もあるのではない? そういったところの発見のきっかけにもなったりします。妙に "いたずら" が多いなぁというのもわかるかも。 "今日は、どんな通知ログ出てるかなぁ" と見る習慣があると良いでしょう。
業務例外だけを専用のログファイルに出力するというのもアリだと思いますし、 あまりログが出過ぎるようであれば、例外の内容によってログを出す出さないを区別するようにしても良いでしょう。 (ああ、そういう機能を付けた方がいいのかも!?って書きながら思った...)
例外ハンドリングこそ負債になりやすい?
正常系はみな意識が高いのでわりと細かいところまで考慮しますが、異常系は適当になりがちです。その理由は...
- 正常系に比べれば異常系は意識が薄い
- テストがしづらく確認が難しい
- そもそもシステム例外と業務例外の区別が付かない人が多い
- そもそも言語の例外の仕組みを理解してない人が多い
- ケースバイケースが多い世界なので体系化しづらい
- 適当でも開発時は困らない (困るのは運用に携わっている人)
いざ発生するときは、わりと緊急な事態です。でも適当な処理でわけわからなくなって対応に時間がかかる、なんてこともよくあります。 システム例外と業務例外の区別が付いておらず、エラーログが "ただの滝" みたいになっていることもよくあります。
多くの現場で、例外が適当に扱われているのを見てきました。 やはり、そのデメリットはもろに食らうんですね。運用を経験しないとわからない。 インクリメンタル開発だと、日常業務が緊急業務でどんどん割り食って先に進まない。
なので、LastaFluteでも気合を入れて例外ハンドリングの仕組みを構築し、できるだけ億劫にならずに例外ライフが送れるように努めています。 ドキュメントをがっつり書いているのも、例外をもっと知ってほしいから。
そして、やはりケースバイケースが多いし、まだまだ考慮しないといけないことあるかもしれませんので、この後もつどつど積極的に整備していこうと思っているレイヤです。 (気軽に相談してください。現場にどんな要件があるのか知りたいですし)