Yes, there is a better way! You could define a bean as follows:
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;
@ControllerAdvice
public class ControllerExceptionHandler extends ResponseEntityExceptionHandler {
}
In this class, you can add exception handlers for Spring exceptions, e.g.
@ExceptionHandler(MethodArgumentTypeMismatchException.class)
public ResponseEntity<Object> handleMethodArgumentTypeMismatch(MethodArgumentTypeMismatchException ex, WebRequest request) {
final String message = Optional.ofNullable(ex.getRequiredType())
.map(Class::getName)
.map(className -> String.format("%s should be of type %s", ex.getName(), className))
.orElseGet(ex::getMessage);
return handleExceptionInternal(ex, message, BAD_REQUEST, request);
}
You can also add exception handlers for custom exceptions, e.g.
@ExceptionHandler(NotFoundException.class)
public ResponseEntity<Object> handleNotFoundException(Exception ex, WebRequest request) {
return handleExceptionInternal(ex, NOT_FOUND, request);
}
where all exceptions you throw when an entity can't be found in the DB extend the NotFoundException
.
You may also want to override certain methods from the ResponseEntityExceptionHandler
super class, e.g.
@NotNull
@Override
protected ResponseEntity<Object> handleMissingServletRequestParameter(MissingServletRequestParameterException ex,
@NotNull HttpHeaders headers,
@NotNull HttpStatus status,
@NotNull WebRequest request) {
final String message = String.format("%s parameter is missing", ex.getParameterName());
return handleExceptionInternal(ex, message, BAD_REQUEST, request);
}
A fallback for all other uncatched exceptions:
@ExceptionHandler(Exception.class)
public ResponseEntity<Object> handleAllExceptions(Exception ex, WebRequest request) {
return handleExceptionInternal(ex, "An internal error occurred", INTERNAL_SERVER_ERROR, request);
}
Now to the logging part: you need to override the following method in order to log your errors in a custom way, please note that I return a special DTO ApiError
when an exception is thrown:
@Override
@NotNull
protected ResponseEntity<Object> handleExceptionInternal(@NotNull Exception ex,
@Nullable Object body,
@NotNull HttpHeaders headers,
@NotNull HttpStatus status,
@NotNull WebRequest webRequest) {
// Log
final String errorId = UUID.randomUUID().toString();
final HttpServletRequest request = ((ServletWebRequest) webRequest).getRequest();
final String path = String.format("%s %s", request.getMethod(), request.getRequestURI());
final BiConsumer<String, Exception> log = status.is4xxClientError() ? logger::info : logger::error;
log.accept(String.format("%s returned for request %s, errorId is %s", status, path, errorId), ex);
// Create API error
final ApiError apiError = ApiError.builder()
.errorId(errorId)
.message(Optional.ofNullable(body)
.filter(String.class::isInstance)
.map(String.class::cast)
.orElseGet(ex::getMessage))
.build();
// Return
return new ResponseEntity<>(apiError, headers, status);
}
Depending on whether it is a 4xx or a 5xx error, I either log on info
or error
level. The @ControllerAdvice
annotation instructs Spring to use this bean as exception handler for all Controllers, thereby being your global error handler.
Everything from above puzzled together (plus helper methods I use for the examples):
@ControllerAdvice
public class ControllerExceptionHandler extends ResponseEntityExceptionHandler {
// ---------------
// 400 Bad Request
// ---------------
@ExceptionHandler(MethodArgumentTypeMismatchException.class)
public ResponseEntity<Object> handleMethodArgumentTypeMismatch(MethodArgumentTypeMismatchException ex, WebRequest request) {
final String message = Optional.ofNullable(ex.getRequiredType())
.map(Class::getName)
.map(className -> String.format("%s should be of type %s", ex.getName(), className))
.orElseGet(ex::getMessage);
return handleExceptionInternal(ex, message, BAD_REQUEST, request);
}
// ---------------
// 404 Bad Request
// ---------------
@ExceptionHandler(NotFoundException.class)
public ResponseEntity<Object> handleNotFoundException(Exception ex, WebRequest request) {
return handleExceptionInternal(ex, NOT_FOUND, request);
}
// -------------------------
// 500 Internal Server Error
// -------------------------
@ExceptionHandler(Exception.class)
public ResponseEntity<Object> handleAllExceptions(Exception ex, WebRequest request) {
return handleExceptionInternal(ex, "An internal error occurred", INTERNAL_SERVER_ERROR, request);
}
// --------------
// Helper methods
// --------------
private ResponseEntity<Object> handleExceptionInternal(Exception ex, HttpStatus httpStatus, WebRequest request) {
return handleExceptionInternal(ex, ex.getMessage(), httpStatus, request);
}
private ResponseEntity<Object> handleExceptionInternal(Exception ex, String errorMessage, HttpStatus status, WebRequest request) {
return handleExceptionInternal(ex, errorMessage, new HttpHeaders(), status, request);
}
@Override
@NotNull
protected ResponseEntity<Object> handleExceptionInternal(@NotNull Exception ex,
@Nullable Object body,
@NotNull HttpHeaders headers,
@NotNull HttpStatus status,
@NotNull WebRequest webRequest) {
// Log
final String errorId = UUID.randomUUID().toString();
final HttpServletRequest request = ((ServletWebRequest) webRequest).getRequest();
final String path = String.format("%s %s", request.getMethod(), request.getRequestURI());
final BiConsumer<String, Exception> log = status.is4xxClientError() ? logger::info : logger::error;
log.accept(String.format("%s returned for request %s, errorId is %s", status, path, errorId), ex);
// Create API error
final ApiError apiError = ApiError.builder()
.errorId(errorId)
.message(Optional.ofNullable(body)
.filter(String.class::isInstance)
.map(String.class::cast)
.orElseGet(ex::getMessage))
.build();
// Return
return new ResponseEntity<>(apiError, headers, status);
}
}
Feel free to add more custom exceptions or Spring exceptions that occurr in the web layer and check out whether you want to override additional ResponseEntityExceptionHandler
methods like handleMethodArgumentNotValid
!