Java・SpringBootを用いたAPI開発をスタート。しかし例外処理の実装する中で、様々な疑問にぶつかる日々、、(元々独学エンジニアだったので例外処理の理解が浅すぎた)
その都度調べて解釈してを繰り返し、分かった気になってきたので、それをまとめてみます。
今回は例外処理の解釈に焦点を当てているため、実装方法については触れません。
例外処理をする意味
まずは大前提として、なぜ例外処理をするのかについて。
利用者視点
- 例外的状況が起きた時専用の処理を設けることで、プログラムが強制終了するのを避けられる
- 例えば、try-catch文を用いて、問題が起きている箇所だけプログラムを実行せずに飛ばしてあげる
- その上で、利用者に「ここの操作が間違っているよ!」と知らせてあげることができる
- 例えば、「送信するファイルの形式が間違っています!」と画面に表示させるなど
開発者視点
- どこで問題が発生したかを特定できる(=バグ修正が容易になる)
- メソッドに例外処理の記述があることで、事前に「このメソッドは問題が起こるかも知れない」と把握することができる
なんでIf文ではだめなの?
例外処理を書いている中で、「これIf文で処理できるじゃん」って考えに一度はなるはず(私はなった)。結論から言うと、If文で処理することは基本的には悪手である。
- 理由①
- 理論上ではif文で全て処理することが可能だが、現実的には全て事前に対処するなんて不可能(そんなこと出来たらそもそもバグなんて作ってない)
- 理由②
- 例外的状況の処理と通常処理のコードの見分けが付きにくくなり、開発者的な視点から良くない
throwとthrowsの使い分け
例外の投げ方が色々あって、何を使うのが正解なんだーと悩んでた。
結論から言うと、どれを使えばいいのかは設計思想によるため正解というものはない。
正解に近づけるための第一歩として、まずはそれぞれの強み弱みを知ることが大事。
throwについて
例外的状況が起こったメソッドの中で問題を処理するもの。特徴としては、例外を監視しているJVMに対して任意のタイミングで「例外的状況になりました」と伝えられる点。
- ポジティブ
- 独自に作った例外を投げたい時に使える
throw new FileNotAcceptException(message); //例えばこんな感じ
- ネガティブ
- いちいち例外処理を記述する必要があり、コードが冗長になりがち
throwsについて
「throws」をメソッドの後ろにつけることによって、メソッド内で例外が発生した時に自身のメソッド内でcatchするのではなく、呼び出し元に例外を投げられる。
private void checkFile(引数) throws IOException {
// 例外処理はcheckFile() を呼び出しているメソッドに委ねられるよ
//以下略
}
- ポジティブ
- 呼び出し元でまとめて例外処理できるため、コードの記述量が少なく済む
- try-catchをいちいち書かなくていい
- ResponseEntityExceptionHandlerを継承したhandlerクラスを実装する際に相性がいい
- 「ResponseEntityExceptionHandlerを継承したhandlerクラス」については次の章で解説
- 正直ここの理解はまだ浅いため断定は避けたい(小声)
- 呼び出し元でまとめて例外処理できるため、コードの記述量が少なく済む
- ネガティブ
- 例外処理のバケツリレーがおこり、かえって可読性の悪いコードになる恐れも
- 呼び出し元で適切に例外処理を実装してくれるのかといった懸念が残る
落とし所
私自身の考える落とし所
Java・Spring Bootを用いたAPI開発において、現状の私自身が考える使い分けとしては以下の通りである。
- 自作例外を投げたい時
- throwで投げる
- その他
- throwsで呼び出し元に任せる
- 自作ハンドラークラス(ResponseEntityExceptionHandlerを継承したhandlerクラス)で一括処理
- throwsで呼び出し元に任せる
他の方が考えた落とし所
「非検査例外を扱いつつ、メソッド定義のthrows節に書くという方法がよいのではないか」と言う意見を発見。 詳しくはこの記事を参考に。
ResponseEntityExceptionHandlerを継承したhandlerクラスを作る意味
SpringBootでAPIを作るときは、基本的にはResponseEntityExceptionHandlerを継承させたハンドラークラスを自作しましょーとのことです。
@Slf4j
@RestControllerAdvice
@RequiredArgsConstructor
public class aaaExceptionHandler extends ResponseEntityExceptionHandler {
/** {@inheritDoc} */
@Override
protected ResponseEntity<Object> handleAsyncRequestTimeoutException(
AsyncRequestTimeoutException ex,
HttpHeaders headers,
HttpStatus status,
WebRequest webRequest) {
return super.handleExceptionInternal(
ex, ErrorDTO.of(status, ex.getMessage()), headers, status, webRequest);
}
//以下略
// こんな感じでを15個ほどオーバーライドする。
}
オーバーライドするものについてはここを参照。
いちいちオーバーライドする意味
実は元々用意されている例外処理のままだと、bodyの値がnullになっている。
//以下は継承元であるhandleAsyncRequestTimeoutExceptionクラスの一部
@Nullable
protected ResponseEntity<Object> handleAsyncRequestTimeoutException(
AsyncRequestTimeoutException ex, HttpHeaders headers, HttpStatus status, WebRequest webRequest) {
if (webRequest instanceof ServletWebRequest) {
ServletWebRequest servletWebRequest = (ServletWebRequest) webRequest;
HttpServletResponse response = servletWebRequest.getResponse();
if (response != null && response.isCommitted()) {
if (logger.isWarnEnabled()) {
logger.warn("Async request timed out");
}
return null;
}
}
return handleExceptionInternal(ex, null, headers, status, webRequest);
//bodyがnullになっている!!!!!
}
API利用者のことを考えると、bodyの値がnullなのは親切ではない。
bodyにエラーの情報をセットするために、オーバーライドするのです!(ちなみに情報はJSON形式で渡してあげるのが一般的)
最後に
まだまだ理解は浅い。。
間違っている箇所については、指摘いただけると助かります。