Skip to content

Commit

Permalink
chore: RestControllerAdvice
Browse files Browse the repository at this point in the history
  • Loading branch information
char-yb committed Apr 5, 2024
1 parent 14df19d commit 021aaba
Show file tree
Hide file tree
Showing 6 changed files with 246 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.unit.daybook.global.common.response;

import java.time.LocalDateTime;

import com.unit.daybook.global.error.ErrorResponse;

public record GlobalResponse(int status, Object data, LocalDateTime timestamp) {
public static GlobalResponse success(int status, Object data) {
return new GlobalResponse(status, data, LocalDateTime.now());
}

public static GlobalResponse fail(int status, ErrorResponse errorResponse) {
return new GlobalResponse(status, errorResponse, LocalDateTime.now());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package com.unit.daybook.global.common.response;

import org.springframework.core.MethodParameter;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.http.server.ServletServerHttpResponse;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;

import jakarta.servlet.http.HttpServletResponse;

@RestControllerAdvice(basePackages = "com.unit.daybook")
public class GlobalResponseAdvice implements ResponseBodyAdvice {
@Override
public boolean supports(MethodParameter returnType, Class converterType) {
return true;
}

@Override
public Object beforeBodyWrite(
Object body,
MethodParameter returnType,
MediaType selectedContentType,
Class selectedConverterType,
ServerHttpRequest request,
ServerHttpResponse response) {
HttpServletResponse servletResponse =
((ServletServerHttpResponse) response).getServletResponse();
int status = servletResponse.getStatus();
HttpStatus resolve = HttpStatus.resolve(status);
if (resolve == null || body instanceof String) {
return body;
}
if (resolve.is2xxSuccessful()) {
return GlobalResponse.success(status, body);
}
return body;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.unit.daybook.global.error;

public record ErrorResponse(String errorClassName, String message) {

public static ErrorResponse of(String errorClassName, String message) {
return new ErrorResponse(errorClassName, message);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
package com.unit.daybook.global.error;

import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.HttpStatusCode;
import org.springframework.http.ResponseEntity;
import org.springframework.web.HttpRequestMethodNotSupportedException;
import org.springframework.web.bind.MethodArgumentNotValidException;
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.method.annotation.MethodArgumentTypeMismatchException;
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;

import com.unit.daybook.global.common.response.GlobalResponse;
import com.unit.daybook.global.error.exception.CustomException;
import com.unit.daybook.global.error.exception.ErrorCode;

import jakarta.validation.ConstraintViolationException;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;

@Slf4j
@RestControllerAdvice
@RequiredArgsConstructor
public class GlobalExceptionHandler extends ResponseEntityExceptionHandler {

@Override
protected ResponseEntity<Object> handleExceptionInternal(
Exception ex,
Object body,
HttpHeaders headers,
HttpStatusCode statusCode,
WebRequest request) {
ErrorResponse errorResponse =
ErrorResponse.of(ex.getClass().getSimpleName(), ex.getMessage());
return super.handleExceptionInternal(ex, errorResponse, headers, statusCode, request);
}

/**
* javax.validation.Valid or @Validated 으로 binding error 발생시 발생한다. HttpMessageConverter 에서 등록한
* HttpMessageConverter binding 못할경우 발생 주로 @RequestBody, @RequestPart 어노테이션에서 발생
*/
@SneakyThrows
@Override
protected ResponseEntity<Object> handleMethodArgumentNotValid(
MethodArgumentNotValidException e,
HttpHeaders headers,
HttpStatusCode status,
WebRequest request) {
log.error("MethodArgumentNotValidException : {}", e.getMessage(), e);
String errorMessage = e.getBindingResult().getAllErrors().get(0).getDefaultMessage();
final ErrorResponse errorResponse =
ErrorResponse.of(e.getClass().getSimpleName(), errorMessage);
GlobalResponse response = GlobalResponse.fail(status.value(), errorResponse);
return ResponseEntity.status(status).body(response);
}

/** Request Param Validation 예외 처리 */
@ExceptionHandler(ConstraintViolationException.class)
public ResponseEntity<GlobalResponse> handleConstraintViolationException(
ConstraintViolationException e) {
log.error("ConstraintViolationException : {}", e.getMessage(), e);

Map<String, Object> bindingErrors = new HashMap<>();
e.getConstraintViolations()
.forEach(
constraintViolation -> {
List<String> propertyPath =
List.of(
constraintViolation
.getPropertyPath()
.toString()
.split("\\."));
String path =
propertyPath.stream()
.skip(propertyPath.size() - 1L)
.findFirst()
.orElse(null);
bindingErrors.put(path, constraintViolation.getMessage());
});

final ErrorResponse errorResponse =
ErrorResponse.of(e.getClass().getSimpleName(), bindingErrors.toString());
final GlobalResponse response =
GlobalResponse.fail(HttpStatus.BAD_REQUEST.value(), errorResponse);
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(response);
}

/** PathVariable, RequestParam, RequestHeader, RequestBody 에서 타입이 일치하지 않을 경우 발생 */
@ExceptionHandler(MethodArgumentTypeMismatchException.class)
protected ResponseEntity<GlobalResponse> handleMethodArgumentTypeMismatchException(
MethodArgumentTypeMismatchException e) {
log.error("MethodArgumentTypeMismatchException : {}", e.getMessage(), e);
final ErrorCode errorCode = ErrorCode.METHOD_ARGUMENT_TYPE_MISMATCH;
final ErrorResponse errorResponse =
ErrorResponse.of(e.getClass().getSimpleName(), errorCode.getMessage());
final GlobalResponse response =
GlobalResponse.fail(errorCode.getStatus().value(), errorResponse);
return ResponseEntity.status(errorCode.getStatus()).body(response);
}

/** 지원하지 않은 HTTP method 호출 할 경우 발생 */
@Override
protected ResponseEntity<Object> handleHttpRequestMethodNotSupported(
HttpRequestMethodNotSupportedException e,
HttpHeaders headers,
HttpStatusCode status,
WebRequest request) {
log.error("HttpRequestMethodNotSupportedException : {}", e.getMessage(), e);
final ErrorCode errorCode = ErrorCode.METHOD_NOT_ALLOWED;
final ErrorResponse errorResponse =
ErrorResponse.of(e.getClass().getSimpleName(), errorCode.getMessage());
final GlobalResponse response =
GlobalResponse.fail(errorCode.getStatus().value(), errorResponse);
return ResponseEntity.status(errorCode.getStatus()).body(response);
}

/** CustomException 예외 처리 */
@ExceptionHandler(CustomException.class)
public ResponseEntity<GlobalResponse> handleCustomException(CustomException e) {
log.error("CustomException : {}", e.getMessage(), e);
final ErrorCode errorCode = e.getErrorCode();
final ErrorResponse errorResponse =
ErrorResponse.of(errorCode.name(), errorCode.getMessage());
final GlobalResponse response =
GlobalResponse.fail(errorCode.getStatus().value(), errorResponse);
return ResponseEntity.status(errorCode.getStatus()).body(response);
}

/** 500번대 에러 처리 */
@ExceptionHandler(Exception.class)
protected ResponseEntity<GlobalResponse> handleException(Exception e) {
log.error("Internal Server Error : {}", e.getMessage(), e);
final ErrorCode internalServerError = ErrorCode.INTERNAL_SERVER_ERROR;
final ErrorResponse errorResponse =
ErrorResponse.of(e.getClass().getSimpleName(), internalServerError.getMessage());
final GlobalResponse response =
GlobalResponse.fail(internalServerError.getStatus().value(), errorResponse);
return ResponseEntity.status(internalServerError.getStatus()).body(response);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.unit.daybook.global.error.exception;

import lombok.Getter;

@Getter
public class CustomException extends RuntimeException {

private final ErrorCode errorCode;

public CustomException(ErrorCode errorCode) {
super(errorCode.getMessage());
this.errorCode = errorCode;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.unit.daybook.global.error.exception;

import org.springframework.http.HttpStatus;

import lombok.AllArgsConstructor;
import lombok.Getter;

@Getter
@AllArgsConstructor
public enum ErrorCode {
SAMPLE_ERROR(HttpStatus.BAD_REQUEST, "Sample Error Message"),

// Common
METHOD_ARGUMENT_TYPE_MISMATCH(HttpStatus.BAD_REQUEST, "요청 한 값 타입이 잘못되어 binding에 실패하였습니다."),
METHOD_NOT_ALLOWED(HttpStatus.METHOD_NOT_ALLOWED, "지원하지 않는 HTTP method 입니다."),
INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "서버 오류, 관리자에게 문의하세요"),

;
private final HttpStatus status;
private final String message;
}

0 comments on commit 021aaba

Please sign in to comment.