Pesudocode :-
@RestController
class Controller {
@GetMapping()
public ResponseEntity<?> getAPI(@Valid @RequestBody Request request) {
try {
// success response.
} catch (RequestParamsException e) {
// log.error(e request) log error and request.
return ResponseEntity.status(HttpStatus.BAD_REQUEST).build();
} catch (Exception e) {
// log.error(e request) log error and request
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
}
}
}
We want to log the request for all error logs at the controller level. Simplest approach is that we can go to every api in all controllers
and log the request
there, Is there any better way we can do this for all APIs in spring using AOP/filters/interceptors somehow.
CodePudding user response:
Try to use error handling and @ControllerAdvice
There you can have your common method to log errors
CodePudding user response:
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
!