Spring MVC Custom Validation

Spring MVC Custom Validation

1. обзор

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

Однако, когда нам нужно проверить ввод более конкретного типа,we have the possibility of creating our own, custom validation logic.

В этой статье мы сделаем именно это - мы создадим настраиваемый валидатор для проверки формы с полем номера телефона, а затем покажем настраиваемый валидатор для нескольких полей.

2. Настроить

Чтобы воспользоваться преимуществами API, добавьте зависимость в файлpom.xml:


    org.hibernate
    hibernate-validator
    6.0.10.Final

Последнюю версию зависимости можно проверитьhere.

Если мы используем Spring Boot, то мы можем добавить толькоspring-boot-starter-web,, что также приведет к зависимостиhibernate-validator.

3. Пользовательская проверка

Создание собственного валидатора влечет за собой развертывание нашей собственной аннотации и использование ее в нашей модели для обеспечения соблюдения правил валидации.

Итак, давайте создадим нашcustom validator – which checks phone numbers. Номер телефона должен быть числом из более чем восьми цифр, но не более чем из 11 цифр.

4. Новая аннотация

Давайте создадим новый@interface для определения нашей аннотации:

@Documented
@Constraint(validatedBy = ContactNumberValidator.class)
@Target( { ElementType.METHOD, ElementType.FIELD })
@Retention(RetentionPolicy.RUNTIME)
public @interface ContactNumberConstraint {
    String message() default "Invalid phone number";
    Class[] groups() default {};
    Class[] payload() default {};
}

С помощью аннотации@Constraint мы определили класс, который будет проверять наше поле,message() - это сообщение об ошибке, которое отображается в пользовательском интерфейсе, а дополнительный код - это наиболее шаблонный код, соответствующий требованиям Стандарты весны.

5. Создание валидатора

Давайте теперь создадим класс валидатора, который применяет правила нашей валидации:

public class ContactNumberValidator implements
  ConstraintValidator {

    @Override
    public void initialize(ContactNumberConstraint contactNumber) {
    }

    @Override
    public boolean isValid(String contactField,
      ConstraintValidatorContext cxt) {
        return contactField != null && contactField.matches("[0-9]+")
          && (contactField.length() > 8) && (contactField.length() < 14);
    }

}

Класс проверки реализует интерфейсConstraintValidator и должен реализовывать методisValid; именно в этом методе мы определили наши правила проверки.

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

ConstraintValidator dопределяет логику для проверки данного ограничения для данного объекта. Реализации должны соответствовать следующим ограничениям:

  • объект должен преобразовываться в не параметризованный тип

  • универсальные параметры объекта должны быть неограниченными подстановочными типами

6. Применение аннотации проверки

В нашем случае мы создали простой класс с одним полем для применения правил проверки. Здесь мы настраиваем аннотированное поле для проверки:

@ContactNumberConstraint
private String phone;

Мы определили строковое поле и аннотировали его нашей собственной аннотацией@ContactNumberConstraint.. В нашем контроллере мы создали сопоставления и обработали ошибку, если таковая имеется:

@Controller
public class ValidatedPhoneController {

    @GetMapping("/validatePhone")
    public String loadFormPage(Model m) {
        m.addAttribute("validatedPhone", new ValidatedPhone());
        return "phoneHome";
    }

    @PostMapping("/addValidatePhone")
    public String submitForm(@Valid ValidatedPhone validatedPhone,
      BindingResult result, Model m) {
        if(result.hasErrors()) {
            return "phoneHome";
        }
        m.addAttribute("message", "Successfully saved phone: "
          + validatedPhone.toString());
        return "phoneHome";
    }
}

Мы определили этот простой контроллер, у которого есть одна страницаJSP, и мы используем методsubmitForm для принудительной проверки нашего номера телефона.

7. Вид

Наше представление - это базовая страница JSP с формой, имеющей одно поле. Когда пользователь отправляет форму, поле проверяется нашим пользовательским валидатором и перенаправляет на ту же страницу с сообщением об успешной или неудачной проверке:


    
    
    
    

8. тесты

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

@Test
public void givenPhonePageUri_whenMockMvc_thenReturnsPhonePage(){
    this.mockMvc.
      perform(get("/validatePhone")).andExpect(view().name("phoneHome"));
}

Кроме того, давайте проверим, что наше поле проверено на основе ввода данных пользователем:

@Test
public void
  givenPhoneURIWithPostAndFormData_whenMockMVC_thenVerifyErrorResponse() {

    this.mockMvc.perform(MockMvcRequestBuilders.post("/addValidatePhone").
      accept(MediaType.TEXT_HTML).
      param("phoneInput", "123")).
      andExpect(model().attributeHasFieldErrorCode(
          "validatedPhone","phone","ContactNumberConstraint")).
      andExpect(view().name("phoneHome")).
      andExpect(status().isOk()).
      andDo(print());
}

В тесте мы предоставляем пользователю ввод «123» и, как мы и ожидали, все работает иwe’re seeing the error on the client side.

9. Проверка уровня класса пользователя

Пользовательская аннотация проверки также может быть определена на уровне класса для проверки более одного атрибута класса.

Обычный вариант использования для этого сценария - проверка, имеют ли два поля класса совпадающие значения.

9.1. Создание аннотации

Давайте добавим новую аннотацию под названиемFieldsValueMatch, которую позже можно будет применить к классу. В аннотации будет два параметраfield иfieldMatch, которые представляют имена полей для сравнения:

@Constraint(validatedBy = FieldsValueMatchValidator.class)
@Target({ ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
public @interface FieldsValueMatch {

    String message() default "Fields values don't match!";

    String field();

    String fieldMatch();

    @Target({ ElementType.TYPE })
    @Retention(RetentionPolicy.RUNTIME)
    @interface List {
        FieldsValueMatch[] value();
    }
}

Мы можем видеть, что наша настраиваемая аннотация также содержит субинтерфейсList для определения нескольких аннотацийFieldsValueMatch для класса.

9.2. Создание валидатора

Затем нам нужно добавить классFieldsValueMatchValidator, который будет содержать фактическую логику проверки:

public class FieldsValueMatchValidator
  implements ConstraintValidator {

    private String field;
    private String fieldMatch;

    public void initialize(FieldsValueMatch constraintAnnotation) {
        this.field = constraintAnnotation.field();
        this.fieldMatch = constraintAnnotation.fieldMatch();
    }

    public boolean isValid(Object value,
      ConstraintValidatorContext context) {

        Object fieldValue = new BeanWrapperImpl(value)
          .getPropertyValue(field);
        Object fieldMatchValue = new BeanWrapperImpl(value)
          .getPropertyValue(fieldMatch);

        if (fieldValue != null) {
            return fieldValue.equals(fieldMatchValue);
        } else {
            return fieldMatchValue == null;
        }
    }
}

МетодisValid() извлекает значения двух полей и проверяет, равны ли они.

9.3. Применение аннотации

Давайте создадим класс моделиNewUserForm, предназначенный для данных, необходимых для регистрации пользователя, который имеет два атрибутаemail иpassword, а также два атрибутаverifyEmail иverifyPassword. для повторного ввода двух значений.

Поскольку у нас есть два поля для проверки на соответствие соответствующим им совпадающим полям, давайте добавим две аннотации@FieldsValueMatch к классуNewUserForm, одну для значенийemail и одну для значенийpassword:

@FieldsValueMatch.List({
    @FieldsValueMatch(
      field = "password",
      fieldMatch = "verifyPassword",
      message = "Passwords do not match!"
    ),
    @FieldsValueMatch(
      field = "email",
      fieldMatch = "verifyEmail",
      message = "Email addresses do not match!"
    )
})
public class NewUserForm {
    private String email;
    private String verifyEmail;
    private String password;
    private String verifyPassword;

    // standard constructor, getters, setters
}

Чтобы проверить модель в Spring MVC, давайте создадим контроллер с отображением POST/user, который получает объектNewUserForm, аннотированный@Valid, и проверяет, есть ли какие-либо ошибки проверки:

@Controller
public class NewUserController {

    @GetMapping("/user")
    public String loadFormPage(Model model) {
        model.addAttribute("newUserForm", new NewUserForm());
        return "userHome";
    }

    @PostMapping("/user")
    public String submitForm(@Valid NewUserForm newUserForm,
      BindingResult result, Model model) {
        if (result.hasErrors()) {
            return "userHome";
        }
        model.addAttribute("message", "Valid form");
        return "userHome";
    }
}

9.4. Тестирование аннотации

Чтобы проверить нашу аннотацию на уровне настраиваемого класса, давайте напишем тестJUnit, который отправляет соответствующую информацию в конечную точку/user, а затем проверяет, что ответ не содержит ошибок:

public class ClassValidationMvcTest {
  private MockMvc mockMvc;

    @Before
    public void setup(){
        this.mockMvc = MockMvcBuilders
          .standaloneSetup(new NewUserController()).build();
    }

    @Test
    public void givenMatchingEmailPassword_whenPostNewUserForm_thenOk()
      throws Exception {
        this.mockMvc.perform(MockMvcRequestBuilders
          .post("/user")
          .accept(MediaType.TEXT_HTML).
          .param("email", "[email protected]")
          .param("verifyEmail", "[email protected]")
          .param("password", "pass")
          .param("verifyPassword", "pass"))
          .andExpect(model().errorCount(0))
          .andExpect(status().isOk());
    }
}

Затем давайте также добавим тестJUnit, который отправляет несоответствующую информацию в конечную точку/user и утверждает, что результат будет содержать две ошибки:

@Test
public void givenNotMatchingEmailPassword_whenPostNewUserForm_thenOk()
  throws Exception {
    this.mockMvc.perform(MockMvcRequestBuilders
      .post("/user")
      .accept(MediaType.TEXT_HTML)
      .param("email", "[email protected]")
      .param("verifyEmail", "[email protected]")
      .param("password", "pass")
      .param("verifyPassword", "passsss"))
      .andExpect(model().errorCount(2))
      .andExpect(status().isOk());
    }

10. Резюме

В этой быстрой статье мы показали, как создавать пользовательские валидаторы для проверки поля или класса и их привязки к Spring MVC.

Как всегда, вы можете найти код из статьиover on Github.