Часто на практике возникает необходимость централизованной обработки исключений в рамках контроллера или даже всего приложения. В данной статье разберём основные возможности, которые предоставляет 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 — просто выбрасываем исключение и передаём нужный статус-код. Конечно тут возвращаемся к проблеме отсутствия тела сообщения, но в простых случаях такой подход может быть удобен.
Пример вызова:
Резюме: мы познакомились с разными способами обработки исключений, каждый из которых имеет свои особенности. В рамках большого приложения можно встретить сразу несколько подходов, но при этом нужно быть очень осторожным и стараться не переусложнять логику обработки ошибок. Иначе получится что какое-нибудь исключение обработается не в том обработчике и на выходе ответ будет отличаться от ожидаемого. Например если в приложении есть несколько эдвайсов, то при создании нового нужно убедиться, что он не сломает существующий порядок обработки исключений из старых контроллеров.
Так что будьте внимательны и всё будет работать замечательно!
Ссылка на исходники из статьи
TL/DR: Let’s take a look at everything required to build custom error handling logic in both Spring Boot Web and Spring Boot Security
REST applications developed in Spring Boot automatically take advantage of its default error handling logic. Specifically, whenever an error occurs, a default response containing some information is returned. The problem is that this information may be poor or insufficient for the API callers to deal with the error properly. This is why implementing custom error handling logic is such a common and desirable task. Achieving it requires more effort than you might think, and you need to delve into a few essential Spring Boot notions. Let’s see everything required to get started with custom error handling in Spring Boot and Java.
Prerequisites
This is the list of all the prerequisites for following the article:
- Java >= 1.8 (Java >= 13 recommended)
- Spring Boot >= 2.5
- Spring Boot Starter Web >= 2.5
- Spring Security >= 5.5
- Project Lombok >= 1.18
- Gradle >= 4.x or Maven 3.6.x
Default Error Handling in Spring Boot
By default, Spring Boot offers a fallback error-handling page, as well as an error-handling response in case of REST requests. Particularly, Spring Boot looks for a mapping for the /error
endpoint during the start-up. This mapping depends on what is set on a ViewResolver
class. When no valid mappings can be found, Spring Boot automatically configures a default fallback error page. This so-called Whitelabel Error Page is nothing more than a white HTML page containing the HTTP status code and a vague error message. This is what such a page looks like:
<html>
<head></head>
<body data-new-gr-c-s-check-loaded="14.1026.0" data-gr-ext-installed="">
<h1>Whitelabel Error Page</h1>
<p>This application has no explicit mapping for /error, so you are seeing this as a fallback.</p>
<div id="created">Sun Aug 15 14:32:17 UTC 2021</div>
<div>There was an unexpected error (type=Internal Server Error, status=500).</div>
<div></div>
</body>
</html>
This is what the Whitelabel HTML page looks like in your browser:
The Spring Boot Whitelabel HTML Error Page
Similarly, when dealing with REST requests, Spring Boot automatically returns a default JSON response in case of errors. This contains the same information as the aforementioned Whitelabel HTML error page and looks as follows:
{
"timestamp": "2021-15-08T14:32:17.947+0000",
"status": 500,
"error": "Internal Server Error",
"path": "/test"
}
As you can see, the default Spring Boot error handling responses for REST does not provide much information. This can quickly become a problem, especially when trying to debug. It is also problematic for front-end developers, who need detailed information coming from API error response messages to be able to explain to the end users what happened properly.
Let’s see how to replace this default response with custom-defined messages. While this may appear like an easy task, this is actually a tricky one. To achieve it, you first need to know a few Spring Boot fundamentals. Let’s learn more about them.
Custom Error Handling in Spring Boot
You are about to see two different approaches to custom error handling in Spring Boot REST applications. Both are based on a @ControllerAdvice
annotated class handling all exceptions that may occur. So, let’s first see what a @ControllerAdvice
annotated class is, why to use it, how, and when. Then, you will learn how to implement the two different approaches in detail. Finally, the pros and cons of each method will be explained.
Handling Exceptions with @ControllerAdvice
The @ControllerAdvice
annotation was introduced in Spring 3.2 to make exception handling logic easier and entirely definable in one place. In fact, @ControllerAdvice
allows you to address exception handling across the whole application. In other words, a single @ControllerAdvice
annotated class can handle exceptions thrown from any place in your application. Thus, classes annotated with @ControllerAdvice
are powerful and flexible tools. Not only do they allow you to centralize exception-handling logic into a global component, but also give you control over the body response, as well as the HTTP status code. This is especially important when trying to achieve custom error handling. Let’s see @ControllerAdvice
in action.
Now, you are about to see everything required to implement two custom error handling approaches based on @ControllerAdvice
. First, you should clone the GitHub repository supporting this article. By analyzing the codebase, going through this article will become easier. Also, you will be able to immediately see the two approaches in action.
So, clone the repository with the following command:
git clone https://github.com/Tonel/spring-boot-custom-error-handling
Then, run the DemoApplication
main class by following this guide from the Spring Boot official documentation, and reach one of the following 4 endpoints to see the custom error handling responses:
http://localhost:8080/test-custom-data-not-found-exception
http://localhost:8080/test-custom-parameter-constraint-exception?value=12
http://localhost:8080/test-custom-error-exception
http://localhost:8080/test-generic-exception
The first two APIs apply the first approach to error handling you are about to see, while the third API uses the second approach. The fourth and last API shows the fallback error handling logic presented above in action. Now, let’s delve into implementing these two approaches to custom error handling in Spring Boot.
Both of them rely on an ErrorMessage
class representing the custom error body placed in an error
package, containing everything needed to deal with custom error handling logic. This can be implemented as follows:
// src/main/java/com/customerrorhandling/demo/errors/ErrorResponse.java
package com.customerrorhandling.demo.errors;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Getter;
import lombok.Setter;
import org.springframework.http.HttpStatus;
import java.util.Date;
@Getter
@Setter
public class ErrorResponse {
// customizing timestamp serialization format
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "dd-MM-yyyy hh:mm:ss")
private Date timestamp;
private int code;
private String status;
private String message;
private String stackTrace;
private Object data;
public ErrorResponse() {
timestamp = new Date();
}
public ErrorResponse(
HttpStatus httpStatus,
String message
) {
this();
this.code = httpStatus.value();
this.status = httpStatus.name();
this.message = message;
}
public ErrorResponse(
HttpStatus httpStatus,
String message,
String stackTrace
) {
this(
httpStatus,
message
);
this.stackTrace = stackTrace;
}
public ErrorResponse(
HttpStatus httpStatus,
String message,
String stackTrace,
Object data
) {
this(
httpStatus,
message,
stackTrace
);
this.data = data;
}
}
The @Getter
and @Setter
annotations used in the code examples above are part of the Project Lombok
. They are used to automatically generate getters and setters. This is not mandatory and is just an additional way to avoid boilerplate code. Read this article to find out more about Lombok.
ErrorResponse
carries information such as an HTTP status code
and name
, a timestamp
indicating when the error occurred, an optional error message
, an optional exception stacktrace
, and an optional object containing any kind of data
. You should try to provide values to the first three fields, while the latter should be used only when required. In particular, the stackTrace
field should be valorized only in staging or development environments, as explained here. Similarly, the data field should be used only when additional data is required. Specifically, to explain in detail what happened or let the front-end better handle the error.
This class can be used to achieve a custom response when handling exceptions with @ControllerAdvice
as below:
// src/main/java/com/customerrorhandling/demo/errors/CustomControllerAdvice.java
package com.customerrorhandling.demo.errors;
import exceptions.CustomDataNotFoundException;
import exceptions.CustomErrorException;
import exceptions.CustomParameterConstraintException;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import java.io.PrintWriter;
import java.io.StringWriter;
@ControllerAdvice
class CustomControllerAdvice {
@ExceptionHandler(NullPointerException.class) // exception handled
public ResponseEntity<ErrorResponse> handleNullPointerExceptions(
Exception e
) {
// ... potential custom logic
HttpStatus status = HttpStatus.NOT_FOUND; // 404
return new ResponseEntity<>(
new ErrorResponse(
status,
e.getMessage()
),
status
);
}
// fallback method
@ExceptionHandler(Exception.class) // exception handled
public ResponseEntity<ErrorResponse> handleExceptions(
Exception e
) {
// ... potential custom logic
HttpStatus status = HttpStatus.INTERNAL_SERVER_ERROR; // 500
// converting the stack trace to String
StringWriter stringWriter = new StringWriter();
PrintWriter printWriter = new PrintWriter(stringWriter);
e.printStackTrace(printWriter);
String stackTrace = stringWriter.toString();
return new ResponseEntity<>(
new ErrorResponse(
status,
e.getMessage(),
stackTrace // specifying the stack trace in case of 500s
),
status
);
}
}
As you can see, @ControllerAdvice
works by employing the @ExceptionHandler
method-level annotation. This annotation allows you to define which method should be called in case of an error. Specifically, the exception thrown is compared to the exceptions passed as parameters to @ExceptionHandler
based on type. The first method where there is a match is called. If none matched, then the exception’s parent class is tested, and so on. This is also why you should implement a fallback method to cover all remaining cases. You can achieve this by passing the Exception
class to the @ExceptionHandler
annotation, just like in the handleExceptions
method. In fact, any exception in Java must have Exception
as one of its ancestors in their inheritance chain. So, they all extend directly — or as subclasses — the Exception
superclass.
Then, each method handles the error and might even implement custom logic, such as logging. In this example, each exception is handled by returning a ResponseEntity
having the desired HttpStatus
. This will be used as an HTTP status code associated with the error response. Similarly, the ErrorResponse
instance passed to the ResponseEntity
constructor will be automatically serialized in JSON and used as the message body. This way, custom error handling has just been achieved.
Now, you will dive into how to use @ConfrollerAdvice
to implement two different approaches to custom error handling for REST in Spring Boot Web. The first one involves boilerplate code, but it is clean and best-practice based. In contrast, the second represents a good solution in terms of convenience, although it is a bit dirty.
Defining Many Custom Exceptions
This approach involves having as many methods in your @ControllerAdvice
as many HTTP error status codes you want to handle. These methods will be related to one or more exceptions and return an error message with a particular HTTP status code. Implementing such an approach required three steps. First, you have to think about all the HTTP error status codes you want your application to return. Then, you have to define a method for each of them in your @ControllerAdvice
annotated class. Lastly, you have to associate these methods with their exceptions with the @ExceptionHandler
annotation.
This means that all exceptions of a particular type will be traced back to their relative method in the @ControllerAdvice
annotated class. This may represent a problem, especially considering some exceptions are more common than others, such as NullPointerException
. Since these exceptions can be thrown in many parts of your logic, they might have different meanings. Thus, they represent various errors and, therefore, other HTTP status codes.
The solution is to introduce new custom exceptions wrapping these frequent exceptions. For example, a NullPointerException
can become a CustomParameterConstraintException
exception at the controller layer, and a CustomDataNotFoundException
at the DAO (Data Access Object) layer. In this case, the first one can be associated with a 400 Bad Request, and the second with a 404 Not Found HTTP status. The idea behind these exceptions is to give the error that occurred a more specific meaning. This better characterizes the error and makes it more handleable in the @ControllerAdvice
annotated class accordingly. So, you should define a custom exception for each particular error you want to handle. Also, using custom exception classes represents undoubtedly a clean code principle. Thus, by adopting it, you are going to have more than one benefit.
So, let’s see this approach in action through an example. Firstly, you have to define custom exceptions, as shown here:
// src/main/java/exceptions/CustomParameterConstraintException.java
package exceptions;
public class CustomParameterConstraintException extends RuntimeException {
public CustomParameterConstraintException() {
super();
}
public CustomParameterConstraintException(String message) {
super(message);
}
}
// src/main/java/exceptions/CustomDataNotFoundException.java
package exceptions;
public class CustomDataNotFoundException extends RuntimeException {
public CustomDataNotFoundException() {
super();
}
public CustomDataNotFoundException(String message) {
super(message);
}
}
Then, use them to wrap frequent exceptions, or to throw them in case of particular circumstances representing errors in your business logic. Let’s see how with two examples:
// DAO-level method
public Foo retrieveFooById(
int id
) {
try {
// data retrieving logic
} catch (NullPointerException e) {
throw new CustomDataNotFoundException(e.getMessage());
}
}
As shown above, a generic NullPointerException
is turned into a more meaningful CustomDataNotFoundException
.
// controller-level method method
public ResponseEntity<Void> performOperation(
int numberOfAttempts
) {
if (numberOfAttempts <= 0 || numberOfAttempts >= 5)
throw new CustomParameterConstraintException("numberOfAttempts must be >= 0 and <= 5!");
// business logic
}
Here, a particular behavior that should not happen is intercepted. Then, the custom CustomParameterConstraintException
exception describing it is thrown.
Finally, all you have to do is add two particular methods to your @ControllerAdvice
annotated class, one for each specific error.
// src/main/java/com/customerrorhandling/demo/errors/CustomControllerAdvice.java
package com.customerrorhandling.demo.errors;
import exceptions.CustomDataNotFoundException;
import exceptions.CustomErrorException;
import exceptions.CustomParameterConstraintException;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import java.io.PrintWriter;
import java.io.StringWriter;
@ControllerAdvice
class CustomControllerAdvice {
// ...
@ExceptionHandler(CustomDataNotFoundException.class)
public ResponseEntity<ErrorResponse> handleCustomDataNotFoundExceptions(
Exception e
) {
HttpStatus status = HttpStatus.NOT_FOUND; // 404
// converting the stack trace to String
StringWriter stringWriter = new StringWriter();
PrintWriter printWriter = new PrintWriter(stringWriter);
e.printStackTrace(printWriter);
String stackTrace = stringWriter.toString();
return new ResponseEntity<>(
new ErrorResponse(
status,
e.getMessage(),
stackTrace, // assuming to be in staging environment, otherwise stackTrace should not be valorized
),
status
);
}
@ExceptionHandler(CustomParameterConstraintException.class)
public ResponseEntity<ErrorResponse> handleCustomParameterConstraintExceptions(
Exception e
) {
HttpStatus status = HttpStatus.BAD_REQUEST; // 400
return new ResponseEntity<>(
new ErrorResponse(
status,
e.getMessage()
),
status
);
}
// ...
}
Et voilà! Both errors originally related to the same exception were first characterized and then handled accordingly.
Now, let’s see the difference. This is what the default error response would look like on a 404 error:
{
"timestamp": "2021-15-08T14:32:17.947+0000",
"status": 404,
"error": "Not Found",
"path": "/test404"
}
And this is what the custom error response just implemented looks like:
{
"timestamp": "2021-15-08 14:32:17",
"code": 404,
"status": "NOT_FOUND",
"message": "Resource not found",
"stackTrace": "Exception in thread "main" com.example.demo.exceptions.CustomDataNotFoundException
at com.example.demo.AuthorController.getAuthor(AuthorController.java:16)
at com.example.demo.AuthorService.getAuthor(AuthorService.java:37)
at com.example.demo.AuthorDao.getById(AuthorDao.java:24)"
}
Defining a Single Custom Exception Carrying All Data
This approach involves defining a custom exception carrying the HTTP status to use, and all the data required to describe the error that occurred. The idea is to turn every exception you want to handle, or you would like to throw under special circumstances, into an instance of this particular exception. This way, you are spreading the error characterization logic into all your code. So, you will only have to add a new method in your @ControllerAdvice
annotated class to handle this custom exception accordingly.
First, you have to define a custom error handling exception. This can be achieved as follows:
// src/main/java/exceptions/CustomErrorException.java
package exceptions;
import lombok.Getter;
import lombok.Setter;
import org.springframework.http.HttpStatus;
@Getter
@Setter
public class CustomErrorException extends RuntimeException {
private HttpStatus status = null;
private Object data = null;
public CustomErrorException() {
super();
}
public CustomErrorException(
String message
) {
super(message);
}
public CustomErrorException(
HttpStatus status,
String message
) {
this(message);
this.status = status;
}
public CustomErrorException(
HttpStatus status,
String message,
Object data
) {
this(
status,
message
);
this.data = data;
}
}
Again, the @Getter
and @Setter
annotations were used to avoid boilerplate code and are not mandatory. As you can see, the CustomErrorException
class carries the same data used in the ErrorResponse
class to better describe what happened and present the errors to the end-users.
So, you can use this exception to wrap other exceptions, or you can throw it in case of particular circumstances constituting errors in your business logic. Now, let’s see how with two examples:
// DAO-level method
public Foo retrieveFooById(
int id
) {
try {
// data retrieving logic
} catch (NullPointerException e) {
throw new CustomErrorException(
HttpStatus.NOT_FOUND,
e.getMessage(),
(Integer) id
);
}
}
Here, an insufficiently significant NullPointerException
is turned into a more detailed CustomErrorException
containing all the data to describe why the error occurred.
// controller-level method method
public ResponseEntity<Void> performOperation(
int numberOfAttempts
) {
if (numberOfAttempts <= 0 || numberOfAttempts >= 5) {
throw new CustomErrorException(
HttpStatus.BAD_REQUEST,
"numberOfAttempts must be >= 0 and <= 5!",
(Integer) numberOfAttempts
);
}
// business logic
}
Similarly, a particular behavior that is not supposed to happen is intercepted. Consequently, a CustomErrorException
exception containing all the useful data to represent the error is thrown.
Lastly, add one method to handle CustomErrorException
exception instances to your @ControllerAdvice
annotated class, as below:
// src/main/java/com/customerrorhandling/demo/errors/CustomControllerAdvice.java
package com.customerrorhandling.demo.errors;
import exceptions.CustomDataNotFoundException;
import exceptions.CustomErrorException;
import exceptions.CustomParameterConstraintException;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import java.io.PrintWriter;
import java.io.StringWriter;
@ControllerAdvice
class CustomControllerAdvice {
// ...
@ExceptionHandler(CustomErrorException.class)
public ResponseEntity<ErrorResponse> handleCustomErrorExceptions(
Exception e
) {
// casting the generic Exception e to CustomErrorException
CustomErrorException customErrorException = (CustomErrorException) e;
HttpStatus status = customErrorException.getStatus();
// converting the stack trace to String
StringWriter stringWriter = new StringWriter();
PrintWriter printWriter = new PrintWriter(stringWriter);
customErrorException.printStackTrace(printWriter);
String stackTrace = stringWriter.toString();
return new ResponseEntity<>(
new ErrorResponse(
status,
customErrorException.getMessage(),
stackTrace,
customErrorException.getData()
),
status
);
}
// ...
}
Note that @ExceptionHandler
can accept more than one exception type. This means that the parameter of the method representing the exception must be downcasted. Otherwise, a ClassCastException
will be throw. So, upcast the exception e
to CustomErrorException
inside the method. Then, you will be able to access its particular fields and define a valid ErrorResponse
instance.
Done! This way each error that occurs is encapsulated into an exception containing everything required to describe it.
Now, let’s see the difference. This is what the default error response on a 404 error would look like:
{
"timestamp": "2021-15-08T14:32:17.947+0000",
"status": 404,
"error": "Not Found",
"message": "",
"path": "/test404"
}
And this is what the custom error response just implemented looks like:
{
"timestamp": "2021-15-08 14:32:17",
"code": 404,
"status": "NOT_FOUND",
"message": "Resource not found",
"stackTrace": "Exception in thread "main" com.example.demo.exceptions.CustomErrorException
at com.example.demo.AuthorController.getAuthor(AuthorController.java:16)
at com.example.demo.AuthorService.getAuthor(AuthorService.java:37)
at com.example.demo.AuthorDao.getById(AuthorDao.java:24)"
}
Pros and Cons of Each Approach
The first approach should be used when you do not want to spread error handling logic all over your codebase. In fact, the HTTP status code is only associated with errors in your @ControllerAdvice
annotated class. This means that no layer knows how the error will be handled and presented to users. Although this should be the desired behavior because it respects the principle of least privilege, it does involve boilerplate code. In fact, you may easily end up with dozens of custom exceptions, and define them is a tedious and not-scalable approach.
So, you may want a less restricting approach, and this is why the second approach was presented. Unfortunately, this one is definitely dirtier. In fact, it requires you to spread detail about error handling logic in many different points of your code. In contrast, it is scalable and quicker to be implemented. So, despite not being the cleanest approach, it allows you to achieve the desired result with little effort. Plus, it is more maintainable than the first approach because it involves only a custom exception.
Custom Error Handling in Spring Security
Spring Security is a powerful and highly customizable framework that provides both authentication and authorization. It is one of the most widely used Spring dependencies and represents the de-facto standard for securing a Spring Boot application.
In case of authentication and authorization failures, AuthenticationException
and AccessDeniedException
are thrown respectively. Then, Spring Security takes care of encapsulating them in default error handling responses. If you want to customize them, the two approaches presented above are of no use. This is because @ControllerAdvice
can handle only exceptions thrown by controllers, but AuthenticationException
and AccessDeniedException
are thrown by the Spring Security AbstractSecurityInterceptor
component — which is not a controller. In other words, a @ControllerAdvice
annotated class cannot catch them. Achieving this requires custom logic.
Implementing Custom Error Handling Logic in Spring Security
Let’s take a look at how to implement custom error handling in Spring Security. Luckily, this is not too complex since you can easily provide Spring Security with two components to handle authentication and authorization errors, respectively. What you need to do is to provide the AuthenticationFailureHandler
interface with implementation, as follows:
// src/main/java/com/auth0/hotsauces/security/CustomAuthenticationFailureHandler.java
package com.auth0.hotsauces.security;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.http.HttpStatus;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import java.util.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.HashMap;
public class CustomAuthenticationFailureHandler implements AuthenticationFailureHandler {
// Jackson JSON serializer instance
private ObjectMapper objectMapper = new ObjectMapper();
@Override
public void onAuthenticationFailure(
HttpServletRequest request,
HttpServletResponse response,
AuthenticationException exception
) throws IOException, ServletException {
HttpStatus httpStatus = HttpStatus.UNAUTHORIZED; // 401
Map<String, Object> data = new HashMap<>();
data.put(
"timestamp",
new Date()
);
data.put(
"code",
httpStatus.value();
);
data.put(
"status",
httpStatus.name();
);
data.put(
"message",
exception.getMessage()
);
// setting the response HTTP status code
response.setStatus(httpStatus.value());
// serializing the response body in JSON
response
.getOutputStream()
.println(
objectMapper.writeValueAsString(data)
);
}
}
This will be used to handle AuthenticationExceptions
.
Similarly, you can provide the AccessDeniedHandler
interface with implementation to handle AccessDeniedExceptions
.
// src/main/java/com/auth0/hotsauces/security/CustomAccessDeniedHandler.java
package com.auth0.hotsauces.security;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.http.HttpStatus;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import java.util.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.HashMap;
public class CustomAccessDeniedHandler implements AccessDeniedHandler {
// Jackson JSON serializer instance
private ObjectMapper objectMapper = new ObjectMapper();
@Override
public void handle(
HttpServletRequest request,
HttpServletResponse response,
AccessDeniedException exception
) throws IOException, ServletException {
HttpStatus httpStatus = HttpStatus.FORBIDDEN; // 403
Map<String, Object> data = new HashMap<>();
data.put(
"timestamp",
new Date()
);
data.put(
"code",
httpStatus.value();
);
data.put(
"status",
httpStatus.name();
);
data.put(
"message",
exception.getMessage()
);
// setting the response HTTP status code
response.setStatus(httpStatus.value());
// serializing the response body in JSON
response
.getOutputStream()
.println(
objectMapper.writeValueAsString(data)
);
}
}
Now, you just need to register these two custom implementations as authentication and authorization error handlers. You can do this as below:
// src/main/java/com/auth0/hotsauces/security/SecurityConfig.java
package com.auth0.hotsauces.security;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.oauth2.core.DelegatingOAuth2TokenValidator;
import org.springframework.security.oauth2.core.OAuth2TokenValidator;
import org.springframework.security.oauth2.jwt.*;
import org.springframework.security.oauth2.server.resource.web.BearerTokenAuthenticationFilter;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
// ...
@Override
protected void configure(HttpSecurity http)
throws Exception {
http
.authorizeRequests()
.anyRequest()
.authenticated()
.and()
.formLogin()
.failureHandler(authenticationFailureHandler())
.and()
.exceptionHandling()
.accessDeniedHandler(accessDeniedHandler());
}
@Bean
public AuthenticationFailureHandler authenticationFailureHandler() {
return new CustomAuthenticationFailureHandler();
}
@Bean
public AccessDeniedHandler accessDeniedHandler() {
return new CustomAccessDeniedHandler();
}
}
Et voilà! Custom error handling in Spring Boot has just been achieved thanks to the failureHandler
and accessDeniedHandler
methods, which allows you to register a custom authentication error handler and a custom authorization error handler.
Spring Security Custom Error Handling in Action
Now, let’s see how to implement it in a real-world example. First, read this article on how to protect APIs with Spring Security and Auth0. In the demo application produced in that article, no custom error handling is implemented. So, by making a request to a protected API including a wrong access token, the default Spring Boot error handling logic is applied. Let’s test it out.
If you are a macOS or Linux user, enter this command into the terminal:
curl -i --request GET
--url http://localhost:8080/api/hotsauces/
-H "Content-Type: application/json"
-H "authorization: Bearer wrong-token"
Otherwise, if you are a Windows user, enter this command into PowerShell:
$accessToken = "wrong-token"
$headers = @{
Authorization = "Bearer $accessToken"
}
$response = Invoke-RestMethod "http://localhost:8080/api/hotsauces/" `
-Headers $headers
$response | ConvertTo-Json
Then, the following response will be returned:
Invoke-WebRequest: The remote server returned an error: (401) Unauthorized.
At line:1 char:1
+ Invoke-WebRequest "http://localhost:8080/api/hotsauces/"
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : InvalidOperation: (System.Net.HttpWebRequest:HttpWebRequest) [Invoke-WebRequest], WebExc
eption
+ FullyQualifiedErrorId : WebCmdletWebResponseException,Microsoft.PowerShell.Commands.InvokeWebRequestCommand
As you can see, a 401 error status code is returned, but with no details on what happened.
Now, let’s test the demo application extended with custom error handling logic. You can find it in this GitHub repository. The application is exactly the same as the previous one, except for the error handling logic. In particular, the aforementioned presented logic was implemented.
In this case, by launching the commands above, this message will be returned:
Invoke-RestMethod : {"code":401,"message":"An error occurred while attempting to decode the Jwt: Invalid JWT serialization: Missing dot delimiter(s)","timestamp":1629880611013,"status":"UNAUTHORIZED"}
At line:1 char:1
+ $response = Invoke-RestMethod "http://localhost:8080/api/hotsauces/" ...
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : InvalidOperation: (System.Net.HttpWebRequest:HttpWebRequest) [Invoke-WebRequest], WebExc
eption
+ FullyQualifiedErrorId : WebCmdletWebResponseException,Microsoft.PowerShell.Commands.InvokeWebRequestCommand
As you can see, a JSON message representing the custom error handling logic was returned as expected. This contains the status code, the exception message, a timestamp, and the HTTP status code name, as follows:
{
"code": 401,
"message": "An error occurred while attempting to decode the Jwt: Invalid JWT serialization: Missing dot delimiter(s)",
"timestamp": 1629880611013,
"status": "UNAUTHORIZED"
}
Conclusion
In this article, we looked at how to implement custom error handling logic when dealing with REST applications in Spring Boot. This is not as easy a task as it may seem, and it requires knowing a few Spring Boot fundamentals. First, we delved into default error handling in Spring Boot and saw how poor the responses are. Then, we looked at @ControllerAdvice
and learn everything required to implement custom error handling logic. In particular, two different approaches were shown. Both allow you to define custom error handling responses but have specific pros and cons. Finally, we learned how to achieve the same result when dealing with Spring Boot Security errors, which requires specific logic. As shown, achieving custom error handling in Spring Boot is not easy but definitely possible, and explaining when, why, and how to do it was what this article was aimed at.
Thanks for reading! I hope that you found this article helpful. Feel free to reach out to me with any questions, comments, or suggestions.
Во время работы вашего приложения часто будут возникать исключительные ситуации. Когда у вас простое консольное приложение, то все просто – ошибка выводится в консоль. Но как быть с веб-приложением?
Допустим у пользователя отсутсвует доступ, или он передал некорректные данные. Лучшим вариантом будет в ответ на такие ситуации, отправлять пользователю сообщения с описанием ошибки. Это позволит клиенту вашего API скорректировать свой запрос.
В данной статье разберём основные возможности, которые предоставляет SpringBoot для решения этой задачи и на простых примерах посмотрим как всё работает.
@ExceptionHandler
@ExceptionHandler
позволяет обрабатывать исключения на уровне отдельного контроллера. Для этого достаточно объявить метод в контроллере, в котором будет содержаться вся логика обработки нужного исключения, и пометить его аннотацией.
Для примера у нас будет сущность Person
, бизнес сервис к ней и контроллер. Контроллер имеет один эндпойнт, который возвращает пользователя по логину. Рассмотрим классы нашего приложения:
Сущность Person
:
package dev.struchkov.general.sort;
import java.text.MessageFormat;
public class Person {
private String lastName;
private String firstName;
private Integer age;
//getters and setters
}
Контроллер PersonController
:
package dev.struchkov.example.controlleradvice.controller;
import dev.struchkov.example.controlleradvice.domain.Person;
import dev.struchkov.example.controlleradvice.service.PersonService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.UUID;
@Slf4j
@RestController
@RequestMapping("api/person")
@RequiredArgsConstructor
public class PersonController {
private final PersonService personService;
@GetMapping
public ResponseEntity<Person> getByLogin(@RequestParam("login") String login) {
return ResponseEntity.ok(personService.getByLoginOrThrown(login));
}
@GetMapping("{id}")
public ResponseEntity<Person> getById(@PathVariable("id") UUID id) {
return ResponseEntity.ok(personService.getById(id).orElseThrow());
}
}
И наконец PersonService
, который будет возвращать исключение NotFoundException
, если пользователя не будет в мапе persons
.
package dev.struchkov.example.controlleradvice.service;
import dev.struchkov.example.controlleradvice.domain.Person;
import dev.struchkov.example.controlleradvice.exception.NotFoundException;
import lombok.NonNull;
import org.springframework.stereotype.Service;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;
@Service
public class PersonService {
private final Map<UUID, Person> people = new HashMap<>();
public PersonService() {
final UUID komarId = UUID.randomUUID();
people.put(komarId, new Person(komarId, "komar", "Алексей", "ertyuiop"));
}
public Person getByLoginOrThrown(@NonNull String login) {
return people.values().stream()
.filter(person -> person.getLogin().equals(login))
.findFirst()
.orElseThrow(() -> new NotFoundException("Пользователь не найден"));
}
public Optional<Person> getById(@NonNull UUID id) {
return Optional.ofNullable(people.get(id));
}
}
Перед тем, как проверить работу исключения, давайте посмотрим на успешную работу эндпойнта.
Все отлично. Нам в ответ пришел код 200, а в теле ответа пришел JSON нашей сущности. А теперь мы отправим запрос с логином пользователя, которого у нас нет. Посмотрим, что сделает Spring по умолчанию.
Обратите внимание, ошибка 500 – это стандартный ответ Spring на возникновение любого неизвестного исключения. Также исключение было выведено в консоль.
Как я уже говорил, отличным решением будет сообщить пользователю, что он делает не так. Для этого добавляем метод с аннотацией @ExceptionHandler
, который будет перехватывать исключение и отправлять понятный ответ пользователю.
@RequestMapping("api/person")
@RequiredArgsConstructor
public class PersonController {
private final PersonService personService;
@GetMapping
public ResponseEntity<Person> getByLogin(@RequestParam("login") String login) {
return ResponseEntity.ok(personService.getByLoginOrThrown(login));
}
@ExceptionHandler(NotFoundException.class)
public ResponseEntity<ErrorMessage> handleException(NotFoundException exception) {
return ResponseEntity
.status(HttpStatus.NOT_FOUND)
.body(new ErrorMessage(exception.getMessage()));
}
}
Вызываем повторно наш метод и видим, что мы стали получать понятное описание ошибки.
Но теперь вернулся 200 http код, куда корректнее вернуть 404 код.
Однако некоторые разработчики предпочитают возвращать объект, вместо ResponseEntity<T>
. Тогда вам необходимо воспользоваться аннотацией @ResponseStatus
.
import org.springframework.web.bind.annotation.ResponseStatus;
@ResponseStatus(HttpStatus.NOT_FOUND)
@ExceptionHandler(NotFoundException.class)
public ErrorMessage handleException(NotFoundException exception) {
return new ErrorMessage(exception.getMessage());
}
Если попробовать совместить ResponseEntity<T>
и @ResponseStatus
, http-код будет взят из ResponseEntity<T>
.
Главный недостаток @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) |
Мы можем создать собственный HandlerExceptionResolver
. Назовем его CustomExceptionResolver
и вот как он будет выглядеть:
package dev.struchkov.example.controlleradvice.service;
import dev.struchkov.example.controlleradvice.exception.NotFoundException;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.handler.AbstractHandlerExceptionResolver;
import org.springframework.web.servlet.view.json.MappingJackson2JsonView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@Component
public class CustomExceptionResolver extends AbstractHandlerExceptionResolver {
@Override
protected ModelAndView doResolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception e) {
final ModelAndView modelAndView = new ModelAndView(new MappingJackson2JsonView());
if (e instanceof NotFoundException) {
modelAndView.setStatus(HttpStatus.NOT_FOUND);
modelAndView.addObject("message", "Пользователь не найден");
return modelAndView;
}
modelAndView.setStatus(HttpStatus.INTERNAL_SERVER_ERROR);
modelAndView.addObject("message", "При выполнении запроса произошла ошибка");
return modelAndView;
}
}
Мы создаем объект представления – ModelAndView
, который будет отправлен пользователю, и заполняем его. Для этого проверяем тип исключения, после чего добавляем в представление сообщение о конкретной ошибке и возвращаем представление из метода. Если ошибка имеет какой-то другой тип, который мы не предусмотрели в этом обработчике, то мы отправляем сообщение об ошибке при выполнении запроса.
Так как мы пометили этот класс аннотацией @Component
, Spring сам найдет и внедрит наш резолвер куда нужно. Посмотрим, как Spring хранит эти резолверы в классе DispatcherServlet
.
Все резолверы хранятся в обычном ArrayList
и в случае исключнеия вызываются по порядку, при этом наш резолвер оказался последним. Таким образом, если непосредственно в контроллере окажется @ExceptionHandler
обработчик, то наш кастомный резолвер не будет вызван, так как обработка будет выполнена в ExceptionHandlerExceptionResolver
.
Важное замечание. У меня не получилось перехватить здесь ни одно Spring исключение, например MethodArgumentTypeMismatchException
, которое возникает если передавать неверный тип для аргументов @RequestParam
.
Этот способ был показан больше для образовательных целей, чтобы показать в общих чертах, как работает этот механизм. Не стоит использовать этот способ, так как есть вариант намного удобнее.
@RestControllerAdvice
Исключения возникают в разных сервисах приложения, но удобнее всего обрабатывать все исключения в каком-то одном месте. Именно для этого в SpringBoot предназначены аннотации @ControllerAdvice
и @RestControllerAdvice
. В статье мы рассмотрим @RestControllerAdvice
, так как у нас REST API.
На самом деле все довольно просто. Мы берем методы помеченные аннотацией @ExceptionHandler
, которые у нас были в контроллерах и переносим в отдельный класс аннотированный @RestControllerAdvice
.
package dev.struchkov.example.controlleradvice.controller;
import dev.struchkov.example.controlleradvice.domain.ErrorMessage;
import dev.struchkov.example.controlleradvice.exception.NotFoundException;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;
@RestControllerAdvice
public class ExceptionApiHandler {
@ExceptionHandler(NotFoundException.class)
public ResponseEntity<ErrorMessage> notFoundException(NotFoundException exception) {
return ResponseEntity
.status(HttpStatus.NOT_FOUND)
.body(new ErrorMessage(exception.getMessage()));
}
@ExceptionHandler(MethodArgumentTypeMismatchException.class)
public ResponseEntity<ErrorMessage> mismatchException(MethodArgumentTypeMismatchException exception) {
return ResponseEntity
.status(HttpStatus.NOT_FOUND)
.body(new ErrorMessage(exception.getMessage()));
}
}
За обработку этих методов класса точно также отвечает класс ExceptionHandlerExceptionResolver
. При этом мы можем здесь перехватывать даже стандартные исключения Spring, такие как MethodArgumentTypeMismatchException
.
На мой взгляд, это самый удобный и простой способ обработки возвращаемых пользователю исключений.
Еще про обработку
Все написанное дальше относится к любому способу обработки исключений.
Запись в лог
Важно отметить, что исключения больше не записываются в лог. Если помимо ответа пользователю, вам все же необходимо записать это событие в лог, то необходимо добавить строчку записи в методе обработчике, например вот так:
@ExceptionHandler(NotFoundException.class)
public ResponseEntity<ErrorMessage> handleException(NotFoundException exception) {
log.error(exception.getMessage(), exception);
return ResponseEntity
.status(HttpStatus.NOT_FOUND)
.body(new ErrorMessage(exception.getMessage()));
}
Перекрытие исключений
Вы можете использовать иерархию исключений с наследованием и обработчики исключений для всей своей иерархии. В таком случае обработка исключения будет попадать в самый специализированный обработчик.
Допустим мы бросаем NotFoundException
, как в примере выше, который наследуется от RuntimeException
. И у вас будет два обработчика исключений для NotFoundException
и RuntimeException
. Исключение попадет в обработчик для NotFoundException
. Если этот обработчик убрать, то попадет в обработчик для RuntimeException
.
Резюмирую
Обработка исключений это важная часть REST API. Она позволяет возвращать клиентам информационные сообщения, которые помогут им скорректировать свой запрос.
Мы можем по разному реализовать обработку в зависимости от нашей архитектуры. Предпочитаемым способом считаю вариант с @RestControllerAdvice
. Этот вариант самый чистый и понятный.
Editor’s note: This article was updated on September 5, 2022, by our editorial team. It has been modified to include recent sources and to align with our current editorial standards.
The ability to handle errors correctly in APIs while providing meaningful error messages is a desirable feature, as it can help the API client respond to issues. The default behavior returns stack traces that are hard to understand and ultimately useless for the API client. Partitioning the error information into fields enables the API client to parse it and provide better error messages to the user. In this article, we cover how to implement proper Spring Boot exception handling when building a REST API .
Building REST APIs with Spring became the standard approach for Java developers. Using Spring Boot helps substantially, as it removes a lot of boilerplate code and enables auto-configuration of various components. We assume that you’re familiar with the basics of API development with those technologies. If you are unsure about how to develop a basic REST API, you should start with this article about Spring MVC or this article about building a Spring REST Service.
Making Error Responses Clearer
We’ll use the source code hosted on GitHub as an example application that implements a REST API for retrieving objects that represent birds. It has the features described in this article and a few more examples of error handling scenarios. Here’s a summary of endpoints implemented in that application:
GET /birds/{birdId} |
Gets information about a bird and throws an exception if not found. |
GET /birds/noexception/{birdId} |
This call also gets information about a bird, except it doesn’t throw an exception when a bird doesn’t exist with that ID. |
POST /birds |
Creates a bird. |
The Spring framework MVC module has excellent features for error handling. But it is left to the developer to use those features to treat the exceptions and return meaningful responses to the API client.
Let’s look at an example of the default Spring Boot answer when we issue an HTTP POST to the /birds
endpoint with the following JSON object that has the string “aaa” on the field “mass,” which should be expecting an integer:
{
"scientificName": "Common blackbird",
"specie": "Turdus merula",
"mass": "aaa",
"length": 4
}
The Spring Boot default answer, without proper error handling, looks like this:
{
"timestamp": 1658551020,
"status": 400,
"error": "Bad Request",
"exception": "org.springframework.http.converter.HttpMessageNotReadableException",
"message": "JSON parse error: Unrecognized token 'three': was expecting ('true', 'false' or 'null'); nested exception is com.fasterxml.jackson.core.JsonParseException: Unrecognized token 'aaa': was expecting ('true', 'false' or 'null')n at [Source: java.io.PushbackInputStream@cba7ebc; line: 4, column: 17]",
"path": "/birds"
}
The Spring Boot DefaultErrorAttributes
-generated response has some good fields, but it is too focused on the exception. The timestamp
field is an integer that doesn’t carry information about its measurement unit. The exception
field is only valuable to Java developers, and the message leaves the API consumer lost in implementation details that are irrelevant to them. What if there were more details we could extract from the exception? Let’s learn how to handle exceptions in Spring Boot properly and wrap them into a better JSON representation to make life easier for our API clients.
As we’ll be using Java date and time classes, we first need to add a Maven dependency for the Jackson JSR310 converters. They convert Java date and time classes to JSON representation using the @JsonFormat
annotation:
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
</dependency>
Next, let’s define a class for representing API errors. We’ll create a class called ApiError
with enough fields to hold relevant information about errors during REST calls:
class ApiError {
private HttpStatus status;
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "dd-MM-yyyy hh:mm:ss")
private LocalDateTime timestamp;
private String message;
private String debugMessage;
private List<ApiSubError> subErrors;
private ApiError() {
timestamp = LocalDateTime.now();
}
ApiError(HttpStatus status) {
this();
this.status = status;
}
ApiError(HttpStatus status, Throwable ex) {
this();
this.status = status;
this.message = "Unexpected error";
this.debugMessage = ex.getLocalizedMessage();
}
ApiError(HttpStatus status, String message, Throwable ex) {
this();
this.status = status;
this.message = message;
this.debugMessage = ex.getLocalizedMessage();
}
}
-
The
status
property holds the operation call status, which will be anything from 4xx to signal client errors or 5xx to signal server errors. A typical scenario is an HTTP code 400: BAD_REQUEST when the client, for example, sends an improperly formatted field, like an invalid email address. -
The
timestamp
property holds the date-time instance when the error happened. -
The
message
property holds a user-friendly message about the error. -
The
debugMessage
property holds a system message describing the error in detail. -
The
subErrors
property holds an array of suberrors when there are multiple errors in a single call. An example would be numerous validation errors in which multiple fields have failed. TheApiSubError
class encapsulates this information:
abstract class ApiSubError {
}
@Data
@EqualsAndHashCode(callSuper = false)
@AllArgsConstructor
class ApiValidationError extends ApiSubError {
private String object;
private String field;
private Object rejectedValue;
private String message;
ApiValidationError(String object, String message) {
this.object = object;
this.message = message;
}
}
The ApiValidationError
is a class that extends ApiSubError
and expresses validation problems encountered during the REST call.
Below, you’ll see examples of JSON responses generated after implementing these improvements.
Here is a JSON example returned for a missing entity while calling endpoint GET /birds/2
:
{
"apierror": {
"status": "NOT_FOUND",
"timestamp": "22-07-2022 06:20:19",
"message": "Bird was not found for parameters {id=2}"
}
}
Here is another example of JSON returned when issuing a POST /birds
call with an invalid value for the bird’s mass:
{
"apierror": {
"status": "BAD_REQUEST",
"timestamp": "22-07-2022 06:49:25",
"message": "Validation errors",
"subErrors": [
{
"object": "bird",
"field": "mass",
"rejectedValue": 999999,
"message": "must be less or equal to 104000"
}
]
}
}
Spring Boot Error Handler
Let’s explore some Spring annotations used to handle exceptions.
RestController
is the base annotation for classes that handle REST operations.
ExceptionHandler
is a Spring annotation that provides a mechanism to treat exceptions thrown during execution of handlers (controller operations). This annotation, if used on methods of controller classes, will serve as the entry point for handling exceptions thrown within this controller only.
Altogether, the most common implementation is to use @ExceptionHandler
on methods of @ControllerAdvice
classes so that the Spring Boot exception handling will be applied globally or to a subset of controllers.
ControllerAdvice
is an annotation in Spring and, as the name suggests, is “advice” for multiple controllers. It enables the application of a single ExceptionHandler
to multiple controllers. With this annotation, we can define how to treat such an exception in a single place, and the system will call this handler for thrown exceptions on classes covered by this ControllerAdvice
.
The subset of controllers affected can be defined by using the following selectors on @ControllerAdvice
: annotations()
, basePackageClasses()
, and basePackages()
. ControllerAdvice
is applied globally to all controllers if no selectors are provided
By using @ExceptionHandler
and @ControllerAdvice
, we’ll be able to define a central point for treating exceptions and wrapping them in an ApiError
object with better organization than is possible with the default Spring Boot error-handling mechanism.
Handling Exceptions
Next, we’ll create the class that will handle the exceptions. For simplicity, we call it RestExceptionHandler
, which must extend from Spring Boot’s ResponseEntityExceptionHandler
. We’ll be extending ResponseEntityExceptionHandler
, as it already provides some basic handling of Spring MVC exceptions. We’ll add handlers for new exceptions while improving the existing ones.
Overriding Exceptions Handled in ResponseEntityExceptionHandler
If you take a look at the source code of ResponseEntityExceptionHandler
, you’ll see a lot of methods called handle******()
, like handleHttpMessageNotReadable()
or handleHttpMessageNotWritable()
. Let’s see how can we extend handleHttpMessageNotReadable()
to handle HttpMessageNotReadableException
exceptions. We just have to override the method handleHttpMessageNotReadable()
in our RestExceptionHandler
class:
@Order(Ordered.HIGHEST_PRECEDENCE)
@ControllerAdvice
public class RestExceptionHandler extends ResponseEntityExceptionHandler {
@Override
protected ResponseEntity<Object> handleHttpMessageNotReadable(HttpMessageNotReadableException ex, HttpHeaders headers, HttpStatus status, WebRequest request) {
String error = "Malformed JSON request";
return buildResponseEntity(new ApiError(HttpStatus.BAD_REQUEST, error, ex));
}
private ResponseEntity<Object> buildResponseEntity(ApiError apiError) {
return new ResponseEntity<>(apiError, apiError.getStatus());
}
//other exception handlers below
}
We have declared that in case of a thrownHttpMessageNotReadableException
, the error message will be “Malformed JSON request” and the error will be encapsulated in the ApiError
object. Below, we can see the answer of a REST call with this new method overridden:
{
"apierror": {
"status": "BAD_REQUEST",
"timestamp": "22-07-2022 03:53:39",
"message": "Malformed JSON request",
"debugMessage": "JSON parse error: Unrecognized token 'aaa': was expecting ('true', 'false' or 'null'); nested exception is com.fasterxml.jackson.core.JsonParseException: Unrecognized token 'aaa': was expecting ('true', 'false' or 'null')n at [Source: java.io.PushbackInputStream@7b5e8d8a; line: 4, column: 17]"
}
}
Implementing Custom Exceptions
Next, we’ll create a method that handles an exception not yet declared inside Spring Boot’s ResponseEntityExceptionHandler
.
A common scenario for a Spring application that handles database calls is to provide a method that returns a record by its ID using a repository class. But if we look into the CrudRepository.findOne()
method, we’ll see that it returns null
for an unknown object. If our service calls this method and returns directly to the controller, we’ll get an HTTP code 200 (OK) even if the resource isn’t found. In fact, the proper approach is to return a HTTP code 404 (NOT FOUND) as specified in the HTTP/1.1 spec.
We’ll create a custom exception called EntityNotFoundException
to handle this case. This one is different from javax.persistence.EntityNotFoundException
, as it provides some constructors that ease the object creation, and one may choose to handle the javax.persistence
exception differently.
That said, let’s create an ExceptionHandler
for this newly created EntityNotFoundException
in our RestExceptionHandler
class. Create a method called handleEntityNotFound()
and annotate it with @ExceptionHandler
, passing the class object EntityNotFoundException.class
to it. This declaration signalizes Spring that every time EntityNotFoundException
is thrown, Spring should call this method to handle it.
When annotating a method with @ExceptionHandler
, a wide range of auto-injected parameters like WebRequest
, Locale
, and others may be specified as described here. We’ll provide the exception EntityNotFoundException
as a parameter for this handleEntityNotFound
method:
@Order(Ordered.HIGHEST_PRECEDENCE)
@ControllerAdvice
public class RestExceptionHandler extends ResponseEntityExceptionHandler {
//other exception handlers
@ExceptionHandler(EntityNotFoundException.class)
protected ResponseEntity<Object> handleEntityNotFound(
EntityNotFoundException ex) {
ApiError apiError = new ApiError(NOT_FOUND);
apiError.setMessage(ex.getMessage());
return buildResponseEntity(apiError);
}
}
Great! In the handleEntityNotFound()
method, we set the HTTP status code to NOT_FOUND
and usethe new exception message. Here is what the response for the GET /birds/2
endpoint looks like now:
{
"apierror": {
"status": "NOT_FOUND",
"timestamp": "22-07-2022 04:02:22",
"message": "Bird was not found for parameters {id=2}"
}
}
The Importance of Spring Boot Exception Handling
It is important to control exception handling so we can properly map exceptions to the ApiError
object and inform API clients appropriately. Additionally, we would need to create more handler methods (the ones with @ExceptionHandler) for thrown exceptions within the application code. The GitHub code provides more more examples for other common exceptions like MethodArgumentTypeMismatchException
, ConstraintViolationException
.
Here are some additional resources that helped in the composition of this article:
-
Error Handling for REST With Spring
-
Exception Handling in Spring MVC
Further Reading on the Toptal Engineering Blog:
- Top 10 Most Common Spring Framework Mistakes
- Spring Security with JWT for REST API
- Using Spring Boot for OAuth2 and JWT REST Protection
- Building an MVC Application With Spring Framework: A Beginner’s Tutorial
- Spring Batch Tutorial: Batch Processing Made Easy with Spring
Understanding the basics
-
Why should the API have a uniform error format?
A uniform error format allows an API client to parse error objects. A more complex error could implement the ApiSubError class and provide more details about the problem so the client can know which actions to take.
-
How does Spring know which ExceptionHandler to use?
The Spring MVC class, ExceptionHandlerExceptionResolver, performs most of the work in its doResolveHandlerMethodException() method.
-
What information is important to provide to API consumers?
Usually, it is helpful to include the error origination, the input parameters, and some guidance on how to fix the failing call.
Everyone of us spend ample amount of time in learning big topics of Spring & Spring Boot. For example, Spring Boot REST, Spring Boot MVC, Spring Boot Security and many more. But generally we don’t think about ‘How to handle Exceptions & Errors in Spring Boot?’.
This topic might become the most important for running the application without any interference. Also, it is very helpful in making other developers understand our code flow easily. Even finding out the origin of errors & exceptions becomes very irritating, if we have not handled them properly in our code. Sometimes we have to debug the whole flow of code to find it out & resolve accordingly. In this way Exception handling plays an important role in our software development engagement.
In this topic ‘How to handle Exceptions & Errors in Spring Boot?’ we will learn handling of Errors & Exceptions step by step covering all aspects. However, Spring Boot has made our job very easy by handling most common exception at the framework level internally. Even we can observe the fascination of Spring Boot while learning & implementing the exceptions. In my opinion, every developer should go through this topic and subsequently apply the concepts learned in the real project. Let’s discuss ‘How to handle Exceptions & Errors in Spring Boot?’ accordingly.
What Can You Expect from This Article as a Whole?
Once you complete going through this article, you will be able to answer :
1) What are the importance & benefits of implementing Exception Handling in a Spring Boot Application?
2) How does Spring Boot’s inbuilt Exception/Error handling work internally?
3) Further, How does predefined BasicErrorController work in different scenarios?
4) How to display a meaningful custom error/exception pages?
5) Additionally, What is an Http Response Status Code?
6) Also, What are some most commonly used status codes?
7) How to create a specific error page with respect to a Http Response Status Code?
How to create a custom exception and map it to a specific status code page?
9) Consequently, How to add custom error attributes in a custom Error Controller?
10) How does a predefined ErrorController handles exception raised by a REST call?
11) How can we customize error status code & error details attribute?
12) In the end, How can we customize all error attributes?
13) Use of annotations @ControllerAdvice, @RestControllerAdvice, @ExceptionHandler, @ResponseStatus, @ResponseBody etc.
14) Last but not the Least, ‘How to handle Exceptions & Errors in Spring Boot?’.
How does Spring Boot application handle errors/exceptions internally via inbuilt functionality?
To illustrate the inbuilt exception handling in a Spring Boot Project, we will consider the most commonly used flows which are Spring Boot MVC and Spring Boot REST. Both flows work based on a Controller, either it is a normal controller or a RestController. Also in both the cases, any request first interacts with DispatcherServlet. Further, any request interacts with the DispatcherServlet before sending the response to the client, whether it is returning a successful result or failure after raising any exception. If it returns any failure result, DispatcherServlet calls a predefined functional interface ErrorController. In this case BasicErrorController (an implementation class of interface ErrorController) handles the request. BasicErrorController has below two methods to handle such type of requests.
♦ errorHtml() – This method is called when the request comes from a browser (MVC call)
♦ error() – This method is called when the request comes from non-browser medium such as postman tool/client app/other app (REST call)
BasicErrorController shows error/exception because of below default path at @RequestMapping annotation.
♦ @RequestMapping(“${server.error.path:${error.path:/error}}”)
Finally a default whitelabel Error page with some status code appears on the screen in case of MVC call. Similarly, if it is REST call, you will receive an error message as a JSON response in the form of default error attributes like status, error, message, timestamp etc. Further If we want to display a meaningful custom page on the screen, then what will we do? Check it in the next section.
How to display meaningful Custom error/exception pages?
Instead of getting Whitelabel Error Page, we can just create ‘error.html’ page under src/main/resources/templates folder as below.
error.html
<!DOCTYPE html>
<html xmlns:th="https://www.thymeleaf.org/">
<head>
<meta charset="ISO-8859-1">
<title>Insert title here</title>
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css" >
</head>
<body>
<div class="container">
<h3>SOMTHING WENT WRONG! PLZ CONTACT MAINTENANCE TEAM</h3>
<table class="table table-bordered">
<tr>
<td>DATE</td>
<td th:text="${timestamp}"></td>
</tr> <tr>
<td>status</td>
<td th:text="${status}"></td>
</tr>
<tr>
<td>error</td>
<td th:text="${error}"></td>
</tr>
<tr>
<td>path</td>
<td th:text="${path}"></td>
</tr>
<tr>
<td>Trace</td>
<td th:text="${trace}"></td>
</tr>
</table>
</div>
</body>
</html>
On creating our custom error.html page we can get a meaningful error information separated by default error attributes.
error.html will handle all the errors with status 4xx & 5xx. For example 400, 401 , 403 , 404 , .. 500 , 501 etc. only error.html page will be executed.
What is Http Response Status code?
In a client-server request-response model Http Response Status code is a three digit code which is provided by a server to the client. This Status code indicates whether the requested action is successful or any error occurred during the processing. For example, when a search engine or website visitor makes a request to a web server, a three digit HTTP Response Status Code is returned. This code indicates what is about to happen. A response code of 200 means “OK” which indicates that the request has succeeded. Similarly, other status codes have some meaning.
Generally Http Status Codes come under below five categories.
Informational responses (100–199) Successful responses (200–299) Redirects (300–399) Client errors (400–499) : denotes Errors in Java Server errors (500–599) : denotes Exceptions in Java
The below status codes are defined by section 10 of RFC 2616. You can find an updated specification in RFC 7231.
♥ If you receive a response that is not in this list, it is a non-standard response, possibly custom to the server’s software. If error status code is of type 4xx or 5xx, aforementioned error.html will handle that request.
What are some commonly used Http Response Status codes?
OK : 200
CREATED : 201
NO_CONTENT : 204
MULTIPLE_CHOICES : 300
NOT_MODIFIED : 304
PERMANENT_REDIRECT : 308
BAD_REQUEST : 400
UNAUTHORIZED : 401
PAYMENT_REQUIRED : 402
FORBIDDEN : 403
NOT_FOUND : 404
METHOD_NOT_ALLOWED : 405
PROXY_AUTHENTICATION_REQUIRED : 407
REQUEST_TIMEOUT : 408
CONFLICT : 409
URI_TOO_LONG : 414
UNSUPPORTED_MEDIA_TYPE : 415
TOO_MANY_REQUESTS : 429
REQUEST_HEADER_FIELDS_TOO_LARGE : 431
INTERNAL_SERVER_ERROR : 500
NOT_IMPLEMENTED : 501
BAD_GATEWAY : 502
SERVICE_UNAVAILABLE : 503
GATEWAY_TIMEOUT : 504
HTTP_VERSION_NOT_SUPPORTED : 505
NETWORK_AUTHENTICATION_REQUIRED : 511
How to create a specific error page for a particular Http Response Status code?
Aforementioned error.html will handle all the request irrespective of a particular Http Response Status Code. It works as a generic error page. Suppose we want to display a separate error page in case of status code-404. Additionally, in case of status code-500 we want to display another distinct error page. Then how will we create these pages?
Step#1: Create a sub-folder ‘error’ under ‘src/main/resources/templates‘ folder.
Step#2: Create an html page with the same name as error status code in ‘StatusCode.html’ format. For example, for status code 404 create ‘404.html’ and place it under ‘error’ sub-folder.
Your folder structure should look like below screen.
If no page with specific status code found, it will display error.html by default. If the controller is throwing any exception, then by default Http Status code will be 500 which is ‘INTERNAL SERVER ERROR’. To change error code to any other status code, apply @ResponseStatus(code= HttpStatus.StatusCodeName) on top of your custom exception.
Even we can create a single page for a error status code 400-499 as ‘4xx.html’. Similarly, for 500-599 as ‘5xx.html’.
How to create a custom exception and map it to an error page with specific Http Response Status code?
To illustrate this requirement, let’s assume that we have an Invoice Processing application. Further in this application we have one controller with a getInvoice() method to serve the request as given below. We will take some random integers in if condition. Therefore this method will sometimes provide successful response. But sometimes it will also throw an exception. We will call it InvoiceNotFoundException and create it as a custom exception. Additionally we want a custom 404.html display this exception.
InvoiceController.java
package com.dev.spring.exception.controller;import java.util.Random;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import com.dev.spring.exception.custom.ImplementationNotFoundException;
import com.dev.spring.exception.custom.InvoiceNotFoundException;
@Controller
public class InvoiceController {
@GetMapping("/getInvoice")
public String getInvoice() {
if(new Random().nextInt(10)>5)
throw new InvoiceNotFoundException("Invoice Not Found!");
return "showInvoice";
}
}
Creating Custom Exception
Create a class InvoiceNotFoundException which will extend java.lang.RuntimeException. Then apply @ResponseStatus(code= HttpStatus.NOT_FOUND) on top of it as below.
InvoiceNotFoundException.java
package com.dev.spring.exception.custom;import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;
@ResponseStatus(code= HttpStatus.NOT_FOUND)
public class InvoiceNotFoundException extends RuntimeException {
private static final long serialVersionUID = 1L;
public InvoiceNotFoundException() {
super();
}
public InvoiceNotFoundException(String message) {
super(message);
}
}
Here httpStatus.NOT_FOUND indicates error status code 404. Therefore we will create a custom 404.html page so that it can be called when InvoiceNotFoundException occurs.
Creating Custom Error Page (404.html)
Create 404.html as below and place it inside ‘src/main/resources/templates/error’ folder as aforementioned.
404.html
<!DOCTYPE html>
<html xmlns:th="https://www.thymeleaf.org/">
<head>
<meta charset="ISO-8859-1">
<title>Insert title here</title>
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css" >
</head>
<body>
<div class="container">
<h4>SOME RESOURCE NOT FOUND! PLEASE CONTACT MAINTENANCE TEAM</h4>
<table class="table table-bordered">
<tr>
<td>DATE</td>
<td th:text="${timestamp}"></td>
</tr> <tr>
<td>STATUS</td>
<td th:text="${status}"></td>
</tr>
<tr>
<td>ERROR</td>
<td th:text="${error}"></td>
</tr>
<tr>
<td>PATH</td>
<td th:text="${path}"></td>
</tr>
</table>
</div>
</body>
</html>
Whenever exception occurs, 404.html will be called and it will display the error description as in below meaningful format.
How to write custom Error Controller in Spring Boot?
Instead of utilizing the ErrorController functionalities already provided by Spring Boot, we can implement our own Error Controller. Since already provided ErrorController.class is an interface, we can create an implementer class of it to make it possible. If we define implementation class of ErrorController(I) then Spring Boot selects our custom class on priority to show up the errors/exceptions. Further in this case error.hml, 4xx.html, 5xx.html or any other error page will not work at all. Spring Boot by default provides some error attributes like timestamp, status, message, path, exception, trace etc. We can use them in our custom controller via @Autowired. Now to read values of attributes for current request, use below lines of code.
ServletWebRequest swr = new ServletWebRequest(request);
Map<String, Object> errors= errorAttribues.getErrorAttributes(swr,true);
Custom Error Controller to Get output in HTML format
To illustrate a custom error controller which will display the error output in HTML format, below is the code.
CustomErrorController.java
package com.dev.spring.exception.controller;import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.web.servlet.error.ErrorAttributes;
import org.springframework.boot.web.servlet.error.ErrorController;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.context.request.ServletWebRequest;
@Controller
public class CustomErrorController implements ErrorController {
@Autowired
private ErrorAttributes errorAttributes;
@Override
public String getErrorPath() {
return "/error"; //mandatory path
}
@RequestMapping("/error") //mandatory mapping
public @ResponseBody String handleError(HttpServletRequest req) {
ServletWebRequest servletWebRequest = new ServletWebRequest(req);
@SuppressWarnings("deprecation")
Map<String, Object> errors = errorAttributes.getErrorAttributes(servletWebRequest, true);
StringBuilder builder = new StringBuilder();
builder.append("<html><body>");
builder.append("<h2>ERROR SUMMARY</h2>");
builder.append("<table border='1.5'>");
errors.forEach((key, value) -> {
builder.append("<tr>").append("<td>").append(key).append("</td>").append("<td>").append(value).append("</td>")
.append("</tr>");
});
builder.append("</table>");
builder.append("</body></html>");
return builder.toString();
}
}
Output
Below is the Output format.
Custom Error Controller to Get output in JSON format
To illustrate a custom error controller which will display the error output in JSON format, below is the code.
♥ Remember that Default output of @ResponseBody in Spring Boot is either String or JSON format. Any non-string return type in method will provide output in JSON format. Therefore, make sure that your controller method return type is a non-string (Lis/Set/Map)
Also apply @ResponseBody over method.
CustomErrorControllerWithJSONResponse.java
package com.dev.spring.exception.controller;import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.web.servlet.error.ErrorAttributes;
import org.springframework.boot.web.servlet.error.ErrorController;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.context.request.ServletWebRequest;
@Controller
public class CustomErrorControllerWithJSONResponse implements ErrorController {
@Autowired
private ErrorAttributes errorAttributes;
@RequestMapping("/error")
public @ResponseBody Map<String, Object> handleError(HttpServletRequest req)
{
ServletWebRequest webRequest = new ServletWebRequest(req);
@SuppressWarnings("deprecation")
Map<String, Object> errors = errorAttributes.getErrorAttributes(webRequest, true);
return errors;
}
@Override
public String getErrorPath() {
// TODO Auto-generated method stub
return null;
}
}
Output
Below is the Output format.
♥ To see JSON output use Firefox, JSON View extension with Google Chrome or even Postman tool.
How to add Custom Error Attributes in Custom Error Controller ?
Sometimes we may get a requirement to display some additional attribute with the error details. In that case we can just add the value of attribute in key-value format in the Map itself as shown below. Here we are only providing method code. Subsequently rest part of Custom Controller code will be as it is as in previous code example.
Adding Custom Error Attribute to Custom Error Controller
@RequestMapping("/error")
public @ResponseBody Map<String, Object> handleError(HttpServletRequest req)
{
ServletWebRequest webRequest = new ServletWebRequest(req);
@SuppressWarnings("deprecation")
Map<String, Object> errors = errorAttributes.getErrorAttributes(webRequest, true);
errors.put("Error Output Format", "JSON");
return errors;
}
Output
Below is the output.
How does predefined ErrorController handles exception raised by a REST call by default ?
To illustrate this, let’s create a Spring Boot Starter project which will implement a simple REST call step by step. Consider an Invoice Processing use case where we will have a RestController as ‘InvoiceRestController’ with a method as getInvoice(). In addition we will have a model class as ‘Invoice’ and a Custom exception class as ‘InvoiceNotFoundException’.
Step#1 : Create a Spring Boot Starter project in STS(Spring Tool Suite)
While creating Starter Project select ‘Spring Web’, ‘Lombok’ and ‘Spring Boot DevTools’ as starter project dependencies. Even If you don’t know how to create Spring Boot Starter Project, Kindly visit Internal Link. Also, if you want to know more about Lombok, then visit a separate article on ‘Lombok‘.
Step#2 : Create Model class as Invoice.java
Invoice.java
package com.dev.spring.exception.entity;import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Invoice {
private Integer id;
private String name;
private Double amount;
private String number;
}
Step#3 : Create Controller class as InvoiceRestController.java
Here we are intentionally throwing a InvoiceNotFoundException if value of invoice id is 24.
InvoiceRestController.java
package com.dev.spring.exception.controller;import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
import com.dev.spring.exception.custom.InvoiceNotFoundException;
import com.dev.spring.exception.entity.Invoice;
@RestController
public class InvoiceRestController {
@GetMapping("/find/{id}")
public ResponseEntity<Invoice> getInvoice(@PathVariable Integer id){
if(id ==24) {
throw new InvoiceNotFoundException("Invoice with id: " +id +" does not exist!");
}
return ResponseEntity.ok(new Invoice(id,"INV001",2378.75,"CRTQW224"));
}
}
Step#4 : Create Custom Exception class as InvoiceNotFoundException.java
InvoiceNotFoundException.java
package com.dev.spring.exception.custom;public class InvoiceNotFoundException extends RuntimeException {
private static final long serialVersionUID = 1L;
public InvoiceNotFoundException() {
super();
}
public InvoiceNotFoundException(String message) {
super(message);
}
}
Testing the Exception
Enter URL ‘http://localhost:8080/find/24‘ using Postman tool, select ‘GET’ method and then click on ‘Send’ button. Consequently you will see output something like below. Here you can choose any browser of your choice to hit the URL.
{ "timestamp": "2020-12-29T19:32:29.056+00:00", "status": 500, "error": "Internal Server Error", "trace": "com.dev.spring.exception.custom.InvoiceNotFoundException: Invoice with id: 24 does not exist!rntat com.dev.spring.exception.controller.InvoiceRestController.getInvoice(InvoiceRestController.java:18)rntat java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)rntat java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)rntat java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)rntat java.base/java.lang.reflect.Method.invoke(Method.java:564)rntat org.springframework.web.method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:190)rntat org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:138)rntat org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:105)rntat org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:878)rntat org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:792)rntat org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:87)rntat org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1040)rntat org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:943)rntat org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1006)rntat org.springframework.web.servlet.FrameworkServlet.doGet(FrameworkServlet.java:898)rntat javax.servlet.http.HttpServlet.service(HttpServlet.java:626)rntat org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:883)rntat javax.servlet.http.HttpServlet.service(HttpServlet.java:733)rntat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:231)rntat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)rntat org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:53)rntat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)rntat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)rntat org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:100)rntat org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119)rntat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)rntat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)rntat org.springframework.web.filter.FormContentFilter.doFilterInternal(FormContentFilter.java:93)rntat org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119)rntat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)rntat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)rntat org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:201)rntat org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119)rntat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)rntat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)rntat org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:202)rntat org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:97)rntat org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:542)rntat org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:143)rntat org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:92)rntat org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:78)rntat org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:343)rntat org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:374)rntat org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:65)rntat org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:888)rntat org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1597)rntat org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49)rntat java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1130)rntat java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:630)rntat org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)rntat java.base/java.lang.Thread.run(Thread.java:832)rn", "message": "Invoice with id: 24 does not exist!", "path": "/find/24" }
Conclusion
Although from the above output it is clear that by default Spring Boot’s predefined ErrorController provides Status code as ‘505’ and error as ‘Internal Server Error’ on any type of exception. Further we can customize the status code & error as per our wish.
How can we customize error status code & error details attribute?
Further to customize error status code & error we can just apply annotation @ResponseStatus(code= HttpStatus.StatusName) on top of custom controller itself as below.
InvoiceNotFoundException.java
package com.dev.spring.exception.custom;import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;
@ResponseStatus(code= HttpStatus.NOT_FOUND)
public class InvoiceNotFoundException extends RuntimeException {
private static final long serialVersionUID = 1L;
public InvoiceNotFoundException() {
super();
}
public InvoiceNotFoundException(String message) {
super(message);
}
}
Customized Output
{ "timestamp": "2020-12-29T20:17:21.207+00:00", "status": 404, "error": "Not Found", "trace": "com.dev.spring.exception.custom.InvoiceNotFoundException: Invoice with id: 24 does not exist!rntat com.dev.spring.exception.controller.InvoiceRestController.getInvoice(InvoiceRestController.java:18)rntat java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)rntat java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)rntat java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)rntat java.base/java.lang.reflect.Method.invoke(Method.java:564)rntat org.springframework.web.method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:190)rntat org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:138)rntat org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:105)rntat org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:878)rntat org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:792)rntat org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:87)rntat org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1040)rntat org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:943)rntat org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1006)rntat org.springframework.web.servlet.FrameworkServlet.doGet(FrameworkServlet.java:898)rntat javax.servlet.http.HttpServlet.service(HttpServlet.java:626)rntat org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:883)rntat javax.servlet.http.HttpServlet.service(HttpServlet.java:733)rntat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:231)rntat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)rntat org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:53)rntat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)rntat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)rntat org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:100)rntat org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119)rntat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)rntat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)rntat org.springframework.web.filter.FormContentFilter.doFilterInternal(FormContentFilter.java:93)rntat org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119)rntat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)rntat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)rntat org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:201)rntat org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119)rntat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)rntat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)rntat org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:202)rntat org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:97)rntat org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:542)rntat org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:143)rntat org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:92)rntat org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:78)rntat org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:343)rntat org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:374)rntat org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:65)rntat org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:888)rntat org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1597)rntat org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49)rntat java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1130)rntat java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:630)rntat org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)rntat java.base/java.lang.Thread.run(Thread.java:832)rn", "message": "Invoice with id: 24 does not exist!", "path": "/find/24" }
How can we customize all error attributes ?
To achieve this we will create one new Exception Handler class by using below annotations accordingly.
@ControllerAdvice : At Handler class level
@ExceptionHandler : At method level
@ResponseBody : At method level
♦ Although we can optionally use @RestControllerAdvice at class level. Further in that case we don’t need to apply @ResponseBody at method level. In order to get more details on these annotations, kindly visit Annotations on Spring Boot Errors & Exceptions.
Step#1 : Write a new model class as ‘ErrorType.java’
In order to customize all attributes, we will create a model class as ‘ErrorType.java’ and define all desired attributes as the fields. We will also need object of this class in our handler class.
ErrorType.java
package com.dev.spring.exception.entity;import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class ErrorType {
private String message;
private String code;
private String error;
private String staus;
}
Step#2 : Write a new handler class as ‘InvoiceExceptionhandler.java’
We have to write a handler class and apply aforementioned annotations as below.
InvoiceExceptionHandler.java
package com.dev.spring.exception.handler;import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
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.RestControllerAdvice;
import com.dev.spring.exception.custom.InvoiceNotFoundException;
import com.dev.spring.exception.entity.ErrorType;
//@RestControllerAdvice
@ControllerAdvice
public class InvoiceExceptionHandler {
@ExceptionHandler(InvoiceNotFoundException.class)
@ResponseBody
public ResponseEntity<ErrorType> handleInvoiceNotFoundException(
InvoiceNotFoundException ine){
return new ResponseEntity<ErrorType>(
new ErrorType(
ine.getMessage(),
"INVOICE_NOT_FOUND",
"DATA NOT FOUND FOR REQUESTED ID",
"406"),
HttpStatus.NOT_ACCEPTABLE);
}
}
Step#3 : Write a new custom exception class and Rest Controller
You can use InvoiceNotFoundException.java from previous section. Additionally, make sure that @ResponseStatus is not applied on top of it. Similarly, use InvoiceRestController from the previous section itself.
Testing the customized error attributes
Enter URL ‘http://localhost:8080/find/24‘ using Postman tool, select ‘GET’ method and then click on ‘Send’ button. Consequently you will see output something like below. Here you can choose any browser of your choice to hit the URL.
Summary
After going through all the theoretical & examples part of ‘How to handle Exceptions & Errors in Spring Boot?’, finally, we are able to implement Exceptions/Errors handling in a Spring Boot project. Of course, In this article we have thoroughly learned about the Spring Boot Exception handling. Similarly, we expect from you to further extend these examples and implement them in your project accordingly. You can also check other details on Exception Handling in Spring MVC from spring.io. In addition, If there is any update in future, we will also update the article accordingly. Moreover, Feel free to provide your comments in comments section.