«1. Обзор

В этом кратком руководстве мы сосредоточимся на различиях между аннотациями @Valid и @Validated в Spring.

Проверка ввода пользователей является обычной функцией в большинстве наших приложений. В экосистеме Java мы специально используем Java Standard Bean Validation API для поддержки этого. Кроме того, это также хорошо интегрировано с Spring, начиная с версии 4.0. Аннотации @Valid и @Validated основаны на этом API стандартного компонента.

В следующих разделах мы рассмотрим их подробно.

2. Аннотации @Valid и @Validated

В Spring мы используем аннотацию @Valid из JSR-303 для проверки на уровне метода. Более того, мы также используем его, чтобы пометить атрибут члена для проверки. Однако эта аннотация не поддерживает групповую проверку.

Группы помогают ограничить ограничения, применяемые во время проверки. Одним из конкретных вариантов использования являются мастера пользовательского интерфейса. Здесь на первом этапе у нас может быть определенная подгруппа полей. На следующем шаге может быть другая группа, принадлежащая тому же компоненту. Следовательно, нам нужно применять ограничения к этим ограниченным полям на каждом этапе, но @Valid не поддерживает это.

В этом случае для группового уровня мы должны использовать Spring @Validated, который является вариантом @Valid этого JSR-303. Это используется на уровне метода. А для маркировки атрибутов членов мы продолжаем использовать аннотацию @Valid.

Теперь давайте углубимся и посмотрим на использование этих аннотаций на примере.

3. Пример

Рассмотрим простую форму регистрации пользователя, разработанную с использованием Spring Boot. Для начала у нас будут только атрибуты имени и пароля:

public class UserAccount {

    @NotNull
    @Size(min = 4, max = 15)
    private String password;

    @NotBlank
    private String name;

    // standard constructors / setters / getters / toString
     
}

Далее рассмотрим контроллер. Здесь у нас будет метод saveBasicInfo с аннотацией @Valid для проверки ввода пользователя:

@RequestMapping(value = "/saveBasicInfo", method = RequestMethod.POST)
public String saveBasicInfo(
  @Valid @ModelAttribute("useraccount") UserAccount useraccount, 
  BindingResult result, 
  ModelMap model) {
    if (result.hasErrors()) {
        return "error";
    }
    return "success";
}

Теперь давайте протестируем этот метод:

@Test
public void givenSaveBasicInfo_whenCorrectInput_thenSuccess() throws Exception {
    this.mockMvc.perform(MockMvcRequestBuilders.post("/saveBasicInfo")
      .accept(MediaType.TEXT_HTML)
      .param("name", "test123")
      .param("password", "pass"))
      .andExpect(view().name("success"))
      .andExpect(status().isOk())
      .andDo(print());
}

Убедившись, что тест выполняется успешно, давайте расширим функциональность. Следующим логическим шагом будет преобразование этой формы регистрации в многоступенчатую, как это делается в большинстве мастеров. Первый шаг с именем и паролем остается без изменений. На втором этапе мы получим дополнительную информацию, например возраст и номер телефона. Следовательно, мы добавим в наш объект предметной области следующие дополнительные поля:

public class UserAccount {
    
    @NotNull
    @Size(min = 4, max = 15)
    private String password;
 
    @NotBlank
    private String name;
 
    @Min(value = 18, message = "Age should not be less than 18")
    private int age;
 
    @NotBlank
    private String phone;
    
    // standard constructors / setters / getters / toString   
    
}

Однако на этот раз мы заметим, что предыдущий тест не прошел. Это связано с тем, что мы не пропускаем поля возраста и телефона, которых по-прежнему нет на картинке в пользовательском интерфейсе. Для поддержки такого поведения нам потребуется групповая проверка и аннотация @Validated.

Для этого нам нужно сгруппировать поля, создав две отдельные группы. Во-первых, нам нужно создать два интерфейса маркеров. Отдельный для каждой группы или каждого шага. Мы можем обратиться к нашей статье о групповой проверке для точной реализации этого. Здесь давайте сосредоточимся на различиях в аннотациях.

У нас будет интерфейс BasicInfo для первого шага и AdvanceInfo для второго шага. Кроме того, мы обновим наш класс UserAccount для использования этих интерфейсов маркеров следующим образом:

public class UserAccount {
    
    @NotNull(groups = BasicInfo.class)
    @Size(min = 4, max = 15, groups = BasicInfo.class)
    private String password;
 
    @NotBlank(groups = BasicInfo.class)
    private String name;
 
    @Min(value = 18, message = "Age should not be less than 18", groups = AdvanceInfo.class)
    private int age;
 
    @NotBlank(groups = AdvanceInfo.class)
    private String phone;
    
    // standard constructors / setters / getters / toString   
    
}

Кроме того, теперь мы обновим наш контроллер, чтобы он использовал аннотацию @Validated вместо @Valid:

@RequestMapping(value = "/saveBasicInfoStep1", method = RequestMethod.POST)
public String saveBasicInfoStep1(
  @Validated(BasicInfo.class) 
  @ModelAttribute("useraccount") UserAccount useraccount, 
  BindingResult result, ModelMap model) {
    if (result.hasErrors()) {
        return "error";
    }
    return "success";
}

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

@Test
public void givenSaveBasicInfoStep1_whenCorrectInput_thenSuccess() throws Exception {
    this.mockMvc.perform(MockMvcRequestBuilders.post("/saveBasicInfoStep1")
      .accept(MediaType.TEXT_HTML)
      .param("name", "test123")
      .param("password", "pass"))
      .andExpect(view().name("success"))
      .andExpect(status().isOk())
      .andDo(print());
}

Этот тоже работает успешно. Следовательно, мы видим, насколько важно использование @Validated для групповой проверки.

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

4. Использование аннотации @Valid для пометки вложенных объектов

Аннотация @Valid используется, в частности, для пометки вложенных атрибутов. Это запускает проверку вложенного объекта. Например, в нашем текущем сценарии давайте создадим объект UserAddress:

public class UserAddress {

    @NotBlank
    private String countryCode;

    // standard constructors / setters / getters / toString
}

Чтобы гарантировать проверку этого вложенного объекта, мы украсим атрибут аннотацией @Valid:

public class UserAccount {
    
    //...
    
    @Valid
    @NotNull(groups = AdvanceInfo.class)
    private UserAddress useraddress;
    
    // standard constructors / setters / getters / toString 
}

5. Плюсы и Минусы

Давайте рассмотрим некоторые плюсы и минусы использования аннотаций @Valid и @Validated в Spring.

«Аннотация @Valid обеспечивает проверку всего объекта. Важно отметить, что он выполняет проверку всех графов объектов. Однако это создает проблемы для сценариев, требующих только частичной проверки.

С другой стороны, мы можем использовать @Validated для групповой проверки, включая приведенную выше частичную проверку. Однако в этом случае проверенные объекты должны знать правила проверки для всех групп или вариантов использования, в которых они используются. Здесь мы смешиваем проблемы, поэтому это может привести к анти-шаблону.

6. Заключение

В этом кратком руководстве мы рассмотрели ключевые различия между аннотациями @Valid и @Validated.

В заключение, для любой базовой проверки мы будем использовать аннотацию JSR @Valid в наших вызовах методов. С другой стороны, для любой групповой проверки, включая групповые последовательности, нам нужно будет использовать аннотацию Spring @Validated в вызове нашего метода. Аннотация @Valid также необходима для запуска проверки вложенных свойств.

Как всегда, код, представленный в этой статье, доступен на GitHub.