Spring 3.2.15, MVC-based REST API here (not Spring Boot, sadly!). I am trying to implement an exception mapper/handler that meets the following criteria:
- No matter what happens (success or error), the Spring app always returns a response entity of
MyAppResponse
(see below); and
- In the event of processing a request successfully, return an HTTP status of 200 (typical); and
- In the event of processing a request and an exception occurs, I need to control the mapping of the specific exception to a particular HTTP status code
- Spring MVC framework errors (such as
BlahException
) must map to HTTP 422
- Custom app exceptions, such as my
FizzBuzzException
have their own status mapping schemes:
FizzBuzzException
-> HTTP 401
FooBarException
-> HTTP 403
OmgException
-> HTTP 404
- All other exceptions, that is, non-Spring exceptions, and non-custom app exceptions (the 3 listed above), should produce an HTTP 500
Where the MyAppResponse
object is:
// Groovy pseudo-code
@Canonical
class MyAppResponse {
String detail
String randomNumber
}
It appears like ResponseEntityExceptionHandler
might be able to do this for me, but I'm not seeing the forest through the trees w.r.t. how it gets passed arguments. I'm hoping I can do something like:
// Groovy-pseudo code
@ControllerAdvice
class MyAppExceptionMapper extends ResponseEntityExceptionHandler {
ResponseEntity<Object> handleFizzBuzzException(FizzBuzzException fbEx, HttpHeaders headers, HttpStatus status) {
// TODO: How to reset status to 401?
status = ???
new ResponseEntity(fbEx.message, headers, status)
}
ResponseEntity<Object> handleFooBarException(FooBarException fbEx, HttpHeaders headers, HttpStatus status) {
// TODO: How to reset status to 403?
status = ???
new ResponseEntity(fbEx.message, headers, status)
}
ResponseEntity<Object> handleOmgException(OmgException omgEx, HttpHeaders headers, HttpStatus status) {
// TODO: How to reset status to 404?
status = ???
new ResponseEntity(omgEx.message, headers, status)
}
// Now map all Spring-generated exceptions to 422
ResponseEntity<Object> handleAllSpringExceptions(SpringException springEx, HttpHeaders headers, HttpStatus status) {
// TODO: How to reset status to 422?
status = ???
new ResponseEntity(springEx.message, headers, status)
}
// Everything else is a 500...
ResponseEntity<Object> handleAllOtherExceptions(Exception ex, HttpHeaders headers, HttpStatus status) {
// TODO: How to reset status to 500?
status = ???
new ResponseEntity("Whoops, something happened. Lol.", headers, status)
}
}
Any idea how I can fully implement this mapping logic and the requirement for the entity to be a MyAppResponse
instance and not just a string?
Then, is annotating the class with @ControllerAdvice
the only thing that I need to do to configure Spring to use it?
To reduce @bond-java-bond answer you do not need to build ResponseEntity
by yourself:
- Use
@ResponseStatus
for each handleSomeException
method (e.g. @ResponseStatus(HttpStatus.UNAUTHORIZED)
)
- Return custom
MyAppResponse
from those methods
But if each kind of exceptions will be processed by the same way (diffs by HTTP status only) I suggest to reduce MyAppExceptionMapper
like this:
@ControllerAdvice
public class MyAppExceptionMapper {
private final Map<Class<?>, HttpStatus> map;
{
map = new HashMap<>();
map.put(FizzBuzzException.class, HttpStatus.UNAUTHORIZED);
map.put(FooBarException.class, HttpStatus.FORBIDDEN);
map.put(NoSuchRequestHandlingMethodException.class, HttpStatus.UNPROCESSABLE_ENTITY);
/* List Spring specific exceptions here as @bond-java-bond suggested */
map.put(Exception.class, HttpStatus.INTERNAL_SERVER_ERROR);
}
// Handle all exceptions
@ExceptionHandler(Exception.class)
@ResponseBody
public ResponseEntity<MyAppResponse> handleException(Exception exception) {
MyAppResponse response = new MyAppResponse();
// Fill response with details
HttpStatus status = map.get(exception.getClass());
if (status == null) {
status = map.get(Exception.class);// By default
}
return new ResponseEntity<>(response, status);
}
}
Pros:
- Pretty short.
- No code duplication.
- Slightly more effective.
- Easy to extend.
Also, you can move mapping configuration outside and inject it.
How to configure MVC Dispatcher Servlet
First of all, check if mvc-dispatcher-servlet.xml
(or another contextConfigLocation
from web.xml
) contains:
<context:component-scan base-package="base.package"/>
<mvc:annotation-driven/>
Secondly, check if @ControllerAdvice annotated class and @Controller annotated class both belong to subpackage of base.package
.
See complete examples at Exception Handling in Spring MVC or Spring MVC @ExceptionHandler Example for more details.
First the error / exception handler should not worry about the success response.
Thus the responsibility of success response should lie with controller (plain or REST controller) method(s) annotated with @RequestMapping
as below
@RequestMapping(value = "/demo", method = RequestMethod.GET)
@ResponseStatus(value = HttpStatus.OK)
public MyAppResponse doSomething() { .... }
For mapping a particular HTTP response code with exception(s) simply write a @ControllerAdvice
as below (no additional configuration required)
@ControllerAdvice
public class CustomExceptionHandler {
// Handle FizzBuzzException with status code as 401
@ExceptionHandler(value = FizzBuzzException.class)
@ResponseBody
public ResponseEntity<MyAppResponse> handleException(FizzBuzzException ex) {
return new ResponseEntity<MyAppResponse>(buildResponse(ex), HttpStatus.UNAUTHORIZED);
}
// Handle FooBarException with status code as 403
@ExceptionHandler(value = FooBarException.class)
@ResponseBody
public ResponseEntity<MyAppResponse> handleException(FooBarException ex) {
return new ResponseEntity<MyAppResponse>(buildResponse(ex), HttpStatus.FORBIDDEN);
}
// Handle OmgException with status code as 404
@ExceptionHandler(value = OmgException.class)
@ResponseBody
public ResponseEntity<MyAppResponse> handleException(OmgException ex) {
return new ResponseEntity<MyAppResponse>(buildResponse(ex), HttpStatus.NOT_FOUND);
}
// handle Spring MVC specific exceptions with status code 422
@ExceptionHandler(value = {NoSuchRequestHandlingMethodException.class, HttpRequestMethodNotSupportedException.class, HttpMediaTypeNotSupportedException.class,
HttpMediaTypeNotAcceptableException.class, MissingPathVariableException.class, MissingServletRequestParameterException.class, ServletRequestBindingException.class,
ConversionNotSupportedException.class, TypeMismatchException.class, HttpMessageNotReadableException.class, HttpMessageNotWritableException.class, MethodArgumentNotValidException.class,
MissingServletRequestPartException.class, BindException.class, NoHandlerFoundException.class, AsyncRequestTimeoutException.class})
@ResponseBody
public ResponseEntity<MyAppResponse> handleException(Exception ex) {
return new ResponseEntity<MyAppResponse>(buildResponse(ex), HttpStatus.UNPROCESSABLE_ENTITY);
}
// Handle rest of the exception(s) with status code as 500
@ExceptionHandler(value = Exception.class)
@ResponseBody
public ResponseEntity<MyAppResponse> handleException(Exception ex) {
return new ResponseEntity<MyAppResponse>(buildResponse(ex), HttpStatus.INTERNAL_SERVER_ERROR);
}
private MyAppResponse buildResponse(Throwable t) {
MyAppResponse response = new MyAppResponse();
// supply value to response object
return response;
}
}
Let know in comments if any further information is required.
P.S.: List of Spring MVC exception reference