From 93c92c56feb183d424e5a9335114330a1ab17d1b Mon Sep 17 00:00:00 2001 From: arsgsg1 Date: Thu, 20 Jan 2022 15:32:02 +0900 Subject: [PATCH 1/2] Add Default Exception Handling Template --- .../com/classvar/error/ControllerAdvice.java | 30 -- .../error/GlobalApiExceptionHandler.java | 331 ++++++++++++++++++ .../error/exception/ApplicationException.java | 53 +++ .../exception/ApplicationExceptionReason.java | 17 + .../error/exception/BusinessException.java | 47 +++ .../exception/BusinessExceptionReason.java | 24 ++ .../error/exception/dto/ErrorResponseDto.java | 22 ++ .../exception/dto/InvalidParameterDto.java | 19 + .../policy/ApplicationExceptionPolicy.java | 5 + .../policy/BusinessExceptionPolicy.java | 7 + .../exception/policy/ExceptionPolicy.java | 6 + .../exception/util/ErrorResponseUtil.java | 66 ++++ 12 files changed, 597 insertions(+), 30 deletions(-) delete mode 100644 src/main/java/com/classvar/error/ControllerAdvice.java create mode 100644 src/main/java/com/classvar/error/GlobalApiExceptionHandler.java create mode 100644 src/main/java/com/classvar/error/exception/ApplicationException.java create mode 100644 src/main/java/com/classvar/error/exception/ApplicationExceptionReason.java create mode 100644 src/main/java/com/classvar/error/exception/BusinessException.java create mode 100644 src/main/java/com/classvar/error/exception/BusinessExceptionReason.java create mode 100644 src/main/java/com/classvar/error/exception/dto/ErrorResponseDto.java create mode 100644 src/main/java/com/classvar/error/exception/dto/InvalidParameterDto.java create mode 100644 src/main/java/com/classvar/error/exception/policy/ApplicationExceptionPolicy.java create mode 100644 src/main/java/com/classvar/error/exception/policy/BusinessExceptionPolicy.java create mode 100644 src/main/java/com/classvar/error/exception/policy/ExceptionPolicy.java create mode 100644 src/main/java/com/classvar/error/exception/util/ErrorResponseUtil.java diff --git a/src/main/java/com/classvar/error/ControllerAdvice.java b/src/main/java/com/classvar/error/ControllerAdvice.java deleted file mode 100644 index bc43f4b..0000000 --- a/src/main/java/com/classvar/error/ControllerAdvice.java +++ /dev/null @@ -1,30 +0,0 @@ -package com.classvar.error; - -import com.classvar.error.exception.UnauthenticatedUserException; -import org.apache.catalina.connector.Response; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.MethodArgumentNotValidException; -import org.springframework.web.bind.annotation.ExceptionHandler; -import org.springframework.web.bind.annotation.RestControllerAdvice; - -@RestControllerAdvice -public class ControllerAdvice { - @ExceptionHandler(MethodArgumentNotValidException.class) - public ResponseEntity MethodArgumentNotValidAdvice(MethodArgumentNotValidException e) { - final ErrorResponse response = new ErrorResponse(e.getBindingResult()); - return new ResponseEntity(response, HttpStatus.BAD_REQUEST); - } - - @ExceptionHandler(IllegalArgumentException.class) - public ResponseEntity IllegalArgumentAdvice(IllegalArgumentException e) { - final ErrorResponse response = new ErrorResponse(e.getMessage()); - return new ResponseEntity(response, HttpStatus.BAD_REQUEST); - } - - @ExceptionHandler(UnauthenticatedUserException.class) - public ResponseEntity UnauthenticatedUserAdvice(UnauthenticatedUserException e) { - final ErrorResponse response = new ErrorResponse(e.getMessage()); - return new ResponseEntity(response, HttpStatus.BAD_REQUEST); - } -} diff --git a/src/main/java/com/classvar/error/GlobalApiExceptionHandler.java b/src/main/java/com/classvar/error/GlobalApiExceptionHandler.java new file mode 100644 index 0000000..37204f0 --- /dev/null +++ b/src/main/java/com/classvar/error/GlobalApiExceptionHandler.java @@ -0,0 +1,331 @@ +package com.classvar.error; + +import static com.classvar.error.exception.util.ErrorResponseUtil.build; +import static java.lang.String.format; +import static java.util.Collections.singletonList; +import static java.util.stream.Collectors.toList; + +import com.classvar.error.exception.ApplicationException; +import com.classvar.error.exception.BusinessException; +import com.classvar.error.exception.dto.ErrorResponseDto; +import com.classvar.error.exception.dto.InvalidParameterDto; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.Optional; +import javax.validation.ConstraintViolationException; +import javax.validation.Path; +import javax.validation.Path.Node; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.TypeMismatchException; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.http.converter.HttpMessageNotReadableException; +import org.springframework.web.HttpRequestMethodNotSupportedException; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.MissingPathVariableException; +import org.springframework.web.bind.MissingRequestHeaderException; +import org.springframework.web.bind.MissingServletRequestParameterException; +import org.springframework.web.bind.ServletRequestBindingException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; +import org.springframework.web.context.request.ServletWebRequest; +import org.springframework.web.context.request.WebRequest; +import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException; +import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler; + +@Order(Ordered.HIGHEST_PRECEDENCE) +@RestControllerAdvice +@Slf4j +public class GlobalApiExceptionHandler extends ResponseEntityExceptionHandler { + + /** + * Handles the uncaught {@link Exception} exceptions and returns a JSON formatted response. + * + * @param ex the ex + * @param request the request on which the ex occurred + * @return a JSON formatted response containing the ex details and additional fields + */ + @ExceptionHandler(value = {Exception.class}) + public ResponseEntity handleUncaughtException(final Exception ex, + final ServletWebRequest request) { + log(ex, request); + final ErrorResponseDto errorResponseDto = build(Exception.class.getSimpleName(), + HttpStatus.INTERNAL_SERVER_ERROR.getReasonPhrase(), + HttpStatus.INTERNAL_SERVER_ERROR); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(errorResponseDto); + } + + /** + * Handles the uncaught {@link BusinessException} exceptions and returns a JSON formatted + * response. + * + * @param ex the ex + * @param request the request on which the ex occurred + * @return a JSON formatted response containing the ex details and additional fields + */ + @ExceptionHandler({BusinessException.class}) + public ResponseEntity handleCustomUncaughtBusinessException(final BusinessException ex, + final ServletWebRequest request) { + log(ex, request); + final ErrorResponseDto errorResponseDto = build(ex.getCode(), ex.getMessage(), + ex.getHttpStatus()); + return ResponseEntity.status(ex.getHttpStatus()).body(errorResponseDto); + } + + /** + * Handles the uncaught {@link ApplicationException} exceptions and returns a JSON formatted + * response. + * + * @param ex the ex + * @param request the request on which the ex occurred + * @return a JSON formatted response containing the ex details and additional fields + */ + @ExceptionHandler({ApplicationException.class}) + public ResponseEntity handleCustomUncaughtApplicationException( + final ApplicationException ex, + final ServletWebRequest request) { + log(ex, request); + final ErrorResponseDto errorResponseDto = build(ex.getCode(), ex.getMessage(), + HttpStatus.INTERNAL_SERVER_ERROR); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(errorResponseDto); + } + + /** + * Handles the uncaught {@link ConstraintViolationException} exceptions and returns a JSON + * formatted response. + * + * @param ex the ex + * @param request the request on which the ex occurred + * @return a JSON formatted response containing the ex details and additional fields + */ + @ExceptionHandler(value = {ConstraintViolationException.class}) + public ResponseEntity handleConstraintViolationException( + final ConstraintViolationException ex, + final ServletWebRequest request) { + log(ex, request); + + final List invalidParameters = new ArrayList<>(); + ex.getConstraintViolations().forEach(constraintViolation -> { + final Iterator it = constraintViolation.getPropertyPath().iterator(); + if (it.hasNext()) { + try { + it.next(); + final Path.Node n = it.next(); + final InvalidParameterDto invalidParameter = new InvalidParameterDto(); + invalidParameter.setParameter(n.getName()); + invalidParameter.setMessage(constraintViolation.getMessage()); + invalidParameters.add(invalidParameter); + } catch (final Exception e) { + log.warn("[Advocatus] Can't extract the information about constraint violation"); + } + } + }); + + final ErrorResponseDto errorResponseDto = build( + ConstraintViolationException.class.getSimpleName(), + HttpStatus.BAD_REQUEST.getReasonPhrase(), HttpStatus.BAD_REQUEST, invalidParameters); + + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(errorResponseDto); + } + + /** + * Handles the uncaught {@link HttpMessageNotReadableException} exceptions and returns a JSON + * formatted response. + * + * @param ex the ex + * @param request the request on which the ex occurred + * @return a JSON formatted response containing the ex details and additional fields + */ + @Override + protected ResponseEntity handleHttpMessageNotReadable( + final HttpMessageNotReadableException ex, + final HttpHeaders headers, final HttpStatus status, final WebRequest request) { + log(ex, (ServletWebRequest) request); + final ErrorResponseDto errorResponseDto = build( + HttpMessageNotReadableException.class.getSimpleName(), ex.getMessage(), + HttpStatus.BAD_REQUEST); + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(errorResponseDto); + } + + /** + * Handles the uncaught {@link HttpRequestMethodNotSupportedException} exceptions and returns a + * JSON formatted response. + * + * @param ex the ex + * @param request the request on which the ex occurred + * @return a JSON formatted response containing the ex details and additional fields + */ + @Override + protected ResponseEntity handleHttpRequestMethodNotSupported( + final HttpRequestMethodNotSupportedException ex, final HttpHeaders headers, + final HttpStatus status, + final WebRequest request) { + log(ex, (ServletWebRequest) request); + final ErrorResponseDto errorResponseDto = build( + HttpRequestMethodNotSupportedException.class.getSimpleName(), ex.getMessage(), + HttpStatus.METHOD_NOT_ALLOWED); + return ResponseEntity.status(HttpStatus.METHOD_NOT_ALLOWED).body(errorResponseDto); + } + + /** + * Handles the uncaught {@link MethodArgumentNotValidException} exceptions and returns a JSON + * formatted response. + * + * @param ex the ex + * @param request the request on which the ex occurred + * @return a JSON formatted response containing the ex details and additional fields + */ + @Override + protected ResponseEntity handleMethodArgumentNotValid( + final MethodArgumentNotValidException ex, + final HttpHeaders headers, final HttpStatus status, final WebRequest request) { + log(ex, (ServletWebRequest) request); + final List invalidParameters = ex.getBindingResult().getFieldErrors() + .stream() + .map(fieldError -> InvalidParameterDto.builder() + .parameter(fieldError.getField()) + .message(fieldError.getDefaultMessage()) + .build()).collect(toList()); + + final ErrorResponseDto errorResponseDto = build( + MethodArgumentNotValidException.class.getSimpleName(), + HttpStatus.BAD_REQUEST.getReasonPhrase(), HttpStatus.BAD_REQUEST, invalidParameters); + + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(errorResponseDto); + } + + /** + * Handles the uncaught {@link ServletRequestBindingException} exceptions and returns a JSON + * formatted response. + * + * @param ex the ex + * @param request the request on which the ex occurred + * @return a JSON formatted response containing the ex details and additional fields + */ + @Override + protected ResponseEntity handleServletRequestBindingException( + final ServletRequestBindingException ex, + final HttpHeaders headers, final HttpStatus status, final WebRequest request) { + log(ex, (ServletWebRequest) request); + + final String missingParameter; + final String missingParameterType; + + if (ex instanceof MissingRequestHeaderException) { + missingParameter = ((MissingRequestHeaderException) ex).getHeaderName(); + missingParameterType = "header"; + } else if (ex instanceof MissingServletRequestParameterException) { + missingParameter = ((MissingServletRequestParameterException) ex).getParameterName(); + missingParameterType = "query"; + } else if (ex instanceof MissingPathVariableException) { + missingParameter = ((MissingPathVariableException) ex).getVariableName(); + missingParameterType = "path"; + } else { + missingParameter = "unknown"; + missingParameterType = "unknown"; + } + + final InvalidParameterDto missingParameterDto = InvalidParameterDto.builder() + .parameter(missingParameter) + .message( + format("Missing %s parameter with name '%s'", missingParameterType, missingParameter)) + .build(); + + final ErrorResponseDto errorResponseDto = build( + ServletRequestBindingException.class.getSimpleName(), + HttpStatus.BAD_REQUEST.getReasonPhrase(), HttpStatus.BAD_REQUEST, + singletonList(missingParameterDto)); + + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(errorResponseDto); + } + + /** + * Handles the uncaught {@link TypeMismatchException} exceptions and returns a JSON formatted + * response. + * + * @param ex the ex + * @param request the request on which the ex occurred + * @return a JSON formatted response containing the ex details and additional fields + */ + @Override + protected ResponseEntity handleTypeMismatch(final TypeMismatchException ex, + final HttpHeaders headers, + final HttpStatus status, final WebRequest request) { + log(ex, (ServletWebRequest) request); + + String parameter = ex.getPropertyName(); + if (ex instanceof MethodArgumentTypeMismatchException) { + parameter = ((MethodArgumentTypeMismatchException) ex).getName(); + } + + final ErrorResponseDto errorResponseDto = build(TypeMismatchException.class.getSimpleName(), + format("Unexpected type specified for '%s' parameter. Required '%s'", parameter, + ex.getRequiredType()), + HttpStatus.BAD_REQUEST); + + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(errorResponseDto); + } + + /** + * Handles the uncaught {@link MissingPathVariableException} exceptions and returns a JSON + * formatted response. + * + * @param ex the ex + * @param request the request on which the ex occurred + * @return a JSON formatted response containing the ex details and additional fields + */ + @Override + protected ResponseEntity handleMissingPathVariable(final MissingPathVariableException ex, + final HttpHeaders headers, final HttpStatus status, final WebRequest request) { + return handleServletRequestBindingException(ex, headers, status, request); + } + + /** + * Handles the uncaught {@link MissingServletRequestParameterException} exceptions and returns a + * JSON formatted response. + * + * @param ex the ex + * @param request the request on which the ex occurred + * @return a JSON formatted response containing the ex details and additional fields + */ + @Override + protected ResponseEntity handleMissingServletRequestParameter( + final MissingServletRequestParameterException ex, final HttpHeaders headers, + final HttpStatus status, + final WebRequest request) { + log(ex, (ServletWebRequest) request); + return handleServletRequestBindingException(ex, headers, status, request); + } + + private void log(final Exception ex, final ServletWebRequest request) { + final Optional httpMethod; + final Optional requestUrl; + + final Optional possibleIncomingNullRequest = Optional.ofNullable(request); + if (possibleIncomingNullRequest.isPresent()) { + // get the HTTP Method + httpMethod = Optional.ofNullable(possibleIncomingNullRequest.get().getHttpMethod()); + if (Optional.ofNullable(possibleIncomingNullRequest.get().getRequest()).isPresent()) { + // get the Request URL + requestUrl = Optional.of( + possibleIncomingNullRequest.get().getRequest().getRequestURL().toString()); + } else { + requestUrl = Optional.empty(); + } + } else { + httpMethod = Optional.empty(); + requestUrl = Optional.empty(); + } + + log.error("Request {} {} failed with exception reason: {}", + (httpMethod.isPresent() ? httpMethod.get() : "'null'"), + (requestUrl.orElse("'null'")), ex.getMessage(), ex); + } + + +} \ No newline at end of file diff --git a/src/main/java/com/classvar/error/exception/ApplicationException.java b/src/main/java/com/classvar/error/exception/ApplicationException.java new file mode 100644 index 0000000..0ff0f77 --- /dev/null +++ b/src/main/java/com/classvar/error/exception/ApplicationException.java @@ -0,0 +1,53 @@ +package com.classvar.error.exception; + +import static java.lang.String.format; + +import com.classvar.error.exception.policy.ApplicationExceptionPolicy; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class ApplicationException extends RuntimeException implements ApplicationExceptionPolicy { + + private final String code; + private final String message; + + /** + * Constructor accepting an exception reason. + * + * @param reason the reason of the exception + */ + public ApplicationException(final ApplicationExceptionReason reason) { + this.code = reason.getCode(); + this.message = reason.getMessage(); + } + + + /** + * Constructor accepting an excepting reason and optional parameters which are replaced in the + * message. + * + * @param reason the reason of the exception + * @param parameters the optional parameters + */ + public ApplicationException(final ApplicationExceptionReason reason, final Object... parameters) { + if (parameters != null) { + this.message = format(reason.getMessage(), parameters); + } else { + this.message = reason.getMessage(); + } + + this.code = reason.getCode(); + } + + + @Override + public String getLocalizedMessage() { + return getMessage(); + } + + public String toString() { + return format("ApplicationException(code=%s, message=%s)", this.getCode(), this.getMessage()); + } +} \ No newline at end of file diff --git a/src/main/java/com/classvar/error/exception/ApplicationExceptionReason.java b/src/main/java/com/classvar/error/exception/ApplicationExceptionReason.java new file mode 100644 index 0000000..82bed06 --- /dev/null +++ b/src/main/java/com/classvar/error/exception/ApplicationExceptionReason.java @@ -0,0 +1,17 @@ +package com.classvar.error.exception; + +import com.classvar.error.exception.policy.ApplicationExceptionPolicy; +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public enum ApplicationExceptionReason implements ApplicationExceptionPolicy { + + BEAN_PROPERTY_NOT_EXISTS("Property '%s' for object '%s' doesn't exists"); + + private final String code = ApplicationExceptionReason.class.getSimpleName(); + private final String message; + + +} \ No newline at end of file diff --git a/src/main/java/com/classvar/error/exception/BusinessException.java b/src/main/java/com/classvar/error/exception/BusinessException.java new file mode 100644 index 0000000..3442c41 --- /dev/null +++ b/src/main/java/com/classvar/error/exception/BusinessException.java @@ -0,0 +1,47 @@ +package com.classvar.error.exception; + +import com.classvar.error.exception.policy.BusinessExceptionPolicy; +import lombok.Getter; +import org.springframework.http.HttpStatus; + +@Getter +public class BusinessException extends RuntimeException implements BusinessExceptionPolicy { + + protected final String code; + protected final String message; + protected final HttpStatus httpStatus; + + public BusinessException(final BusinessExceptionReason reason) { + this.code = reason.getCode(); + this.message = reason.getMessage(); + this.httpStatus = reason.getHttpStatus(); + } + + public BusinessException(final BusinessExceptionReason reason, + final HttpStatus overridingHttpStatus) { + this.code = reason.getCode(); + this.message = reason.getMessage(); + this.httpStatus = overridingHttpStatus; + } + + public BusinessException(final BusinessExceptionReason reason, final Object... parameters) { + if (parameters != null) { + this.message = String.format(reason.getMessage(), parameters); + } else { + this.message = reason.getMessage(); + } + this.code = reason.getCode(); + this.httpStatus = reason.getHttpStatus(); + } + + @Override + public String getLocalizedMessage() { + return getMessage(); + } + + public String toString() { + return String.format("BusinessException(code=%s, message=%s, httpStatus=%s)", this.getCode(), + this.getMessage(), + this.getHttpStatus().value()); + } +} diff --git a/src/main/java/com/classvar/error/exception/BusinessExceptionReason.java b/src/main/java/com/classvar/error/exception/BusinessExceptionReason.java new file mode 100644 index 0000000..995b58e --- /dev/null +++ b/src/main/java/com/classvar/error/exception/BusinessExceptionReason.java @@ -0,0 +1,24 @@ +package com.classvar.error.exception; + +import com.classvar.error.exception.policy.BusinessExceptionPolicy; +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.springframework.http.HttpStatus; + +@Getter +@AllArgsConstructor +public enum BusinessExceptionReason implements BusinessExceptionPolicy { + + //기본 코드만 표시하고 싶을 때 + // "message": "No such id" + NO_SUCH_ID("No such id", HttpStatus.BAD_REQUEST), + + //예외 발생 이유를 상세하게 표시하고 싶을 때 + //new BusinessException(BusinessExceptionReason.NO_SUCH_ID_DETAIL, "지울 ID의 시험이 없습니다."); + //Output) "message": "No such id: 지울 ID의 시험이 없습니다." + NO_SUCH_ID_DETAIL("No such id: %s", HttpStatus.BAD_REQUEST); + + private final String code = BusinessExceptionReason.class.getSimpleName(); + private final String message; + private final HttpStatus httpStatus; +} diff --git a/src/main/java/com/classvar/error/exception/dto/ErrorResponseDto.java b/src/main/java/com/classvar/error/exception/dto/ErrorResponseDto.java new file mode 100644 index 0000000..8d10da6 --- /dev/null +++ b/src/main/java/com/classvar/error/exception/dto/ErrorResponseDto.java @@ -0,0 +1,22 @@ +package com.classvar.error.exception.dto; + +import java.time.LocalDateTime; +import java.util.List; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@NoArgsConstructor +@AllArgsConstructor +@Getter +@Setter +public class ErrorResponseDto { + + private String code; + private String message; + private Integer status; + private LocalDateTime timestamp; + private List invalidParameters; + +} diff --git a/src/main/java/com/classvar/error/exception/dto/InvalidParameterDto.java b/src/main/java/com/classvar/error/exception/dto/InvalidParameterDto.java new file mode 100644 index 0000000..46a7db8 --- /dev/null +++ b/src/main/java/com/classvar/error/exception/dto/InvalidParameterDto.java @@ -0,0 +1,19 @@ +package com.classvar.error.exception.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@NoArgsConstructor +@AllArgsConstructor +@Getter +@Setter +@Builder +public class InvalidParameterDto { + + private String parameter; + private String message; + +} \ No newline at end of file diff --git a/src/main/java/com/classvar/error/exception/policy/ApplicationExceptionPolicy.java b/src/main/java/com/classvar/error/exception/policy/ApplicationExceptionPolicy.java new file mode 100644 index 0000000..a643eaf --- /dev/null +++ b/src/main/java/com/classvar/error/exception/policy/ApplicationExceptionPolicy.java @@ -0,0 +1,5 @@ +package com.classvar.error.exception.policy; + +public interface ApplicationExceptionPolicy extends ExceptionPolicy { + +} diff --git a/src/main/java/com/classvar/error/exception/policy/BusinessExceptionPolicy.java b/src/main/java/com/classvar/error/exception/policy/BusinessExceptionPolicy.java new file mode 100644 index 0000000..97ec366 --- /dev/null +++ b/src/main/java/com/classvar/error/exception/policy/BusinessExceptionPolicy.java @@ -0,0 +1,7 @@ +package com.classvar.error.exception.policy; + +import org.springframework.http.HttpStatus; + +public interface BusinessExceptionPolicy extends ExceptionPolicy{ + HttpStatus getHttpStatus(); +} diff --git a/src/main/java/com/classvar/error/exception/policy/ExceptionPolicy.java b/src/main/java/com/classvar/error/exception/policy/ExceptionPolicy.java new file mode 100644 index 0000000..77f8af5 --- /dev/null +++ b/src/main/java/com/classvar/error/exception/policy/ExceptionPolicy.java @@ -0,0 +1,6 @@ +package com.classvar.error.exception.policy; + +public interface ExceptionPolicy { + String getCode(); + String getMessage(); +} diff --git a/src/main/java/com/classvar/error/exception/util/ErrorResponseUtil.java b/src/main/java/com/classvar/error/exception/util/ErrorResponseUtil.java new file mode 100644 index 0000000..2e2b5f5 --- /dev/null +++ b/src/main/java/com/classvar/error/exception/util/ErrorResponseUtil.java @@ -0,0 +1,66 @@ +package com.classvar.error.exception.util; + +import com.classvar.error.exception.dto.ErrorResponseDto; +import com.classvar.error.exception.dto.InvalidParameterDto; +import java.time.LocalDateTime; +import java.util.List; +import java.util.Objects; +import org.springframework.http.HttpStatus; +import org.springframework.util.CollectionUtils; + +public final class ErrorResponseUtil { + + private ErrorResponseUtil() { + } + + /** + * Builds an error response based on given parameters. + * + * @param code the error code + * @param message the error message + * @param status the http status attached to the error + * @return the error response + */ + public static ErrorResponseDto build(final String code, final String message, + final HttpStatus status) { + return buildDetails(code, message, status); + } + + /** + * Builds an error response based on given parameters. + * + * @param code the error code + * @param message the error message + * @param status the http status attached to the error + * @param invalidParameters the list of invalid parameters + * @return the error response + */ + public static ErrorResponseDto build(final String code, final String message, + final HttpStatus status, + final List invalidParameters) { + return buildDetails(code, message, status, invalidParameters); + } + + + private static ErrorResponseDto buildDetails(final String code, final String message, + final HttpStatus status) { + return buildDetails(code, message, status, null); + } + + private static ErrorResponseDto buildDetails(final String code, final String message, + final HttpStatus status, + final List invalidParameters) { + final ErrorResponseDto errorResponseDetails = new ErrorResponseDto(); + errorResponseDetails.setCode(code); + errorResponseDetails.setMessage(message); + if (!Objects.isNull(status)) { + errorResponseDetails.setStatus(status.value()); + } + errorResponseDetails.setTimestamp(LocalDateTime.now()); + if (!CollectionUtils.isEmpty(invalidParameters)) { + errorResponseDetails.setInvalidParameters(invalidParameters); + } + return errorResponseDetails; + } + +} \ No newline at end of file From 83b781c9a85e788eebf43639a91d632a7fc8161f Mon Sep 17 00:00:00 2001 From: arsgsg1 Date: Wed, 26 Jan 2022 13:41:15 +0900 Subject: [PATCH 2/2] Unifying error responses schema --- .../error/GlobalApiExceptionHandler.java | 174 +----------------- .../error/exception/ApplicationException.java | 53 ------ .../exception/ApplicationExceptionReason.java | 17 -- .../exception/BusinessExceptionReason.java | 10 +- .../error/exception/dto/ErrorResponseDto.java | 2 - .../policy/ApplicationExceptionPolicy.java | 5 - .../exception/util/ErrorResponseUtil.java | 37 +--- 7 files changed, 20 insertions(+), 278 deletions(-) delete mode 100644 src/main/java/com/classvar/error/exception/ApplicationException.java delete mode 100644 src/main/java/com/classvar/error/exception/ApplicationExceptionReason.java delete mode 100644 src/main/java/com/classvar/error/exception/policy/ApplicationExceptionPolicy.java diff --git a/src/main/java/com/classvar/error/GlobalApiExceptionHandler.java b/src/main/java/com/classvar/error/GlobalApiExceptionHandler.java index 37204f0..e38d18e 100644 --- a/src/main/java/com/classvar/error/GlobalApiExceptionHandler.java +++ b/src/main/java/com/classvar/error/GlobalApiExceptionHandler.java @@ -1,11 +1,8 @@ package com.classvar.error; import static com.classvar.error.exception.util.ErrorResponseUtil.build; -import static java.lang.String.format; -import static java.util.Collections.singletonList; import static java.util.stream.Collectors.toList; -import com.classvar.error.exception.ApplicationException; import com.classvar.error.exception.BusinessException; import com.classvar.error.exception.dto.ErrorResponseDto; import com.classvar.error.exception.dto.InvalidParameterDto; @@ -17,7 +14,6 @@ import javax.validation.Path; import javax.validation.Path.Node; import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.TypeMismatchException; import org.springframework.core.Ordered; import org.springframework.core.annotation.Order; import org.springframework.http.HttpHeaders; @@ -28,14 +24,11 @@ import org.springframework.web.HttpRequestMethodNotSupportedException; import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.MissingPathVariableException; -import org.springframework.web.bind.MissingRequestHeaderException; import org.springframework.web.bind.MissingServletRequestParameterException; -import org.springframework.web.bind.ServletRequestBindingException; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; import org.springframework.web.context.request.ServletWebRequest; import org.springframework.web.context.request.WebRequest; -import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException; import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler; @Order(Ordered.HIGHEST_PRECEDENCE) @@ -43,31 +36,15 @@ @Slf4j public class GlobalApiExceptionHandler extends ResponseEntityExceptionHandler { - /** - * Handles the uncaught {@link Exception} exceptions and returns a JSON formatted response. - * - * @param ex the ex - * @param request the request on which the ex occurred - * @return a JSON formatted response containing the ex details and additional fields - */ @ExceptionHandler(value = {Exception.class}) public ResponseEntity handleUncaughtException(final Exception ex, final ServletWebRequest request) { log(ex, request); final ErrorResponseDto errorResponseDto = build(Exception.class.getSimpleName(), - HttpStatus.INTERNAL_SERVER_ERROR.getReasonPhrase(), - HttpStatus.INTERNAL_SERVER_ERROR); + "서버의 문제로 응답에 문제가 발생했습니다.", HttpStatus.INTERNAL_SERVER_ERROR); return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(errorResponseDto); } - /** - * Handles the uncaught {@link BusinessException} exceptions and returns a JSON formatted - * response. - * - * @param ex the ex - * @param request the request on which the ex occurred - * @return a JSON formatted response containing the ex details and additional fields - */ @ExceptionHandler({BusinessException.class}) public ResponseEntity handleCustomUncaughtBusinessException(final BusinessException ex, final ServletWebRequest request) { @@ -77,32 +54,6 @@ public ResponseEntity handleCustomUncaughtBusinessException(final Busine return ResponseEntity.status(ex.getHttpStatus()).body(errorResponseDto); } - /** - * Handles the uncaught {@link ApplicationException} exceptions and returns a JSON formatted - * response. - * - * @param ex the ex - * @param request the request on which the ex occurred - * @return a JSON formatted response containing the ex details and additional fields - */ - @ExceptionHandler({ApplicationException.class}) - public ResponseEntity handleCustomUncaughtApplicationException( - final ApplicationException ex, - final ServletWebRequest request) { - log(ex, request); - final ErrorResponseDto errorResponseDto = build(ex.getCode(), ex.getMessage(), - HttpStatus.INTERNAL_SERVER_ERROR); - return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(errorResponseDto); - } - - /** - * Handles the uncaught {@link ConstraintViolationException} exceptions and returns a JSON - * formatted response. - * - * @param ex the ex - * @param request the request on which the ex occurred - * @return a JSON formatted response containing the ex details and additional fields - */ @ExceptionHandler(value = {ConstraintViolationException.class}) public ResponseEntity handleConstraintViolationException( final ConstraintViolationException ex, @@ -121,7 +72,7 @@ public ResponseEntity handleConstraintViolationException( invalidParameter.setMessage(constraintViolation.getMessage()); invalidParameters.add(invalidParameter); } catch (final Exception e) { - log.warn("[Advocatus] Can't extract the information about constraint violation"); + log.warn("Can't extract the information about constraint violation"); } } }); @@ -133,14 +84,7 @@ public ResponseEntity handleConstraintViolationException( return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(errorResponseDto); } - /** - * Handles the uncaught {@link HttpMessageNotReadableException} exceptions and returns a JSON - * formatted response. - * - * @param ex the ex - * @param request the request on which the ex occurred - * @return a JSON formatted response containing the ex details and additional fields - */ + //메시지 컨버터에서 변환할 수 없는 경우 @Override protected ResponseEntity handleHttpMessageNotReadable( final HttpMessageNotReadableException ex, @@ -152,14 +96,6 @@ protected ResponseEntity handleHttpMessageNotReadable( return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(errorResponseDto); } - /** - * Handles the uncaught {@link HttpRequestMethodNotSupportedException} exceptions and returns a - * JSON formatted response. - * - * @param ex the ex - * @param request the request on which the ex occurred - * @return a JSON formatted response containing the ex details and additional fields - */ @Override protected ResponseEntity handleHttpRequestMethodNotSupported( final HttpRequestMethodNotSupportedException ex, final HttpHeaders headers, @@ -172,14 +108,6 @@ protected ResponseEntity handleHttpRequestMethodNotSupported( return ResponseEntity.status(HttpStatus.METHOD_NOT_ALLOWED).body(errorResponseDto); } - /** - * Handles the uncaught {@link MethodArgumentNotValidException} exceptions and returns a JSON - * formatted response. - * - * @param ex the ex - * @param request the request on which the ex occurred - * @return a JSON formatted response containing the ex details and additional fields - */ @Override protected ResponseEntity handleMethodArgumentNotValid( final MethodArgumentNotValidException ex, @@ -199,107 +127,25 @@ protected ResponseEntity handleMethodArgumentNotValid( return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(errorResponseDto); } - /** - * Handles the uncaught {@link ServletRequestBindingException} exceptions and returns a JSON - * formatted response. - * - * @param ex the ex - * @param request the request on which the ex occurred - * @return a JSON formatted response containing the ex details and additional fields - */ @Override - protected ResponseEntity handleServletRequestBindingException( - final ServletRequestBindingException ex, + protected ResponseEntity handleMissingPathVariable(final MissingPathVariableException ex, final HttpHeaders headers, final HttpStatus status, final WebRequest request) { - log(ex, (ServletWebRequest) request); - - final String missingParameter; - final String missingParameterType; - - if (ex instanceof MissingRequestHeaderException) { - missingParameter = ((MissingRequestHeaderException) ex).getHeaderName(); - missingParameterType = "header"; - } else if (ex instanceof MissingServletRequestParameterException) { - missingParameter = ((MissingServletRequestParameterException) ex).getParameterName(); - missingParameterType = "query"; - } else if (ex instanceof MissingPathVariableException) { - missingParameter = ((MissingPathVariableException) ex).getVariableName(); - missingParameterType = "path"; - } else { - missingParameter = "unknown"; - missingParameterType = "unknown"; - } - - final InvalidParameterDto missingParameterDto = InvalidParameterDto.builder() - .parameter(missingParameter) - .message( - format("Missing %s parameter with name '%s'", missingParameterType, missingParameter)) - .build(); - final ErrorResponseDto errorResponseDto = build( - ServletRequestBindingException.class.getSimpleName(), - HttpStatus.BAD_REQUEST.getReasonPhrase(), HttpStatus.BAD_REQUEST, - singletonList(missingParameterDto)); - - return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(errorResponseDto); - } - - /** - * Handles the uncaught {@link TypeMismatchException} exceptions and returns a JSON formatted - * response. - * - * @param ex the ex - * @param request the request on which the ex occurred - * @return a JSON formatted response containing the ex details and additional fields - */ - @Override - protected ResponseEntity handleTypeMismatch(final TypeMismatchException ex, - final HttpHeaders headers, - final HttpStatus status, final WebRequest request) { - log(ex, (ServletWebRequest) request); - - String parameter = ex.getPropertyName(); - if (ex instanceof MethodArgumentTypeMismatchException) { - parameter = ((MethodArgumentTypeMismatchException) ex).getName(); - } - - final ErrorResponseDto errorResponseDto = build(TypeMismatchException.class.getSimpleName(), - format("Unexpected type specified for '%s' parameter. Required '%s'", parameter, - ex.getRequiredType()), - HttpStatus.BAD_REQUEST); - + MissingPathVariableException.class.getSimpleName(), + HttpStatus.BAD_REQUEST.getReasonPhrase(), HttpStatus.BAD_REQUEST); return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(errorResponseDto); } - /** - * Handles the uncaught {@link MissingPathVariableException} exceptions and returns a JSON - * formatted response. - * - * @param ex the ex - * @param request the request on which the ex occurred - * @return a JSON formatted response containing the ex details and additional fields - */ - @Override - protected ResponseEntity handleMissingPathVariable(final MissingPathVariableException ex, - final HttpHeaders headers, final HttpStatus status, final WebRequest request) { - return handleServletRequestBindingException(ex, headers, status, request); - } - - /** - * Handles the uncaught {@link MissingServletRequestParameterException} exceptions and returns a - * JSON formatted response. - * - * @param ex the ex - * @param request the request on which the ex occurred - * @return a JSON formatted response containing the ex details and additional fields - */ @Override protected ResponseEntity handleMissingServletRequestParameter( final MissingServletRequestParameterException ex, final HttpHeaders headers, final HttpStatus status, final WebRequest request) { log(ex, (ServletWebRequest) request); - return handleServletRequestBindingException(ex, headers, status, request); + final ErrorResponseDto errorResponseDto = build( + MissingServletRequestParameterException.class.getSimpleName(), + HttpStatus.BAD_REQUEST.getReasonPhrase(), HttpStatus.BAD_REQUEST); + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(errorResponseDto); } private void log(final Exception ex, final ServletWebRequest request) { diff --git a/src/main/java/com/classvar/error/exception/ApplicationException.java b/src/main/java/com/classvar/error/exception/ApplicationException.java deleted file mode 100644 index 0ff0f77..0000000 --- a/src/main/java/com/classvar/error/exception/ApplicationException.java +++ /dev/null @@ -1,53 +0,0 @@ -package com.classvar.error.exception; - -import static java.lang.String.format; - -import com.classvar.error.exception.policy.ApplicationExceptionPolicy; -import lombok.Getter; -import lombok.Setter; - -@Getter -@Setter -public class ApplicationException extends RuntimeException implements ApplicationExceptionPolicy { - - private final String code; - private final String message; - - /** - * Constructor accepting an exception reason. - * - * @param reason the reason of the exception - */ - public ApplicationException(final ApplicationExceptionReason reason) { - this.code = reason.getCode(); - this.message = reason.getMessage(); - } - - - /** - * Constructor accepting an excepting reason and optional parameters which are replaced in the - * message. - * - * @param reason the reason of the exception - * @param parameters the optional parameters - */ - public ApplicationException(final ApplicationExceptionReason reason, final Object... parameters) { - if (parameters != null) { - this.message = format(reason.getMessage(), parameters); - } else { - this.message = reason.getMessage(); - } - - this.code = reason.getCode(); - } - - - @Override - public String getLocalizedMessage() { - return getMessage(); - } - - public String toString() { - return format("ApplicationException(code=%s, message=%s)", this.getCode(), this.getMessage()); - } -} \ No newline at end of file diff --git a/src/main/java/com/classvar/error/exception/ApplicationExceptionReason.java b/src/main/java/com/classvar/error/exception/ApplicationExceptionReason.java deleted file mode 100644 index 82bed06..0000000 --- a/src/main/java/com/classvar/error/exception/ApplicationExceptionReason.java +++ /dev/null @@ -1,17 +0,0 @@ -package com.classvar.error.exception; - -import com.classvar.error.exception.policy.ApplicationExceptionPolicy; -import lombok.AllArgsConstructor; -import lombok.Getter; - -@Getter -@AllArgsConstructor -public enum ApplicationExceptionReason implements ApplicationExceptionPolicy { - - BEAN_PROPERTY_NOT_EXISTS("Property '%s' for object '%s' doesn't exists"); - - private final String code = ApplicationExceptionReason.class.getSimpleName(); - private final String message; - - -} \ No newline at end of file diff --git a/src/main/java/com/classvar/error/exception/BusinessExceptionReason.java b/src/main/java/com/classvar/error/exception/BusinessExceptionReason.java index 995b58e..3cee54c 100644 --- a/src/main/java/com/classvar/error/exception/BusinessExceptionReason.java +++ b/src/main/java/com/classvar/error/exception/BusinessExceptionReason.java @@ -8,15 +8,7 @@ @Getter @AllArgsConstructor public enum BusinessExceptionReason implements BusinessExceptionPolicy { - - //기본 코드만 표시하고 싶을 때 - // "message": "No such id" - NO_SUCH_ID("No such id", HttpStatus.BAD_REQUEST), - - //예외 발생 이유를 상세하게 표시하고 싶을 때 - //new BusinessException(BusinessExceptionReason.NO_SUCH_ID_DETAIL, "지울 ID의 시험이 없습니다."); - //Output) "message": "No such id: 지울 ID의 시험이 없습니다." - NO_SUCH_ID_DETAIL("No such id: %s", HttpStatus.BAD_REQUEST); + NO_SUCH_ID("No such id: %s", HttpStatus.BAD_REQUEST); private final String code = BusinessExceptionReason.class.getSimpleName(); private final String message; diff --git a/src/main/java/com/classvar/error/exception/dto/ErrorResponseDto.java b/src/main/java/com/classvar/error/exception/dto/ErrorResponseDto.java index 8d10da6..50afb67 100644 --- a/src/main/java/com/classvar/error/exception/dto/ErrorResponseDto.java +++ b/src/main/java/com/classvar/error/exception/dto/ErrorResponseDto.java @@ -5,12 +5,10 @@ import lombok.AllArgsConstructor; import lombok.Getter; import lombok.NoArgsConstructor; -import lombok.Setter; @NoArgsConstructor @AllArgsConstructor @Getter -@Setter public class ErrorResponseDto { private String code; diff --git a/src/main/java/com/classvar/error/exception/policy/ApplicationExceptionPolicy.java b/src/main/java/com/classvar/error/exception/policy/ApplicationExceptionPolicy.java deleted file mode 100644 index a643eaf..0000000 --- a/src/main/java/com/classvar/error/exception/policy/ApplicationExceptionPolicy.java +++ /dev/null @@ -1,5 +0,0 @@ -package com.classvar.error.exception.policy; - -public interface ApplicationExceptionPolicy extends ExceptionPolicy { - -} diff --git a/src/main/java/com/classvar/error/exception/util/ErrorResponseUtil.java b/src/main/java/com/classvar/error/exception/util/ErrorResponseUtil.java index 2e2b5f5..1729d3e 100644 --- a/src/main/java/com/classvar/error/exception/util/ErrorResponseUtil.java +++ b/src/main/java/com/classvar/error/exception/util/ErrorResponseUtil.java @@ -3,8 +3,8 @@ import com.classvar.error.exception.dto.ErrorResponseDto; import com.classvar.error.exception.dto.InvalidParameterDto; import java.time.LocalDateTime; +import java.util.ArrayList; import java.util.List; -import java.util.Objects; import org.springframework.http.HttpStatus; import org.springframework.util.CollectionUtils; @@ -13,52 +13,33 @@ public final class ErrorResponseUtil { private ErrorResponseUtil() { } - /** - * Builds an error response based on given parameters. - * - * @param code the error code - * @param message the error message - * @param status the http status attached to the error - * @return the error response - */ public static ErrorResponseDto build(final String code, final String message, final HttpStatus status) { return buildDetails(code, message, status); } - /** - * Builds an error response based on given parameters. - * - * @param code the error code - * @param message the error message - * @param status the http status attached to the error - * @param invalidParameters the list of invalid parameters - * @return the error response - */ public static ErrorResponseDto build(final String code, final String message, final HttpStatus status, final List invalidParameters) { return buildDetails(code, message, status, invalidParameters); } - private static ErrorResponseDto buildDetails(final String code, final String message, final HttpStatus status) { - return buildDetails(code, message, status, null); + ErrorResponseDto errorResponseDto = new ErrorResponseDto(code, message, status.value(), + LocalDateTime.now(), new ArrayList<>()); + return errorResponseDto; } private static ErrorResponseDto buildDetails(final String code, final String message, final HttpStatus status, final List invalidParameters) { - final ErrorResponseDto errorResponseDetails = new ErrorResponseDto(); - errorResponseDetails.setCode(code); - errorResponseDetails.setMessage(message); - if (!Objects.isNull(status)) { - errorResponseDetails.setStatus(status.value()); - } - errorResponseDetails.setTimestamp(LocalDateTime.now()); + ErrorResponseDto errorResponseDetails = new ErrorResponseDto(code, message, + status.value(), LocalDateTime.now(), new ArrayList<>()); + if (!CollectionUtils.isEmpty(invalidParameters)) { - errorResponseDetails.setInvalidParameters(invalidParameters); + errorResponseDetails = new ErrorResponseDto(code, message, + status.value(), LocalDateTime.now(), invalidParameters); } return errorResponseDetails; }