DEV Community

Uiratan Cavalcante
Uiratan Cavalcante

Posted on

Criando um Validador Customizado no Spring usando `Validator`

A validação de dados é um aspecto essencial no desenvolvimento de APIs para garantir a consistência e integridade das informações. O Spring oferece suporte às anotações de Bean Validation, como @NotBlank e @NotNull, mas, em alguns casos, precisamos de validações mais complexas. Um exemplo clássico é a necessidade de verificar se um valor já existe no banco de dados antes de persistir um novo registro. Neste post, vamos criar um validador customizado para evitar a duplicação de categorias utilizando a interface Validator do Spring, explicando cada etapa detalhadamente.


Passo 1: Criando a Entidade Categoria

A primeira etapa é definir a entidade Categoria, que será persistida no banco de dados. Essa entidade representa uma categoria no sistema e deve ter um nome único.

@Entity public class Categoria { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @NotBlank @Column(nullable = false, unique = true) private String nome; public Categoria(String nome) { Assert.hasText(nome, "O nome é obrigatório"); this.nome = nome; } @Deprecated public Categoria() { } @Override public String toString() { return "Categoria{" + "id=" + id + ", nome='" + nome + '\'' + '}'; } } 
Enter fullscreen mode Exit fullscreen mode

Explicação:

  • A anotação @Entity indica que esta classe será mapeada para uma tabela no banco de dados.
  • @Id e @GeneratedValue definem a chave primária e sua estratégia de geração.
  • @Column(nullable = false, unique = true) garante que o nome seja único.
  • O construtor principal recebe o nome da categoria e verifica se ele não está vazio.
  • Um construtor padrão (deprecated) é mantido para uso do JPA.

Passo 2: Criando o DTO NovaCategoriaRequest

Para evitar expor diretamente a entidade no endpoint, criamos um record chamado NovaCategoriaRequest:

public record NovaCategoriaRequest( @NotBlank String nome ) { public Categoria toModel() { return new Categoria(this.nome); } } 
Enter fullscreen mode Exit fullscreen mode

Explicação:

  • @NotBlank garante que o nome da categoria não pode ser vazio ou nulo.
  • O método toModel() converte o DTO para a entidade Categoria.

Passo 3: Criando o Repositório

Precisamos de um repositório para buscar categorias no banco de dados:

public interface CategoriaRepository extends CrudRepository<Categoria, Long> { boolean existsByNome(@NotBlank String nome); } 
Enter fullscreen mode Exit fullscreen mode

Explicação:

  • CrudRepository fornece métodos básicos para manipulação de entidades.
  • existsByNome(String nome) permite verificar se um nome já existe no banco de dados.

Passo 4: Criando o Validador Customizado

Agora criamos a classe ProibeNomeDuplicadoCategoriaValidator, que implementa a interface Validator do Spring para verificar a existência da categoria no banco de dados:

@Component public class ProibeNomeDuplicadoCategoriaValidator implements Validator { private final CategoriaRepository categoriaRepository; public ProibeNomeDuplicadoCategoriaValidator(CategoriaRepository categoriaRepository) { this.categoriaRepository = categoriaRepository; } @Override public boolean supports(Class<?> clazz) { return NovaCategoriaRequest.class.isAssignableFrom(clazz); } @Override public void validate(Object target, Errors errors) { if (errors.hasErrors()) { return; } NovaCategoriaRequest request = (NovaCategoriaRequest) target; if (categoriaRepository.existsByNome(request.nome())) { errors.rejectValue("nome", null, "Já existe uma categoria cadastrada com este nome: " + request.nome()); } } } 
Enter fullscreen mode Exit fullscreen mode

Explicação:

  • Validator é uma interface do Spring usada para criar validadores customizados.
  • supports(Class<?> clazz) define para qual classe esse validador será aplicado.
  • validate(Object target, Errors errors) verifica se o nome da categoria já existe no banco e rejeita a entrada caso positivo.
  • errors.hasErrors() verifica se já há erros anteriores, evitando validações desnecessárias.
  • NovaCategoriaRequest request = (NovaCategoriaRequest) target; faz um cast seguro do objeto recebido.
  • categoriaRepository.existsByNome(request.nome()) consulta o banco para verificar se o nome já foi cadastrado.
  • errors.rejectValue("nome", null, "Já existe uma categoria cadastrada com este nome: " + request.nome()); adiciona um erro ao campo nome, impedindo a criação de categorias duplicadas.

Passo 5: Integrando o Validador no Controller

No CategoriasController, registramos o validador customizado no WebDataBinder dentro do método init:

@RestController public class CategoriasController { private final EntityManager entityManager; private final ProibeNomeDuplicadoCategoriaValidator proibeNomeDuplicadoCategoriaValidator; public CategoriasController( final EntityManager entityManager, final ProibeNomeDuplicadoCategoriaValidator proibeNomeDuplicadoCategoriaValidator) { this.entityManager = entityManager; this.proibeNomeDuplicadoCategoriaValidator = proibeNomeDuplicadoCategoriaValidator; } @InitBinder public void init(WebDataBinder binder) { binder.addValidators(proibeNomeDuplicadoCategoriaValidator); } @PostMapping("/categorias") @ResponseStatus(HttpStatus.OK) @Transactional public String criarCategoria(@RequestBody @Valid NovaCategoriaRequest novaCategoriaRequest) { Categoria novaCategoria = novaCategoriaRequest.toModel(); entityManager.persist(novaCategoria); return novaCategoria.toString(); } } 
Enter fullscreen mode Exit fullscreen mode

Explicação:

  • @InitBinder garante que o validador seja aplicado antes do processamento da requisição.
  • @Valid ativa a validação antes de processar o POST.
  • entityManager.persist(novaCategoria) salva a nova categoria no banco.

O que está acontecendo aqui?

  1. Método anotado com @InitBinder

    • O Spring chama métodos marcados com @InitBinder antes de processar requisições para um controlador.
    • Esse método permite personalizar o WebDataBinder, que é responsável por converter e validar os dados da requisição antes que eles sejam passados ao método do controlador.
  2. Parâmetro WebDataBinder binder

    • O WebDataBinder gerencia a conversão e validação de objetos que chegam na requisição HTTP.
    • Ele pode ser configurado para adicionar conversores personalizados, configurações específicas e validadores.
  3. binder.addValidators(proibeNomeDuplicadoCategoriaValidator);

    • Esse trecho adiciona o validador ProibeNomeDuplicadoCategoriaValidator ao WebDataBinder.
    • Com isso, toda vez que o NovaCategoriaRequest for recebido em uma requisição, o Spring automaticamente executará esse validador antes de chamar o método do controlador.
    • Se o validador encontrar um erro (como um nome duplicado), ele adicionará esse erro ao contexto da validação, impedindo que a requisição continue.

Por que usar @InitBinder?

  • Garante que a validação seja aplicada a todas as requisições que manipulam NovaCategoriaRequest, sem precisar chamá-la explicitamente em cada endpoint.
  • Mantém o código do controlador mais limpo, pois a validação ocorre antes do método do controlador ser chamado.
  • Permite adicionar múltiplos validadores de maneira centralizada.

Em qual contexto esse método é chamado?

Sempre que um POST /categorias for feito, e o @RequestBody @Valid NovaCategoriaRequest for processado, o Spring chamará automaticamente esse @InitBinder, garantindo que o validador personalizado seja executado antes da persistência da entidade.


Passo 6: Testando o Validador

Requisição Válida:

{ "nome": "Tecnologia" } 
Enter fullscreen mode Exit fullscreen mode

Resposta:

HTTP 200 OK { "id": 1, "nome": "Tecnologia" } 
Enter fullscreen mode Exit fullscreen mode

Requisição Duplicada:

{ "nome": "Tecnologia" } 
Enter fullscreen mode Exit fullscreen mode

Resposta:

HTTP 400 BAD REQUEST { "errors": [ { "field": "nome", "message": "Já existe uma categoria cadastrada com este nome: Tecnologia" } ] } 
Enter fullscreen mode Exit fullscreen mode

Conclusão

Criar validadores customizados no Spring com Validator permite adicionar regras de negócio específicas sem poluir o controlador. Com essa abordagem, garantimos que a lógica de validação fique separada da lógica de controle, tornando o código mais modular e fácil de manter.

Se você deseja um sistema robusto e confiável, considere usar validadores customizados para garantir a integridade dos seus dados! 🚀


Top comments (0)