はじめに
Spring BootでREST APIを作成し、Exceptionごとに作成した独自のエラーレスポンスボディをログ出力したくなった。
ログ出力は、Spring AOPを使用して行っていたため、AOPのアノテーションで実現したい。
Spring Bootを用いたREST API開発を初めて行ったため、フレームワークでどのアノテーションが先に実行されるか、といった実行順序までは理解していなかった(今でも完全に理解していない)ため、調査に難航した。
一応出力できるようになったため、過程を含めその方法を記載する。
他のアプローチ方法をご存知の方がいましたら是非教えていただきたいです!
最初に試したこと(失敗)
@RestControllerAdvice
を付けたクラスでExceptionをハンドリングし、返却値を@Aspect
を付与したクラスの@AfterReturning
で補足。
補足した値をログ出力する。
実装例
Exceptionハンドラー
@RestControllerAdvice
public class ControllerExceptionHandler extends ResponseEntityExceptionHandler {
@Override
protected ResponseEntity<Object> handleMethodArgumentNotValid(MethodArgumentNotValidException ex,
HttpHeaders headers, HttpStatus status, WebRequest request) {
return handleExceptionInternal(ex, ResponseErrorBody.validErrResBuild(ex.getMessage()), headers, status,
request);
}
@ExceptionHandler(ConstraintViolationException.class)
protected ResponseEntity<Object> handleConstraintViolation(
ConstraintViolationException ex, WebRequest request) {
return handleExceptionInternal(ex,
ResponseErrorBody.validErrResBuild(getResultMessage(ex.getConstraintViolations().iterator())), null,
HttpStatus.BAD_REQUEST,
request);
}
/* 以下略 */
}
Aspectクラス
@Aspect
@Component
public class AopLogging {
private static final Logger logger = LoggerFactory.getLogger(AopLogging.class);
@AfterReturning(value = "ExceptionハンドラーのPointCut", returning = "returnValue")
public void outReturnValue(JoinPoint jp, Object returnValue) {
ResponseEntity<Object> entity = (ResponseEntity) returnValue;
ObjectMapper mapper = new ObjectMapper().enable(SerializationFeature.INDENT_OUTPUT); // jsonを整形
String body = entity.getBody());
try {
logger.info(mapper.writeValueAsString(body));
} catch (JsonProcessingException e) {
e.getMessage();
}
}
動作検証
curlにてバリデーションエラーとなるリクエストを投げてログ出力を確認。
結果として、AopLoggingクラスによるログ出力はされなかった。
前例がないか調べてみると、同様のことを行おうとしているができない、といったケースがいくつかヒットした。
原因としては、@Aspect
の処理が@RestControllerAdvice
より前に行われるためControllerExceptionHandlerクラスに対して@AfterReturning
が効いていなかったらしい。
そのためこの方法では不可能と判断し、別の方法を試すことに。
次に試したこと(成功)
AbstractErrorControllerを継承したCustomErrorControllerを作成し、返却値を@Aspect
を付与したクラスの@AfterReturning
で補足。
補足した値をログ出力する。
この方法は、@RestControllerAdvice
以外で独自のエラーレスポンスを返す方法として紹介されており、それに対してAOPの@AfterReturning
ができないか試してみたものである。
動作原理としては、通常、コントローラでExceptionをcatchしなかった場合、DispatcherServletでcatchされ、返却する値をBasicErrorControllerにマッピングしてレスポンス内容を設定・返却するが、DispatcherServletからマッピングされるクラスを独自のErrorControllerに変更することによってレスポンス内容を変更しようというものである(分かりづらい or 間違ってたらすいません)。
実装例
カスタムエラーコントローラ
@RestController
@RequestMapping("/error")
public class CustomErrController extends AbstractErrorController {
public CustomErrController(ErrorAttributes errorAttributes) {
super(errorAttributes);
}
@RequestMapping
public ResponseEntity<Map<String, Object>> error(final HttpServletRequest request) {
ServletWebRequest serbletWebReq = new ServletWebRequest(request);
DefaultErrorAttributes defaultErrAtr = new DefaultErrorAttributes();
ErrorAttributeOptions options = ErrorAttributeOptions.of(
ErrorAttributeOptions.Include.BINDING_ERRORS,
ErrorAttributeOptions.Include.EXCEPTION,
ErrorAttributeOptions.Include.MESSAGE);
Map<String, Object> defaultBody = defaultErrAtr.getErrorAttributes(serbletWebReq, options);
HttpStatus status = this.getStatus(request);
return customResponse(defaultBody, status);
}
private ResponseEntity<Map<String, Object>> customResponse(Map<String, Object> defaultBody, HttpStatus status) {
Map<String, Object> body = new LinkedHashMap<>();
body.put("title", defaultBody.get("error"));
body.put("message", defaultBody.get("message"));
body.put("exception", defaultBody.get("exception"));
String exceptionName = defaultBody.get(BODY_EXCEPTION).toString();
// バリデーションチェックエラー
if (ConstraintViolationException.class.getName().equals(exceptionName)
|| BindException.class.getName().equals(exceptionName)
|| MethodArgumentNotValidException.class.getName().equals(exceptionName)) {
body.replace("title", "バリデーションチェックエラー");
status = HttpStatus.BAD_REQUEST;
return new ResponseEntity<>(body, status);
}
/* 以下、その他Exceptionでレスポンス内容を変更したいものを記載 */
return new ResponseEntity<>(defaultBody, status);
}
}
Aspectクラス
前回と同様のものを使用し、PointCut部分を変更。
動作検証
curlにてバリデーションエラーとなるリクエストを投げてログ出力を確認。
結果として、レスポンスボディの内容をログ出力できていた。
今回の方法以外でもっといい方法をご存知の方がいましたら是非教えていただきたいです!!
参考資料
- Spring Boot な REST API でエラーが発生した際に返すJSONはどこで誰が作ってる???
- JSON:API Error Object with Spring Boot REST API
- Spring @ControllerAdvice vs ErrorController
- Spring Boot エラーページの最低限のカスタマイズ (ErrorController インターフェースの実装)
- Spring MVC(+ Spring Boot) における404時の動き
- Spring REST Error Handling Example
- Spring BootでつくったAPIのリクエストのバリデーションで出るExceptionのまとめ
- Spring Boot で 404 Not Found などのエラーが発生した際の表示をカスタマイズする
- Spring Boot 2.3 で DefaultErrorAttributes と ErrorAttributeOptions を使ってエラー情報を取得する
- 4.2. 例外ハンドリング
- SpringMVC の小径 ちょっと寄り道 ロギングの小径