Во время работы вашего приложения часто будут возникать исключительные ситуации. Когда у вас простое консольное приложение, то все просто – ошибка выводится в консоль. Но как быть с веб-приложением?
Допустим у пользователя отсутсвует доступ, или он передал некорректные данные. Лучшим вариантом будет в ответ на такие ситуации, отправлять пользователю сообщения с описанием ошибки. Это позволит клиенту вашего 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. Этот вариант самый чистый и понятный.
В части 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.
Содержание
- Exception Handling for Spring Boot Rest API
- Introduction to API Error Handling
- Best Practices to Handle Exceptions
- Use Meaningful Status Codes
- Use Custom Detailed Exceptions
- Build Rest API with Spring Boot
- Create Spring Boot project
- pom.xml
- Configure MySQL Database
- Create Domain Model User
- Create User Repository Interface
- Create Spring Rest Controller
- Exception Handling in Spring Boot
- Using @ExceptionHandler
- Using @ControllerAdvice
- Using ResponseEntityExceptionHandler Class
- Customizing Exception Handling Using @ResponseStatus
- Handling Exception for Rest API in Practice
- Create Custom Error Response
- Create Global Exception Handler
- Handle Resource Not Found Exception
- Handling HttpMessageNotReadableException
- Handle MethodArgumentNotValidException
- Dealing with MethodArgumentTypeMismatchException
- Handle ConstraintViolationException
- Handle MissingServletRequestParameterException
- Handling HttpMediaTypeNotSupportedException
- Handle NoHandlerFoundException
- Deal with ALL Other Exceptions
- Conclusion
- Обработка исключений в контроллерах Spring
- Обработка исключений на уровне контроллера — @ExceptionHandler
- Обработка исключений с помощью HandlerExceptionResolver
- Обработка исключений с помощью @ControllerAdvice
- Исключение ResponseStatusException.
Exception Handling for Spring Boot Rest API
In this write-up, we are going to shed light on how to handle exceptions in Spring Boot rest API.
Why? Because exception handling brings some sort of order to errors management when unexpected scenarios occur in our applications.
In simple words, it makes our applications more resilient to errors and exceptions.
We will consider here that you are familiar with what a rest API is. Our article on how to build a rest API with Spring Boot does a great job in covering this topic.
So, let’s get started
Introduction to API Error Handling
Think about the consumer. This idea has been always one of the main core design principles for restful APIs.
So, a good rest API implementation must in one way or another promote good and clean error handling.
You may be wondering WHY?
Well, because error handling gives, in a relatively friendly manner, some hint to the consumers that something went wrong.
It gives us, as developers, the flexibility to inform the clients in a more concise way about what was wrong with the API.
In short, handling and catching our exceptions properly can incredibly improve the API’s readability and reliability.
Best Practices to Handle Exceptions
Before we dive deep into how to handle Spring Boot API exceptions, we are going to talk a little bit about best practices that can be adapted to deal with errors.
Use Meaningful Status Codes
The first step to manage errors properly is to provide the clients with human readable informations and proper HTTP status codes.
This can help the consumers to route their responses accordingly.
We should avoid returning the same HTTP status code every single time. It is a big mistake.
Pick carefully a relevant status code that reflects significantly the returned response.
Use always 2xx for successful responses, 3xx for redirections, 4xx and 5xx for error responses.
| 2xx (Success) | indicates that the request was accepted successfully |
| 3xx (Redirection) | informs that the client must take further actions in order to complete the request |
| 4xx (Client error) | indicates an error in the request from the client side |
| 5xx (Server error) | tells that the server failed to fulfil the request |
Simply put, we should not expose 500 errors to clients because they are server errors, not client errors. What happens in our servers is not the client’s business.
Use Custom Detailed Exceptions
In general, standard exceptions — called also Alien — return stack traces that are usually hard to understand.
So, this is where user-defined exceptions come to the rescue.
A rule of thumb: It is always a good practice to throw an exception that means what we want it to mean.
Wrapping an exception in a more specific class can give us more control over how we want to deal with it.
Providing our customized message can make our exception handling mechanism more meaningful and user-friendly.
Build Rest API with Spring Boot
Spring Boot is a platform of choice when it comes to building restful APIs.
So, in this section, we are going to use Spring Boot to develop a simple web application that exposes RESTful web services for users management.
The following, will be our API’s endpoints:
| GET /api/users | get all the users |
| GET /api/users/ | retrieve a single user by ID |
| POST /api/users | create a new user |
| PUT /api/users/ | update a user’s details |
| DELETE /api/users/ | delete a user by ID |
| GET /api/user?username= | get a user by username — this endpoint has no real value for our REST API, it is created just for testing purpose |
In this tutorial, we will assume that you are familiar with the basics of Spring Boot. So, we will include code snippets without diving deep into technical details.
If it is not the case, feel free to check our tutorial about what is Spring Boot.
So, let’s get into coding
Create Spring Boot project
We are going to use Spring Initializr to scaffold and create our Spring Boot project.
To do so, you need to open your favorite browser and navigate to https://start.spring.io
Choose Maven Project, Java language, and Spring Boot version.
After that, you need to fill out the form related to Project Metadata.
Next, search for the following dependencies Web, Data JPA, MySQL, and Lombok and add them to the project.
Now, click on the “Generate” button to download the zip file.
Finally, unzip the file and import the generated project into your favorite IDE.
pom.xml
This is how pom.xml of our project looks like:
Configure MySQL Database
Spring Boot allows us to define the datasource connection parametres in the application.properties file:
spring.profiles.active to set the active profile.
spring.application.name denotes the application name.
Create Domain Model User
Now, let’s create our domain model User:
@Getter, @Setter, @Accessors, @NoArgsConstructor, @AllArgsConstructor are Lombok annotations. They allow to generate getter and setter methods, default and all-args constructors.
Please refer to these articles for more detail:
Create User Repository Interface
Next, we need to create a custom repository for our domain model. For instance, let’s consider UserRepository:
Create Spring Rest Controller
Lastly, we are going to create a spring controller to handle our rest API’s endpoints:
ResourceNotFoundException is a custom Exception that will be thrown when the user with the given ID doesn’t exist
@Valid is used for validation purpose. Spring Boot automatically detect JSR 303 annotations and trigger validation of the annotated arguments
@Min annotation indicates that the value of the annotated parameter must be higher or equal to the specified value. If it is not the case, a validation error will be thrown
Exception Handling in Spring Boot
Spring Framework offers a host of handy ready-to-use features to help us deal with exceptions and errors in a more flexible and appropriate way.
Let’s take a close look at each option.
Using @ExceptionHandler
This annotation, as the name implies, provides a way to define methods in a particular Spring controller to specifically handle exceptions thrown by handler methods of that controller.
The main drawback of the @ExceptionHandler annotation is that the annotated methods will only work for the specific controller where they are defined and not for the whole application.
So, to address this limitation, Spring provides another annotation: @ControllerAdvice
Using @ControllerAdvice
Spring 3.2 comes with a brand new way to centralize the exception handling mechanism in one place.
@ControllerAdvice is mainly introduced to overcome the limitations of the @ExceptionHandler annotated: Handle exceptions globally in our applications.
With @ControllerAdvice, we can create a global handler class with multiple @ExceptionHandler methods to handle exceptions globally across the application.
The purpose of @ControllerAdvice is to separate request handling from error handling. That way, Spring controllers become free from any sort of exception handling logic.
Using ResponseEntityExceptionHandler Class
This base class provides many methods that we can override to customize the way how we want to handle specific exceptions.
The provided methods return an instance of ResponseEntity T>, which means that we can create and generate HTTP responses with the details (body, headers, status code) we want.
Customizing Exception Handling Using @ResponseStatus
Spring offers this annotation to bind a particular Exception to a specific HTTP response status.
So, when Spring catches that particular Exception, it generates an HTTP response with the settings defined in @ResponseStatus .
Handling Exception for Rest API in Practice
Now that we created a rest API using Spring Boot, let’s see how we can implement a good strategy to handle exceptions that may occur when consuming our API’s endpoints.
Create Custom Error Response
Let’s see what will happen when we try to retrieve a user that does not exist.
So, to do that, we need to call the following endpoint: GET /api/users/ .
This is how the output looks like:
Well, the default error representation looks a little bit messy and misleading. Right?
500 is not the right code to describe that a requested resource is NOT FOUND. It should be 404.
Internal Server Error does not provide any meaningful information to our consumers.
Thus, exposing 500 internal server errors to the clients is not a good practice at all. Do you remember?
So, to handle our errors properly, we need to customize the default spring boot API error response with:
Meaningful message indicating what went wrong
Proper status code based on the context
List of errors that occurred during the requests handling
First, let’s create a new class to represent our API error response in a more relevant and user-friendly way:
As we can see, the name of our custom API error class is ApiError.
Create Global Exception Handler
Now, we are going to create our global handler class.
For instance, let’s consider the GlobalExceptionHandler class. We need to annotate it with @ControllerAdvice and make it extend ResponseEntityExceptionHandler.
Now, all we need to do is override ResponseEntityExceptionHandler’s methods and provide our custom exception handling implementation.
we can also define some @ExceptionHandler annotated methods to deal with some specific exceptions across the whole application.
Since the methods of ResponseEntityExceptionHandler class return ResponseEntity T>, we need to create a helper class to map ResponseEntity objects to our custom ApiError objects.
Now, let’s see how to deal with some exceptions that might be thrown when consuming our Spring Boot rest API.
Handle Resource Not Found Exception
As we have seen in the above example, Spring boot tends by default to return 500 error responses when ResourceNotFoundException is thrown.
500 status code is not meant to describe the situation where a requested resource (user in our case) is not found.
So, How can we fix that? The short answer lies in @ResponseStatus annotation.
First, we need to annotate our custom ResourceNotFoundException class with @ResponseStatus(HttpStatus. NOT_FOUND ).
That way, we tell Spring Boot to return an HTTP response with NOT_FOUND status code when ResourceNotFoundException is raised.
Now, we need to create a new exception handler method annotated in our global hander to deal with ResourceNotFoundException the proper way.
Handling HttpMessageNotReadableException
This exception will be triggered if the request body is invalid:
Handle MethodArgumentNotValidException
MethodArgumentNotValidException occurs when a handler method argument annotated with @Valid annotation failed validation.
Dealing with MethodArgumentTypeMismatchException
The exception, as the name implies, is thrown when a method parameter has the wrong type:
Handle ConstraintViolationException
ConstraintViolationException reports the result of constraint violations.
Handle MissingServletRequestParameterException
MissingServletRequestParameterException indicates that a controller method does not receive a required parameter:
Handling HttpMediaTypeNotSupportedException
The Exception informs that the specified request media type (Content type) is not supported.
Handle NoHandlerFoundException
By default, DispatcherServlet sends a 404 response if there is no handler for a particular request.
So, to override the default behavior of our Servlet and throw NoHandlerFoundException instead, we need to add the following properties to the application.properties file.
Now, we can handle NoHandlerFoundException in the same way we did with other exceptions.
Deal with ALL Other Exceptions
Defining a global exception hander allows us to deal with all those exceptions that don’t have specific handlers.
Conclusion
Building a clean and proper exception handling is one of the main fundamental requirements that can make our API design more flexible and efficient.
You can find the full implementation of this tutorial in my Github repository: https://github.com/devwithus/msusers
Take care and stay safe.
Liked the Article? Share it on Social media!
If you enjoy reading my articles, buy me a coffee в•. I would be very grateful if you could consider my request вњЊпёЏ
Источник
Обработка исключений в контроллерах Spring

Часто на практике возникает необходимость централизованной обработки исключений в рамках контроллера или даже всего приложения. В данной статье разберём основные возможности, которые предоставляет Spring Framework для решения этой задачи и на простых примерах посмотрим как всё работает. Кому интересна данная тема — добро пожаловать под кат!
Изначально до Spring 3.2 основными способами обработки исключений в приложении были HandlerExceptionResolver и аннотация @ExceptionHandler. Их мы ещё подробно разберём ниже, но они имеют определённые недостатки. Начиная с версии 3.2 появилась аннотация @ControllerAdvice, в которой устранены ограничения из предыдущих решений. А в Spring 5 добавился новый класс ResponseStatusException, который очень удобен для обработки базовых ошибок для REST API.
А теперь обо всём по порядку, поехали!
Обработка исключений на уровне контроллера — @ExceptionHandler
С помощью аннотации @ExceptionHandler можно обрабатывать исключения на уровне отдельного контроллера. Для этого достаточно объявить метод, в котором будет содержаться вся логика обработки нужного исключения, и проаннотировать его.
В качестве примера разберём простой контроллер:
Тут я сделал метод testExceptionHandler, который вернёт либо исключение BusinessException, либо успешный ответ — всё зависит от того что было передано в параметре запроса. Это нужно для того, чтобы можно было имитировать как штатную работу приложения, так и работу с ошибкой.
А вот следующий метод handleException предназначен уже для обработки ошибок. У него есть аннотация @ExceptionHandler(BusinessException.class), которая говорит нам о том что для последующей обработки будут перехвачены все исключения типа BusinessException. В аннотации @ExceptionHandler можно прописать сразу несколько типов исключений, например так: @ExceptionHandler().
Сама обработка исключения в данном случае примитивная и сделана просто для демонстрации работы метода — по сути вернётся код 200 и JSON с описанием ошибки. На практике часто требуется более сложная логика обработки и если нужно вернуть другой код статуса, то можно воспользоваться дополнительно аннотацией @ResponseStatus, например @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR).
Пример работы с ошибкой:
Пример штатной работы:
Основной недостаток @ExceptionHandler в том что он определяется для каждого контроллера отдельно, а не глобально для всего приложения. Это ограничение можно обойти если @ExceptionHandler определен в базовом классе, от которого будут наследоваться все контроллеры в приложении, но такой подход не всегда возможен, особенно если перед нами старое приложение с большим количеством легаси.
Обработка исключений с помощью HandlerExceptionResolver
HandlerExceptionResolver является общим интерфейсом для обработчиков исключений в Spring. Все исключений выброшенные в приложении будут обработаны одним из подклассов HandlerExceptionResolver. Можно сделать как свою собственную реализацию данного интерфейса, так и использовать существующие реализации, которые предоставляет нам Spring из коробки. Давайте разберем их для начала:
ExceptionHandlerExceptionResolver — этот резолвер является частью механизма обработки исключений с помощью аннотации @ExceptionHandler, о которой я уже упоминал ранее.
DefaultHandlerExceptionResolver — используется для обработки стандартных исключений Spring и устанавливает соответствующий код ответа, в зависимости от типа исключения:
| Exception | HTTP Status Code |
|---|---|
| BindException | 400 (Bad Request) |
| ConversionNotSupportedException | 500 (Internal Server Error) |
| HttpMediaTypeNotAcceptableException | 406 (Not Acceptable) |
| HttpMediaTypeNotSupportedException | 415 (Unsupported Media Type) |
| HttpMessageNotReadableException | 400 (Bad Request) |
| HttpMessageNotWritableException | 500 (Internal Server Error) |
| HttpRequestMethodNotSupportedException | 405 (Method Not Allowed) |
| MethodArgumentNotValidException | 400 (Bad Request) |
| MissingServletRequestParameterException | 400 (Bad Request) |
| MissingServletRequestPartException | 400 (Bad Request) |
| NoSuchRequestHandlingMethodException | 404 (Not Found) |
| TypeMismatchException | 400 (Bad Request) |
Основной недостаток заключается в том что возвращается только код статуса, а на практике для REST API одного кода часто не достаточно. Желательно вернуть клиенту еще и тело ответа с описанием того что произошло. Эту проблему можно решить с помощью ModelAndView, но не нужно, так как есть способ лучше.
ResponseStatusExceptionResolver — позволяет настроить код ответа для любого исключения с помощью аннотации @ResponseStatus.
В качестве примера я создал новый класс исключения ServiceException:
В ServiceException я добавил аннотацию @ResponseStatus и в value указал что данное исключение будет соответствовать статусу INTERNAL_SERVER_ERROR, то есть будет возвращаться статус-код 500.
Для тестирования данного нового исключения я создал простой контроллер:
Если отправить GET-запрос и передать параметр exception=true, то приложение в ответ вернёт 500-ю ошибку:
Из недостатков такого подхода — как и в предыдущем случае отсутствует тело ответа. Но если нужно вернуть только код статуса, то @ResponseStatus довольно удобная штука.
Кастомный HandlerExceptionResolver позволит решить проблему из предыдущих примеров, наконец-то можно вернуть клиенту красивый JSON или XML с необходимой информацией. Но не спешите радоваться, давайте для начала посмотрим на реализацию.
В качестве примера я сделал кастомный резолвер:
Ну так себе, прямо скажем. Код конечно работает, но приходится выполнять всю работу руками: сами проверяем тип исключения, и сами формируем объект древнего класса ModelAndView. На выходе конечно получим красивый JSON, но в коде красоты явно не хватает.
Такой резолвер может глобально перехватывать и обрабатывать любые типы исключений и возвращать как статус-код, так и тело ответа. Формально он даёт нам много возможностей и не имеет недостатков из предыдущих примеров. Но есть способ сделать ещё лучше, к которому мы перейдем чуть позже. А сейчас, чтобы убедиться что всё работает — напишем простой контроллер:
А вот и пример вызова:
Видим что исключение прекрасно обработалось и в ответ получили код 400 и JSON с сообщением об ошибке.
Обработка исключений с помощью @ControllerAdvice
Наконец переходим к самому интересному варианту обработки исключений — эдвайсы. Начиная со Spring 3.2 можно глобально и централизованно обрабатывать исключения с помощью классов с аннотацией @ControllerAdvice.
Разберём простой пример эдвайса для нашего приложения:
Как вы уже догадались, любой класс с аннотацией @ControllerAdvice является глобальным обработчиком исключений, который очень гибко настраивается.
В нашем случае мы создали класс DefaultAdvice с одним единственным методом handleException. Метод handleException имеет аннотацию @ExceptionHandler, в которой, как вы уже знаете, можно определить список обрабатываемых исключений. В нашем случае будем перехватывать все исключения BusinessException.
Можно одним методом обрабатывать и несколько исключений сразу: @ExceptionHandler(). Так же можно в рамках эдвайса сделать сразу несколько методов с аннотациями @ExceptionHandler для обработки разных исключений.
Обратите внимание, что метод handleException возвращает ResponseEntity с нашим собственным типом Response:
Таким образом у нас есть возможность вернуть клиенту как код статуса, так и JSON заданной структуры. В нашем простом примере я записываю в поле message описание ошибки и возвращаю HttpStatus.OK, что соответствует коду 200.
Для проверки работы эдвайса я сделал простой контроллер:
В результате, как и ожидалось, получаем красивый JSON и код 200:
А что если мы хотим обрабатывать исключения только от определенных контроллеров?
Такая возможность тоже есть! Смотрим следующий пример:
Обратите внимание на аннотацию @ControllerAdvice(annotations = CustomExceptionHandler.class). Такая запись означает что CustomAdvice будет обрабатывать исключения только от тех контроллеров, которые дополнительно имеют аннотацию @CustomExceptionHandler.
Аннотацию @CustomExceptionHandler я специально сделал для данного примера:
А вот и исходный код контроллера:
В контроллере Example5Controller присутствует аннотация @CustomExceptionHandler, а так же на то что выбрасывается то же исключение что и в Example4Controller из предыдущего примера. Однако в данном случае исключение BusinessException обработает именно CustomAdvice, а не DefaultAdvice, в чём мы легко можем убедиться.
Для наглядности я немного изменил сообщение об ошибке в CustomAdvice — начал добавлять к нему дату:
На этом возможности эдвайсов не заканчиваются. Если мы посмотрим исходный код аннотации @ControllerAdvice, то увидим что эдвайс можно повесить на отдельные типы или даже пакеты. Не обязательно создавать новые аннотации или вешать его на уже существующие.
Исключение ResponseStatusException.
Сейчас речь пойдёт о формировании ответа путём выброса исключения ResponseStatusException:
Выбрасывая ResponseStatusException можно также возвращать пользователю определённый код статуса, в зависимости от того, что произошло в логике приложения. При этом не нужно создавать кастомное исключение и прописывать аннотацию @ResponseStatus — просто выбрасываем исключение и передаём нужный статус-код. Конечно тут возвращаемся к проблеме отсутствия тела сообщения, но в простых случаях такой подход может быть удобен.
Резюме: мы познакомились с разными способами обработки исключений, каждый из которых имеет свои особенности. В рамках большого приложения можно встретить сразу несколько подходов, но при этом нужно быть очень осторожным и стараться не переусложнять логику обработки ошибок. Иначе получится что какое-нибудь исключение обработается не в том обработчике и на выходе ответ будет отличаться от ожидаемого. Например если в приложении есть несколько эдвайсов, то при создании нового нужно убедиться, что он не сломает существующий порядок обработки исключений из старых контроллеров.
Так что будьте внимательны и всё будет работать замечательно!
Источник
When you develop a Spring Bool RESTful service, you as a programmer are responsible for handling exceptions in the service. For instance, by properly handling exceptions, you can stop the disruption of the normal flow of the application. In addition, proper exception handling ensures that the code doesn’t break when an exception occurs.
Another important thing is to ensure as a programmer is not to send any exceptions or error stacks to clients. Exception and error messages sent to clients should be short and meaningful.
In this post, I will explain how to gracefully handle exceptions in Spring Boot RESTful services.
Dependency
For this post, we will create a Sprinfg Boot RESTful service that performs CRUD operations on Blog entities. We will use embedded H2 as the database. The following code shows the dependencies of the application in the pom.xml file.
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<version>1.4.200</version>
</dependency>
In the context of our Blog RESTful service, the application may encounter several types of exceptions. For example, the database may be down. Another scenario can be a user trying to save an already existing blog. Or a user trying to access a blog yet to be published.
You should handle such scenarios gracefully in the application.
As an example, for database failure, the application throws SQLException. Instead of returning the exception stack trace to client, you should return a meaningful exception message.
The Entity Class
The code for the Blog Entity class is this.
Blog.java
@Entity
public class Blog {
@Id
private int blogId;
private String blogTitle;
private String blogCreator;
private int yearOfPost;
// No-Args and Parametrized Constructor
//Getters and Setters
}
It is a JPA Entity class annotated with the @Entity annotation and corresponding getters and setters for the fields.
The Repository
This is the Blog Repository Interface.
BlogRepository.java
package org.springframework.guru.repository;
import org.springframework.guru.model.Blog;
import org.springframework.data.repository.CrudRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface BlogRepository extends CrudRepository<Blog,Integer> {
}
Here, the BlogRepository extends the CrudRepository of Spring Data JPA.
Custom Exception Classes
In our application, we will create custom exception classes. Such classes enable us to customize an exception according to the callers’ needs.
We will create two custom exception classes:
BlogAlreadyExistsException: Is thrown when a user tries to add an already existing blog.BlogNotFoundException: Is thrown when a user tries to access a blog that is not present.
The code of the BlogAlreadyExistsException class is this.
BlogAlreadyExistsException.java
package org.springframework.guru.exception;
public class BlogAlreadyExistsException extends RuntimeException {
private String message;
public BlogAlreadyExistsException(String message) {
super(message);
this.message = message;
}
public BlogAlreadyExistsException() {
}
}
The code for the BlogNotFoundException class is this.
BlogNotFoundException.java
package org.springframework.guru.exception;
public class BlogNotFoundException extends RuntimeException {
private String message;
public BlogNotFoundException(String message) {
super(message);
this.message = message;
}
public BlogNotFoundException() {
}
}
The Service
This is the BlogService interface which has various methods to perform operations on Blog entities.
BlogService.java
package org.springframework.guru.service;
import org.springframework.guru.exception.BlogAlreadyExistsException;
import org.springframework.guru.exception.BlogNotFoundException;
import org.springframework.guru.model.Blog;
import java.util.List;
public interface BlogService {
Blog saveBlog(Blog blog) throws BlogAlreadyExistsException;
List getAllBlogs() throws BlogNotFoundException;
Blog getBlogById(int id) throws BlogNotFoundException;
}
In the preceding BlogService interface, the saveBlog() method declares that it throws BlogAlreadyExistsException. The two other methods, getAllBlogs() and getBlogById() declares that they throw BlogNotFoundException.
The service implementation class for BlogService is this.
BlogServiceImpl.java
@Service
public class BlogServiceImpl implements BlogService {
private BlogRepository blogRepository;
@Autowired
public BlogServiceImpl(BlogRepository blogRepository) {
this.blogRepository = blogRepository;
}
@Override
public Blog saveBlog(Blog blog) {
if (blogRepository.existsById(blog.getBlogId())) {
throw new BlogAlreadyExistsException();
}
Blog savedBlog = blogRepository.save(blog);
return savedBlog;
}
@Override
public List getAllBlogs() {
return (List) blogRepository.findAll();
}
@Override
public Blog getBlogById(int id) throws BlogNotFoundException {
Blog blog;
if (blogRepository.findById(id).isEmpty()) {
throw new BlogNotFoundException();
} else {
blog = blogRepository.findById(id).get();
}
return blog;
}
}
The preceding BlogServiceImpl class implements the methods declared in the BlogService interface.
There are two paths in exception handling. One is the code handles the exception using a try-catch block. The other is to propagate back a custom exception to the caller. The preceding service class uses the latter approach.
Line 12 – Line 3 checks if the blog already exists in the database. If true the method throws a BlogAlreadyExistsException. Else, the method saves the Blog object.
Line 27 – Line 28 throws a BlogNotFoundException if the Blog with the specified Id is not present in the database.
The Controller
The code for the BlogController is this.
BlogController.java
@RestController
@RequestMapping("api/v1")
public class BlogController {
private BlogService blogService;
@Autowired
public BlogController(BlogService blogService) {
this.blogService = blogService;
}
@PostMapping("/blog")
public ResponseEntity saveBlog(@RequestBody Blog blog) throws BlogAlreadyExistsException {
Blog savedBlog = blogService.saveBlog(blog);
return new ResponseEntity<>(savedBlog, HttpStatus.CREATED);
}
@GetMapping("/blogs")
public ResponseEntity<List> getAllBlogs() throws BlogNotFoundException {
return new ResponseEntity<List>((List) blogService.getAllBlogs(), HttpStatus.OK);
}
@GetMapping("blog/{id}")
public ResponseEntity getBlogById(@PathVariable("id") int id) throws BlogNotFoundException {
return new ResponseEntity(blogService.getBlogById(id), HttpStatus.OK);
}
The preceding controller class is not handling the custom exceptions. Instead, it throws the exceptions back to the caller – which in our scenario is a REST client. This is not what we want – directly sending back exceptions to clients.
Instead, we should handle the exception and send back a short and meaningful exception message to the client. We can use different approaches to achieve this.
Approach 1: Traditional try-catch Block
The first approach is to use Java try-catch block to handle the exception in the controller methods. The code to handle BlogNotFoundException in the getBlogById() method is this.
@GetMapping("blog/{id}")
public ResponseEntity getBlogById(@PathVariable("id") int id) {
try{
return new ResponseEntity(blogService.getBlogById(id), HttpStatus.OK);
}
catch(BlogNotFoundException blogNotFoundException ){
return new ResponseEntity(blogNotFoundException.getMessage(), HttpStatus.CONFLICT);
}
}
In the preceding code, the call to the BlogService.getBlogById() method is wrapped in a try block. If a method call to getBlogById() throws BlogNotFoundException, the catch block handles the exception. In the catch block, the ResponseEntity object is used to send a custom error message with a status code as a response.
Approach 2: Spring @ExceptionHandler Annotation
Spring provides the @ExceptionHandlerannotation to handle exceptions in specific handler classes or handler methods.
Spring configuration will detect this annotation and register the method as an exception handler. The method will handle the exception and its subclasses passed to the annotation.
@ExceptionHandler(value = BlogAlreadyExistsException.class)
public ResponseEntity handleBlogAlreadyExistsException(BlogAlreadyExistsException blogAlreadyExistsException) {
return new ResponseEntity("Blog already exists", HttpStatus.CONFLICT);
}
When any method in the controller throws the BlogAlreadyExistsException exception, Spring invokes the handleBlogAlreadyExistsException() method. This method returns a ResponseEntity that wraps a custom error message and a status code.
When you run the application and send a POST request to add an existing blog, you will get this output.

Approach 3: Global Exception Handling with @ControllerAdvice
The @ExceptionHandler annotation is only active for that particular class where it is declared. If you want a global exception handler you can use Spring AOP. A global exception handler provides a standard way of handling exceptions throughout the application. In addition, it considerably reduces the amount of code written for exception handling.
The Spring @ExceptionHandler along with @ControllerAdvice of Spring AOP enables a mechanism to handle exceptions globally.
The code for the GlobalExceptionHandler class is this.
GlobalExceptionHandler.java
@ControllerAdvice
public class GlobalExceptionHandler {
@Value(value = "${data.exception.message1}")
private String message1;
@Value(value = "${data.exception.message2}")
private String message2;
@Value(value = "${data.exception.message3}")
private String message3;
@ExceptionHandler(value = BlogNotFoundException.class)
public ResponseEntity blogNotFoundException(BlogNotFoundException blogNotFoundException) {
return new ResponseEntity(message2, HttpStatus.NOT_FOUND);
}
@ExceptionHandler(value = Exception.class)
public ResponseEntity<> databaseConnectionFailsException(Exception exception) {
return new ResponseEntity<>(message3, HttpStatus.INTERNAL_SERVER_ERROR);
}
}
The @ControllerAdvice annotation in Line 1 consolidates multiple @ExceptionHandlers into a single, global exception handling component.
The @Value annotation injects exception messages specified in the application.properties file into the fields.
The application.properties file is this.
data.exception.message1=BlogAlreadyExists data.exception.message2=BlogNotFound data.exception.message3=DataConnectivityisLost
Let’s send a GET Request tolocalhost:8080/api/v1/blog/2 to retrieve an unpublished blog. The response is shown in this Figure.

You can find the source code of this post on Github
For in-depth knowledge on the Spring Framework and Spring Boot, you can check my Udemy Best Seller Course Spring Framework 5: Beginner to Guru

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
statusproperty 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
timestampproperty holds the date-time instance when the error happened. -
The
messageproperty holds a user-friendly message about the error. -
The
debugMessageproperty holds a system message describing the error in detail. -
The
subErrorsproperty 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. TheApiSubErrorclass 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.