Created
March 20, 2022 14:14
-
-
Save susimsek/6c37b178a4ac8d980205484cf7bbfcc9 to your computer and use it in GitHub Desktop.
Spring Boot i18n Custom Exception Handling
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package org.hyperledger.fabric.assettransfer.exception; | |
import lombok.*; | |
import lombok.experimental.FieldDefaults; | |
import org.springframework.http.HttpStatus; | |
import java.time.LocalDateTime; | |
import java.util.List; | |
@FieldDefaults(level = AccessLevel.PRIVATE) | |
@Getter | |
@Setter | |
@NoArgsConstructor | |
@AllArgsConstructor | |
@Builder | |
public class ApiError { | |
@Builder.Default | |
LocalDateTime timestamp = LocalDateTime.now(); | |
String locale; | |
HttpStatus status; | |
String title; | |
String message; | |
List<FieldError> fieldErrors; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package org.hyperledger.fabric.assettransfer.exception; | |
public final class ErrorConstants { | |
public static final String INTERNAL_SERVER_ERROR_MESSAGE = "error.internal.server.message"; | |
public static final String MISSING_SERVLET_REQUEST_PARAMETER_MESSAGE = "error.missing.servlet.request.parameter.message"; | |
public static final String METHOD_ARGUMENT_TYPE_MISMATCH_MESSAGE = "error.method.argument.type.mismatch.message"; | |
public static final String HTTP_MEDIA_TYPE_NOT_SUPPORTED_MESSAGE = "error.http.media.type.not.supported.message"; | |
public static final String NO_HANDLER_FOUND_MESSAGE = "error.no.handler.found.message"; | |
private ErrorConstants() { | |
throw new UnsupportedOperationException("This is a constant class and cannot be instantiated"); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package org.hyperledger.fabric.assettransfer.exception; | |
import lombok.AccessLevel; | |
import lombok.Getter; | |
import lombok.RequiredArgsConstructor; | |
import lombok.experimental.FieldDefaults; | |
@FieldDefaults(level = AccessLevel.PRIVATE) | |
@Getter | |
@RequiredArgsConstructor | |
public enum ErrorType { | |
RESOURCE_NOT_FOUND("error.resource.not.found.title"), | |
HTTP_MESSAGE_NOT_READABLE("error.http.message.not.readable.title"), | |
METHOD_ARGUMENT_NOT_VALID ("error.method.argument.not.valid.title"), | |
METHOD_ARGUMENT_TYPE_MISMATCH("error.method.argument.type.mismatch.title"), | |
CONSTRAINT_VIOLATION("error.constraint.violation.title"), | |
MISSING_SERVLET_REQUEST_PARAMETER("error.missing.servlet.request.parameter.title"), | |
HTTP_MEDIA_TYPE_NOT_SUPPORTED("error.http.media.type.not.supported.title"); | |
final String description; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package org.hyperledger.fabric.assettransfer.exception; | |
import lombok.*; | |
import lombok.experimental.FieldDefaults; | |
@FieldDefaults(level = AccessLevel.PRIVATE) | |
@Getter | |
@Setter | |
@NoArgsConstructor | |
@AllArgsConstructor | |
public class FieldError { | |
String field; | |
String message; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package org.hyperledger.fabric.assettransfer.exception; | |
import lombok.NonNull; | |
public class ResourceNotFoundException extends RestException { | |
public ResourceNotFoundException(@NonNull String message, Object[] args) { | |
super(message, args); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package org.hyperledger.fabric.assettransfer.exception; | |
import lombok.AccessLevel; | |
import lombok.AllArgsConstructor; | |
import lombok.Getter; | |
import lombok.experimental.FieldDefaults; | |
@Getter | |
@FieldDefaults(level = AccessLevel.PRIVATE) | |
@AllArgsConstructor | |
public class RestException extends RuntimeException { | |
final String message; | |
final transient Object[] args; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package org.hyperledger.fabric.assettransfer.exception; | |
import lombok.AccessLevel; | |
import lombok.RequiredArgsConstructor; | |
import lombok.experimental.FieldDefaults; | |
import org.springframework.context.MessageSource; | |
import org.springframework.http.HttpHeaders; | |
import org.springframework.http.HttpStatus; | |
import org.springframework.http.ResponseEntity; | |
import org.springframework.http.converter.HttpMessageNotReadableException; | |
import org.springframework.validation.BindException; | |
import org.springframework.validation.BindingResult; | |
import org.springframework.web.HttpMediaTypeNotSupportedException; | |
import org.springframework.web.bind.MethodArgumentNotValidException; | |
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.WebRequest; | |
import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException; | |
import org.springframework.web.servlet.NoHandlerFoundException; | |
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler; | |
import javax.validation.ConstraintViolationException; | |
import java.time.LocalDateTime; | |
import java.util.*; | |
import java.util.stream.Collectors; | |
@RestControllerAdvice | |
@FieldDefaults(level = AccessLevel.PRIVATE) | |
@RequiredArgsConstructor | |
public class RestExceptionHandler extends ResponseEntityExceptionHandler { | |
final MessageSource messageSource; | |
// 400 Request Body | |
@Override | |
protected ResponseEntity<Object> handleMethodArgumentNotValid(MethodArgumentNotValidException ex, | |
HttpHeaders headers, | |
HttpStatus status, | |
WebRequest request) { | |
ApiError apiError = ApiError.builder() | |
.status(HttpStatus.BAD_REQUEST) | |
.title(getValidationErrorsTitle(request.getLocale())) | |
.message(ex.getLocalizedMessage()) | |
.locale(request.getLocale().getLanguage()) | |
.fieldErrors(getValidationErrors(ex.getBindingResult())) | |
.build(); | |
return new ResponseEntity<>(apiError, headers, apiError.getStatus()); | |
} | |
// 400 Request Body | |
@Override | |
protected ResponseEntity<Object> handleBindException(BindException ex, | |
HttpHeaders headers, | |
HttpStatus status, | |
WebRequest request) { | |
ApiError apiError = ApiError.builder() | |
.status(HttpStatus.BAD_REQUEST) | |
.title(getValidationErrorsTitle(request.getLocale())) | |
.message(ex.getLocalizedMessage()) | |
.locale(request.getLocale().getLanguage()) | |
.fieldErrors(getValidationErrors(ex.getBindingResult())) | |
.build(); | |
return new ResponseEntity<>(apiError, headers, apiError.getStatus()); | |
} | |
// 400 request param, path variable | |
@ExceptionHandler({ ConstraintViolationException.class }) | |
public ResponseEntity<Object> handleConstraintViolation( | |
ConstraintViolationException ex, | |
WebRequest request, | |
Locale locale) { | |
String title = messageSource.getMessage( | |
ErrorType.CONSTRAINT_VIOLATION.getDescription(), null, request.getLocale()); | |
List<FieldError> fieldErrors = new ArrayList<>(); | |
ex.getConstraintViolations() | |
.forEach(constraintViolation -> fieldErrors.add( | |
new FieldError( | |
constraintViolation.getPropertyPath().toString(), | |
constraintViolation.getMessage()))); | |
ApiError apiError = ApiError.builder() | |
.status(HttpStatus.BAD_REQUEST) | |
.title(title) | |
.message(ex.getLocalizedMessage()) | |
.locale(locale.getLanguage()) | |
.fieldErrors(fieldErrors) | |
.build(); | |
return new ResponseEntity<>( | |
apiError, new HttpHeaders(), apiError.getStatus()); | |
} | |
// 400 request body is missing or it is unreadable | |
@Override | |
protected ResponseEntity<Object> handleHttpMessageNotReadable(HttpMessageNotReadableException ex, | |
HttpHeaders headers, HttpStatus status, | |
WebRequest request) { | |
String title = messageSource.getMessage( | |
ErrorType.HTTP_MESSAGE_NOT_READABLE.getDescription(), null, request.getLocale()); | |
ApiError apiError = ApiError.builder() | |
.status(HttpStatus.BAD_REQUEST) | |
.title(title) | |
.message(ex.getMessage()) | |
.locale(request.getLocale().getLanguage()) | |
.build(); | |
return new ResponseEntity<>(apiError, apiError.getStatus()); | |
} | |
// 400 request is missing a parameter | |
@Override | |
protected ResponseEntity<Object> handleMissingServletRequestParameter(MissingServletRequestParameterException ex, | |
HttpHeaders headers, | |
HttpStatus status, | |
WebRequest request) { | |
String errorMessage = messageSource.getMessage( | |
ErrorConstants.MISSING_SERVLET_REQUEST_PARAMETER_MESSAGE, new Object[]{ex.getParameterName()}, request.getLocale()); | |
String title = messageSource.getMessage( | |
ErrorType.MISSING_SERVLET_REQUEST_PARAMETER.getDescription(), null, request.getLocale()); | |
ApiError apiError = ApiError.builder() | |
.status(HttpStatus.BAD_REQUEST) | |
.title(title) | |
.message(errorMessage) | |
.locale(request.getLocale().getLanguage()) | |
.build(); | |
return new ResponseEntity<>(apiError, headers, apiError.getStatus()); | |
} | |
// 400 method argument is not the expected type | |
@ExceptionHandler({ MethodArgumentTypeMismatchException.class }) | |
public ResponseEntity<Object> handleMethodArgumentTypeMismatch(MethodArgumentTypeMismatchException ex, | |
WebRequest request, | |
HttpHeaders headers, | |
Locale locale) { | |
String errorMessage = messageSource.getMessage( | |
ErrorConstants.METHOD_ARGUMENT_TYPE_MISMATCH_MESSAGE, | |
new Object[]{ex.getName(), Objects.requireNonNull(ex.getRequiredType()).getName()} | |
, request.getLocale()); | |
String title = messageSource.getMessage( | |
ErrorType.METHOD_ARGUMENT_TYPE_MISMATCH.getDescription(), null, locale); | |
ApiError apiError = ApiError.builder() | |
.status(HttpStatus.BAD_REQUEST) | |
.title(title) | |
.message(errorMessage) | |
.locale(locale.getLanguage()) | |
.build(); | |
return new ResponseEntity<>(apiError, headers, apiError.getStatus()); | |
} | |
// 404 No Handler not found | |
@Override | |
protected ResponseEntity<Object> handleNoHandlerFoundException( | |
NoHandlerFoundException ex, | |
HttpHeaders headers, | |
HttpStatus status, | |
WebRequest request) { | |
String title = messageSource.getMessage( | |
ErrorType.RESOURCE_NOT_FOUND.getDescription(), null, request.getLocale()); | |
String errorMessage = messageSource.getMessage( | |
ErrorConstants.NO_HANDLER_FOUND_MESSAGE, | |
new Object[]{ex.getHttpMethod(), ex.getRequestURL()}, | |
request.getLocale()); | |
ApiError apiError = ApiError.builder() | |
.status(HttpStatus.BAD_REQUEST) | |
.title(title) | |
.message(errorMessage) | |
.locale(request.getLocale().getLanguage()) | |
.build(); | |
return new ResponseEntity<>(apiError, headers, apiError.getStatus()); | |
} | |
// 404 Resource Not Found | |
@ExceptionHandler(ResourceNotFoundException.class) | |
public final ResponseEntity<Object> handleResourceNotFound( | |
ResourceNotFoundException ex, Locale locale) { | |
String title = messageSource.getMessage( | |
ErrorType.RESOURCE_NOT_FOUND.getDescription(), null, locale); | |
String errorMessage = messageSource.getMessage(ex.getMessage(), ex.getArgs(), locale); | |
ApiError apiError = ApiError.builder() | |
.status(HttpStatus.NOT_FOUND) | |
.title(title) | |
.message(errorMessage) | |
.locale(locale.getLanguage()) | |
.build(); | |
return new ResponseEntity<>(apiError, apiError.getStatus()); | |
} | |
// 415 | |
@Override | |
protected ResponseEntity<Object> handleHttpMediaTypeNotSupported( | |
HttpMediaTypeNotSupportedException ex, | |
HttpHeaders headers, | |
HttpStatus status, | |
WebRequest request) { | |
String title = messageSource.getMessage( | |
ErrorType.HTTP_MEDIA_TYPE_NOT_SUPPORTED.getDescription(), null, request.getLocale()); | |
String supportedMediaTypes = ex.getSupportedMediaTypes().stream() | |
.map(String::valueOf) | |
.collect(Collectors.joining(", ")); | |
String errorMessage = messageSource.getMessage( | |
ErrorConstants.HTTP_MEDIA_TYPE_NOT_SUPPORTED_MESSAGE, | |
new Object[]{ex.getContentType(), supportedMediaTypes} | |
, request.getLocale()); | |
ApiError apiError = ApiError.builder() | |
.status(HttpStatus.UNSUPPORTED_MEDIA_TYPE) | |
.title(title) | |
.message(errorMessage) | |
.locale(request.getLocale().getLanguage()) | |
.build(); | |
return new ResponseEntity<>(apiError, headers, apiError.getStatus()); | |
} | |
// 500 | |
@ExceptionHandler(Exception.class) | |
public ResponseEntity<Object> handleAll(Exception ex, | |
WebRequest request, | |
Locale locale) { | |
String errorMessage = messageSource.getMessage(ErrorConstants.INTERNAL_SERVER_ERROR_MESSAGE, null, locale); | |
ApiError apiError = ApiError.builder() | |
.status(HttpStatus.INTERNAL_SERVER_ERROR) | |
.message(errorMessage) | |
.locale(locale.getLanguage()) | |
.build(); | |
return new ResponseEntity<>(apiError, apiError.getStatus()); | |
} | |
private List<FieldError> getValidationErrors(BindingResult bindingResult) { | |
List<FieldError> fieldErrors = new ArrayList<>(); | |
bindingResult.getFieldErrors() | |
.forEach(fieldError -> fieldErrors.add(new FieldError(fieldError.getField(), | |
fieldError.getDefaultMessage()))); | |
bindingResult.getGlobalErrors() | |
.forEach(objectError -> fieldErrors.add(new FieldError(objectError.getObjectName(), | |
objectError.getDefaultMessage()))); | |
return fieldErrors; | |
} | |
private String getValidationErrorsTitle(Locale locale) { | |
return messageSource.getMessage( | |
ErrorType.METHOD_ARGUMENT_NOT_VALID.getDescription(), null, locale); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment