例外の種類
主に、 業務例外とシステム例外に分けられる。
業務例外
業務例外は、業務上想定されるエラー。
そのため、ハンドリングは必須。
vなどが挙げられる。
ただし、これらは例外処理ではなく、ロジックで回避できるものがほとんどなので、できる限り例外処理をしなくても良いような、保守的なコード実装を心がける。
処理を止めずに画面側にはステータスコード200で返却したり、そもそも例外ではなくレスポンスを返すようにしたり、ロジックとしてハンドリングされることもある。
システム例外
システム例外は意図しない挙動。
ネットワークエラーやメモリ不足(OutOfMemoryError)、スタックオーバーフロー(StackOverflowError)などが挙げられる。
むしろキャッチしてもあまり意味をなさないもの。 あくまで、ここでエラーが発生した場合、後続処理でデグレが発生しないように、といった観点でハンドリングする。
後続処理が実行されないように、むしろtry catchしないことも考える。(下手にtry catchすると、以降の処理が実行されてしまう可能性がある)
例外処理の挙動
- 例外が発生した時点で、tryブロック内の処理は即座に中断されてcatchブロックに処理が移る
- 例外を catchしても、try/catcnブロック以降の処理は実行される
- 例外をスローした場合は、以降の処理は実行されない(returnするのに似ている)
- try catchしていない処理で例外が発生する場合、以降の処理は実行されない(例外の種類にかかわらず)
- throw, throwsは、呼び元に例外処理を投げるだけ。結局呼び元で改めてcatchする必要がある。(ServiceのメソッドでServiceしたらサービス呼び出し元であるControllerでtry/catchする)
- 例外をスローすると、レスポンスはクローズされる。呼び元でHttpServretResponseなどを使用していると、クローズされたレスポンスを使用しようとするため、エラー(IllegalStateException: response has already closed.) になるため注意する。
例外の用途は?
主に以下の目的で使用する。
- エラーが起きた箇所で適切に処理を止め、後続の処理で意図しないバグが発生しないようにする。
- エラーをハンドリングしててユーザーに伝える
- ログ調査の際に必要な情報を提供する
- 業務例外については処理を止めずに分岐ロジックとして使用することもある
ログに出力すべきメッセージや情報は異なるため、基本的には例外を発生させたクラスで処理する。ここでいう処理とは、必要なログを出力したり、エラーを発生させたパラメータを画面側に返す、 などである。
パラメータの都合などで呼び出し元でしか得られない情報がある場合のみthrows、もしくはthrowで呼び出し元に処理を委任する。
例外はできる限り使用しない
検査例外は強制されており、非検査例外は、本来ロジックで回避できるものである。
理想論で言えば、保守的なソースコード管理ができていれば、リソース負荷もあるtry catchの独自実装は不要なはず。
とはいえ現実ではそうもいかない。以下のようなケースで使用するのであれば、許容範囲かと思う。
- 処理が煩雑で、オブジェクトやパラメータ不備の可能性がどうしてもぬぐえないとき(ハンドリングするなら業務例外、後続処理を止めるならシステム例外)
- 外部APIとの通信など、管轄外の領域を含むとき(システム例外として処理推奨)
- 例外によって処理やメッセージを分けたいとき(業務例外)
- 特殊なケースで、例外を握りつぶして処理を続行したいとき(業務例外)
例外とエラーの違い
初学者にとって、例外とエラーは特に混同されやすい。
401や500などはエラーを表すステータスだが、れっきとしたレスポンスであり、例外とは異なる。
しかし、HTTPリクエストで上記のステータスが返却されると、SpringFrameworkが例外を送出する。
このため、例外=エラーといった誤解が生まれる。
SpringFramework が出力する例外
例えば400系エラーが返された場合、 SpringFrameworkは HTTPClientError Exception を送出する。
この例外クラスは RestClientResponse Exception を継承しているため、getResponseBodyや、getHttpStatus によって情報を取得し、ハンドリングできる。
参考:【Spring】RestTemplateが投げる例外クラスまとめ
例外処理実装
try {
// 例外が発生する可能性のある処理
} catch (Exception e) {
system.out.println(e.getMessage()); // ハンドリング
}
catchは複数のレイヤーで実装することもできる
try {
// 例外が発生する可能性のある処理
} catch (HttpClientErrorException he) { // 検査例外ハンドリング
system.out.println(he.getMessage());
} catch (Exception e) { // 非検査例外ハンドリング
system.out.println(e.getMessage());
}
必ず例外処理を求められる
例外処理をスローしようとすると、例外処理をしないとエラーとなる場合と、特に処理をしなくてもエラーとならない場合がある。
これは、検査例外と非検査例外の違い。
検査例外はコンパイルエラーとなる、つまり、コンパイル時に検査されるため、検査例外が発生しうる処理では、例外処理を強制される。
同様に、検査例外をスローした場合は、呼び元での例外処理を強制される。
~Service~
public func() {
try {
// 例外が発生する可能性のある処理
} catch (Exception e) {
throw e; // 検査例外をそのままスロー
}
}
~Controller~
try {
service.func();
} catch (Exception e) { // 呼び元でハンドリングを強制される
system.out.println(e.getMessage());
}
eclipseの補完機能でtry catchを挿入すると、デフォルトで catch(Exception e) となり、初心者が何もわからずそのままスローして、呼び元でもtry catchを実装して…といった闇の深いコードにならないよう、try catchの扱いには注意する。
何度も言うが、try catchは、できるだけ実装しなくて済むように意識する。
非検査例外は、RuntimeExceptionを継承している例外たちのことで、例外処理を強制されることは無い。
RuntimeExceptionはつまり、実行時エラーのことで、ビルド時ではなく、メソッドの処理実行時に発生するエラー。
ぬるぽ(NullPointerException)や、リクエストの型不正(IllegalArgumentException)などのこと。
位置関係は以下のようになっている。
Exception // 例外処理必須
┗RuntimeException
┗NullPointerException // 例外処理不要
コメント