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..e38d18e --- /dev/null +++ b/src/main/java/com/classvar/error/GlobalApiExceptionHandler.java @@ -0,0 +1,177 @@ +package com.classvar.error; + +import static com.classvar.error.exception.util.ErrorResponseUtil.build; +import static java.util.stream.Collectors.toList; + +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.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.MissingServletRequestParameterException; +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.servlet.mvc.method.annotation.ResponseEntityExceptionHandler; + +@Order(Ordered.HIGHEST_PRECEDENCE) +@RestControllerAdvice +@Slf4j +public class GlobalApiExceptionHandler extends ResponseEntityExceptionHandler { + + @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); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(errorResponseDto); + } + + @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); + } + + @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("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); + } + + //메시지 컨버터에서 변환할 수 없는 경우 + @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); + } + + @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); + } + + @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); + } + + @Override + protected ResponseEntity handleMissingPathVariable(final MissingPathVariableException ex, + final HttpHeaders headers, final HttpStatus status, final WebRequest request) { + final ErrorResponseDto errorResponseDto = build( + MissingPathVariableException.class.getSimpleName(), + HttpStatus.BAD_REQUEST.getReasonPhrase(), HttpStatus.BAD_REQUEST); + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(errorResponseDto); + } + + @Override + protected ResponseEntity handleMissingServletRequestParameter( + final MissingServletRequestParameterException ex, final HttpHeaders headers, + final HttpStatus status, + final WebRequest request) { + log(ex, (ServletWebRequest) 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) { + 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/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..3cee54c --- /dev/null +++ b/src/main/java/com/classvar/error/exception/BusinessExceptionReason.java @@ -0,0 +1,16 @@ +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 { + NO_SUCH_ID("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..50afb67 --- /dev/null +++ b/src/main/java/com/classvar/error/exception/dto/ErrorResponseDto.java @@ -0,0 +1,20 @@ +package com.classvar.error.exception.dto; + +import java.time.LocalDateTime; +import java.util.List; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@NoArgsConstructor +@AllArgsConstructor +@Getter +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/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..1729d3e --- /dev/null +++ b/src/main/java/com/classvar/error/exception/util/ErrorResponseUtil.java @@ -0,0 +1,47 @@ +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.ArrayList; +import java.util.List; +import org.springframework.http.HttpStatus; +import org.springframework.util.CollectionUtils; + +public final class ErrorResponseUtil { + + private ErrorResponseUtil() { + } + + public static ErrorResponseDto build(final String code, final String message, + final HttpStatus status) { + return buildDetails(code, message, status); + } + + 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) { + 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) { + ErrorResponseDto errorResponseDetails = new ErrorResponseDto(code, message, + status.value(), LocalDateTime.now(), new ArrayList<>()); + + if (!CollectionUtils.isEmpty(invalidParameters)) { + errorResponseDetails = new ErrorResponseDto(code, message, + status.value(), LocalDateTime.now(), invalidParameters); + } + return errorResponseDetails; + } + +} \ No newline at end of file