Рубрики
Без рубрики

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

Автор оригинала: Dhananjay Singh.

Вступление

В этой статье мы рассмотрим несколько подходов к обработке исключений в приложениях Spring REST.

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

Если вы хотите узнать больше об исключениях и пользовательских исключениях в Java, мы подробно рассмотрели это в разделе Обработка исключений в Java: Полное руководство с лучшими и наихудшими практиками и о том, как создавать пользовательские исключения в Java .

Зачем Это Делать?

Предположим, у нас есть простой пользовательский сервис, в котором мы можем получать и обновлять зарегистрированных пользователей. У нас есть простая модель, определенная для пользователей:

public class User {
    private int id;
    private String name;
    private int age;

    // Constructors, getters, and setters

Давайте создадим контроллер REST с отображением, которое ожидает идентификатор и возвращает Пользователя с заданным идентификатором , если он присутствует:

@RestController
public class UserController {

    private static List userList = new ArrayList<>();
    static {
        userList.add(new User(1, "John", 24));
        userList.add(new User(2, "Jane", 22));
        userList.add(new User(3, "Max", 27));
    }

    @GetMapping(value = "/user/{id}")
    public ResponseEntity getUser(@PathVariable int id) {
        if (id < 0) {
            return ResponseEntity.status(HttpStatus.BAD_REQUEST).build();
        }
        User user = findUser(id);
        if (user == null) {
            return ResponseEntity.status(HttpStatus.NOT_FOUND).build();
        }

        return ResponseEntity.ok(user);
    }

    private User findUser(int id) {
        return userList.stream().filter(user -> user.getId().equals(id)).findFirst().orElse(null);
    }
}

Помимо простого поиска пользователя, мы также должны выполнить дополнительные проверки, например, переданный идентификатор всегда должен быть больше 0, в противном случае нам придется возвращать код состояния BAD_REQUEST .

Аналогично, если пользователь не найден, мы должны вернуть код состояния NOT_FOUND . Кроме того, возможно, нам придется добавить клиенту текст для получения более подробной информации об ошибке.

Для каждой проверки мы должны создать объект ResponseEntity , содержащий коды ответов и текст в соответствии с нашими требованиями.

Мы легко можем видеть, что эти проверки придется выполнять несколько раз по мере роста наших API. Например, предположим, что мы добавляем новое сопоставление ИСПРАВЛЕНИЙ запросов для обновления наших пользователей, нам нужно снова создать эти объекты ResponseEntity . Это создает проблему поддержания согласованности внутри приложения.

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

Теперь вы можете использовать встроенные исключения, уже предоставленные Java и Spring , или, при необходимости, вы можете создавать свои собственные исключения и выбрасывать их. Это также позволит централизовать нашу логику проверки/обработки ошибок.

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

Наряду с обработкой исключений необходима документация REST API.

Обработка исключений с помощью @ResponseStatus

Аннотацию @ResponseStatus можно использовать для методов и классов исключений. Его можно настроить с помощью кода состояния, который будет применен к ответу HTTP.

Давайте создадим пользовательское исключение для обработки ситуации, когда пользователь не найден. Это будет исключение во время выполнения, поэтому мы должны расширить класс java.lang.RuntimeException .

Мы также пометим этот класс @ResponseStatus :

@ResponseStatus(value = HttpStatus.NOT_FOUND, reason = "User Not found")
public class UserNotFoundException extends RuntimeException {

}

Когда Spring улавливает это исключение, он использует конфигурацию, указанную в @ResponseStatus .

Изменение нашего контроллера для использования того же:

    @GetMapping(value = "/user/{id}")
    public ResponseEntity getUser(@PathVariable int id) {
        if (id < 0) {
            return ResponseEntity.status(HttpStatus.BAD_REQUEST).build();
        }
        User user = findUser(id);
        return ResponseEntity.ok(user);
    }

    private User findUser(int id) {
        return userList.stream().filter(user -> user.getId().equals(id)).findFirst().orElseThrow(() -> new UserNotFoundException());
    }

Как мы видим, теперь код стал чище с разделением проблем.

@@Pestcontroladvice и @ExceptionHandler

Давайте создадим пользовательское исключение для обработки проверок проверки. Это снова будет исключение RuntimeException :

public class ValidationException extends RuntimeException {
    private static final long serialVersionUID = 1L;
    private String msg;

    public ValidationException(String msg) {
        this.msg = msg;
    }

    public String getMsg() {
        return msg;
    }
}

Git Essentials

Ознакомьтесь с этим практическим руководством по изучению Git, содержащим лучшие практики и принятые в отрасли стандарты. Прекратите гуглить команды Git и на самом деле изучите это!

@RestControllerAdvice – это новая функция Spring, которую можно использовать для написания общего кода для обработки исключений.

Обычно это используется в сочетании с @ExceptionHandler , который фактически обрабатывает различные исключения:

@RestControllerAdvice
public class AppExceptionHandler {

    @ResponseBody
    @ExceptionHandler(value = ValidationException.class)
    public ResponseEntity handleException(ValidationException exception) {
        return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(exception.getMsg());
    }
}

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

Обратите внимание, что в отличие от @ResponseStatus с помощью этого подхода мы могли бы многое сделать, например, выйти из исключений, уведомить и т. Д.

Что делать, если мы хотим обновить возраст существующего пользователя? У нас есть 2 проверки, которые необходимо выполнить:

  • Идентификатор должен быть больше 0
  • Возраст должен быть от 20 до 60 лет

Имея это в виду, давайте сделаем конечную точку именно для этого:

    @PatchMapping(value = "/user/{id}")
    public ResponseEntity updateAge(@PathVariable int id, @RequestParam int age) {
        if (id < 0) {
            throw new ValidationException("Id cannot be less than 0");
        }
        if (age < 20 || age > 60) {
            throw new ValidationException("Age must be between 20 to 60");
        }
        User user = findUser(id);
        user.setAge(age);

        return ResponseEntity.accepted().body(user);
    }

По умолчанию @RestControllerAdvice применим ко всему приложению, но вы можете ограничить его конкретным пакетом, классом или аннотацией.

Для ограничения уровня пакета вы можете сделать что-то вроде:

@RestControllerAdvice(basePackages = "my.package")

или

@RestControllerAdvice(basePackageClasses = MyController.class)

Для применения к определенному классу:

@RestControllerAdvice(assignableTypes = MyController.class)

Чтобы применить его к контроллерам с определенными аннотациями:

@RestControllerAdvice(annotations = RestController.class)

Обработчик ответов на вопросы

ResponseEntityExceptionHandler обеспечивает некоторую базовую обработку исключений Spring.

Мы можем расширить этот класс и переопределить методы, чтобы настроить их:

@RestControllerAdvice
public class GlobalResponseEntityExceptionHandler extends ResponseEntityExceptionHandler {

    @Override
    protected ResponseEntity handleMissingServletRequestParameter(MissingServletRequestParameterException ex, HttpHeaders headers, HttpStatus status, WebRequest request) {
        return errorResponse(HttpStatus.BAD_REQUEST, "Required request params missing");
    }

    private ResponseEntity errorResponse(HttpStatus status, String message) {
        return ResponseEntity.status(status).body(message);
    }
}

Чтобы зарегистрировать этот класс для обработки исключений, мы должны аннотировать его с помощью @ResponseControllerAdvice .

Опять же, здесь можно сделать много вещей, и это зависит от ваших требований.

Что использовать, когда?

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

  • Для пользовательских исключений, в которых ваш код состояния и сообщение исправлены, подумайте о добавлении @ResponseStatus к ним.
  • Для исключений, в которых вам необходимо выполнить некоторое ведение журнала, используйте @RestControllerAdvice с @ExceptionHandler . У вас также есть больше контроля над текстом вашего ответа здесь.
  • Для изменения поведения ответов на весенние исключения по умолчанию можно расширить класс ResponseEntityExceptionHandler .

Примечание : Будьте осторожны при смешивании этих параметров в одном приложении. Если одно и то же обрабатывается в нескольких местах, вы можете получить поведение, отличное от ожидаемого.

Вывод

В этом руководстве мы обсудили несколько способов реализации механизма обработки исключений для API REST весной.

Как всегда, код для примеров, используемых в этой статье, можно найти на Github .