🧸

SpringBoot Exceptionハンドリング

2022/01/04に公開

前回の続き
https://zenn.dev/marumarumeruru/articles/aa2b9bd5607c1e

エラーが発生した際に詳細な内容をJSONで返すようにする

レスポンス例1
{
"handlerName": "handleDuplicateKeyException",
"localDatetime": "2022-01-04T19:38:05.955901",
"exceptionCauseMessage": "Duplicate entry '1' for key 'user_idx'",
"exceptionClassName": "org.springframework.dao.DuplicateKeyException",
"requestUri": "/insert_user/1",
"requestParams": "{name=name2}",
"headers": "{content-length=10, host=localhost:8080, content-type=application/x-www-form-urlencoded}",
"requestSessionId": "89C7B5111202C2A20C8DD2759CBED1FE"
}
レスポンス例2
{
"handlerName": "handleAppException",
"localDatetime": "2022-01-04T19:36:03.087691",
"exceptionCauseMessage": "Data truncation: Data too long for column 'name' at row 1",
"exceptionClassName": "org.springframework.dao.DataIntegrityViolationException",
"requestUri": "/update_user/1",
"requestParams": "{name=too long name}",
"headers": "{content-length=18, host=localhost:8080, content-type=application/x-www-form-urlencoded}",
"requestSessionId": "51C936B64DCBD05B4F2059A151472A16"
}

既存のソースは変更せず、下記の2ファイルを追加することで、エラーハンドリングできるようになる

AppExceptionBean.java
package com.example.demo.component.error;

import java.time.LocalDateTime;

import lombok.AllArgsConstructor;
import lombok.Getter;

@Getter
@AllArgsConstructor
public class AppExceptionBean {

  final String handlerName;
  final LocalDateTime localDatetime;
  final String exceptionCauseMessage;
  final String exceptionClassName;
  final String requestUri;
  final String requestParams;
  final String headers;
  final String requestSessionId;
}

Exception発生時にJSONで返却する項目

AppExceptionHandler.java
package com.example.demo.component.error;

import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.Map;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.context.request.WebRequest;
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;
import org.springframework.web.context.request.ServletWebRequest;

@RestControllerAdvice
public class AppExceptionHandler extends ResponseEntityExceptionHandler {

    private static final Logger log =
    LoggerFactory.getLogger(AppExceptionHandler.class);

    @ExceptionHandler(DuplicateKeyException.class)
    public ResponseEntity<Object> handleDuplicateKeyException(DuplicateKeyException ex, WebRequest request) {

        Map<String,String> requestParams = new HashMap<>();
        request.getParameterNames().forEachRemaining(
                name -> requestParams.put(name, request.getParameter(name))
        );

        Map<String,String> headers = new HashMap<>();
        request.getHeaderNames().forEachRemaining(
            name -> headers.put(name, request.getHeader(name))
        );

        AppExceptionBean appExceptionBean = new AppExceptionBean(
            "handleDuplicateKeyException",
            LocalDateTime.now(),
            ex.getCause().getMessage(),
            ex.getClass().getName(),
            ((ServletWebRequest)request).getRequest().getRequestURI().toString(),
            requestParams.toString(),
            headers.toString(),
            request.getSessionId()
        );

        log.error("handleDuplicateKeyException",ex);
        return super.handleExceptionInternal(
            ex, appExceptionBean, null, HttpStatus.INTERNAL_SERVER_ERROR, request);
    }

    @ExceptionHandler(Exception.class)
    public ResponseEntity<Object> handleAppException(Exception ex, WebRequest request) {

        Map<String,String> requestParams = new HashMap<>();
        request.getParameterNames().forEachRemaining(
                name -> requestParams.put(name, request.getParameter(name))
        );

        Map<String,String> headers = new HashMap<>();
        request.getHeaderNames().forEachRemaining(
            name -> headers.put(name, request.getHeader(name))
        );

        AppExceptionBean appExceptionBean = new AppExceptionBean(
            "handleAppException",
            LocalDateTime.now(),
            ex.getCause().getMessage(),
            ex.getClass().getName(),
            ((ServletWebRequest)request).getRequest().getRequestURI().toString(),
            requestParams.toString(),
            headers.toString(),
            request.getSessionId()
        );

        log.error("handleAppException",ex);
        return super.handleExceptionInternal(
            ex, appExceptionBean, null, HttpStatus.INTERNAL_SERVER_ERROR, request);
    }
}

親クラスResponseEntityExceptionHandlerのhandleExceptionInternalをreturnするメソッドを作成する
@RestControllerAdviceのアノテーションをつけないと、このクラスが反応しない
@ExceptionHandlerでハンドリングするExceptionを指定している
Exception.classを指定すると、全てのExceptionが対象になる
メソッドの書く順番は関係なく、一番該当するものが1つ選ばれてエラーハンドリングされる
DuplicateKeyExceptionの場合、handleDuplicateKeyExceptionのみに反応し、
DataIntegrityViolationExceptionの場合、handleAppExceptionのみに反応する

Discussion