В части 1 мы рассмотрели варианты обработки исключений, выбрасываемых в контроллере.
Самый гибкий из них — @ControllerAdvice — он позволяет изменить как код, так и тело стандартного ответа при ошибке. Кроме того, он позволяет в одном методе обработать сразу несколько исключений — они перечисляются над методом.
В первой части мы создавали @ControllerAdvice с нуля, но в Spring Boot существует заготовка — ResponseEntityExceptionHandler, которую можно расширить. В ней уже обработаны многие исключения, например: NoHandlerFoundException, HttpMessageNotReadableException, MethodArgumentNotValidException и другие (всего десяток-другой исключений).
Приложение
Обрабатывать исключения будем в простом Spring Boot приложении из первой части. Оно предоставляет REST API для сущности Person:
@Entity @NoArgsConstructor @AllArgsConstructor @Data public class Person { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Size(min = 3, max = 10) private String name; }
Только в этот раз поле name аннотировано javax.validation.constraints.Size.
А также перед аргументом Person в методах контроллера стоит аннотация @Valid:
@RestController @RequestMapping("/persons") public class PersonController { @Autowired private PersonRepository personRepository; @GetMapping public List<Person> listAllPersons() { List<Person> persons = personRepository.findAll(); return persons; } @GetMapping(value = "/{personId}") public Person getPerson(@PathVariable("personId") long personId) { return personRepository.findById(personId).orElseThrow(() -> new MyEntityNotFoundException(personId)); } @PostMapping public Person createPerson(@RequestBody @Valid Person person) { return personRepository.save(person); } @PutMapping("/{id}") public Person updatePerson(@RequestBody @Valid Person person, @PathVariable long id) { Person oldPerson = personRepository.getOne(id); oldPerson.setName(person.getName()); return personRepository.save(oldPerson); } }
Аннотация @Valid заставляет Spring проверять валидность полей объекта Person, например условие @Size(min = 3, max = 10). Если пришедший в контроллер объект не соответствует условиям, то будет выброшено MethodArgumentNotValidException — то самое, для которого в ResponseEntityExceptionHandler уже задан обработчик. Правда, он выдает пустое тело ответа. Вообще все обработчики из ResponseEntityExceptionHandler выдают корректный код ответа, но пустое тело.
Мы это исправим. Поскольку для MethodArgumentNotValidException может возникнуть несколько ошибок (по одной для каждого поля сущности Person), добавим в наше пользовательское тело ответа список List с ошибками. Он предназначен именно для MethodArgumentNotValidException (не для других исключений).
Итак, ApiError по сравнению с 1-ой частью теперь содержит еще список errors:
@Data @AllArgsConstructor @NoArgsConstructor public class ApiError { private String message; private String debugMessage; @JsonInclude(JsonInclude.Include.NON_NULL) private List<String> errors; public ApiError(String message, String debugMessage){ this.message=message; this.debugMessage=debugMessage; } }
Благодаря аннотации @JsonInclude(JsonInclude.Include.NON_NULL) этот список будет включен в ответ только в том случае, если мы его зададим. Иначе ответ будет содержать только message и debugMessage, как в первой части.
Класс обработки исключений
Например, на исключение MyEntityNotFoundException ответ не поменяется, обработчик такой же, как в первой части:
@ControllerAdvice public class RestExceptionHandler extends ResponseEntityExceptionHandler { ... @ExceptionHandler({MyEntityNotFoundException.class, EntityNotFoundException.class}) protected ResponseEntity<Object> handleEntityNotFoundEx(MyEntityNotFoundException ex, WebRequest request) { ApiError apiError = new ApiError("Entity Not Found Exception", ex.getMessage()); return new ResponseEntity<>(apiError, HttpStatus.NOT_FOUND); } ... }
Но в отличие от 1 части, теперь RestExceptionHandler расширяет ResponseEntityExceptionHandler. А значит, он наследует различные обработчики исключений, и мы их можем переопределить. Сейчас они все возвращают пустое тело ответа, хотя и корректный код.
HttpMessageNotReadableException
Переопределим обработчик, отвечающий за HttpMessageNotReadableException. Это исключение возникает тогда, когда тело запроса, приходящего в метод контроллер, нечитаемое — например, некорректный JSON.
За это исключение отвечает метод handleHttpMessageNotReadable(), его и переопределим:
@Override protected ResponseEntity<Object> handleHttpMessageNotReadable(HttpMessageNotReadableException ex, HttpHeaders headers, HttpStatus status, WebRequest request) { ApiError apiError = new ApiError("Malformed JSON Request", ex.getMessage()); return new ResponseEntity(apiError, status); }
Проверим ответ, сделав запрос с некорректным JSON-телом запроса (он пойдет в метод updatePerson() контроллера):
PUT localhost:8080/persons/1 { 11"name": "alice" }
Получаем ответ с кодом 400 (Bad Request) и телом:
{ "message": "Malformed JSON Request", "debugMessage": "JSON parse error: Unexpected character ('1' (code 49)): was expecting double-quote to start field name; nested exception is com.fasterxml.jackson.core.JsonParseException: Unexpected character ('1' (code 49)): was expecting double-quote to start field namen at [Source: (PushbackInputStream); line: 2, column: 5]" }
Теперь ответ содержит не только корректный код, но и тело с информативными сообщениями. Если бы мы не переопределяли обработчик, вернулся бы только код 400.
А если бы не расширяли класс ResponseEntityExceptionHandler, все эти обработчики в принципе не были бы задействованы и вернулся бы стандартный ответ из BasicErrorController:
{ "timestamp": "2021-03-01T16:53:04.197+00:00", "status": 400, "error": "Bad Request", "message": "JSON parse error: Unexpected character ('1' (code 49)): was expecting double-quote to start field name; nested exception is com.fasterxml.jackson.core.JsonParseException: Unexpected character ('1' (code 49)): was expecting double-quote to start field namen at [Source: (PushbackInputStream); line: 2, column: 5]", "path": "/persons/1" }
MethodArgumentNotValidException
Как говорилось выше, чтобы выбросилось это исключение, в контроллер должен прийти некорректный Person. В смысле корректный JSON, но условие @Valid чтоб не выполнялось: например, поле name имело бы неверную длину (а она должна быть от 3 до 10, как указано в аннотации @Size).
Попробуем сделать запрос с коротким name:
POST http://localhost:8080/persons { "name": "al" }
Получим ответ:
{ "message": "Method Argument Not Valid", "debugMessage": "Validation failed for argument [0] in public ru.sysout.model.Person ru.sysout.controller.PersonController.createPerson(ru.sysout.model.Person): [Field error in object 'person' on field 'name': rejected value [al]; codes [Size.person.name,Size.name,Size.java.lang.String,Size]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [person.name,name]; arguments []; default message [name],10,3]; default message [размер должен находиться в диапазоне от 3 до 10]] ", "errors": [ "размер должен находиться в диапазоне от 3 до 10" ] }
Тут пошел в ход список ошибок, который мы добавили в ApiError. Мы его заполняем в переопределенном обработчике исключения:
@Override protected ResponseEntity<Object> handleMethodArgumentNotValid(MethodArgumentNotValidException ex, HttpHeaders headers, HttpStatus status, WebRequest request) { List<String> errors = ex.getBindingResult() .getFieldErrors() .stream() .map(x -> x.getDefaultMessage()) .collect(Collectors.toList()); ApiError apiError = new ApiError("Method Argument Not Valid", ex.getMessage(), errors); return new ResponseEntity<>(apiError, status); }
Вообще говоря, стандартный ответ, выдаваемый BasicErrorController, тоже будет содержать этот список ошибок по полям, если в application.properties включить свойство:
server.error.include-binding-errors=always
В этом случае (при отсутствии нашего RestExceptionHandler с @ControlleAdvice) ответ будет таким:
{ "timestamp": "2021-03-01T17:15:37.134+00:00", "status": 400, "error": "Bad Request", "message": "Validation failed for object='person'. Error count: 1", "errors": [ { "codes": [ "Size.person.name", "Size.name", "Size.java.lang.String", "Size" ], "arguments": [ { "codes": [ "person.name", "name" ], "arguments": null, "defaultMessage": "name", "code": "name" }, 10, 3 ], "defaultMessage": "размер должен находиться в диапазоне от 3 до 10", "objectName": "person", "field": "name", "rejectedValue": "al", "bindingFailure": false, "code": "Size" } ], "path": "/persons/" }
Мы просто сократили информацию.
MethodArgumentTypeMismatchException
Полезно знать еще исключение MethodArgumentTypeMismatchException, оно возникает, если тип аргумента неверный. Например, наш метод контроллера получает Person по id:
@GetMapping(value = "/{personId}") public Person getPerson(@PathVariable("personId") Long personId) throws EntityNotFoundException { return personRepository.getOne(personId); }
А мы передаем не целое, а строковое значение id:
GET http://localhost:8080/persons/mn
Тут то и возникает исключение MethodArgumentTypeMismatchException. Давайте его обработаем:
@ExceptionHandler(MethodArgumentTypeMismatchException.class) protected ResponseEntity<Object> handleMethodArgumentTypeMismatch(MethodArgumentTypeMismatchException ex,HttpStatus status, WebRequest request) { ApiError apiError = new ApiError(); apiError.setMessage(String.format("The parameter '%s' of value '%s' could not be converted to type '%s'", ex.getName(), ex.getValue(), ex.getRequiredType().getSimpleName())); apiError.setDebugMessage(ex.getMessage()); return new ResponseEntity<>(apiError, status); }
Проверим ответ сервера (код ответа будет 400):
{ "message": "The parameter 'personId' of value 'mn' could not be converted to type 'long'", "debugMessage": "Failed to convert value of type 'java.lang.String' to required type 'long'; nested exception is java.lang.NumberFormatException: For input string: "mn"" }
NoHandlerFoundException
Еще одно полезное исключение — NoHandlerFoundException. Оно возникает, если на данный запрос не найдено обработчика.
Например, сделаем запрос:
GET http://localhost:8080/pers
По данному адресу у нас нет контроллера, так что возникнет NoHandlerFoundException. Добавим обработку исключения:
@Override protected ResponseEntity<Object> handleNoHandlerFoundException(NoHandlerFoundException ex, HttpHeaders headers, HttpStatus status, WebRequest request) { return new ResponseEntity<Object>(new ApiError("No Handler Found", ex.getMessage()), status); }
Только учтите, для того, чтобы исключение выбрасывалось, надо задать свойства в файле application.properties:
spring.mvc.throw-exception-if-no-handler-found=true spring.web.resources.add-mappings=false
Проверим ответ сервера (код ответа 404):
{ "message": "No Handler Found", "debugMessage": "No handler found for GET /pers" }
Если же не выбрасывать NoHandlerFoundException и не пользоваться нашим обработчиком, то ответ от BasicErrorController довольно непонятный, хотя код тоже 404:
{ "timestamp": "2021-03-01T17:35:59.204+00:00", "status": 404, "error": "Not Found", "message": "No message available", "path": "/pers" }
Обработчик по умолчанию
Этот обработчик будет ловить исключения, не пойманные предыдущими обработчиками:
@ExceptionHandler(Exception.class) protected ResponseEntity<Object> handleAllExceptions(Exception ex, WebRequest request) { ApiError apiError = new ApiError(HttpStatus.INTERNAL_SERVER_ERROR, "prosto exception", ex); return new ResponseEntity<>(apiError, HttpStatus.INTERNAL_SERVER_ERROR); }
Заключение
Мы рассмотрели:
- как сделать обработку исключений в едином классе, аннотированном @ControllerAdvice;
- как переопределить формат JSON-ответа, выдаваемого при возникновении исключения;
- как воспользоваться классом-заготовкой ResponseEntityExceptionHandler и переопределить его обработчики так, чтобы тело ответов не было пустым;
Обратите внимание, что все не переопределенные методы ResponseEntityExceptionHandler будут выдавать пустое тело ответа.
Код примера доступен на GitHub.
NOTE: Revised April 2018
Spring MVC provides several complimentary approaches to exception handling but, when teaching Spring MVC, I often find that my students are confused or not comfortable with them.
Today I’m going to show you the various options available. Our goal is to not handle exceptions explicitly in Controller methods where possible. They are a cross-cutting concern better handled separately in dedicated code.
There are three options: per exception, per controller or globally.
A demonstration application that shows the points discussed here can be found at
http://github.com/paulc4/mvc-exceptions. See Sample Application below for details.
NOTE: The demo applications has been revamped and updated (April 2018) to use Spring Boot 2.0.1 and is (hopefully) easier to use and understand. I also fixed some broken links (thanks for the feedback, sorry it took a while).
Spring Boot
Spring Boot allows a Spring project to be setup with minimal configuration and it is likely that you are using it if your application is less than a few years old.
Spring MVC offers no default (fall-back) error page out-of-the-box. The most common way to set a default error page has always been the SimpleMappingExceptionResolver
(since Spring V1 in fact). We will discuss this later.
However Spring Boot does provide for a fallback error-handling page.
At start-up, Spring Boot tries to find a mapping for /error
. By convention, a URL ending in /error
maps to a logical view of the same name: error
. In the demo application this view maps in turn to the error.html
Thymeleaf template. (If using JSP, it would map to error.jsp
according to the setup of your InternalResourceViewResolver
). The actual mapping will depend on what ViewResolver
(if any) that you or Spring Boot has setup.
If no view-resolver mapping for /error
can be found, Spring Boot defines its own fall-back error page — the so-called “Whitelabel Error Page” (a minimal page with just the HTTP status information and any error details, such as the message from an uncaught exception). In the sample applicaiton, if you rename the error.html
template to, say, error2.html
then restart, you will see it being used.
If you are making a RESTful request (the HTTP request has specified a desired response type other than HTML) Spring Boot returns a JSON representation of the same error information that it puts in the “Whitelabel” error page.
$> curl -H "Accept: application/json" http://localhost:8080/no-such-page
{"timestamp":"2018-04-11T05:56:03.845+0000","status":404,"error":"Not Found","message":"No message available","path":"/no-such-page"}
Spring Boot also sets up a default error-page for the container, equivalent to the<error-page>
directive in web.xml
(although implemented very differently). Exceptions thrown outside the Spring MVC framework, such as from a servlet Filter, are still reported by the Spring Boot fallback error page. The sample application also shows an example of this.
A more in-depth discussion of Spring Boot error-handling can be found at the end of this article.
The rest of this article applies regardless of whether you are using Spring with or without Spring Boot.
Impatient REST developers may choose to skip directly to the section on custom REST error responses. However they should then read the full article as most of it applies equally to all web applications, REST or otherwise.
Using HTTP Status Codes
Normally any unhandled exception thrown when processing a web-request causes the server to return an HTTP 500 response. However, any exception that you write yourself can be annotated with the @ResponseStatus
annotation (which supports all the HTTP status codes defined by the HTTP specification). When an annotated exception is thrown from a controller method, and not handled elsewhere, it will automatically cause the appropriate HTTP response to be returned with the specified status-code.
For example, here is an exception for a missing order.
@ResponseStatus(value=HttpStatus.NOT_FOUND, reason="No such Order") // 404
public class OrderNotFoundException extends RuntimeException {
// ...
}
And here is a controller method using it:
@RequestMapping(value="/orders/{id}", method=GET)
public String showOrder(@PathVariable("id") long id, Model model) {
Order order = orderRepository.findOrderById(id);
if (order == null) throw new OrderNotFoundException(id);
model.addAttribute(order);
return "orderDetail";
}
A familiar HTTP 404 response will be returned if the URL handled by this method includes an unknown order id.
Controller Based Exception Handling
Using @ExceptionHandler
You can add extra (@ExceptionHandler
) methods to any controller to specifically handle exceptions thrown by request handling (@RequestMapping
) methods in the same controller. Such methods can:
- Handle exceptions without the
@ResponseStatus
annotation (typically predefined exceptions that you didn’t write) - Redirect the user to a dedicated error view
- Build a totally custom error response
The following controller demonstrates these three options:
@Controller
public class ExceptionHandlingController {
// @RequestHandler methods
...
// Exception handling methods
// Convert a predefined exception to an HTTP Status code
@ResponseStatus(value=HttpStatus.CONFLICT,
reason="Data integrity violation") // 409
@ExceptionHandler(DataIntegrityViolationException.class)
public void conflict() {
// Nothing to do
}
// Specify name of a specific view that will be used to display the error:
@ExceptionHandler({SQLException.class,DataAccessException.class})
public String databaseError() {
// Nothing to do. Returns the logical view name of an error page, passed
// to the view-resolver(s) in usual way.
// Note that the exception is NOT available to this view (it is not added
// to the model) but see "Extending ExceptionHandlerExceptionResolver"
// below.
return "databaseError";
}
// Total control - setup a model and return the view name yourself. Or
// consider subclassing ExceptionHandlerExceptionResolver (see below).
@ExceptionHandler(Exception.class)
public ModelAndView handleError(HttpServletRequest req, Exception ex) {
logger.error("Request: " + req.getRequestURL() + " raised " + ex);
ModelAndView mav = new ModelAndView();
mav.addObject("exception", ex);
mav.addObject("url", req.getRequestURL());
mav.setViewName("error");
return mav;
}
}
In any of these methods you might choose to do additional processing — the most common example is to log the exception.
Handler methods have flexible signatures so you can pass in obvious servlet-related objects such as HttpServletRequest
, HttpServletResponse
, HttpSession
and/or Principle
.
Important Note: The Model
may not be a parameter of any @ExceptionHandler
method. Instead, setup a model inside the method using a ModelAndView
as shown by handleError()
above.
Exceptions and Views
Be careful when adding exceptions to the model. Your users do not want to see web-pages containing Java exception details and stack-traces. You may have security policies that expressly forbid putting any exception information in the error page. Another reason to make sure you override the Spring Boot white-label error page.
Make sure exceptions are logged usefully so they can be analyzed after the event by your support and development teams.
Please remember the following may be convenient but it is not best practice in production.
It can be useful to hide exception details in the page source as a comment, to assist testing. If using JSP, you could do something like this to output the exception and the corresponding stack-trace (using a hidden <div>
is another option).
<h1>Error Page</h1>
<p>Application has encountered an error. Please contact support on ...</p>
<!--
Failed URL: ${url}
Exception: ${exception.message}
<c:forEach items="${exception.stackTrace}" var="ste"> ${ste}
</c:forEach>
-->
For the Thymeleaf equivalent see support.html in the demo application. The result looks like this.
Global Exception Handling
Using @ControllerAdvice Classes
A controller advice allows you to use exactly the same exception handling techniques but apply them across the whole application, not just to an individual controller. You can think of them as an annotation driven interceptor.
Any class annotated with @ControllerAdvice
becomes a controller-advice and three types of method are supported:
- Exception handling methods annotated with
@ExceptionHandler
. - Model enhancement methods (for adding additional data to the model) annotated with
@ModelAttribute
. Note that these attributes are not available to the exception handling views. - Binder initialization methods (used for configuring form-handling) annotated with
@InitBinder
.
We are only going to look at exception handling — search the online manual for more on @ControllerAdvice
methods.
Any of the exception handlers you saw above can be defined on a controller-advice class — but now they apply to exceptions thrown from any controller. Here is a simple example:
@ControllerAdvice
class GlobalControllerExceptionHandler {
@ResponseStatus(HttpStatus.CONFLICT) // 409
@ExceptionHandler(DataIntegrityViolationException.class)
public void handleConflict() {
// Nothing to do
}
}
If you want to have a default handler for any exception, there is a slight wrinkle. You need to ensure annotated exceptions are handled by the framework. The code looks like this:
@ControllerAdvice
class GlobalDefaultExceptionHandler {
public static final String DEFAULT_ERROR_VIEW = "error";
@ExceptionHandler(value = Exception.class)
public ModelAndView
defaultErrorHandler(HttpServletRequest req, Exception e) throws Exception {
// If the exception is annotated with @ResponseStatus rethrow it and let
// the framework handle it - like the OrderNotFoundException example
// at the start of this post.
// AnnotationUtils is a Spring Framework utility class.
if (AnnotationUtils.findAnnotation
(e.getClass(), ResponseStatus.class) != null)
throw e;
// Otherwise setup and send the user to a default error-view.
ModelAndView mav = new ModelAndView();
mav.addObject("exception", e);
mav.addObject("url", req.getRequestURL());
mav.setViewName(DEFAULT_ERROR_VIEW);
return mav;
}
}
Going Deeper
HandlerExceptionResolver
Any Spring bean declared in the DispatcherServlet
’s application context that implements HandlerExceptionResolver
will be used to intercept and process any exception raised in the MVC system and not handled by a Controller. The interface looks like this:
public interface HandlerExceptionResolver {
ModelAndView resolveException(HttpServletRequest request,
HttpServletResponse response, Object handler, Exception ex);
}
The handler
refers to the controller that generated the exception (remember that @Controller
instances are only one type of handler supported by Spring MVC. For example: HttpInvokerExporter
and the WebFlow Executor are also types of handler).
Behind the scenes, MVC creates three such resolvers by default. It is these resolvers that implement the behaviours discussed above:
ExceptionHandlerExceptionResolver
matches uncaught exceptions against suitable@ExceptionHandler
methods on both the handler (controller) and on any controller-advices.ResponseStatusExceptionResolver
looks for uncaught exceptions annotated by@ResponseStatus
(as described in Section 1)DefaultHandlerExceptionResolver
converts standard Spring exceptions and converts them to HTTP Status Codes (I have not mentioned this above as it is internal to Spring MVC).
These are chained and processed in the order listed — internally Spring creates a dedicated bean (the HandlerExceptionResolverComposite
) to do this.
Notice that the method signature of resolveException
does not include the Model
. This is why @ExceptionHandler
methods cannot be injected with the model.
You can, if you wish, implement your own HandlerExceptionResolver
to setup your own custom exception handling system. Handlers typically implement Spring’s Ordered
interface so you can define the order that the handlers run in.
SimpleMappingExceptionResolver
Spring has long provided a simple but convenient implementation of HandlerExceptionResolver
that you may well find being used in your appication already — the SimpleMappingExceptionResolver
. It provides options to:
- Map exception class names to view names — just specify the classname, no package needed.
- Specify a default (fallback) error page for any exception not handled anywhere else
- Log a message (this is not enabled by default).
- Set the name of the
exception
attribute to add to the Model so it can be used inside a View
(such as a JSP). By default this attribute is namedexception
. Set tonull
to disable. Remember that views returned from@ExceptionHandler
methods do not have access to the exception but views defined toSimpleMappingExceptionResolver
do.
Here is a typical configuration using Java Configuration:
@Configuration
@EnableWebMvc // Optionally setup Spring MVC defaults (if you aren't using
// Spring Boot & haven't specified @EnableWebMvc elsewhere)
public class MvcConfiguration extends WebMvcConfigurerAdapter {
@Bean(name="simpleMappingExceptionResolver")
public SimpleMappingExceptionResolver
createSimpleMappingExceptionResolver() {
SimpleMappingExceptionResolver r =
new SimpleMappingExceptionResolver();
Properties mappings = new Properties();
mappings.setProperty("DatabaseException", "databaseError");
mappings.setProperty("InvalidCreditCardException", "creditCardError");
r.setExceptionMappings(mappings); // None by default
r.setDefaultErrorView("error"); // No default
r.setExceptionAttribute("ex"); // Default is "exception"
r.setWarnLogCategory("example.MvcLogger"); // No default
return r;
}
...
}
Or using XML Configuration:
<bean id="simpleMappingExceptionResolver" class=
"org.springframework.web.servlet.handler.SimpleMappingExceptionResolver">
<property name="exceptionMappings">
<map>
<entry key="DatabaseException" value="databaseError"/>
<entry key="InvalidCreditCardException" value="creditCardError"/>
</map>
</property>
<!-- See note below on how this interacts with Spring Boot -->
<property name="defaultErrorView" value="error"/>
<property name="exceptionAttribute" value="ex"/>
<!-- Name of logger to use to log exceptions. Unset by default,
so logging is disabled unless you set a value. -->
<property name="warnLogCategory" value="example.MvcLogger"/>
</bean>
The defaultErrorView property is especially useful as it ensures any uncaught exception generates a suitable application defined error page. (The default for most application servers is to display a Java stack-trace — something your users should never see). Spring Boot provides another way to do the same thing with its “white-label” error page.
Extending SimpleMappingExceptionResolver
It is quite common to extend SimpleMappingExceptionResolver
for several reasons:
- You can use the constructor to set properties directly — for example to enable exception logging and set the logger to use
- Override the default log message by overriding
buildLogMessage
. The default implementation always returns this fixed text:- Handler execution resulted in exception
- To make additional information available to the error view by overriding
doResolveException
For example:
public class MyMappingExceptionResolver extends SimpleMappingExceptionResolver {
public MyMappingExceptionResolver() {
// Enable logging by providing the name of the logger to use
setWarnLogCategory(MyMappingExceptionResolver.class.getName());
}
@Override
public String buildLogMessage(Exception e, HttpServletRequest req) {
return "MVC exception: " + e.getLocalizedMessage();
}
@Override
protected ModelAndView doResolveException(HttpServletRequest req,
HttpServletResponse resp, Object handler, Exception ex) {
// Call super method to get the ModelAndView
ModelAndView mav = super.doResolveException(req, resp, handler, ex);
// Make the full URL available to the view - note ModelAndView uses
// addObject() but Model uses addAttribute(). They work the same.
mav.addObject("url", request.getRequestURL());
return mav;
}
}
This code is in the demo application as ExampleSimpleMappingExceptionResolver
Extending ExceptionHandlerExceptionResolver
It is also possible to extend ExceptionHandlerExceptionResolver
and override itsdoResolveHandlerMethodException
method in the same way. It has almost the same signature (it just takes the new HandlerMethod
instead of a Handler
).
To make sure it gets used, also set the inherited order property (for example in the constructor of your new class) to a value less than MAX_INT
so it runs before the default ExceptionHandlerExceptionResolver instance (it is easier to create your own handler instance than try to modify/replace the one created by Spring). See ExampleExceptionHandlerExceptionResolver in the demo app for more.
Errors and REST
RESTful GET requests may also generate exceptions and we have already seen how we can return standard HTTP Error response codes. However, what if you want to return information about the error? This is very easy to do. Firstly define an error class:
public class ErrorInfo {
public final String url;
public final String ex;
public ErrorInfo(String url, Exception ex) {
this.url = url;
this.ex = ex.getLocalizedMessage();
}
}
Now we can return an instance from a handler as the @ResponseBody
like this:
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(MyBadDataException.class)
@ResponseBody ErrorInfo
handleBadRequest(HttpServletRequest req, Exception ex) {
return new ErrorInfo(req.getRequestURL(), ex);
}
What to Use When?
As usual, Spring likes to offer you choice, so what should you do? Here are some rules of thumb. However if you have a preference for XML configuration or Annotations, that’s fine too.
- For exceptions you write, consider adding
@ResponseStatus
to them. - For all other exceptions implement an
@ExceptionHandler
method on a@ControllerAdvice
class or use an instance ofSimpleMappingExceptionResolver
. You may well haveSimpleMappingExceptionResolver
configured for your application already, in which case it may be easier to add new exception classes to it than implement a@ControllerAdvice
. - For Controller specific exception handling add
@ExceptionHandler
methods to your controller. - Warning: Be careful mixing too many of these options in the same application. If the same exception can be handed in more than one way, you may not get the behavior you wanted.
@ExceptionHandler
methods on the Controller are always selected before those on any@ControllerAdvice
instance. It is undefined what order controller-advices are processed.
Sample Application
A demonstration application can be found at github. It uses Spring Boot and Thymeleaf to build a simple web application.
The application has been revised twice (Oct 2014, April 2018) and is (hopefully) better and easier to understand. The fundamentals stay the same. It uses Spring Boot V2.0.1 and Spring V5.0.5 but the code is applicable to Spring 3.x and 4.x also.
The demo is running on Cloud Foundry at http://mvc-exceptions-v2.cfapps.io/.
About the Demo
The application leads the user through 5 demo pages, highlighting different exception handling techniques:
- A controller with
@ExceptionHandler
methods to handle its own exceptions - A contoller that throws exceptions for a global ControllerAdvice to handle
- Using a
SimpleMappingExceptionResolver
to handle exceptions - Same as demo 3 but with the
SimpleMappingExceptionResolver
disabled for comparison - Shows how Spring Boot generates its error page
A description of the most important files in the application and how they relate to each demo can be found in the project’s README.md.
The home web-page is index.html which:
- Links to each demo page
- Links (bottom of the page) to Spring Boot endpoints for those interested in Spring Boot.
Each demo page contains several links, all of which deliberately raise exceptions. You will need to use the back-button on your browser each time to return to the demo page.
Thanks to Spring Boot, you can run this demo as a Java application (it runs an embedded Tomcat container). To run the application, you can use one of the following (the second is thanks to the Spring Boot maven plugin):
mvn exec:java
mvn spring-boot:run
Your choice. The home page URL will be http://localhost:8080.
Error Page Contents
Also in the demo application I show how to create a “support-ready” error page with a stack-trace hidden in the HTML source (as a comment). Ideally support should get this information from the logs, but life isn’t always ideal. Regardless, what this page does show is how the underlying error-handling method handleError
creates its own ModelAndView
to provide extra information in the error page. See:
ExceptionHandlingController.handleError()
on githubGlobalControllerExceptionHandler.handleError()
on github
Spring Boot and Error Handling
Spring Boot allows a Spring project to be setup with minimal configuration. Spring Boot creates sensible defaults automatically when it detects certain key classes and packages on the classpath. For example if it sees that you are using a Servlet environment, it sets up Spring MVC with the most commonly used view-resolvers, hander mappings and so forth. If it sees JSP and/or Thymeleaf, it sets up these view-technologies.
Fallback Error Page
How does Spring Boot support the default error-handling described at the beginning of this article?
- In the event of any unhanded error, Spring Boot forwards internally to
/error
. - Boot sets up a
BasicErrorController
to handle any request to/error
. The controller adds error information to the internal Model and returnserror
as the logical view name. - If any view-resolver(s) are configured, they will try to use a corresponding error-view.
- Otherwise, a default error page is provided using a dedicated
View
object (making it independent of any view-resolution system you may be using). - Spring Boot sets up a
BeanNameViewResolver
so that/error
can be mapped to aView
of the same name. - If you look in Boot’s
ErrorMvcAutoConfiguration
class you will see that thedefaultErrorView
is returned as a bean callederror
. This is the View bean found by theBeanNameViewResolver
.
The “Whitelabel” error page is deliberately minimal and ugly. You can override it:
- By defining an error template — in our demo we are using Thymeleaf so the error template is in
src/main/resources/templates/error.html
(this location is set by the Spring Boot propertyspring.thymeleaf.prefix
— similar properties exist for other supported server-side view technologies such as JSP or Mustache). - If you aren’t using server-side rendering
2.1 Define your own error View as a bean callederror
.
2.1 Or disable Spring boot’s “Whitelabel” error page by setting the propertyserver.error.whitelabel.enabled
tofalse
. Your container’s default error page is used instead.
By convention, Spring Boot properties are normally set in application.properties
or application.yml
.
Integration with SimpleMappingExceptionResolver
What if you are already using SimpleMappingExceptionResolver
to setup a default
error view? Simple, use setDefaultErrorView()
to define the same view that Spring Boot uses: error
.
Note that in the demo, the defaultErrorView
property of the SimpleMappingExceptionResolver
is deliberately set not to error
but to defaultErrorPage
so you can see when the handler is generating the error page and when Spring Boot is responsible. Normally both would be set to error
.
Container-Wide Exception Handling
Exceptions thrown outside the Spring Framework, such as from a servlet Filter, are also reported by Spring Boot’s fallback error page.
To do this Spring Boot has to register a default error page for the container. In Servlet 2, there is an <error-page>
directive that you can add to your web.xml
to do this. Sadly Servlet 3 does not offer a Java API equivalent. Instead Spring Boot does the following:
- For a Jar application, with an embedded container, it registers a default error page using Container specific API.
- For a Spring Boot application deployed as a traditional WAR file, a Servlet Filter is used to
catch exceptions raised further down the line and handle it.
Во время работы вашего приложения часто будут возникать исключительные ситуации. Когда у вас простое консольное приложение, то все просто – ошибка выводится в консоль. Но как быть с веб-приложением?
Допустим у пользователя отсутсвует доступ, или он передал некорректные данные. Лучшим вариантом будет в ответ на такие ситуации, отправлять пользователю сообщения с описанием ошибки. Это позволит клиенту вашего 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.
Spring Boot is built on the top of the spring and contains all the features of spring. And is becoming a favorite of developers these days because of its rapid production-ready environment which enables the developers to directly focus on the logic instead of struggling with the configuration and setup. Spring Boot is a microservice-based framework and making a production-ready application in it takes very little time. Exception Handling in Spring Boot helps to deal with errors and exceptions present in APIs so as to deliver a robust enterprise application. This article covers various ways in which exceptions can be handled in a Spring Boot Project. Let’s do the initial setup to explore each approach in more depth.
Initial Setup
In order to create a simple spring boot project using Spring Initializer, please refer to this article. Now let’s develop a Spring Boot Restful Webservice that performs CRUD operations on Customer Entity. We will be using MYSQL database for storing all necessary data.
Step 1: Creating a JPA Entity class Customer with three fields id, name, and address.
Java
package
com.customer.model;
import
javax.persistence.Entity;
import
javax.persistence.GeneratedValue;
import
javax.persistence.GenerationType;
import
javax.persistence.Id;
import
lombok.AllArgsConstructor;
import
lombok.Data;
import
lombok.NoArgsConstructor;
@Entity
@Data
@AllArgsConstructor
@NoArgsConstructor
public
class
Customer {
@Id
@GeneratedValue
(strategy = GenerationType.IDENTITY)
private
Long id;
private
String name;
private
String address;
}
The Customer class is annotated with @Entity annotation and defines getters, setters, and constructors for the fields.
Step 2: Creating a CustomerRepository Interface
Java
package
com.customer.repository;
import
com.customer.model.Customer;
import
org.springframework.data.jpa.repository.JpaRepository;
import
org.springframework.stereotype.Repository;
@Repository
public
interface
CustomerRepository
extends
JpaRepository<Customer, Long> {
}
The CustomerRepository interface is annotated with @Repository annotation and extends the JpaRepository of Spring Data JPA.
Step 3: Creating Custom made Exceptions that can be thrown during necessary scenarios while performing CRUD.
CustomerAlreadyExistsException: This exception can be thrown when the user tries to add a customer that already exists in the database.
Java
package
com.customer.exception;
public
class
CustomerAlreadyExistsException
extends
RuntimeException {
private
String message;
public
CustomerAlreadyExistsException() {}
public
CustomerAlreadyExistsException(String msg)
{
super
(msg);
this
.message = msg;
}
}
NoSuchCustomerExistsException: This exception can be thrown when the user tries to delete or update a customer record that doesn’t exist in the database.
Java
package
com.customer.exception;
public
class
NoSuchCustomerExistsException
extends
RuntimeException {
private
String message;
public
NoSuchCustomerExistsException() {}
public
NoSuchCustomerExistsException(String msg)
{
super
(msg);
this
.message = msg;
}
}
Note: Both Custom Exception classes extend RuntimeException.
Step 4: Creating interface CustomerService and implementing class CustomerServiceImpl of service layer.
The CustomerService interface defines three different methods:
- Customer getCustomer(Long id): To get a customer record by its id. This method throws a NoSuchElementException exception when it doesn’t find a customer record with the given id.
- String addCustomer(Customer customer): To add details of a new Customer to the database. This method throws a CustomerAlreadyExistsException exception when the user tries to add a customer that already exists.
- String updateCustomer(Customer customer): To update details of Already existing Customers. This method throws a NoSuchCustomerExistsException exception when the user tries to update details of a customer that doesn’t exist in the database.
The Interface and service implementation class is as follows:
Java
package
com.customer.service;
import
com.customer.model.Customer;
public
interface
CustomerService {
Customer getCustomer(Long id);
String addCustomer(Customer customer);
String updateCustomer(Customer customer);
}
Java
package
com.customer.service;
import
com.customer.exception.CustomerAlreadyExistsException;
import
com.customer.exception.NoSuchCustomerExistsException;
import
com.customer.model.Customer;
import
com.customer.repository.CustomerRepository;
import
java.util.NoSuchElementException;
import
org.springframework.beans.factory.annotation.Autowired;
import
org.springframework.stereotype.Service;
@Service
public
class
CustomerServiceImpl
implements
CustomerService {
@Autowired
private
CustomerRepository customerRespository;
public
Customer getCustomer(Long id)
{
return
customerRespository.findById(id).orElseThrow(
()
->
new
NoSuchElementException(
"NO CUSTOMER PRESENT WITH ID = "
+ id));
}
public
String addCustomer(Customer customer)
{
Customer existingCustomer
= customerRespository.findById(customer.getId())
.orElse(
null
);
if
(existingCustomer ==
null
) {
customerRespository.save(customer);
return
"Customer added successfully"
;
}
else
throw
new
CustomerAlreadyExistsException(
"Customer already exists!!"
);
}
public
String updateCustomer(Customer customer)
{
Customer existingCustomer
= customerRespository.findById(customer.getId())
.orElse(
null
);
if
(existingCustomer ==
null
)
throw
new
NoSuchCustomerExistsException(
"No Such Customer exists!!"
);
else
{
existingCustomer.setName(customer.getName());
existingCustomer.setAddress(
customer.getAddress());
customerRespository.save(existingCustomer);
return
"Record updated Successfully"
;
}
}
}
Step 5: Creating Rest Controller CustomerController which defines various APIs.
Java
package
com.customer.controller;
import
com.customer.exception.CustomerAlreadyExistsException;
import
com.customer.exception.ErrorResponse;
import
com.customer.model.Customer;
import
com.customer.service.CustomerService;
import
org.springframework.beans.factory.annotation.Autowired;
import
org.springframework.http.HttpStatus;
import
org.springframework.web.bind.annotation.ExceptionHandler;
import
org.springframework.web.bind.annotation.GetMapping;
import
org.springframework.web.bind.annotation.PathVariable;
import
org.springframework.web.bind.annotation.PostMapping;
import
org.springframework.web.bind.annotation.PutMapping;
import
org.springframework.web.bind.annotation.RequestBody;
import
org.springframework.web.bind.annotation.RequestMapping;
import
org.springframework.web.bind.annotation.ResponseStatus;
import
org.springframework.web.bind.annotation.RestController;
@RestController
public
class
CustomerController {
@Autowired
private
CustomerService customerService;
@GetMapping
(
"/getCustomer/{id}"
)
public
Customer getCustomer(
@PathVariable
(
"id"
) Long id)
{
return
customerService.getCustomer(id);
}
@PostMapping
(
"/addCustomer"
)
public
String
addcustomer(
@RequestBody
Customer customer)
{
return
customerService.addCustomer(customer);
}
@PutMapping
(
"/updateCustomer"
)
public
String
updateCustomer(
@RequestBody
Customer customer)
{
return
customerService.updateCustomer(customer);
}
}
Now let’s go through the various ways in which we can handle the Exceptions thrown in this project.
Default Exception Handling by Spring Boot:
The getCustomer() method defined by CustomerController is used to get a customer with a given Id. It throws a NoSuchElementException when it doesn’t find a Customer record with the given id. On Running the Spring Boot Application and hitting the /getCustomer API with an Invalid Customer Id, we get a NoSuchElementException completely handled by Spring Boot as follows:
Spring Boot provides a systematic error response to the user with information such as timestamp, HTTP status code, error, message, and the path.
Using Spring Boot @ExceptionHandler Annotation:
@ExceptionHandler annotation provided by Spring Boot can be used to handle exceptions in particular Handler classes or Handler methods. Any method annotated with this is automatically recognized by Spring Configuration as an Exception Handler Method. An Exception Handler method handles all exceptions and their subclasses passed in the argument. It can also be configured to return a specific error response to the user. So let’s create a custom ErrorResponse class so that the exception is conveyed to the user in a clear and concise way as follows:
Java
package
com.customer.exception;
import
lombok.AllArgsConstructor;
import
lombok.Data;
import
lombok.NoArgsConstructor;
@Data
@AllArgsConstructor
@NoArgsConstructor
public
class
ErrorResponse {
private
int
statusCode;
private
String message;
public
ErrorResponse(String message)
{
super
();
this
.message = message;
}
}
The addCustomer() method defined by CustomerController throws a CustomerAlreadyExistsException when the user tries to add a Customer that already exists in the database else it saves the customer details.
To handle this exception let’s define a handler method handleCustomerAlreadyExistsException() in the CustomerController.So now when addCustomer() throws a CustomerAlreadyExistsException, the handler method gets invoked which returns a proper ErrorResponse to the user.
Java
@ExceptionHandler
(value
= CustomerAlreadyExistsException.
class
)
@ResponseStatus
(HttpStatus.CONFLICT)
public
ErrorResponse
handleCustomerAlreadyExistsException(
CustomerAlreadyExistsException ex)
{
return
new
ErrorResponse(HttpStatus.CONFLICT.value(),
ex.getMessage());
}
Note: Spring Boot allows to annotate a method with @ResponseStatus to return the required Http Status Code.
On Running the Spring Boot Application and hitting the /addCustomer API with an existing Customer, CustomerAlreadyExistsException gets completely handled by handler method as follows:
Using @ControllerAdvice for Global Exception Handler:
In the previous approach, we can see that the @ExceptionHandler annotated method can only handle the exceptions thrown by that particular class. However, if we want to handle any exception thrown throughout the application we can define a global exception handler class and annotate it with @ControllerAdvice.This annotation helps to integrate multiple exception handlers into a single global unit.
The updateCustomer() method defined in CustomerController throws a NoSuchCustomerExistsException exception if the user tries to update details of a customer that doesn’t already exist in the database else it successfully saves the updated details for that particular customer.
To handle this exception, let’s define a GlobalExceptionHandler class annotated with @ControllerAdvice. This class defines the ExceptionHandler method for NoSuchCustomerExistsException exception as follows.
Java
package
com.customer.exception;
import
org.springframework.http.HttpStatus;
import
org.springframework.web.bind.annotation.ControllerAdvice;
import
org.springframework.web.bind.annotation.ExceptionHandler;
import
org.springframework.web.bind.annotation.ResponseBody;
import
org.springframework.web.bind.annotation.ResponseStatus;
@ControllerAdvice
public
class
GlobalExceptionHandler {
@ExceptionHandler
(value
= NoSuchCustomerExistsException.
class
)
@ResponseStatus
(HttpStatus.BAD_REQUEST)
public
@ResponseBody
ErrorResponse
handleException(NoSuchCustomerExistsException ex)
{
return
new
ErrorResponse(
HttpStatus.NOT_FOUND.value(), ex.getMessage());
}
}
On Running the Spring Boot Application and hitting the /updateCustomer API with invalid Customer details, NoSuchCustomerExistsException gets thrown which is completely handled by the handler method defined in GlobalExceptionHandler class as follows:
Spring Boot is built on the top of the spring and contains all the features of spring. And is becoming a favorite of developers these days because of its rapid production-ready environment which enables the developers to directly focus on the logic instead of struggling with the configuration and setup. Spring Boot is a microservice-based framework and making a production-ready application in it takes very little time. Exception Handling in Spring Boot helps to deal with errors and exceptions present in APIs so as to deliver a robust enterprise application. This article covers various ways in which exceptions can be handled in a Spring Boot Project. Let’s do the initial setup to explore each approach in more depth.
Initial Setup
In order to create a simple spring boot project using Spring Initializer, please refer to this article. Now let’s develop a Spring Boot Restful Webservice that performs CRUD operations on Customer Entity. We will be using MYSQL database for storing all necessary data.
Step 1: Creating a JPA Entity class Customer with three fields id, name, and address.
Java
package
com.customer.model;
import
javax.persistence.Entity;
import
javax.persistence.GeneratedValue;
import
javax.persistence.GenerationType;
import
javax.persistence.Id;
import
lombok.AllArgsConstructor;
import
lombok.Data;
import
lombok.NoArgsConstructor;
@Entity
@Data
@AllArgsConstructor
@NoArgsConstructor
public
class
Customer {
@Id
@GeneratedValue
(strategy = GenerationType.IDENTITY)
private
Long id;
private
String name;
private
String address;
}
The Customer class is annotated with @Entity annotation and defines getters, setters, and constructors for the fields.
Step 2: Creating a CustomerRepository Interface
Java
package
com.customer.repository;
import
com.customer.model.Customer;
import
org.springframework.data.jpa.repository.JpaRepository;
import
org.springframework.stereotype.Repository;
@Repository
public
interface
CustomerRepository
extends
JpaRepository<Customer, Long> {
}
The CustomerRepository interface is annotated with @Repository annotation and extends the JpaRepository of Spring Data JPA.
Step 3: Creating Custom made Exceptions that can be thrown during necessary scenarios while performing CRUD.
CustomerAlreadyExistsException: This exception can be thrown when the user tries to add a customer that already exists in the database.
Java
package
com.customer.exception;
public
class
CustomerAlreadyExistsException
extends
RuntimeException {
private
String message;
public
CustomerAlreadyExistsException() {}
public
CustomerAlreadyExistsException(String msg)
{
super
(msg);
this
.message = msg;
}
}
NoSuchCustomerExistsException: This exception can be thrown when the user tries to delete or update a customer record that doesn’t exist in the database.
Java
package
com.customer.exception;
public
class
NoSuchCustomerExistsException
extends
RuntimeException {
private
String message;
public
NoSuchCustomerExistsException() {}
public
NoSuchCustomerExistsException(String msg)
{
super
(msg);
this
.message = msg;
}
}
Note: Both Custom Exception classes extend RuntimeException.
Step 4: Creating interface CustomerService and implementing class CustomerServiceImpl of service layer.
The CustomerService interface defines three different methods:
- Customer getCustomer(Long id): To get a customer record by its id. This method throws a NoSuchElementException exception when it doesn’t find a customer record with the given id.
- String addCustomer(Customer customer): To add details of a new Customer to the database. This method throws a CustomerAlreadyExistsException exception when the user tries to add a customer that already exists.
- String updateCustomer(Customer customer): To update details of Already existing Customers. This method throws a NoSuchCustomerExistsException exception when the user tries to update details of a customer that doesn’t exist in the database.
The Interface and service implementation class is as follows:
Java
package
com.customer.service;
import
com.customer.model.Customer;
public
interface
CustomerService {
Customer getCustomer(Long id);
String addCustomer(Customer customer);
String updateCustomer(Customer customer);
}
Java
package
com.customer.service;
import
com.customer.exception.CustomerAlreadyExistsException;
import
com.customer.exception.NoSuchCustomerExistsException;
import
com.customer.model.Customer;
import
com.customer.repository.CustomerRepository;
import
java.util.NoSuchElementException;
import
org.springframework.beans.factory.annotation.Autowired;
import
org.springframework.stereotype.Service;
@Service
public
class
CustomerServiceImpl
implements
CustomerService {
@Autowired
private
CustomerRepository customerRespository;
public
Customer getCustomer(Long id)
{
return
customerRespository.findById(id).orElseThrow(
()
->
new
NoSuchElementException(
"NO CUSTOMER PRESENT WITH ID = "
+ id));
}
public
String addCustomer(Customer customer)
{
Customer existingCustomer
= customerRespository.findById(customer.getId())
.orElse(
null
);
if
(existingCustomer ==
null
) {
customerRespository.save(customer);
return
"Customer added successfully"
;
}
else
throw
new
CustomerAlreadyExistsException(
"Customer already exists!!"
);
}
public
String updateCustomer(Customer customer)
{
Customer existingCustomer
= customerRespository.findById(customer.getId())
.orElse(
null
);
if
(existingCustomer ==
null
)
throw
new
NoSuchCustomerExistsException(
"No Such Customer exists!!"
);
else
{
existingCustomer.setName(customer.getName());
existingCustomer.setAddress(
customer.getAddress());
customerRespository.save(existingCustomer);
return
"Record updated Successfully"
;
}
}
}
Step 5: Creating Rest Controller CustomerController which defines various APIs.
Java
package
com.customer.controller;
import
com.customer.exception.CustomerAlreadyExistsException;
import
com.customer.exception.ErrorResponse;
import
com.customer.model.Customer;
import
com.customer.service.CustomerService;
import
org.springframework.beans.factory.annotation.Autowired;
import
org.springframework.http.HttpStatus;
import
org.springframework.web.bind.annotation.ExceptionHandler;
import
org.springframework.web.bind.annotation.GetMapping;
import
org.springframework.web.bind.annotation.PathVariable;
import
org.springframework.web.bind.annotation.PostMapping;
import
org.springframework.web.bind.annotation.PutMapping;
import
org.springframework.web.bind.annotation.RequestBody;
import
org.springframework.web.bind.annotation.RequestMapping;
import
org.springframework.web.bind.annotation.ResponseStatus;
import
org.springframework.web.bind.annotation.RestController;
@RestController
public
class
CustomerController {
@Autowired
private
CustomerService customerService;
@GetMapping
(
"/getCustomer/{id}"
)
public
Customer getCustomer(
@PathVariable
(
"id"
) Long id)
{
return
customerService.getCustomer(id);
}
@PostMapping
(
"/addCustomer"
)
public
String
addcustomer(
@RequestBody
Customer customer)
{
return
customerService.addCustomer(customer);
}
@PutMapping
(
"/updateCustomer"
)
public
String
updateCustomer(
@RequestBody
Customer customer)
{
return
customerService.updateCustomer(customer);
}
}
Now let’s go through the various ways in which we can handle the Exceptions thrown in this project.
Default Exception Handling by Spring Boot:
The getCustomer() method defined by CustomerController is used to get a customer with a given Id. It throws a NoSuchElementException when it doesn’t find a Customer record with the given id. On Running the Spring Boot Application and hitting the /getCustomer API with an Invalid Customer Id, we get a NoSuchElementException completely handled by Spring Boot as follows:
Spring Boot provides a systematic error response to the user with information such as timestamp, HTTP status code, error, message, and the path.
Using Spring Boot @ExceptionHandler Annotation:
@ExceptionHandler annotation provided by Spring Boot can be used to handle exceptions in particular Handler classes or Handler methods. Any method annotated with this is automatically recognized by Spring Configuration as an Exception Handler Method. An Exception Handler method handles all exceptions and their subclasses passed in the argument. It can also be configured to return a specific error response to the user. So let’s create a custom ErrorResponse class so that the exception is conveyed to the user in a clear and concise way as follows:
Java
package
com.customer.exception;
import
lombok.AllArgsConstructor;
import
lombok.Data;
import
lombok.NoArgsConstructor;
@Data
@AllArgsConstructor
@NoArgsConstructor
public
class
ErrorResponse {
private
int
statusCode;
private
String message;
public
ErrorResponse(String message)
{
super
();
this
.message = message;
}
}
The addCustomer() method defined by CustomerController throws a CustomerAlreadyExistsException when the user tries to add a Customer that already exists in the database else it saves the customer details.
To handle this exception let’s define a handler method handleCustomerAlreadyExistsException() in the CustomerController.So now when addCustomer() throws a CustomerAlreadyExistsException, the handler method gets invoked which returns a proper ErrorResponse to the user.
Java
@ExceptionHandler
(value
= CustomerAlreadyExistsException.
class
)
@ResponseStatus
(HttpStatus.CONFLICT)
public
ErrorResponse
handleCustomerAlreadyExistsException(
CustomerAlreadyExistsException ex)
{
return
new
ErrorResponse(HttpStatus.CONFLICT.value(),
ex.getMessage());
}
Note: Spring Boot allows to annotate a method with @ResponseStatus to return the required Http Status Code.
On Running the Spring Boot Application and hitting the /addCustomer API with an existing Customer, CustomerAlreadyExistsException gets completely handled by handler method as follows:
Using @ControllerAdvice for Global Exception Handler:
In the previous approach, we can see that the @ExceptionHandler annotated method can only handle the exceptions thrown by that particular class. However, if we want to handle any exception thrown throughout the application we can define a global exception handler class and annotate it with @ControllerAdvice.This annotation helps to integrate multiple exception handlers into a single global unit.
The updateCustomer() method defined in CustomerController throws a NoSuchCustomerExistsException exception if the user tries to update details of a customer that doesn’t already exist in the database else it successfully saves the updated details for that particular customer.
To handle this exception, let’s define a GlobalExceptionHandler class annotated with @ControllerAdvice. This class defines the ExceptionHandler method for NoSuchCustomerExistsException exception as follows.
Java
package
com.customer.exception;
import
org.springframework.http.HttpStatus;
import
org.springframework.web.bind.annotation.ControllerAdvice;
import
org.springframework.web.bind.annotation.ExceptionHandler;
import
org.springframework.web.bind.annotation.ResponseBody;
import
org.springframework.web.bind.annotation.ResponseStatus;
@ControllerAdvice
public
class
GlobalExceptionHandler {
@ExceptionHandler
(value
= NoSuchCustomerExistsException.
class
)
@ResponseStatus
(HttpStatus.BAD_REQUEST)
public
@ResponseBody
ErrorResponse
handleException(NoSuchCustomerExistsException ex)
{
return
new
ErrorResponse(
HttpStatus.NOT_FOUND.value(), ex.getMessage());
}
}
On Running the Spring Boot Application and hitting the /updateCustomer API with invalid Customer details, NoSuchCustomerExistsException gets thrown which is completely handled by the handler method defined in GlobalExceptionHandler class as follows: