Часто на практике возникает необходимость централизованной обработки исключений в рамках контроллера или даже всего приложения. В данной статье разберём основные возможности, которые предоставляет Spring Framework для решения этой задачи и на простых примерах посмотрим как всё работает. Кому интересна данная тема — добро пожаловать под кат!
Изначально до Spring 3.2 основными способами обработки исключений в приложении были HandlerExceptionResolver и аннотация @ExceptionHandler. Их мы ещё подробно разберём ниже, но они имеют определённые недостатки. Начиная с версии 3.2 появилась аннотация @ControllerAdvice, в которой устранены ограничения из предыдущих решений. А в Spring 5 добавился новый класс ResponseStatusException, который очень удобен для обработки базовых ошибок для REST API.
А теперь обо всём по порядку, поехали!
Обработка исключений на уровне контроллера — @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 и устанавливает соответствующий код ответа, в зависимости от типа исключения:
Основной недостаток заключается в том что возвращается только код статуса, а на практике для 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");
}
}
А вот и пример вызова:
Видим что исключение прекрасно обработалось и в ответ получили код 400 и 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 — просто выбрасываем исключение и передаём нужный статус-код. Конечно тут возвращаемся к проблеме отсутствия тела сообщения, но в простых случаях такой подход может быть удобен.
Пример вызова:
Резюме: мы познакомились с разными способами обработки исключений, каждый из которых имеет свои особенности. В рамках большого приложения можно встретить сразу несколько подходов, но при этом нужно быть очень осторожным и стараться не переусложнять логику обработки ошибок. Иначе получится что какое-нибудь исключение обработается не в том обработчике и на выходе ответ будет отличаться от ожидаемого. Например если в приложении есть несколько эдвайсов, то при создании нового нужно убедиться, что он не сломает существующий порядок обработки исключений из старых контроллеров.
Так что будьте внимательны и всё будет работать замечательно!
Ссылка на исходники из статьи
Custom Error Message Handling for REST API
1. Overview
In this tutorial – we’ll discuss how to implement an global error handler for a Spring REST API.
We will use the semantics of each exception to build out meaningful error messages for client, with the clear goal of giving that client all the info to easily diagnose the problem.
Further reading:
2. A Custom Error Message
Let’s start by implementing a simple structure for sending errors over the wire – the ApiError:
public class ApiError {
private HttpStatus status;
private String message;
private List<String> errors;
public ApiError(HttpStatus status, String message, List<String> errors) {
super();
this.status = status;
this.message = message;
this.errors = errors;
}
public ApiError(HttpStatus status, String message, String error) {
super();
this.status = status;
this.message = message;
errors = Arrays.asList(error);
}
}
The information here should be straightforward:
-
status: the HTTP status code
-
message: the error message associated with exception
-
error: List of constructed error messages
And of course, for the actual exception handling logic in Spring, we’ll use the @ControllerAdvice annotation:
@ControllerAdvice
public class CustomRestExceptionHandler extends ResponseEntityExceptionHandler {
...
}
3. Handle Bad Request Exceptions
==== 3.1. Handling the Exceptions
Now, let’s see how we can handle the most common client errors – basically scenarios of a client sent an invalid request to the API:
-
BindException: This exception is thrown when fatal binding errors occur.
-
MethodArgumentNotValidException: This exception is thrown when argument annotated with @Valid failed validation:
@Override
protected ResponseEntity<Object> handleMethodArgumentNotValid(
MethodArgumentNotValidException ex,
HttpHeaders headers,
HttpStatus status,
WebRequest request) {
List<String> errors = new ArrayList<String>();
for (FieldError error : ex.getBindingResult().getFieldErrors()) {
errors.add(error.getField() + ": " + error.getDefaultMessage());
}
for (ObjectError error : ex.getBindingResult().getGlobalErrors()) {
errors.add(error.getObjectName() + ": " + error.getDefaultMessage());
}
ApiError apiError =
new ApiError(HttpStatus.BAD_REQUEST, ex.getLocalizedMessage(), errors);
return handleExceptionInternal(
ex, apiError, headers, apiError.getStatus(), request);
}
As you can see, we are overriding a base method out of the ResponseEntityExceptionHandler and providing our own custom implementation.
That’s not always going to be the case – sometimes we’re going to need to handle a custom exception that doesn’t have a default implementation in the base class, as we’ll get to see later on here.
Next:
-
MissingServletRequestPartException: This exception is thrown when when the part of a multipart request not found
-
MissingServletRequestParameterException: This exception is thrown when request missing parameter:
@Override
protected ResponseEntity<Object> handleMissingServletRequestParameter(
MissingServletRequestParameterException ex, HttpHeaders headers,
HttpStatus status, WebRequest request) {
String error = ex.getParameterName() + " parameter is missing";
ApiError apiError =
new ApiError(HttpStatus.BAD_REQUEST, ex.getLocalizedMessage(), error);
return new ResponseEntity<Object>(
apiError, new HttpHeaders(), apiError.getStatus());
}
-
ConstrainViolationException: This exception reports the result of constraint violations:
@ExceptionHandler({ ConstraintViolationException.class })
public ResponseEntity<Object> handleConstraintViolation(
ConstraintViolationException ex, WebRequest request) {
List<String> errors = new ArrayList<String>();
for (ConstraintViolation<?> violation : ex.getConstraintViolations()) {
errors.add(violation.getRootBeanClass().getName() + " " +
violation.getPropertyPath() + ": " + violation.getMessage());
}
ApiError apiError =
new ApiError(HttpStatus.BAD_REQUEST, ex.getLocalizedMessage(), errors);
return new ResponseEntity<Object>(
apiError, new HttpHeaders(), apiError.getStatus());
}
-
TypeMismatchException: This exception is thrown when try to set bean property with wrong type.
-
MethodArgumentTypeMismatchException: This exception is thrown when method argument is not the expected type:
@ExceptionHandler({ MethodArgumentTypeMismatchException.class })
public ResponseEntity<Object> handleMethodArgumentTypeMismatch(
MethodArgumentTypeMismatchException ex, WebRequest request) {
String error =
ex.getName() + " should be of type " + ex.getRequiredType().getName();
ApiError apiError =
new ApiError(HttpStatus.BAD_REQUEST, ex.getLocalizedMessage(), error);
return new ResponseEntity<Object>(
apiError, new HttpHeaders(), apiError.getStatus());
}
3.2. Consuming the API from the Client
Let’s now have a look at a a test that runs into a MethodArgumentTypeMismatchException: we’ll send a request with id as String instead of long:
@Test
public void whenMethodArgumentMismatch_thenBadRequest() {
Response response = givenAuth().get(URL_PREFIX + "/api/foos/ccc");
ApiError error = response.as(ApiError.class);
assertEquals(HttpStatus.BAD_REQUEST, error.getStatus());
assertEquals(1, error.getErrors().size());
assertTrue(error.getErrors().get(0).contains("should be of type"));
}
And finally – considering this same request: :
Request method: GET
Request path: http://localhost:8080/spring-security-rest/api/foos/ccc
Here’s what this kind of JSON error response will look like:
{
"status": "BAD_REQUEST",
"message":
"Failed to convert value of type [java.lang.String]
to required type [java.lang.Long]; nested exception
is java.lang.NumberFormatException: For input string: "ccc"",
"errors": [
"id should be of type java.lang.Long"
]
}
4. Handle NoHandlerFoundException
Next, we can customize our servlet to throw this exception instead of send 404 response – as follows:
<servlet>
<servlet-name>api</servlet-name>
<servlet-class>
org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>throwExceptionIfNoHandlerFound</param-name>
<param-value>true</param-value>
</init-param>
</servlet>
Then, once this happens, we we can simply handle it just as any other exception:
@Override
protected ResponseEntity<Object> handleNoHandlerFoundException(
NoHandlerFoundException ex, HttpHeaders headers, HttpStatus status, WebRequest request) {
String error = "No handler found for " + ex.getHttpMethod() + " " + ex.getRequestURL();
ApiError apiError = new ApiError(HttpStatus.NOT_FOUND, ex.getLocalizedMessage(), error);
return new ResponseEntity<Object>(apiError, new HttpHeaders(), apiError.getStatus());
}
Here is a simple test:
@Test
public void whenNoHandlerForHttpRequest_thenNotFound() {
Response response = givenAuth().delete(URL_PREFIX + "/api/xx");
ApiError error = response.as(ApiError.class);
assertEquals(HttpStatus.NOT_FOUND, error.getStatus());
assertEquals(1, error.getErrors().size());
assertTrue(error.getErrors().get(0).contains("No handler found"));
}
Let’s have a look at the full request:
Request method: DELETE
Request path: http://localhost:8080/spring-security-rest/api/xx
And the error JSON response:
{
"status":"NOT_FOUND",
"message":"No handler found for DELETE /spring-security-rest/api/xx",
"errors":[
"No handler found for DELETE /spring-security-rest/api/xx"
]
}
5. Handle HttpRequestMethodNotSupportedException
Next, let’s have a look at another interesting exception – the HttpRequestMethodNotSupportedException – which occurs when you send a requested with an unsupported HTTP method:
@Override
protected ResponseEntity<Object> handleHttpRequestMethodNotSupported(
HttpRequestMethodNotSupportedException ex,
HttpHeaders headers,
HttpStatus status,
WebRequest request) {
StringBuilder builder = new StringBuilder();
builder.append(ex.getMethod());
builder.append(
" method is not supported for this request. Supported methods are ");
ex.getSupportedHttpMethods().forEach(t -> builder.append(t + " "));
ApiError apiError = new ApiError(HttpStatus.METHOD_NOT_ALLOWED,
ex.getLocalizedMessage(), builder.toString());
return new ResponseEntity<Object>(
apiError, new HttpHeaders(), apiError.getStatus());
}
Here is a simple test reproducing this exception:
@Test
public void whenHttpRequestMethodNotSupported_thenMethodNotAllowed() {
Response response = givenAuth().delete(URL_PREFIX + "/api/foos/1");
ApiError error = response.as(ApiError.class);
assertEquals(HttpStatus.METHOD_NOT_ALLOWED, error.getStatus());
assertEquals(1, error.getErrors().size());
assertTrue(error.getErrors().get(0).contains("Supported methods are"));
}
And here’s the full request:
Request method: DELETE
Request path: http://localhost:8080/spring-security-rest/api/foos/1
And the error JSON response:
{
"status":"METHOD_NOT_ALLOWED",
"message":"Request method 'DELETE' not supported",
"errors":[
"DELETE method is not supported for this request. Supported methods are GET "
]
}
6. Handle HttpMediaTypeNotSupportedException
Now, let’s handle HttpMediaTypeNotSupportedException – which occurs when the client send a request with unsupported media type – as follows:
@Override
protected ResponseEntity<Object> handleHttpMediaTypeNotSupported(
HttpMediaTypeNotSupportedException ex,
HttpHeaders headers,
HttpStatus status,
WebRequest request) {
StringBuilder builder = new StringBuilder();
builder.append(ex.getContentType());
builder.append(" media type is not supported. Supported media types are ");
ex.getSupportedMediaTypes().forEach(t -> builder.append(t + ", "));
ApiError apiError = new ApiError(HttpStatus.UNSUPPORTED_MEDIA_TYPE,
ex.getLocalizedMessage(), builder.substring(0, builder.length() - 2));
return new ResponseEntity<Object>(
apiError, new HttpHeaders(), apiError.getStatus());
}
Here is a simple test running into this issue:
@Test
public void whenSendInvalidHttpMediaType_thenUnsupportedMediaType() {
Response response = givenAuth().body("").post(URL_PREFIX + "/api/foos");
ApiError error = response.as(ApiError.class);
assertEquals(HttpStatus.UNSUPPORTED_MEDIA_TYPE, error.getStatus());
assertEquals(1, error.getErrors().size());
assertTrue(error.getErrors().get(0).contains("media type is not supported"));
}
Finally – here’s a sample request:
Request method: POST
Request path: http://localhost:8080/spring-security-
Headers: Content-Type=text/plain; charset=ISO-8859-1
And the error JSON response:
{
"status":"UNSUPPORTED_MEDIA_TYPE",
"message":"Content type 'text/plain;charset=ISO-8859-1' not supported",
"errors":["text/plain;charset=ISO-8859-1 media type is not supported.
Supported media types are text/xml
application/x-www-form-urlencoded
application/*+xml
application/json;charset=UTF-8
application/*+json;charset=UTF-8 */"
]
}
7. Default Handler
Finally, let’s implement a fall-back handler – a catch-all type of logic that deals with all other exceptions that don’t have specific handlers:
@ExceptionHandler({ Exception.class })
public ResponseEntity<Object> handleAll(Exception ex, WebRequest request) {
ApiError apiError = new ApiError(
HttpStatus.INTERNAL_SERVER_ERROR, ex.getLocalizedMessage(), "error occurred");
return new ResponseEntity<Object>(
apiError, new HttpHeaders(), apiError.getStatus());
}
8. Conclusion
Building a proper, mature error handler for a Spring REST API is tough and definitely an iterative process. Hopefully this tutorial will be a good starting point towards doing that for your API and also a good anchor for how you should look at helping your the clients of your API quickly and easily diagnose errors and move past them.
The full implementation of this tutorial can be found in the github project – this is an Eclipse based project, so it should be easy to import and run as it is.
- Details
- Written by
- Last Updated on 26 March 2022 | Print Email
Spring framework offers developers several options for handling exceptions in their applications. One of which is global exception handler with @ControllerAdvice and @ExceptionHandler annotations. And in this article, I’d like to share with you some code examples about application-wide exception handling in Spring with both Web MVC controller and REST controller.
1. Why Global Exception Handling?
In this kind of exception handling, you write a separate class that handles all exceptions which might be thrown by the application — in one central place:
@ControllerAdvice public class GlobalExceptionHandler { @ExceptionHandler(Exception1.class) public String handleException1() { // handles exception type Exception1 } @ExceptionHandler(Exception2.class) public String handleException2() { // handles exception type Exception2 } // handlers for other exceptions... }
You see, the class is annotated with @ControllerAdvice, which is a kind of @Component. It tells Spring to intercept (via AOP) all handler methods in all controllers and execute the exception handler methods when the matching exceptions are thrown.
In exception handler methods, you can log the exception, redirect to user-friendly error pages, modify the response status code, any custom logics you want to process when the matching exception occurs.
Compared with controller-based exception handler, this approach is more convenient in terms of centralizing all exception handling code in one place.
In a classic Spring Web MVC application with template-based response, the exception handler method should return a logical view name, which is then resolved by Spring’s view resolver. Here’s an example:
package net.codejava; import org.hibernate.exception.JDBCConnectionException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.bind.annotation.ExceptionHandler; @ControllerAdvice public class GlobalExceptionHandler { private static final Logger LOGGER = LoggerFactory.getLogger( GlobalExceptionHandler.class); @ExceptionHandler(JDBCConnectionException.class) public String handleConnectionError(Exception ex) { LOGGER.error(ex.getMessage(), ex); return "connect_error"; } }
Here, the handleConnectionError()method will get executed if an exception of type JDBCConnectionException is thrown (by any controller). It returns a logical view name “connect_error” which will be processed by a template engine (JSP or Thymeleaf).
In the signature of exception handler method, you can access the request, response and exception objects if needed. Here’s another example:
@ExceptionHandler(Exception.class) public String handleGeneralError(HttpServletRequest request, HttpServletResponse response, Exception ex) { LOGGER.error(ex.getMessage(), ex); // do something with request and response return "general_error"; }
In case you want to put an object to model, the method should return a ModelAndView object, as shown below:
@ExceptionHandler(ProductNotFoundException.class) public ModelAndView handleProductNotFoundError(Exception ex) { ModelAndView mav = new ModelAndView("productNotFound"); mav.addObject("message", ex.getLocalizedMessage()); return mav; }
Then in the view template, you can access the object like this (Thymeleaf):
<!DOCTYPE html> <html> <head> <meta charset="ISO-8859-1"> <title>Product Not Found</title> </head> <body> <h2>Sorry, the product you requested not found</h2> <h3>[[${message}]]</h3> </body> </html>
You can also catch multiple exceptions in a single hander, as shown below:
@ExceptionHandler({Exception1.class, Exception2.class, Exception3.class})
3. Spring REST Global Exception Handler Examples
In a Spring application with REST APIs, an exception handler method should be annotated with @ExceptionHandler, @ResponseStatus and @ResponseBody annotations. Let’s look at the following example:
package net.codejava.payroll; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.bind.annotation.ResponseStatus; @ControllerAdvice public class GlobalErrorHandler { @ResponseBody @ExceptionHandler(EmployeeNotFoundException.class) @ResponseStatus(HttpStatus.NOT_FOUND) String employeeNotFoundHandler(HttpServletRequest request, HttpServletResponse response, Exception ex) { // do something with request or response return ex.getMessage(); } }
Here, the @ResponseStatus annotation specifies the HTTP status code of the response, and the @ResponseBody annotation tells Spring to include the return value of the handler in the response’s body.
You can also return a custom object in exception handler methods, as shown in the below example:
@ControllerAdvice public class GlobalExceptionHandler { @ExceptionHandler(MultipartException.class) @ResponseStatus(HttpStatus.BAD_REQUEST) @ResponseBody public ErrorInfo handleMultipartException(HttpServletRequest request) { return new ErrorInfo(request, "Invalid Upload Request"); } @ExceptionHandler(HttpRequestMethodNotSupportedException.class) @ResponseStatus(HttpStatus.METHOD_NOT_ALLOWED) @ResponseBody public ErrorInfo handleMethodNotSupported(HttpServletRequest request) { return new ErrorInfo(request, "HTTP request method not supported for this operation."); } @ExceptionHandler(IOException.class) @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) @ResponseBody public ErrorInfo handleIOException(HttpServletRequest request, Exception ex) { return new ErrorInfo(request, "IO Error: " + ex.getMessage()); } }
The ErrorInfo class is as follows:
package net.codejava.payroll; public class ErrorInfo { private final String url; private final String ex; public ErrorInfo(String url, Exception ex) { this.url = url; this.ex = ex.getLocalizedMessage(); } // getters and setters are not shown for brevity }
Then in case of error, the response would be like this:
{ "url": "http://localhost:8080/orders/38", "ex": "Could not find order ID 38" }
NOTES: You can combine both exception handlers for classic Web MVC & REST in one global exception handler class. And if an exception is handled at both controller level and global level, the controller-based one takes precedence.
That’s a few code examples about global exception handling in Spring application. I hope you find them helpful. To see the coding in action, I recommend you watch the following video:
Related Tutorials:
- Spring Boot Error Handling Guide
- How to handle exceptions in Spring MVC
- Spring Boot Controller-Based Exception Handler Examples
Other Spring Boot Tutorials:
- Spring Boot automatic restart using Spring Boot DevTools
- Spring Boot Form Handling Tutorial with Spring Form Tags and JSP
- How to create a Spring Boot Web Application (Spring MVC with JSP/ThymeLeaf)
- Spring Boot — Spring Data JPA — MySQL Example
- Spring Boot Hello World RESTful Web Services Tutorial
- How to use JDBC with Spring Boot
- Spring Boot CRUD Web Application with JDBC — Thymeleaf — Oracle
- Spring Boot RESTful CRUD API Examples with MySQL database
- How to package Spring Boot application to JAR and WAR
- Spring Boot Security Authentication with JPA, Hibernate and MySQL
- Spring Data JPA Paging and Sorting Examples
- All Spring Boot Tutorials
About the Author:
Nam Ha Minh is certified Java programmer (SCJP and SCWCD). He started programming with Java in the time of Java 1.4 and has been falling in love with Java since then. Make friend with him on Facebook and watch his Java videos you YouTube.