Статьи Утилиты Telegram YouTube RuTube Отзывы

Валидация бинов в Spring

Исходники

17 ноября 2024

Тэги: Java, json, maven, rest, Spring.

Содержание

  1. Основные аннотации для проверки полей
  2. Тестируем валидацию запроса
  3. Кастомизация формата ответа при ошибке валидации
  4. Выводы

Spring позволяет проверять входящие запросы в декларативном стиле при помощи специальных аннотаций из пакета jakarta.validation.

Создадим простой проект на Java и добавим в pom.xml следующие зависимости:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

Компонент spring-boot-starter-web добавляет базовый функционал по работе с rest-контроллерами, а spring-boot-starter-validation – сам механизм валидации.

Теперь создадим rest-контроллер для работы с пользователями. В нём есть метод, создающий нового пользователя. Согласно restful-соглашениям, это будет POST-запрос:

@RestController
@RequestMapping("/users")
public class UserController {

    @PostMapping
    public String createUser(@Valid @RequestBody CreateUserRequest request) {
        // создание пользователя в БД
        return "Пользователь создан успешно!";
    }
}

На вход метод принимает CreateUserRequest, который мы будем передавать в формате json. Этот параметр метода снабжён аннотациями @RequestBody (говорит, что параметры будут именно в теле запроса) и @Valid (аннотация, которая активирует механизм валидации для данного бина). Если не указать аннотацию @Valid, то валидация работать не будет!

Поскольку сама логика добавления пользователя в БД нас сейчас не интересует, метод в случае успеха просто возвращает строку «Пользователь создан успешно!».

Класс, представляющий тело запроса, выглядит так:

public record CreateUserRequest(
        @NotBlank(message = "имя не может состоять из одних пробелов")
        @Size(min = 2, message = "имя не может быть короче 2-х символов")
        String name,

        @Past(message = "дата рождения не может быть в будущем")
        @NotNull
        LocalDate birthDate,

        @Positive
        int friendsCount,

        @NotNull
        @Size(min = 1, max = 10)
        List<String> documents
) {
}

Тип record означает, что компилятор автоматически создаст для каждого поля get-методы, конструктор с параметрами и ряд вспомогательных методов, таких как equals(), hashCode() и toString(). Вы также можете использовать обычный class, но тогда придётся написать чуть больше шаблонного кода или использовать Lombok.

Основные аннотации для проверки полей

Здесь представлены основные аннотации, используемые для валидации входных данных.

Аннотация @NotNull указывает на обязательность параметра. Если мы такую аннотацию вешаем на числовые типы, то следует использовать именно ссылочные типы, а не примитивные, иначе смысл аннотации теряется. Например, тип int всегда имеет значение по умолчанию, а потому проверка не сработает даже если в теле запроса этот параметр не будет указан. Чтобы отловить эту ситуацию, используйте ссылочный Integer.

Аннотация @NotBlank проверяет, что строка не состоит из одних пробелов. Это особенно актуально для ключевых полей, которые должны содержать какую-то информацию.

Аннотация @Past применительно к датам проверяет, что указана уже прошедшая дата. Очевидно, что дата рождения пользователя всегда меньше текущей даты.

Аннотация @Positive указывает, что число должно быть положительным. В нашем случае это поле friendsCount, ведь количественные характеристики не могут быть отрицательными.

Аннотация @Size применительно к спискам позволяет задать минимальное и максимальное количество элементов. Если эту аннотацию поставить на строку, то мы будем проверять минимальное и максимальное количество символов в строке.

Если проверка не прошла, мы получим какое-то стандартное сообщение об ошибке. Но вы всегда можете задать своё сообщение, используя параметр message в соответствующей аннотации.

Тестируем валидацию запроса

Давайте проверим работу валидации и отправим POST-запрос с пустым телом на эндпоинт /users.

{
  
}

В ответ получим http-статус 400 Bad Request и стандартный json:

{
    "timestamp": "2024-11-16T20:14:03.080+00:00",
    "status": 400,
    "error": "Bad Request",
    "path": "/users"
}

Такой ответ приходит по умолчанию и он не очень информативный. Только с помощью логов мы сможем узнать, что возникло исключение MethodArgumentNotValidException и в чём была причина его возникновения. Возможно, с точки зрения безопасности иногда это оправдано, но чаще требуется явно указать пользователю, какие именно поля не прошли валидацию. Поэтому давайте изменим стандартный ответ при ошибке.

Кастомизация формата ответа при ошибке валидации

Это можно сделать с помощью аннотации @ExceptionHandler. В самом простом случае достаточно добавить в контроллер следующий метод:

@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(MethodArgumentNotValidException.class)
public Map<String, String> handleValidationExceptions(MethodArgumentNotValidException e) {
    Map<String, String> errors = new HashMap<>();
    e.getBindingResult().getAllErrors().forEach((error) -> {
        String fieldName = ((FieldError) error).getField();
        String errorMessage = error.getDefaultMessage();
        errors.put(fieldName, errorMessage);
    });
    return errors;
}

Здесь с помощью аннотации @ResponseStatus мы явно указываем, какой http-статус будем отдавать в случае ошибке. Аннотация @ExceptionHandler обрабатывает только исключение MethodArgumentNotValidException. Благодаря этому мы можем получать исключение в случае его возникновения как параметр метода. Внутри просто строим мапу, где ключом будет имя поля, из-за которого возникла ошибка, а значением – текстовое сообщение об ошибке.

Тогда формат ответа при пустом теле запроса будет таким:

{
    "documents": "must not be null",
    "name": "имя не может состоять из одних пробелов",
    "friendsCount": "must be greater than 0",
    "birthDate": "must not be null"
}

Как видите, собственные сообщения об ошибке выглядят более информативно.

Теперь отправим json, удовлетворяющий всем этим проверкам:

{
  "name": "Иван",
  "birthDate": "1988-01-01",
  "friendsCount": 100,
  "documents": [
    "паспорт",
    "водительское удостоверение"
  ]
}

В ответ получим http-статус 200 и сообщение «Пользователь создан успешно!».

Выводы

Мы убедились, что компонент spring-boot-starter-validation позволяет легко производить валидацию входящих запросов в декларативном стиле с помощью набора базовых проверок. Если проверка не пройдена – возникает исключение MethodArgumentNotValidException.

Также мы рассмотрели, как с помощью @ExceptionHandler сделать стандартный ответ при ошибке валидации более информативным для клиента.



Комментарии

18.11.2024 23:52 devmark

Актуализировал статью про валидацию бинов в Spring Boot. Также для этой статьи создал репозиторий с примерами на github.

Добавить комментарий

×

devmark.ru