Обработка ошибок в spring boot

Во время работы вашего приложения часто будут возникать исключительные ситуации. Когда у вас простое консольное приложение, то все просто – ошибка выводится в консоль. Но как быть с веб-приложением?

Допустим у пользователя отсутсвует доступ, или он передал некорректные данные. Лучшим вариантом будет в ответ на такие ситуации, отправлять пользователю сообщения с описанием ошибки. Это позволит клиенту вашего 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. Этот вариант самый чистый и понятный.

Время на прочтение
5 мин

Количество просмотров 32K

Часто при работе с микросервисами, построенными с помощью технологии Spring Boot, можно видеть стандартный вывод ошибок подобный этому:

{
    "timestamp": 1510417124782,
    "status": 500,
    "error": "Internal Server Error",
    "exception": "com.netflix.hystrix.exception.HystrixRuntimeException",
    "message": "ApplicationRepository#save(Application) failed and no fallback available.",
    "path": "/application"
}

Такой вывод может быть излишним и ненужным клиентам вашего сервиса. Если вы хотите упростить жизнь сторонним сервисам в случае ошибки, то как раз об этом и пойдет речь в данном посте.

Начнем мы с построения небольшого сервиса с одним контроллером. Наш сервис будет принимать запрос на получение пользователя и в случае успеха отдавать данные по пользователю. В случае провала к нам возвращается ошибка. Начнем с простого и далее в статье будем усовершенствовать проект.

Итак, первое, что нам понадобится, это пользователь:

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class User {
    private int id;
    private String firstName;
    private String lastName;
}

Здесь я использовал библиотеку lombok. Аннотация Data подставляет геттеры и сеттеры в класс. Остальные аннотации добавляют пустой конструктор и конструктор с параметрами. Если вы хотите повторить данный пример у себя в IntelliJ Idea, то вам необходимо поставить галочку в пункте enable annotation processing, либо написать все руками.

Далее нам понадобится сервис (для краткости репозиторий создавать не будем):

import org.springframework.stereotype.Service;

import java.util.HashMap;
import java.util.Map;

@Service
public class UserService {

    private static final Map<Integer, User> userStorage
            = new HashMap<>();

    static {
        userStorage.put(1, new User(1, "Petr", "Petrov"));
        userStorage.put(2, new User(2, "Ivan", "Ivanov"));
        userStorage.put(3, new User(3, "Sergei", "Sidorov"));
    }

    public User get(int id) {
        return userStorage.get(id);
    }

}

Ну и, конечно, сам контроллер:

import org.springframework.beans.factory.annotation.Autowired;
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.RestController;

@RestController
@RequestMapping("user")
public class UserController {

    private UserService userService;

    @Autowired
    public UserController(UserService userService) {
        this.userService = userService;
    }

    @GetMapping("{id}")
    public User get(@PathVariable(name = "id") int id) {
        return userService.get(id);
    }

}

Итак, у нас есть почти полноценный сервис с пользователями. Запускаем его и смотрим.

При запросе на URL localhost:8080/user/1 нам возвращается json в таком формате:

{
    "id": 1,
    "firstName": "Petr",
    "lastName": "Petrov"
}

Все отлично. Но что будет, если сделать запрос на URL localhost:8080/user/4 (у нас всего 3 пользователя)? Правильный ответ: мы получим статус 200 и ничего в ответе. Ситуация не особо приятная. Ошибки нет, но и запрашиваемого объекта тоже нет.

Давайте улучшим наш сервис и добавим в него выбрасывание ошибки в случае неудачи. Для начала создадим исключение:

public class ThereIsNoSuchUserException extends RuntimeException { }

Теперь добавим пробрасывание ошибки в сервис:

public User get(int id) {
    User user = userStorage.get(id);
    if (user == null) {
        throw new ThereIsNoSuchUserException();
    }
    
    return user;
}

Сделаем перезапуск сервиса и снова посмотрим, что будет при запросе несуществующего пользователя:

{
    "timestamp": 1510479979781,
    "status": 500,
    "error": "Internal Server Error",
    "exception": "org.faoxis.habrexception.ThereIsNoSuchUserException",
    "message": "No message available",
    "path": "/user/4"
}

Это уже лучше. Гораздо более информативно и код статуса не 200. Такую ситуацию клиент на своей стороне уже сможет успешно и легко обработать. Но, как говорится, есть один нюанс. Ошибки могут быть совершенно разными, и клиенту нашего сервиса придется ставить кучу условных операторов и исследовать, что у нас пошло не так и как это можно поправить. Получается немного грубо с нашей стороны.

Как раз для таких случаев и была придумана аннотация ResponseStatus. Подставим ее на место нашего исключения и на практике посмотрим, как это работает:

@ResponseStatus(code = HttpStatus.NOT_FOUND, reason = "There is no such user")
public class ThereIsNoSuchUserException extends RuntimeException {
}

Повторим запрос и посмотрим результат:

{
    "timestamp": 1510480307384,
    "status": 404,
    "error": "Not Found",
    "exception": "org.faoxis.habrexception.ThereIsNoSuchUserException",
    "message": "There is no such user",
    "path": "/user/4"
}

Отлично! Код статуса и сообщение поменялись. Теперь клиент сможет определись по коду ответа причину ошибки и даже уточнить ее по полю message. Но все же есть проблема. Большинство полей клиенту могут быть просто не нужны. Например, код ответа как отдельное поле может быть излишним, поскольку мы его и так получаем с кодом ответа. С этим нужно что-то делать.

К счастью, со spring boot сделать последний шаг к нашему успешному оповещению об ошибке не так сложно.

Все, что для этого требуется, разобрать пару аннотаций и один класс:

  • Аннотация ExceptionHandler. Используется для обработки собственных и каких-то специфичных исключений. Далее в примере будет понятно, что это значит. На всякий случай ссылка на документацию.
  • Аннотация ControllerAdvice. Данная аннотация дает «совет» группе констроллеров по определенным событиям. В нашем случае — это обработка ошибок. По умолчанию применяется ко всем контроллерам, но в параметрах можно указать отпределенную группу. Подбронее тут.
  • Класс ResponseEntityExceptionHandler. Данный класс занимается обработкой ошибок. У него куча методов, название которых построенно по принципу handle + название исключения. Если мы хотим обработать какое-то базовое исключение, то наследуемся от этого класса и переопределяем нужный метод.

Давайте теперь посмотрим, как все это обЪединить и построить наше уникальное и неповторимое сообщение об ошибке:

import lombok.AllArgsConstructor;
import lombok.Data;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;

@ControllerAdvice
public class AwesomeExceptionHandler extends ResponseEntityExceptionHandler {

    @ExceptionHandler(ThereIsNoSuchUserException.class)
    protected ResponseEntity<AwesomeException> handleThereIsNoSuchUserException() {
        return new ResponseEntity<>(new AwesomeException("There is no such user"), HttpStatus.NOT_FOUND);
    }

    @Data
    @AllArgsConstructor
    private static class AwesomeException {
        private String message;
    }
}

Сделаем все тот же запрос и увидим статус ответа 404 и наше сообщение с единственным полем:

{
    "message": "There is no such user"
}

Аннотацию ResponseStatus над нашим исключением можно смело убирать.

В итоге у нас получилось приложение, в котором обработка ошибок настраивается максимально гибко и просто. Полный проект можно найти в репозитории github. Надеюсь, что все было просто и понятно. Спасибо за внимание и пишите комментарии! Буду рад вашим замечаниям и уточнениям!

В части 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.

Handling exceptions is an important part of building a robust application. Spring Boot offers more than one way of doing it.

This article will explore these ways and will also provide some pointers on when a given way might be preferable over another.

Example Code

This article is accompanied by a working code example on GitHub.

Introduction

Spring Boot provides us tools to handle exceptions beyond simple ‘try-catch’ blocks. To use these tools, we apply a couple of annotations
that allow us to treat exception handling as a cross-cutting concern:

  • @ResponseStatus
  • @ExceptionHandler
  • @ControllerAdvice

Before jumping into these annotations we will first look at how Spring handles exceptions thrown by our web controllers — our last line of defense for catching an exception.

We will also look at some configurations provided by Spring Boot to modify the default behavior.

We’ll identify the challenges we face while doing that, and then we will try to overcome those using these annotations.

Spring Boot’s Default Exception Handling Mechanism

Let’s say we have a controller named ProductController whose getProduct(...) method is throwing a NoSuchElementFoundException runtime exception when a Product with a given id is not found:

@RestController
@RequestMapping("/product")
public class ProductController {
  private final ProductService productService;
  //constructor omitted for brevity...
  
  @GetMapping("/{id}")
  public Response getProduct(@PathVariable String id){
    // this method throws a "NoSuchElementFoundException" exception
    return productService.getProduct(id);
  }
  
}

If we call the /product API with an invalid id the service will throw a NoSuchElementFoundException runtime exception and we’ll get the
following response:

{
  "timestamp": "2020-11-28T13:24:02.239+00:00",
  "status": 500,
  "error": "Internal Server Error",
  "message": "",
  "path": "/product/1"
}

We can see that besides a well-formed error response, the payload is not giving us any useful information. Even the message
field is empty, which we might want to contain something like “Item with id 1 not found”.

Let’s start by fixing the error message issue.

Spring Boot provides some properties with which we can add the exception message, exception class, or even a stack trace
as part of the response payload
:

server:
  error:
  include-message: always
  include-binding-errors: always
  include-stacktrace: on_trace_param
  include-exception: false

Using these Spring Boot server properties in our application.yml we can alter the error response to some extent.

Now if we call the /product API again with an invalid id we’ll get the following response:

{
  "timestamp": "2020-11-29T09:42:12.287+00:00",
  "status": 500,
  "error": "Internal Server Error",
  "message": "Item with id 1 not found",
  "path": "/product/1"
} 

Note that we’ve set the property include-stacktrace to on_trace_param which means that only if we include the trace param in the URL (?trace=true), we’ll get a stack trace in the response payload:

{
  "timestamp": "2020-11-29T09:42:12.287+00:00",
  "status": 500,
  "error": "Internal Server Error",
  "message": "Item with id 1 not found",
  "trace": "io.reflectoring.exception.exception.NoSuchElementFoundException: Item with id 1 not found...", 
  "path": "/product/1"
} 

We might want to keep the value of include-stacktrace flag to never, at least in production, as it might reveal the internal
workings of our application.

Moving on! The status and error message — 500 — indicates that something is wrong with our server code but actually it’s a client error because the client provided an invalid id.

Our current status code doesn’t correctly reflect that. Unfortunately, this is as far as we can go with the server.error configuration properties, so we’ll have to look at the annotations that Spring Boot offers.

@ResponseStatus

As the name suggests, @ResponseStatus allows us to modify the HTTP status of our response. It can be applied in the following
places:

  • On the exception class itself
  • Along with the @ExceptionHandler annotation on methods
  • Along with the @ControllerAdvice annotation on classes

In this section, we’ll be looking at the first case only.

Let’s come back to the problem at hand which is that our error responses are always giving us the HTTP status 500 instead of a more descriptive status code.

To address this we can we annotate our Exception class with @ResponseStatus and pass in the desired HTTP response status
in its value property:

@ResponseStatus(value = HttpStatus.NOT_FOUND)
public class NoSuchElementFoundException extends RuntimeException {
  ...
}

This change will result in a much better response if we call our controller with an invalid ID:

{
  "timestamp": "2020-11-29T09:42:12.287+00:00",
  "status": 404,
  "error": "Not Found",
  "message": "Item with id 1 not found",
  "path": "/product/1"
} 

Another way to achieve the same is by extending the ResponseStatusException class:

public class NoSuchElementFoundException extends ResponseStatusException {

  public NoSuchElementFoundException(String message){
    super(HttpStatus.NOT_FOUND, message);
  }

  @Override
  public HttpHeaders getResponseHeaders() {
      // return response headers
  }
}

This approach comes in handy when we want to manipulate the response headers, too, because we can override the getResponseHeaders() method.

@ResponseStatus, in combination with the server.error configuration properties, allows us to manipulate almost all the fields
in our Spring-defined error response payload.

But what if want to manipulate the structure of the response payload as well?

Let’s see how
we can achieve that in the next section.

@ExceptionHandler

The @ExceptionHandler annotation gives us a lot of flexibility in terms of handling exceptions. For starters, to use it, we
simply need to create a method either in the controller itself or in a @ControllerAdvice class and
annotate it with @ExceptionHandler:

@RestController
@RequestMapping("/product")
public class ProductController { 
    
  private final ProductService productService;
  
  //constructor omitted for brevity...

  @GetMapping("/{id}")
  public Response getProduct(@PathVariable String id) {
    return productService.getProduct(id);
  }

  @ExceptionHandler(NoSuchElementFoundException.class)
  @ResponseStatus(HttpStatus.NOT_FOUND)
  public ResponseEntity<String> handleNoSuchElementFoundException(
      NoSuchElementFoundException exception
  ) {
    return ResponseEntity
        .status(HttpStatus.NOT_FOUND)
        .body(exception.getMessage());
  }

}

The exception handler method takes in an exception or a list of exceptions as an argument that we want to handle in the defined
method. We annotate the method with @ExceptionHandler and @ResponseStatus to define the exception we want to handle and the status code we want to return.

If we don’t wish to use these annotations, then simply defining the exception as a parameter of the method will also do:

@ExceptionHandler
public ResponseEntity<String> handleNoSuchElementFoundException(
    NoSuchElementFoundException exception)

Although it’s a good idea to mention the exception class in the annotation even though we have mentioned it in the method signature already. It gives better readability.

Also, the annotation @ResponseStatus(HttpStatus.NOT_FOUND) on the handler method is not required as the HTTP status passed into the ResponseEnity
will take precedence, but we have kept it anyway for the same readability reasons.

Apart from the exception parameter, we can also have HttpServletRequest, WebRequest, or HttpSession types as parameters.

Similarly, the handler
methods support a variety of return types such as ResponseEntity, String, or even void.

Find more input and return types in @ExceptionHandler java documentation.

With many different options available to us in form of both input parameters and return types in our exception handling function,
we are in complete control of the error response.

Now, let’s finalize an error response payload for our APIs. In case of any error, clients usually expect two things:

  • An error code that tells the client what kind of error it is. Error codes can be used by clients in their code to drive
    some business logic based on it. Usually, error codes are standard HTTP status codes, but I have also seen APIs returning
    custom errors code likes E001.
  • An additional human-readable message which gives more information on the error and even some hints
    on how to fix them or a link to API docs.

We will also add an optional stackTrace field which will help us with debugging in the development environment.

Lastly, we also want to handle validation errors in the response. You can find out more about bean
validations in this article on Handling Validations with Spring Boot.

Keeping these points in mind we will go with the following payload for the error response:

@Getter
@Setter
@RequiredArgsConstructor
@JsonInclude(JsonInclude.Include.NON_NULL)
public class ErrorResponse {
  private final int status;
  private final String message;
  private String stackTrace;
  private List<ValidationError> errors;

  @Getter
  @Setter
  @RequiredArgsConstructor
  private static class ValidationError {
    private final String field;
    private final String message;
  }

  public void addValidationError(String field, String message){
    if(Objects.isNull(errors)){
      errors = new ArrayList<>();
    }
    errors.add(new ValidationError(field, message));
  }
}

Now, let’s apply all these to our NoSuchElementFoundException handler method.

@RestController
@RequestMapping("/product")
@AllArgsConstructor
public class ProductController {
  public static final String TRACE = "trace";

  @Value("${reflectoring.trace:false}")
  private boolean printStackTrace;
  
  private final ProductService productService;

  @GetMapping("/{id}")
  public Product getProduct(@PathVariable String id){
    return productService.getProduct(id);
  }

  @PostMapping
  public Product addProduct(@RequestBody @Valid ProductInput input){
    return productService.addProduct(input);
  }

  @ExceptionHandler(NoSuchElementFoundException.class)
  @ResponseStatus(HttpStatus.NOT_FOUND)
  public ResponseEntity<ErrorResponse> handleItemNotFoundException(
      NoSuchElementFoundException exception, 
      WebRequest request
  ){
    log.error("Failed to find the requested element", exception);
    return buildErrorResponse(exception, HttpStatus.NOT_FOUND, request);
  }

  @ExceptionHandler(MethodArgumentNotValidException.class)
  @ResponseStatus(HttpStatus.UNPROCESSABLE_ENTITY)
  public ResponseEntity<ErrorResponse> handleMethodArgumentNotValid(
      MethodArgumentNotValidException ex,
      WebRequest request
  ) {
    ErrorResponse errorResponse = new ErrorResponse(
        HttpStatus.UNPROCESSABLE_ENTITY.value(), 
        "Validation error. Check 'errors' field for details."
    );
    
    for (FieldError fieldError : ex.getBindingResult().getFieldErrors()) {
      errorResponse.addValidationError(fieldError.getField(), 
          fieldError.getDefaultMessage());
    }
    return ResponseEntity.unprocessableEntity().body(errorResponse);
  }

  @ExceptionHandler(Exception.class)
  @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
  public ResponseEntity<ErrorResponse> handleAllUncaughtException(
      Exception exception, 
      WebRequest request){
    log.error("Unknown error occurred", exception);
    return buildErrorResponse(
        exception,
        "Unknown error occurred", 
        HttpStatus.INTERNAL_SERVER_ERROR, 
        request
    );
  }

  private ResponseEntity<ErrorResponse> buildErrorResponse(
      Exception exception,
      HttpStatus httpStatus,
      WebRequest request
  ) {
    return buildErrorResponse(
        exception, 
        exception.getMessage(), 
        httpStatus, 
        request);
  }

  private ResponseEntity<ErrorResponse> buildErrorResponse(
      Exception exception,
      String message,
      HttpStatus httpStatus,
      WebRequest request
  ) {
    ErrorResponse errorResponse = new ErrorResponse(
        httpStatus.value(), 
        exception.getMessage()
    );
    
    if(printStackTrace && isTraceOn(request)){
      errorResponse.setStackTrace(ExceptionUtils.getStackTrace(exception));
    }
    return ResponseEntity.status(httpStatus).body(errorResponse);
  }

  private boolean isTraceOn(WebRequest request) {
    String [] value = request.getParameterValues(TRACE);
    return Objects.nonNull(value)
        && value.length > 0
        && value[0].contentEquals("true");
  }
}

Couple of things to note here:

Providing a Stack Trace

Providing stack trace in the error response can save our developers and QA engineers the trouble of crawling through the log files.

As we saw in Spring Boot’s Default Exception Handling Mechanism, Spring already provides us
with this functionality. But now, as we are handling error responses ourselves, this also needs to be handled by us.

To achieve this, we have first introduced a server-side configuration property named reflectoring.trace which, if set to true,
To achieve this, we have first introduced a server-side configuration property named reflectoring.trace which, if set to true,
will enable the stackTrace field in the response. To actually get a stackTrace in an API response, our clients must additionally pass the
trace parameter with the value true:

curl --location --request GET 'http://localhost:8080/product/1?trace=true'

Now, as the behavior of stackTrace is controlled by our feature flag in our properties file, we can remove it or set it
to false when we deploy in production environments.

Catch-All Exception Handler

Gotta catch em all:

try{
  performSomeOperation();
} catch(OperationSpecificException ex){
  //...
} catch(Exception catchAllExcetion){
  //...  
}

As a cautionary measure, we often surround our top-level method’s body with a catch-all try-catch exception handler block, to avoid any unwanted side effects or behavior. The handleAllUncaughtException() method in our controller behaves
similarly. It will catch all the exceptions for which we don’t have a specific handler.

One thing I would like to note here is that even if we don’t have this catch-all exception handler, Spring will handle it
anyway. But we want the response to be in our format rather than Spring’s, so we have to handle the exception ourselves.

A catch-all handler method is also be a good place to log exceptions as
they might give insight into a possible bug. We can skip logging on field validation exceptions such as MethodArgumentNotValidException
as they are raised because of syntactically invalid input, but we should always log unknown exceptions in the catch-all handler.

Order of Exception Handlers

The order in which you mention the handler methods doesn’t matter. Spring will first look for the most specific exception handler method.

If it fails to find it then it will look for a handler of the parent exception, which in our case is RuntimeException, and if none is found, the
handleAllUncaughtException() method will finally handle the exception.

This should help us handle the exceptions in this particular controller, but what if these same exceptions are being thrown
by other controllers too? How do we handle those? Do we create the same handlers in all controllers or create a base class with
common handlers and extend it in all controllers?

Luckily, we don’t have to do any of that. Spring provides a very elegant solution to this problem in form of “controller advice”.

Let’s study them.

@ControllerAdvice

Why is it called «Controller Advice»?

The term ‘Advice’ comes from Aspect-Oriented Programming (AOP) which allows us to inject cross-cutting code (called «advice») around existing methods. A controller advice allows us to intercept and modify the return values of controller methods, in our case to handle exceptions.

Controller advice classes allow us to apply exception handlers to more than one or all controllers in our application:

@ControllerAdvice
public class GlobalExceptionHandler extends ResponseEntityExceptionHandler {

  public static final String TRACE = "trace";

  @Value("${reflectoring.trace:false}")
  private boolean printStackTrace;

  @Override
  @ResponseStatus(HttpStatus.UNPROCESSABLE_ENTITY)
  protected ResponseEntity<Object> handleMethodArgumentNotValid(
      MethodArgumentNotValidException ex,
      HttpHeaders headers,
      HttpStatus status,
      WebRequest request
  ) {
      //Body omitted as it's similar to the method of same name
      // in ProductController example...  
      //.....
  }

  @ExceptionHandler(ItemNotFoundException.class)
  @ResponseStatus(HttpStatus.NOT_FOUND)
  public ResponseEntity<Object> handleItemNotFoundException(
      ItemNotFoundException itemNotFoundException, 
      WebRequest request
  ){
      //Body omitted as it's similar to the method of same name
      // in ProductController example...  
      //.....  
  }

  @ExceptionHandler(RuntimeException.class)
  @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
  public ResponseEntity<Object> handleAllUncaughtException(
      RuntimeException exception, 
      WebRequest request
  ){
      //Body omitted as it's similar to the method of same name
      // in ProductController example...  
      //.....
  }
  
  //....

  @Override
  public ResponseEntity<Object> handleExceptionInternal(
      Exception ex,
      Object body,
      HttpHeaders headers,
      HttpStatus status,
      WebRequest request) {

    return buildErrorResponse(ex,status,request);
  }

}

The bodies of the handler functions and the other support code are omitted as they’re almost
identical to the code we saw in the @ExceptionHandler section. Please find the full code in the Github Repo’s
GlobalExceptionHandler class.

A couple of things are new which we will talk about in a while. One major difference here is that these handlers will handle exceptions thrown by all the controllers
in the application and not just ProductController
.

If we want to selectively apply or limit the scope of the controller advice to a particular controller, or a package, we can use the properties provided by the annotation:

  • @ControllerAdvice("com.reflectoring.controller"): we can pass a package name or list of package names in the annotation’s value
    or basePackages parameter. With this, the controller advice will only handle exceptions of this package’s controllers.
  • @ControllerAdvice(annotations = Advised.class): only controllers marked with the @Advised annotation will be handled
    by the controller advice.

Find other parameters in the @ControllerAdvice annotation docs.

ResponseEntityExceptionHandler

ResponseEntityExceptionHandler is a convenient base class for controller advice classes. It provides
exception handlers for internal Spring exceptions. If we don’t extend it, then all the exceptions will be redirected to DefaultHandlerExceptionResolver
which returns a ModelAndView object. Since we are on the mission to shape our own error response, we don’t want that.

As you can see we have overridden two of the ResponseEntityExceptionHandler methods:

  • handleMethodArgumentNotValid(): in the @ExceptionHandler section we have implemented a handler for it ourselves. In here we have only
    overridden its behavior.
  • handleExceptionInternal(): all the handlers in the ResponseEntityExceptionHandler use this function to build the
    ResponseEntity similar to our buildErrorResponse(). If we don’t override this then the clients will receive only the HTTP status
    in the response header but since we want to include the HTTP status in our response bodies as well, we have overridden the method.

Handling NoHandlerFoundException Requires a Few Extra Steps

This exception occurs when you try to call an API that doesn’t exist in the system. Despite us implementing its handler
via ResponseEntityExceptionHandler class the exception is redirected to DefaultHandlerExceptionResolver.

To redirect the exception to our advice we need to set a couple of properties in the the properties file: spring.mvc.throw-exception-if-no-handler-found=true and spring.web.resources.add-mappings=false

Credit: Stackoverflow user mengchengfeng.

Some Points to Keep in Mind when Using @ControllerAdvice

  • To keep things simple always have only one controller advice class in the project. It’s good to have a single repository of
    all the exceptions in the application. In case you create multiple controller advice, try to utilize the basePackages or annotations properties
    to make it clear what controllers it’s going to advise.
  • Spring can process controller advice classes in any order unless we have annotated it with the @Order annotation. So, be mindful when you write a catch-all handler if you have more than one controller advice. Especially
    when you have not specified basePackages or annotations in the annotation.

How Does Spring Process The Exceptions?

Now that we have introduced the mechanisms available to us for handling exceptions in Spring, let’s
understand in brief how Spring handles it and when one mechanism gets prioritized over the other.

Have a look through the following flow chart that traces the process of the exception handling by Spring if we have not built our own exception handler:

Spring Exception Handling Flow

Conclusion

When an exception crosses the boundary of the controller, it’s destined to reach the client, either in form of a JSON response
or an HTML web page.

In this article, we saw how Spring Boot translates those exceptions into a user-friendly output for our
clients and also configurations and annotations that allow us to further mold them into the shape we desire.

Thank you for reading! You can find the working code at GitHub.

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:

  1. 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.
  2. 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.
  3. 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:

Last Updated :
05 Jul, 2022

Like Article

Save Article

Понравилась статья? Поделить с друзьями:
  • Обработать ошибку от сервера okhttp kotlin
  • Обработка ошибок в powershell скрипте
  • Обоюдная ошибка или переигровка жест
  • Обоюдная ошибка и переигровка волейбол
  • Обоюдная ошибка и переигровка в волейболе