Skip to content

Instantly share code, notes, and snippets.

@susimsek
Created March 20, 2022 14:14
Show Gist options
  • Save susimsek/6c37b178a4ac8d980205484cf7bbfcc9 to your computer and use it in GitHub Desktop.
Save susimsek/6c37b178a4ac8d980205484cf7bbfcc9 to your computer and use it in GitHub Desktop.
Spring Boot i18n Custom Exception Handling
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;
}
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");
}
}
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;
}
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;
}
package org.hyperledger.fabric.assettransfer.exception;
import lombok.NonNull;
public class ResourceNotFoundException extends RestException {
public ResourceNotFoundException(@NonNull String message, Object[] args) {
super(message, args);
}
}
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;
}
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