In practice, it is often necessary to centrally handle exceptions within a controller or even an entire application. In this article, we will analyze the main features that the Spring Framework provides to solve this problem and, using simple examples, let's see how everything works. Who is interested in this topic - welcome under the cut!
Initially, prior to Spring 3.2, the main ways to handle exceptions in an application were HandlerExceptionResolver and @ExceptionHandler annotation . We will analyze them in more detail below, but they have certain disadvantages. Starting with version 3.2, the @ControllerAdvice annotation appeared , which removes the limitations from the previous solutions. And in Spring 5, a new ResponseStatusException class was added that is very handy for handling basic errors for REST APIs .
And now, first things first, let's go!
Controller Exception Handling - @ExceptionHandler
@ExceptionHandler . , , .
:
@RestController
public class Example1Controller {
@GetMapping(value = "/testExceptionHandler", produces = APPLICATION_JSON_VALUE)
public Response testExceptionHandler(@RequestParam(required = false, defaultValue = "false") boolean exception)
throws BusinessException {
if (exception) {
throw new BusinessException("BusinessException in testExceptionHandler");
}
return new Response("OK");
}
@ExceptionHandler(BusinessException.class)
public Response handleException(BusinessException e) {
return new Response(e.getMessage());
}
}
testExceptionHandler, BusinessException, — . , , .
handleException . @ExceptionHandler(BusinessException.class), BusinessException. @ExceptionHandler , : @ExceptionHandler({BusinessException.class, ServiceException.class}).
— 200 JSON . , @ResponseStatus, @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR).
:
:
@ExceptionHandler , . @ExceptionHandler , , , .
HandlerExceptionResolver
HandlerExceptionResolver Spring. HandlerExceptionResolver. , , Spring . :
ExceptionHandlerExceptionResolver — @ExceptionHandler, .
DefaultHandlerExceptionResolver — Spring , :
Exception | HTTP Status Code |
---|---|
BindException | 400 (Bad Request) |
ConversionNotSupportedException | 500 (Internal Server Error) |
HttpMediaTypeNotAcceptableException | 406 (Not Acceptable) |
HttpMediaTypeNotSupportedException | 415 (Unsupported Media Type) |
HttpMessageNotReadableException | 400 (Bad Request) |
HttpMessageNotWritableException | 500 (Internal Server Error) |
HttpRequestMethodNotSupportedException | 405 (Method Not Allowed) |
MethodArgumentNotValidException | 400 (Bad Request) |
MissingServletRequestParameterException | 400 (Bad Request) |
MissingServletRequestPartException | 400 (Bad Request) |
NoSuchRequestHandlingMethodException | 404 (Not Found) |
TypeMismatchException | 400 (Bad Request) |
, REST API . . ModelAndView, , .
ResponseStatusExceptionResolver — @ResponseStatus.
ServiceException:
@ResponseStatus(value = HttpStatus.INTERNAL_SERVER_ERROR)
public class ServiceException extends Exception {
public ServiceException(String message) {
super(message);
}
}
ServiceException @ResponseStatus value INTERNAL_SERVER_ERROR, - 500.
:
@RestController
public class Example2Controller {
@GetMapping(value = "/testResponseStatusExceptionResolver", produces = APPLICATION_JSON_VALUE)
public Response testResponseStatusExceptionResolver(@RequestParam(required = false, defaultValue = "false") boolean exception)
throws ServiceException {
if (exception) {
throw new ServiceException("ServiceException in testResponseStatusExceptionResolver");
}
return new Response("OK");
}
}
GET- exception=true, 500- :
— . , @ResponseStatus .
HandlerExceptionResolver , - JSON XML . , .
:
@Component
public class CustomExceptionResolver extends AbstractHandlerExceptionResolver {
@Override
protected ModelAndView doResolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
ModelAndView modelAndView = new ModelAndView(new MappingJackson2JsonView());
if (ex instanceof CustomException) {
modelAndView.setStatus(HttpStatus.BAD_REQUEST);
modelAndView.addObject("message", "CustomException was handled");
return modelAndView;
}
modelAndView.setStatus(HttpStatus.INTERNAL_SERVER_ERROR);
modelAndView.addObject("message", "Another exception was handled");
return modelAndView;
}
}
, . , : , ModelAndView. JSON, .
-, . . , . , — :
@RestController
public class Example3Controller {
@GetMapping(value = "/testCustomExceptionResolver", produces = APPLICATION_JSON_VALUE)
public Response testCustomExceptionResolver(@RequestParam(required = false, defaultValue = "false") boolean exception)
throws CustomException {
if (exception) {
throw new CustomException("CustomException in testCustomExceptionResolver");
}
return new Response("OK");
}
}
:
200 JSON .
@ControllerAdvice
— . Spring 3.2 @ControllerAdvice.
:
@ControllerAdvice
public class DefaultAdvice {
@ExceptionHandler(BusinessException.class)
public ResponseEntity<Response> handleException(BusinessException e) {
Response response = new Response(e.getMessage());
return new ResponseEntity<>(response, HttpStatus.OK);
}
}
, @ControllerAdvice , .
DefaultAdvice handleException. handleException @ExceptionHandler, , , . BusinessException.
: @ExceptionHandler({BusinessException.class, ServiceException.class}). @ExceptionHandler .
, handleException ResponseEntity Response:
public class Response {
private String message;
public Response() {
}
public Response(String message) {
this.message = message;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
}
, JSON . message HttpStatus.OK, 200.
:
@RestController
public class Example4Controller {
@GetMapping(value = "/testDefaultControllerAdvice", produces = APPLICATION_JSON_VALUE)
public Response testDefaultControllerAdvice(@RequestParam(required = false, defaultValue = "false") boolean exception)
throws BusinessException {
if (exception) {
throw new BusinessException("BusinessException in testDefaultControllerAdvice");
}
return new Response("OK");
}
}
, , JSON 200:
?
! :
@ControllerAdvice(annotations = CustomExceptionHandler.class)
public class CustomAdvice {
@ExceptionHandler(BusinessException.class)
public ResponseEntity<Response> handleException(BusinessException e) {
String message = String.format("%s %s", LocalDateTime.now(), e.getMessage());
Response response = new Response(message);
return new ResponseEntity<>(response, HttpStatus.OK);
}
}
@ControllerAdvice(annotations = CustomExceptionHandler.class). CustomAdvice , @CustomExceptionHandler.
@CustomExceptionHandler :
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface CustomExceptionHandler {
}
:
@RestController
@CustomExceptionHandler
public class Example5Controller {
@GetMapping(value = "/testCustomControllerAdvice", produces = APPLICATION_JSON_VALUE)
public Response testCustomControllerAdvice(@RequestParam(required = false, defaultValue = "false") boolean exception)
throws BusinessException {
if (exception) {
throw new BusinessException("BusinessException in testCustomControllerAdvice");
}
return new Response("OK");
}
}
Example5Controller @CustomExceptionHandler, Example4Controller . BusinessException CustomAdvice, DefaultAdvice, .
CustomAdvice — :
. @ControllerAdvice, . .
ResponseStatusException.
ResponseStatusException:
@RestController
public class Example6Controller {
@GetMapping(value = "/testResponseStatusException", produces = APPLICATION_JSON_VALUE)
public Response testResponseStatusException(@RequestParam(required = false, defaultValue = "false") boolean exception) {
if (exception) {
throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, "ResponseStatusException in testResponseStatusException");
}
return new Response("OK");
}
}
ResponseStatusException , , . @ResponseStatus — -. , .
:
Summary : we have seen different ways of handling exceptions, each of which has its own characteristics. Within a large application, you can find several approaches at once, but you need to be very careful and try not to overcomplicate the error handling logic. Otherwise, it will turn out that some exception will be handled in the wrong handler and the response will differ from the expected one. For example, if the application has several advisors, then when creating a new one, you need to make sure that it does not break the existing order of handling exceptions from old controllers.
So be careful and everything will work great!