1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

REST APIの独自のエラーレスポンスボディをSpring AOPでログ出力したい

Posted at

はじめに

Spring BootでREST APIを作成し、Exceptionごとに作成した独自のエラーレスポンスボディをログ出力したくなった。
ログ出力は、Spring AOPを使用して行っていたため、AOPのアノテーションで実現したい。
Spring Bootを用いたREST API開発を初めて行ったため、フレームワークでどのアノテーションが先に実行されるか、といった実行順序までは理解していなかった(今でも完全に理解していない)ため、調査に難航した。
一応出力できるようになったため、過程を含めその方法を記載する。
他のアプローチ方法をご存知の方がいましたら是非教えていただきたいです!

最初に試したこと(失敗)

@RestControllerAdviceを付けたクラスでExceptionをハンドリングし、返却値を@Aspectを付与したクラスの@AfterReturningで補足。
補足した値をログ出力する。

実装例

Exceptionハンドラー

ControllerExceptionHandler.java
@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クラス

AopLogging.java
@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 間違ってたらすいません)。

実装例

カスタムエラーコントローラ

CustomErrController.java
@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にてバリデーションエラーとなるリクエストを投げてログ出力を確認。
結果として、レスポンスボディの内容をログ出力できていた。

今回の方法以外でもっといい方法をご存知の方がいましたら是非教えていただきたいです!!

参考資料

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?