Validação personalizada do Spring MVC

Validação personalizada do Spring MVC

1. Visão geral

Geralmente, quando precisamos validar a entrada do usuário, o Spring MVC oferece validadores predefinidos padrão.

No entanto, quando precisamos validar uma entrada de tipo mais particular,we have the possibility of creating our own, custom validation logic.

Neste artigo, faremos exatamente isso - criaremos um validador personalizado para validar um formulário com um campo de número de telefone e, em seguida, mostraremos um validador personalizado para vários campos.

2. Configuração

Para se beneficiar da API, adicione a dependência ao seu arquivopom.xml:


    org.hibernate
    hibernate-validator
    6.0.10.Final

A versão mais recente da dependência pode ser verificadahere.

Se estivermos usando Spring Boot, então podemos adicionar apenas ospring-boot-starter-web, que trará a dependênciahibernate-validator também.

3. Validação Personalizada

Criar um validador customizado implica implementar nossa própria anotação e usá-la em nosso modelo para impor as regras de validação.

Então, vamos criar nossocustom validator – which checks phone numbers. O número de telefone deve ser um número com mais de oito dígitos, mas não mais que 11 dígitos.

4. A Nova Anotação

Vamos criar um novo@interface para definir nossa anotação:

@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 {};
}

Com a anotação@Constraint, definimos a classe que vai validar nosso campo, omessage() é a mensagem de erro que é mostrada na interface do usuário e o código adicional é a maioria dos códigos clichê para obedecer ao Padrões de primavera.

5. Criando um validador

Vamos agora criar uma classe validadora que impõe regras de nossa validação:

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);
    }

}

A classe de validação implementa a interfaceConstraintValidator e deve implementar o métodoisValid; é neste método que definimos nossas regras de validação.

Naturalmente, vamos usar uma regra de validação simples aqui, para mostrar como funciona o validador.

ConstraintValidator ddefine a lógica para validar uma determinada restrição para um determinado objeto. As implementações devem obedecer à seguinte restrição:

  • o objeto deve resolver para um tipo não parametrizado

  • parâmetros genéricos do objeto devem ser tipos curinga ilimitados

6. Aplicação de anotação de validação

Em nosso caso, criamos uma classe simples com um campo para aplicar as regras de validação. Aqui, estamos configurando nosso campo anotado para ser validado:

@ContactNumberConstraint
private String phone;

Definimos um campo de string e o anotamos com nossa anotação personalizada@ContactNumberConstraint. Em nosso controlador, criamos nossos mapeamentos e tratamos o erro, se houver:

@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";
    }
}

Definimos este controlador simples que tem uma única páginaJSP e usamos o métodosubmitForm para forçar a validação do nosso número de telefone.

7. A vista

Nossa visão é uma página JSP básica com um formulário que possui um único campo. Quando o usuário envia o formulário, o campo é validado pelo nosso validador personalizado e redireciona para a mesma página com a mensagem de validação bem-sucedida ou com falha:


    
    
    
    

8. Testes

Vamos agora testar nosso controlador e verificar se ele está nos dando a resposta e visão adequadas:

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

Além disso, vamos testar se nosso campo é validado, com base na entrada do usuário:

@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());
}

No teste, fornecemos a um usuário a entrada “123” e - como esperávamos - tudo está funcionando ewe’re seeing the error on the client side.

9. Validação de nível de classe personalizada

Uma anotação de validação personalizada também pode ser definida no nível da classe para validar mais de um atributo da classe.

Um caso de uso comum para esse cenário é verificar se dois campos de uma classe têm valores correspondentes.

9.1. Criação da anotação

Vamos adicionar uma nova anotação chamadaFieldsValueMatch que pode ser aplicada posteriormente em uma classe. A anotação terá dois parâmetrosfield efieldMatch que representam os nomes dos campos a serem comparados:

@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();
    }
}

Podemos ver que nossa anotação personalizada também contém uma subinterfaceList para definir várias anotaçõesFieldsValueMatch em uma classe.

9.2. Criação do validador

Em seguida, precisamos adicionar a classeFieldsValueMatchValidator que conterá a lógica de validação real:

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;
        }
    }
}

O métodoisValid() recupera os valores dos dois campos e verifica se eles são iguais.

9.3. Aplicando a Anotação

Vamos criar uma classe de modeloNewUserForm destinada aos dados necessários para um registro de usuário, que tem dois atributosemailepassword, junto com dois atributosverifyEmaileverifyPassword para inserir novamente os dois valores.

Como temos dois campos para verificar em relação aos campos correspondentes correspondentes, vamos adicionar duas anotações@FieldsValueMatch na classeNewUserForm, uma para os valoresemail e uma para os valorespassword:

@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
}

Para validar o modelo no Spring MVC, vamos criar um controlador com um mapeamento POST/user que recebe um objetoNewUserForm anotado com@Valid e verifica se há algum erro de validação:

@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. Testando a Anotação

Para verificar nossa anotação de nível de classe personalizada, vamos escrever um testeJUnit que envia informações correspondentes ao endpoint/user e, em seguida, verifica se a resposta não contém erros:

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());
    }
}

A seguir, vamos também adicionar um testeJUnit que envia informações não correspondentes ao endpoint/user e afirmar que o resultado conterá dois erros:

@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. Sumário

Neste artigo rápido, mostramos como criar validadores personalizados para verificar um campo ou classe e conectá-los ao Spring MVC.

Como sempre, você pode encontrar o código no artigoover on Github.