Spring boot rest обработка ошибок

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

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

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

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

Каждый раз, когда я начинаю реализацию нового REST API с помощью Spring, мне сложно решить, как выполнять валидацию запросов и обрабатывать бизнес-исключения. В отличие от других типичных проблем API, Spring и его сообщество, похоже, не согласны с лучшими методами решения этих проблем, и трудно найти полезные статьи по этому поводу.

В этой статье я обобщаю свой опыт и даю несколько советов по валидации интерфейсов.

Архитектура и терминология

Я создаю свои приложения, которые предоставляют веб-API, следуя шаблону луковой архитектуры (Onion Architecture). Эта статья не об архитектуре Onion, но я хотел бы упомянуть некоторые из ее ключевых моментов, которые важны для понимания моих мыслей:

  • Контроллеры REST и любые веб-компоненты и конфигурации являются частью внешнего «инфраструктурного» уровня .

  • Средний «сервисный» уровень содержит сервисы, которые объединяют бизнес-функции и решают общие проблемы, такие как безопасность или транзакции.

  • Внутренний уровень «домена» содержит бизнес-логику без каких-либо задач, связанных с инфраструктурой, таких как доступ к базе данных, конечные точки web и т.д.

Набросок слоев луковой архитектуры и места размещения типичных классов Spring.

Набросок слоев луковой архитектуры и места размещения типичных классов Spring.

Архитектура допускает зависимости от внешних уровней к внутренним, но не наоборот. Для конечной точки REST поток запроса может выглядеть следующим образом:

  • Запрос отправляется контроллеру на уровне «инфраструктуры».

  • Контроллер десериализует запрос и — в случае успеха — запрашивает результат у соответствующего сервиса на уровне сервисы.

  • Служба проверяет, есть ли у текущего пользователя разрешение на вызов функции, и инициализирует транзакцию базы данных (при необходимости).

  • Затем он извлекает данные из репозиториев домена , манипулирует ими и, возможно, сохраняет их обратно в репозиторий.

  • Сервис также может вызывать несколько репозиториев, преобразовывать и агрегировать результаты.

  • Репозиторий на уровне домена возвращает бизнес-объекты. Этот уровень отвечает за поддержание всех объектов в допустимом состоянии.

  • В зависимости от ответа сервиса, который является допустимым результатом или исключением, уровень инфраструктуры сериализует ответ.

Проверка на уровне запроса, уровня обслуживания и домена.

Проверка на уровне запроса, уровня обслуживания и домена.

В этой архитектуре у нас есть три интерфейса, для каждого из которых требуется разная валидация:

  • Контроллер определяет первый интерфейс. Чтобы десериализовать запрос, нужно выполнить его валидацию по нашей схеме API . Это делается неявно с помощью фреймворка маппирования, такого как Jackson, и явно с помощью ограничений, таких как @NotNull. Мы называем это валидацией запроса .

  • Сервис может проверять права текущего пользователя и обеспечивать выполнение предварительных условий, которые сделают возможным вызов уровня домена. Назовем это валидацией сервиса.

  • В то время как предыдущие валидации обеспечивают выполнение некоторых основных предварительных условий, только уровень домена отвечает за поддержание допустимого состояния. Валидация уровня домена является наиболее важной.

Валидация запроса

Обычно мы десериализуем входящий запрос, для которого уже выполнена неявная валидация параметров запроса и тела запроса. Spring Boot автоматически настраивает Jackson десериализацию и общую обработку исключений. Например, взгляните на пример контроллера моей демонстрации BGG:

@GetMapping("/newest")
Flux<ThreadsPerBoardGame> getThreads(@RequestParam String user, @RequestParam(defaultValue = "PT1H") Duration since) {
    return threadService.findNewestThreads(user, since);
}

Оба вызова с отсутствующим параметром и неправильным типом возвращают сообщения об ошибках с правильным кодом состояния :

curl -i localhost:8080/threads/newest
HTTP/1.1 400 Bad Request
Content-Type: application/json
Content-Length: 189

{"timestamp":"2020-04-15T03:40:00.460+0000","path":"/threads/newest","status":400,"error":"Bad Request","message":"Required String parameter 'user' is not present","requestId":"98427b15-7"}

curl -i "localhost:8080/threads/newest?user=chrigu&since=a"
HTTP/1.1 400 Bad Request
Content-Type: application/json
Content-Length: 156

{"timestamp":"2020-04-15T03:40:06.952+0000","path":"/threads/newest","status":400,"error":"Bad Request","message":"Type mismatch.","requestId":"7600c788-8"}

С конфигурацией по умолчанию Spring Boot мы также получим трассировки стека . Я выключил их, установив

server:
  error:
    include-stacktrace: never

в application.yml . Эта обработка ошибок по умолчанию обеспечивается BasicErrorController в классическом Web MVC и по DefaultErrorWebExceptionHandler в WebFlux, и извлечение тела ответа от ErrorAttributes.

Связывание данных

В приведенных выше примерах демонстрируются атрибуты @RequestParam или любой простой атрибут метода контроллера без аннотации. Проверка запроса становится иной при проверке @ModelAttribute , @RequestBody или непростых параметров, как в

@GetMapping("/newest/obj")
Flux<ThreadsPerBoardGame> getThreads(@Valid ThreadRequest params) {
    return threadService.findNewestThreads(params.user, params.since);
}

static class ThreadRequest {
    @NotNull
    private final String user;
    @NotNull
    private final Duration since;

    public ThreadRequest(String user, Duration since) {
        this.user = user;
        this.since = since == null ? Duration.ofHours(1) : since;
    }
}

Если аннотации @RequestParam могут использоваться, чтобы сделать параметр обязательным или со значением по умолчанию , в командных объектах это делается с помощью ограничений проверки bean-компонентов, таких как @NotNull и простой Java / Kotlin. Чтобы активировать проверку bean-компонента, аргумент метода должен быть аннотирован @Valid.

Когда проверка bean-компонента завершается неудачно, в реактивном стеке выдается исключение BindException или WebExchangeBindException . Оба исключения реализуют BindingResult, который предоставляет вложенные ошибки для каждого недопустимого значения поля. Вышеуказанный метод контроллера приведет к сообщениям об ошибках, например

curl "localhost:8080/java/threads/newest/obj" -i
HTTP/1.1 400 Bad Request
Content-Type: application/json
Content-Length: 1138

{"timestamp":"2020-04-17T13:52:39.500+0000","path":"/java/threads/newest/obj","status":400,"error":"Bad Request","message":"Validation failed for argument at index 0 in method: reactor.core.publisher.Flux<ch.chrigu.bgg.service.ThreadsPerBoardGame> ch.chrigu.bgg.infrastructure.web.JavaThreadController.getThreads(ch.chrigu.bgg.infrastructure.web.JavaThreadController$ThreadRequest), with 1 error(s): [Field error in object 'threadRequest' on field 'user': rejected value [null]; codes [NotNull.threadRequest.user,NotNull.user,NotNull.java.lang.String,NotNull]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [threadRequest.user,user]; arguments []; default message [user]]; default message [darf nicht null sein]] ","requestId":"c87c7cbb-17","errors":[{"codes":["NotNull.threadRequest.user","NotNull.user","NotNull.java.lang.String","NotNull"],"arguments":[{"codes":["threadRequest.user","user"],"arguments":null,"defaultMessage":"user","code":"user"}],"defaultMessage":"darf nicht null sein","objectName":"threadRequest","field":"user","rejectedValue":null,"bindingFailure":false,"code":"NotNull"}]}

Настройка обработки исключений

Приведенное выше ответное сообщение не является удобным для клиента, поскольку оно содержит имена классов и другие внутренние подсказки, которые не могут быть понятны клиентом API. Еще худший пример обработки исключений по умолчанию Spring Boot:

curl "localhost:8080/java/threads/newest/obj?user=chrigu&since=a" -i
HTTP/1.1 500 Internal Server Error
Content-Type: application/json
Content-Length: 513

{"timestamp":"2020-04-17T13:56:42.922+0000","path":"/java/threads/newest/obj","status":500,"error":"Internal Server Error","message":"Failed to convert value of type 'java.lang.String' to required type 'java.time.Duration'; nested exception is org.springframework.core.convert.ConversionFailedException: Failed to convert from type [java.lang.String] to type [java.time.Duration] for value 'a'; nested exception is java.lang.IllegalArgumentException: Parse attempt failed for value [a]","requestId":"4c0dc6bd-21"}

Он также возвращает неправильный код ошибки, подразумевающий ошибку сервера, даже если клиент указал неправильный тип для параметра since. Оба примера были сгенерированы с помощью реактивного стека, MVC имеет лучшие значения по умолчанию. Для обоих случаев нам нужно настроить обработку исключений. Это можно сделать, предоставив собственный bean-компонент ErrorAttributes , который записывает желаемое тело ответа. Код состояния ответа предоставляется значением status.

Или мы можем пойти на меньшее вмешательство и использовать реализацию DefaultErrorAttributes, либо добавив в исключения аннотацию @ResponseStatus, либо позволив всем исключениям расширять ResponseStatusException . Оба способа позволяют настроить статус ответа и значение сообщения. К сожалению, большинство исключений, создаваемых на уровне инфраструктуры, предоставляются фреймворком и не могут быть настроены, поэтому нам нужно другое решение. Одна из возможностей для аннотированных контроллеров — использовать @ExceptionHandler для отдельных исключений. Тогда мы могли бы создать ответ с нуля, но это пропустило бы обработку исключений по умолчанию, и мы хотели бы иметь одинаковую обработку для каждого исключения. Таким образом, чтобы улучшить ответ выше, просто повторно вызовите исключения (rethrow):

@ControllerAdvice
class GlobalExceptionHandler {

    @ExceptionHandler(TypeMismatchException::class)
    fun handleTypeMismatchException(e: TypeMismatchException): HttpStatus {
        throw ResponseStatusException(HttpStatus.BAD_REQUEST, "Invalid value '${e.value}'", e)
    }

    @ExceptionHandler(WebExchangeBindException::class)
    fun handleWebExchangeBindException(e: WebExchangeBindException): HttpStatus {
        throw object : WebExchangeBindException(e.methodParameter!!, e.bindingResult) {
            override val message = "${fieldError?.field} has invalid value '${fieldError?.rejectedValue}'"
        }
    }
}

Резюме

Я много писал о конфигурациях Spring Boot по умолчанию, которые, на мой взгляд, всегда являются хорошим началом для Spring. С другой стороны, обработка исключений по умолчанию довольно сложна, и вы можете начать вмешиваться на многих уровнях, сверху вниз:

  • Непосредственно в контроллере с помощью try/catch (MVC) или onErrorResume() (Webflux). Я не рекомендую это в большинстве случаев, потому что сквозная проблема, такая как обработка исключений, должна быть определена глобально, чтобы гарантировать согласованное поведение.

  • Перехватить исключения в функциях @ExceptionHandler . Создайте свои собственные ответы с помощью @ExceptionHandler (Throwable.class) для случая по умолчанию.

  • Или повторно генерируйте исключения , аннотируйте их с помощью @ResponseStatus или расширяйте ResponseStatusException, чтобы настроить ответ для определенных случаев.

Мне нравится запускать приложения Spring Boot с конфигурацией по умолчанию и заменять части там, где это необходимо. В этом случае я рекомендовал начать с третьего варианта, а если требуется дополнительная настройка, переключиться на второй.

В этом блоге я лишь поверхностно коснулся всего того, чему я научился за эти годы. Существует гораздо больше тем, касающихся валидации и обработки исключений, таких как внутренняя обработка сообщений об ошибках, пользовательские аннотации ограничений, различия между Java и Kotlin, автоматическое документирование ограничений и, конечно же, проверка данных на внутренних уровнях. Я продолжу эту тему в будущих статьях начиная с внутренних слоев и свяжу их.

This guide will help you implement effective exception/error handling for a REST API/Service with Spring Boot.

Image

You will learn

  • What is exception handling?
  • Why do you need exception handling?
  • What are the default exception handling capabilities provided by Spring Boot?
  • How to implement exception handling with Spring Boot for RESTful Services?
  • What are different return statuses you can use based on the API?
  • How/What to think when you design error handling for REST API?

Project Code Structure

Following screen shot shows the structure of the project we will create.

Image

A few details:

  • SpringBoot2RestServiceApplication.java — The Spring Boot Application class generated with Spring Initializer. This class acts as the launching point for application.
  • pom.xml — Contains all the dependencies needed to build this project. We will use Spring Boot Starter AOP.
  • Student.java — Student JPA Entity
  • StudentRepository.java — Student JPA Repository. This is created using Spring Data JpaRepository.
  • StudentResource.java — Spring Rest Controller exposing all services on the student resource.
  • CustomizedResponseEntityExceptionHandler.java — Component to implement global exception handling and customize the response based on the exception type.
  • ErrorDetails.java — Response Bean to use when exceptions are thrown from API.
  • StudentNotFoundException.java — Exception thrown from resources when student is not found.
  • data.sql — Initial data for the student table. Spring Boot would execute this script after the tables are created from the entities.
  • Maven 3.0+ is your build tool
  • Your favorite IDE. We use Eclipse.
  • JDK 1.8+

Complete Maven Project With Code Examples

Our Github repository has all the code examples — https://github.com/in28minutes/spring-boot-examples/tree/master/spring-boot-2-rest-service-exception-handling

What is Exception Handling?

Consider these

  • What do you do when something goes wrong in your RESTful API?
  • What do you do when you get an invalid request?
  • What do you do when you something unexpected happens?

Think. What should you do?

Can I just return a Page Not Found — 404? Can I return a generic message Something went wrong.. Is that good enough?

One of the core design principles for RESTful services is

Think about the consumer

So, what should you when an error or exception happens in a RESTful service?

You should return a proper error response

  • Clear message indicating what went wrong and what the consumer can do to fix the error.
  • Include information necessary to solve the error.
  • Proper Response Status based on the context.
  • Do not include sensitive information in the response.

Response Statuses for Errors

Use appropriate status code based on the error.

  • 404 — RESOURCE NOT FOUND
  • 400 — BAD REQUEST
  • 401 — UNAUTHORIZED
  • 415 — UNSUPPORTED TYPE — Representation not supported for the resource
  • 500 — SERVER ERROR

Let’s consider a few HTTP Methods:

  • GET : Should not update anything. Should be idempotent (same result in multiple calls). Possible Return Codes 200 (OK) + 404 (NOT FOUND) +400 (BAD REQUEST)
  • POST : Should create new resource. Ideally return JSON with link to newly created resource. Same return codes as get possible. In addition — Return code 201 (CREATED) can be used.
  • PUT : Update a known resource. ex: update client details. Possible Return Codes : 200(OK) + 404 (NOT FOUND) +400 (BAD REQUEST)
  • DELETE : Used to delete a resource. Possible Return Codes : 200(OK).

500 — SERVER ERROR is possible with all the above HTTP methods. In the case of a 500, include the contact details of the help desk in the response.

Bootstrapping a Project with REST Resouce

In the previous article in the series — http://www.springboottutorial.com/spring-boot-crud-rest-service-with-jpa-hibernate, we set up a simple restful service with a resource exposing CRUD methods.

We will use the same example to discuss about Exception Handling.

Default Exception Handling with Spring Boot

Spring Boot provides good default implementation for exception handling for RESTful Services. Let’s quickly look at the default Exception Handling features provided by Spring Boot.

Resource Not Present

Heres what happens when you fire a request to a non existent resource http://localhost:8080/some-dummy-url

{
  "timestamp": 1512713804164,
  "status": 404,
  "error": "Not Found",
  "message": "No message available",
  "path": "/some-dummy-url"
}

Thats a cool error response. It contains all the details that are typically needed.

What happens when you throw an Exception?

Let’s see what Spring Boot does when an exception is thrown from a Resource.

Lets create a StudentNotFoundException.

/src/main/java/com/in28minutes/springboot/rest/example/student/StudentNotFoundException.java

public class StudentNotFoundException extends RuntimeException {

  public StudentNotFoundException(String exception) {
    super(exception);
  }

}

Let’s enhance the GET method to throw this exception when a student is not found.

  @GetMapping("/students/{id}")
  public Resource<Student> retrieveStudent(@PathVariable long id) {
    Optional<Student> student = studentRepository.findById(id);

    if (!student.isPresent())
      throw new StudentNotFoundException("id-" + id);

    Resource<Student> resource = new Resource<Student>(student.get());

    ControllerLinkBuilder linkTo = linkTo(methodOn(this.getClass()).retrieveAllStudents());

    resource.add(linkTo.withRel("all-students"));

    return resource;
  }

This is the response when you try getting details of a non existing student http://localhost:8080/students/9.

{
  "timestamp": 1512714275589,
  "status": 500,
  "error": "Internal Server Error",
  "message": "id-9",
  "path": "/students/9"
}

One thing I do not like is the status code of the response. It is 500 — Server Error. Actually, the error is with the request. So, I would rather return a 404 — Resource not found.

Let’s see how to customize this and more in the next section.

Customizing Exception Handling with Spring Boot

A combination of Spring and Spring Boot provide multiple options to customize responses for errors.

Customizing Return Status for a Specific Exception

You can specify the Response Status for a specific exception along with the definition of the Exception with ‘@ResponseStatus’ annotation.

@ResponseStatus(HttpStatus.NOT_FOUND)
public class StudentNotFoundException extends RuntimeException {

This is the response when you try getting details of a non existing student http://localhost:8080/students/9.

{
  "timestamp": 1512714594153,
  "status": 404,
  "error": "Not Found",
  "message": "id-9",
  "path": "/students/9"
}

Customizing Error Response Structure

Default error response provided by Spring Boot contains all the details that are typically needed.

However, you might want to create a framework independent response structure for your organization. In that case, you can define a specific error response structure.

Let’s define a simple error response bean.

public class ErrorDetails {
  private Date timestamp;
  private String message;
  private String details;

  public ErrorDetails(Date timestamp, String message, String details) {
    super();
    this.timestamp = timestamp;
    this.message = message;
    this.details = details;
  }

To use ErrorDetails to return the error response, let’s define a ControllerAdvice as shown below.

@ControllerAdvice
@RestController
public class CustomizedResponseEntityExceptionHandler extends ResponseEntityExceptionHandler {

  @ExceptionHandler(StudentNotFoundException.class)
  public final ResponseEntity<ErrorDetails> handleUserNotFoundException(StudentNotFoundException ex, WebRequest request) {
    ErrorDetails errorDetails = new ErrorDetails(new Date(), ex.getMessage(),
        request.getDescription(false));
    return new ResponseEntity<>(errorDetails, HttpStatus.NOT_FOUND);
  }

Notes

  • @ExceptionHandler(StudentNotFoundException.class) indicates that this method would handle exceptions of the specific type.
  • new ResponseEntity<>(errorDetails, HttpStatus.NOT_FOUND) — Create an error response object and return it with a specific Http Status.

This is the response when you try getting details of a non existing student http://localhost:8080/students/9.

{
  "timestamp": 1512714887537,
  "message": "id-9",
  "details": "uri=/students/9"
}

Response uses the custom error structure that we had defined earlier.

Using Error Response Structure for all Exceptions

You can further enhance CustomizedResponseEntityExceptionHandler to handle all other exceptions.

@ExceptionHandler(Exception.class)
public final ResponseEntity<ErrorDetails> handleAllExceptions(Exception ex, WebRequest request) {
  ErrorDetails errorDetails = new ErrorDetails(new Date(), ex.getMessage(),
      request.getDescription(false));
  return new ResponseEntity<>(errorDetails, HttpStatus.INTERNAL_SERVER_ERROR);
}

Complete Code Example

Our Github repository has all the code examples — https://github.com/in28minutes/spring-boot-examples/tree/master/spring-boot-2-rest-service-exception-handling

Just Released

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.

@Exception Handler output for Blog already exists exception

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.

@Controller Advice Output

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

Spring Framework 5

Понравилась статья? Поделить с друзьями:
  • Spotify ошибка при запуске
  • Spotlight ошибка списка исключений
  • Spotify что то пошло не так ошибка
  • Spn 523613 fm1 16 камаз ошибка
  • Spn 715 fmi 3 камаз 5490 ошибка