SpringBoot/例外処理
の編集
https://cs27.org/wiki/kobespiral2021/?SpringBoot/%E4%BE%8B%E5%A4%96%E5%87%A6%E7%90%86
[
トップ
] [
編集
|
差分
|
履歴
|
添付
|
リロード
] [
新規
|
一覧
|
検索
|
最終更新
|
ヘルプ
]
-- 雛形とするページ --
(no template pages)
[[SpringBoot]] * Spring Boot における例外処理 [#qbf253f0] Spring Bootにおける例外処理のプラクティスを色々調べたのでまとめる. ** 基本的な考え方 [#ne8afdc9] + Javaプログラムにおいて何らかのエラーを検出する際には,必ず ''例外''を発生させること -- 返り値でエラーを表現してはいけない. + 例外は必ず ''非検査例外 (extends RuntimeException)''とすること -- 検査例外(extends Exception)をすると,throws宣言やtry-catchが必要になりコードが汚くなる -- 呼び出し先の検査例外をcatchする必要があれば,非検査例外にラップして投げ直す try { //検査例外を投げる呼び出し } catch(検査例外 e) { throw new 非検査例外("メッセージ", e); //非検査例外で投げる } + 例外のハンドリングは,専用の ''例外ハンドラ'' で横断的に行う + 例外が発生した場合,Webアプリ・Web-APIの呼び出し元であるクライアントにエラーを伝えること -- サーバ側で復旧・解決できるエラーについてはこの限りではない + エラーを伝える際には,適切なHTTPステータスをつけられればなお良い -- デフォルトではすべて500(Internal Server Error)になってしまう ** アーキテクチャ [#ebd73770] &attachref(exception-handling.png); ** 拾うべき例外とその層 [#s76ad8ed] 拾うべきエラーはそれぞれの層で種類が異なる. Springに関する記事,マニュアルをいろいろと調べた中で, 大体下記のような感じか. |Springの層|拾うべき例外の種類|例|h |コントローラ層|サービス利用の前提に違反した例外|フォームの入力間違い,パラメータの型違い,認証・認可の失敗など| |サービス層|ビジネスルールに違反した例外|ユーザIDは他の人と同じものを使えない,他人の口座から引き出せない,残高以上の額を引き出せない,口座は1つしか作れないなど| |レポジトリ層|インフラやシステムの不具合による例外|入出力例外,通信例外,データベース例外など| ** カスタム例外 [#ubf799e2] Spring Frameworkが発生させる例外は一般的なものなので, ''アプリケーション固有の例外 (カスタム例外)''でラップしてやるのが良い. |Spring例外|例外発生場所|ラップするカスタム例外|h |NoSuchElementException|@ServiceでRepository内の口座を参照しようとしたがなかった|AccountNotFoundException| |MethodArgumentNotValidException|@RestController でフォーム検査に引っかかった|PitCoinValidationException| 例外の種類だけでなく,起こった場所や文脈に応じて,例外に''適切なエラーコード''を埋め込んであげると,呼び出し側で対処しやすい |例外タイプ|検査エラーの内容|エラーコード|h |不正な操作|フォーム検査にひっかかった|PirCoinValidationException.INVALID_FORM| |不正な操作|新規ユーザIDが既に存在した|PirCoinValidationException.USER_ALREADY_EXISTS| |不正な操作|残高以上のお金を出勤しようとした|PirCoinValidationException.INSUFFICIENT_BALANCE| 例外サンプル /** * アカウントに対する不正な操作に関する例外 */ public class AccountValidationException extends RuntimeException { /** 定義済みエラーコード */ public static final int ACCOUNT_ALREADY_EXISTS = 11; public static final int INVALID_UID = 12; public static final int INVALID_AMOUNT = 13; public static final int INSUFFICIENT_BALANCE = 14; private static final long serialVersionUID = 1L; /** エラーコード */ public final int code; public AccountValidationException(int code, String str, Throwable cause) { super(str, cause); this.code = code; } public AccountValidationException(int code, String str) { super(str); this.code = code; } } 使用例 /** * 与えられたユーザIDの口座からコインを出金する. * * @param uid 口座のユーザID * @param amount 出金する枚数 * @return 出金後の口座情報 */ public Account useCoin(String uid, int amount) { if (amount <= 0) { throw new AccountValidationException(AccountValidationException.INVALID_AMOUNT, "amount must be a positive number"); } else { int coin = getCoin(uid); if (coin - amount < 0) { // 残高が0未満にならないかチェック throw new AccountValidationException(AccountValidationException.INSUFFICIENT_BALANCE, "Cannot afford " + amount + " coins"); } else { return setCoin(uid, coin - amount); } } } ** 例外のタイプと対応するHTTPステータスコード [#f01f9b3b] |一般的なJava例外|カスタム例外|対応するHTTPコード|h |ElementNotFound|そんなリソースないよ例外|404 Not found| |IllegalArgumentException|そんな引数おかしいよ例外|400 Bad Request| |IllegalStateException|アクセスのタイミングがおかしいよ例外|503 Service Unavailable| |AuthenticationException|認証失敗したよ例外|401 Unauthorized| |AccessDeniedException,SecurityException|アクセスしてはダメだよ例外|403 Forbidden| |UnsupportedOperationException|そんな操作ないよ例外|501 Not Implemented| |Throwable (anything else)|その他の例外|500 Internal Server Error| ** 例外ハンドラ [#p77d4ca6] - @RestControllerに対応する例外ハンドラは,@RestControllerAdvice アノテーションを付けたクラス - アーキテクチャ的には,例外ハンドラは ''コントローラ層'' に置く (アーキテクチャ図参照) - キャッチする例外の型とその時に返すHTTPステータスコード,実行する処理を書く サンプルソース @RestControllerAdvice public class PitCoinErrorHandler { /** * アカウントが存在しない * @param req * @param ex * @return */ @ExceptionHandler(AccountNotFoundException.class) @ResponseStatus(HttpStatus.NOT_FOUND) public PitCoinResponse handleNotFoundException(HttpServletRequest req, AccountNotFoundException ex) { log.warn(ex.getMessage()); return PitCoinResponse.createErrorResponse(ex.code, ex); } /** * アカウントに対する操作が不正 */ @ExceptionHandler(AccountValidationException.class) @ResponseStatus(HttpStatus.BAD_REQUEST) public PitCoinResponse handleIlleagalArgumentException(HttpServletRequest req, AccountValidationException ex) { log.warn(ex.getMessage()); return PitCoinResponse.createErrorResponse(ex.code, ex); } /** * 許可されないメソッドの実行 * @param request * @param ex * @return */ @ExceptionHandler(HttpRequestMethodNotSupportedException.class) @ResponseStatus(HttpStatus.FORBIDDEN) public PitCoinResponse handleMethodNotSupportedException(HttpServletRequest request, HttpRequestMethodNotSupportedException ex){ return PitCoinResponse.createErrorResponse(ex); } /** * 上記で処理していないエラー * * @param request * @param ex * @return */ @ExceptionHandler(Exception.class) @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) public PitCoinResponse handleException(HttpServletRequest request, Exception ex){ log.error(ex.getMessage(),ex); return PitCoinResponse.createErrorResponse(ex); } } ** レスポンスの設計 [#m2a7ec34] APIの呼び出しが,正常に完了した場合もエラーの場合も,''同じ型のレスポンス'' を返すように設計すれば呼び出し側のハンドリングが楽. レスポンスの設計例 - code: エラーコード - message: メッセージ - result: 成功だった場合の結果 例えば,POST /accounts/masa-n/use?amount=250 を実行する際 - 成功 200 { "code": "OK", "message": null, "result": {"uid": "masa-n", "coin": 750}} - 失敗 404 { "code": "E14", "message": "Cannot afford 250 coins", "result": null} ソース /** * PitCoinコントローラーが返すレスポンス */ @Data public class PitCoinResponse { public static final String CODE_OK = "OK"; public static final String ERROR_PREFIX = "E"; public static final int DEFAULT_ERROR_CODE = 99; /** PitCoin-APIのレスポンスコード */ private String code; /** メッセージ */ private String message; /** データ */ private Object result; /** * 外部から生成禁止 */ private PitCoinResponse() { } /** * エラーコード付きでエラーレスポンスを作成する * @param code エラーコード * @param ex 例外 * @return エラーレスポンス */ public static PitCoinResponse createErrorResponse(int code, Exception ex) { PitCoinResponse res = new PitCoinResponse(); res.setCode(ERROR_PREFIX + String.format("%02d", code)); res.setMessage(ex.getMessage()); res.setResult(null); return res; } /** * デフォルトエラーコードでエラーレスポンスを作成する * @param ex 例外 * @return エラーレスポンス */ public static PitCoinResponse createErrorResponse(Exception ex) { return createErrorResponse(DEFAULT_ERROR_CODE, ex); } /** * メッセージ付きで成功レスポンスを作成する * @param data データ * @param message メッセージ * @return レスポンス */ public static PitCoinResponse createSuccessResponse(Object data, String message) { PitCoinResponse res = new PitCoinResponse(); res.setCode(CODE_OK); res.setMessage(message); res.setResult(data); return res; } /** * 成功レスポンスを作成する * @param data データ * @return レスポンス */ public static PitCoinResponse createSuccessResponse(Object data) { return createSuccessResponse(data, null); } } ** 参考文献 [#ob45d9c1] - 段階的に理解する Java 例外処理 -- https://qiita.com/ts7i/items/d7f6c1cd5a14e55943d4 - Where should I put my exception handling in Spring MVC? -- https://stackoverflow.com/questions/33966211/where-should-i-put-my-exception-handling-in-spring-mvc/33966288#33966288 - Service and controller layer exception handling design pattern -- https://stackoverflow.com/questions/54677994/service-and-controller-layer-exception-handling-design-pattern - Where to handle Exceptions in Spring Applications -- https://stackoverflow.com/questions/45034371/where-to-handle-exceptions-in-spring-applications
タイムスタンプを変更しない
[[SpringBoot]] * Spring Boot における例外処理 [#qbf253f0] Spring Bootにおける例外処理のプラクティスを色々調べたのでまとめる. ** 基本的な考え方 [#ne8afdc9] + Javaプログラムにおいて何らかのエラーを検出する際には,必ず ''例外''を発生させること -- 返り値でエラーを表現してはいけない. + 例外は必ず ''非検査例外 (extends RuntimeException)''とすること -- 検査例外(extends Exception)をすると,throws宣言やtry-catchが必要になりコードが汚くなる -- 呼び出し先の検査例外をcatchする必要があれば,非検査例外にラップして投げ直す try { //検査例外を投げる呼び出し } catch(検査例外 e) { throw new 非検査例外("メッセージ", e); //非検査例外で投げる } + 例外のハンドリングは,専用の ''例外ハンドラ'' で横断的に行う + 例外が発生した場合,Webアプリ・Web-APIの呼び出し元であるクライアントにエラーを伝えること -- サーバ側で復旧・解決できるエラーについてはこの限りではない + エラーを伝える際には,適切なHTTPステータスをつけられればなお良い -- デフォルトではすべて500(Internal Server Error)になってしまう ** アーキテクチャ [#ebd73770] &attachref(exception-handling.png); ** 拾うべき例外とその層 [#s76ad8ed] 拾うべきエラーはそれぞれの層で種類が異なる. Springに関する記事,マニュアルをいろいろと調べた中で, 大体下記のような感じか. |Springの層|拾うべき例外の種類|例|h |コントローラ層|サービス利用の前提に違反した例外|フォームの入力間違い,パラメータの型違い,認証・認可の失敗など| |サービス層|ビジネスルールに違反した例外|ユーザIDは他の人と同じものを使えない,他人の口座から引き出せない,残高以上の額を引き出せない,口座は1つしか作れないなど| |レポジトリ層|インフラやシステムの不具合による例外|入出力例外,通信例外,データベース例外など| ** カスタム例外 [#ubf799e2] Spring Frameworkが発生させる例外は一般的なものなので, ''アプリケーション固有の例外 (カスタム例外)''でラップしてやるのが良い. |Spring例外|例外発生場所|ラップするカスタム例外|h |NoSuchElementException|@ServiceでRepository内の口座を参照しようとしたがなかった|AccountNotFoundException| |MethodArgumentNotValidException|@RestController でフォーム検査に引っかかった|PitCoinValidationException| 例外の種類だけでなく,起こった場所や文脈に応じて,例外に''適切なエラーコード''を埋め込んであげると,呼び出し側で対処しやすい |例外タイプ|検査エラーの内容|エラーコード|h |不正な操作|フォーム検査にひっかかった|PirCoinValidationException.INVALID_FORM| |不正な操作|新規ユーザIDが既に存在した|PirCoinValidationException.USER_ALREADY_EXISTS| |不正な操作|残高以上のお金を出勤しようとした|PirCoinValidationException.INSUFFICIENT_BALANCE| 例外サンプル /** * アカウントに対する不正な操作に関する例外 */ public class AccountValidationException extends RuntimeException { /** 定義済みエラーコード */ public static final int ACCOUNT_ALREADY_EXISTS = 11; public static final int INVALID_UID = 12; public static final int INVALID_AMOUNT = 13; public static final int INSUFFICIENT_BALANCE = 14; private static final long serialVersionUID = 1L; /** エラーコード */ public final int code; public AccountValidationException(int code, String str, Throwable cause) { super(str, cause); this.code = code; } public AccountValidationException(int code, String str) { super(str); this.code = code; } } 使用例 /** * 与えられたユーザIDの口座からコインを出金する. * * @param uid 口座のユーザID * @param amount 出金する枚数 * @return 出金後の口座情報 */ public Account useCoin(String uid, int amount) { if (amount <= 0) { throw new AccountValidationException(AccountValidationException.INVALID_AMOUNT, "amount must be a positive number"); } else { int coin = getCoin(uid); if (coin - amount < 0) { // 残高が0未満にならないかチェック throw new AccountValidationException(AccountValidationException.INSUFFICIENT_BALANCE, "Cannot afford " + amount + " coins"); } else { return setCoin(uid, coin - amount); } } } ** 例外のタイプと対応するHTTPステータスコード [#f01f9b3b] |一般的なJava例外|カスタム例外|対応するHTTPコード|h |ElementNotFound|そんなリソースないよ例外|404 Not found| |IllegalArgumentException|そんな引数おかしいよ例外|400 Bad Request| |IllegalStateException|アクセスのタイミングがおかしいよ例外|503 Service Unavailable| |AuthenticationException|認証失敗したよ例外|401 Unauthorized| |AccessDeniedException,SecurityException|アクセスしてはダメだよ例外|403 Forbidden| |UnsupportedOperationException|そんな操作ないよ例外|501 Not Implemented| |Throwable (anything else)|その他の例外|500 Internal Server Error| ** 例外ハンドラ [#p77d4ca6] - @RestControllerに対応する例外ハンドラは,@RestControllerAdvice アノテーションを付けたクラス - アーキテクチャ的には,例外ハンドラは ''コントローラ層'' に置く (アーキテクチャ図参照) - キャッチする例外の型とその時に返すHTTPステータスコード,実行する処理を書く サンプルソース @RestControllerAdvice public class PitCoinErrorHandler { /** * アカウントが存在しない * @param req * @param ex * @return */ @ExceptionHandler(AccountNotFoundException.class) @ResponseStatus(HttpStatus.NOT_FOUND) public PitCoinResponse handleNotFoundException(HttpServletRequest req, AccountNotFoundException ex) { log.warn(ex.getMessage()); return PitCoinResponse.createErrorResponse(ex.code, ex); } /** * アカウントに対する操作が不正 */ @ExceptionHandler(AccountValidationException.class) @ResponseStatus(HttpStatus.BAD_REQUEST) public PitCoinResponse handleIlleagalArgumentException(HttpServletRequest req, AccountValidationException ex) { log.warn(ex.getMessage()); return PitCoinResponse.createErrorResponse(ex.code, ex); } /** * 許可されないメソッドの実行 * @param request * @param ex * @return */ @ExceptionHandler(HttpRequestMethodNotSupportedException.class) @ResponseStatus(HttpStatus.FORBIDDEN) public PitCoinResponse handleMethodNotSupportedException(HttpServletRequest request, HttpRequestMethodNotSupportedException ex){ return PitCoinResponse.createErrorResponse(ex); } /** * 上記で処理していないエラー * * @param request * @param ex * @return */ @ExceptionHandler(Exception.class) @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) public PitCoinResponse handleException(HttpServletRequest request, Exception ex){ log.error(ex.getMessage(),ex); return PitCoinResponse.createErrorResponse(ex); } } ** レスポンスの設計 [#m2a7ec34] APIの呼び出しが,正常に完了した場合もエラーの場合も,''同じ型のレスポンス'' を返すように設計すれば呼び出し側のハンドリングが楽. レスポンスの設計例 - code: エラーコード - message: メッセージ - result: 成功だった場合の結果 例えば,POST /accounts/masa-n/use?amount=250 を実行する際 - 成功 200 { "code": "OK", "message": null, "result": {"uid": "masa-n", "coin": 750}} - 失敗 404 { "code": "E14", "message": "Cannot afford 250 coins", "result": null} ソース /** * PitCoinコントローラーが返すレスポンス */ @Data public class PitCoinResponse { public static final String CODE_OK = "OK"; public static final String ERROR_PREFIX = "E"; public static final int DEFAULT_ERROR_CODE = 99; /** PitCoin-APIのレスポンスコード */ private String code; /** メッセージ */ private String message; /** データ */ private Object result; /** * 外部から生成禁止 */ private PitCoinResponse() { } /** * エラーコード付きでエラーレスポンスを作成する * @param code エラーコード * @param ex 例外 * @return エラーレスポンス */ public static PitCoinResponse createErrorResponse(int code, Exception ex) { PitCoinResponse res = new PitCoinResponse(); res.setCode(ERROR_PREFIX + String.format("%02d", code)); res.setMessage(ex.getMessage()); res.setResult(null); return res; } /** * デフォルトエラーコードでエラーレスポンスを作成する * @param ex 例外 * @return エラーレスポンス */ public static PitCoinResponse createErrorResponse(Exception ex) { return createErrorResponse(DEFAULT_ERROR_CODE, ex); } /** * メッセージ付きで成功レスポンスを作成する * @param data データ * @param message メッセージ * @return レスポンス */ public static PitCoinResponse createSuccessResponse(Object data, String message) { PitCoinResponse res = new PitCoinResponse(); res.setCode(CODE_OK); res.setMessage(message); res.setResult(data); return res; } /** * 成功レスポンスを作成する * @param data データ * @return レスポンス */ public static PitCoinResponse createSuccessResponse(Object data) { return createSuccessResponse(data, null); } } ** 参考文献 [#ob45d9c1] - 段階的に理解する Java 例外処理 -- https://qiita.com/ts7i/items/d7f6c1cd5a14e55943d4 - Where should I put my exception handling in Spring MVC? -- https://stackoverflow.com/questions/33966211/where-should-i-put-my-exception-handling-in-spring-mvc/33966288#33966288 - Service and controller layer exception handling design pattern -- https://stackoverflow.com/questions/54677994/service-and-controller-layer-exception-handling-design-pattern - Where to handle Exceptions in Spring Applications -- https://stackoverflow.com/questions/45034371/where-to-handle-exceptions-in-spring-applications
テキスト整形のルールを表示する
添付ファイル:
exception-handling.png
58件
[
詳細
]