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.
<span>@Entity</span><span>public</span> <span>class</span> <span>Categoria</span> <span>{</span><span>@Id</span><span>@GeneratedValue</span><span>(</span><span>strategy</span> <span>=</span> <span>GenerationType</span><span>.</span><span>IDENTITY</span><span>)</span><span>private</span> <span>Long</span> <span>id</span><span>;</span><span>@NotBlank</span><span>@Column</span><span>(</span><span>nullable</span> <span>=</span> <span>false</span><span>,</span> <span>unique</span> <span>=</span> <span>true</span><span>)</span><span>private</span> <span>String</span> <span>nome</span><span>;</span><span>public</span> <span>Categoria</span><span>(</span><span>String</span> <span>nome</span><span>)</span> <span>{</span><span>Assert</span><span>.</span><span>hasText</span><span>(</span><span>nome</span><span>,</span> <span>"O nome é obrigatório"</span><span>);</span><span>this</span><span>.</span><span>nome</span> <span>=</span> <span>nome</span><span>;</span><span>}</span><span>@Deprecated</span><span>public</span> <span>Categoria</span><span>()</span> <span>{</span> <span>}</span><span>@Override</span><span>public</span> <span>String</span> <span>toString</span><span>()</span> <span>{</span><span>return</span> <span>"Categoria{"</span> <span>+</span><span>"id="</span> <span>+</span> <span>id</span> <span>+</span><span>", nome='"</span> <span>+</span> <span>nome</span> <span>+</span> <span>'\''</span> <span>+</span><span>'}'</span><span>;</span><span>}</span><span>}</span><span>@Entity</span> <span>public</span> <span>class</span> <span>Categoria</span> <span>{</span> <span>@Id</span> <span>@GeneratedValue</span><span>(</span><span>strategy</span> <span>=</span> <span>GenerationType</span><span>.</span><span>IDENTITY</span><span>)</span> <span>private</span> <span>Long</span> <span>id</span><span>;</span> <span>@NotBlank</span> <span>@Column</span><span>(</span><span>nullable</span> <span>=</span> <span>false</span><span>,</span> <span>unique</span> <span>=</span> <span>true</span><span>)</span> <span>private</span> <span>String</span> <span>nome</span><span>;</span> <span>public</span> <span>Categoria</span><span>(</span><span>String</span> <span>nome</span><span>)</span> <span>{</span> <span>Assert</span><span>.</span><span>hasText</span><span>(</span><span>nome</span><span>,</span> <span>"O nome é obrigatório"</span><span>);</span> <span>this</span><span>.</span><span>nome</span> <span>=</span> <span>nome</span><span>;</span> <span>}</span> <span>@Deprecated</span> <span>public</span> <span>Categoria</span><span>()</span> <span>{</span> <span>}</span> <span>@Override</span> <span>public</span> <span>String</span> <span>toString</span><span>()</span> <span>{</span> <span>return</span> <span>"Categoria{"</span> <span>+</span> <span>"id="</span> <span>+</span> <span>id</span> <span>+</span> <span>", nome='"</span> <span>+</span> <span>nome</span> <span>+</span> <span>'\''</span> <span>+</span> <span>'}'</span><span>;</span> <span>}</span> <span>}</span>@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
:
<span>public</span> <span>record</span> <span>NovaCategoriaRequest</span><span>(</span><span>@NotBlank</span><span>String</span> <span>nome</span><span>)</span> <span>{</span><span>public</span> <span>Categoria</span> <span>toModel</span><span>()</span> <span>{</span><span>return</span> <span>new</span> <span>Categoria</span><span>(</span><span>this</span><span>.</span><span>nome</span><span>);</span><span>}</span><span>}</span><span>public</span> <span>record</span> <span>NovaCategoriaRequest</span><span>(</span> <span>@NotBlank</span> <span>String</span> <span>nome</span> <span>)</span> <span>{</span> <span>public</span> <span>Categoria</span> <span>toModel</span><span>()</span> <span>{</span> <span>return</span> <span>new</span> <span>Categoria</span><span>(</span><span>this</span><span>.</span><span>nome</span><span>);</span> <span>}</span> <span>}</span>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 entidadeCategoria
.
Passo 3: Criando o Repositório
Precisamos de um repositório para buscar categorias no banco de dados:
<span>public</span> <span>interface</span> <span>CategoriaRepository</span> <span>extends</span> <span>CrudRepository</span><span><</span><span>Categoria</span><span>,</span> <span>Long</span><span>></span> <span>{</span><span>boolean</span> <span>existsByNome</span><span>(</span><span>@NotBlank</span> <span>String</span> <span>nome</span><span>);</span><span>}</span><span>public</span> <span>interface</span> <span>CategoriaRepository</span> <span>extends</span> <span>CrudRepository</span><span><</span><span>Categoria</span><span>,</span> <span>Long</span><span>></span> <span>{</span> <span>boolean</span> <span>existsByNome</span><span>(</span><span>@NotBlank</span> <span>String</span> <span>nome</span><span>);</span> <span>}</span>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:
<span>@Component</span><span>public</span> <span>class</span> <span>ProibeNomeDuplicadoCategoriaValidator</span> <span>implements</span> <span>Validator</span> <span>{</span><span>private</span> <span>final</span> <span>CategoriaRepository</span> <span>categoriaRepository</span><span>;</span><span>public</span> <span>ProibeNomeDuplicadoCategoriaValidator</span><span>(</span><span>CategoriaRepository</span> <span>categoriaRepository</span><span>)</span> <span>{</span><span>this</span><span>.</span><span>categoriaRepository</span> <span>=</span> <span>categoriaRepository</span><span>;</span><span>}</span><span>@Override</span><span>public</span> <span>boolean</span> <span>supports</span><span>(</span><span>Class</span><span><?></span> <span>clazz</span><span>)</span> <span>{</span><span>return</span> <span>NovaCategoriaRequest</span><span>.</span><span>class</span><span>.</span><span>isAssignableFrom</span><span>(</span><span>clazz</span><span>);</span><span>}</span><span>@Override</span><span>public</span> <span>void</span> <span>validate</span><span>(</span><span>Object</span> <span>target</span><span>,</span> <span>Errors</span> <span>errors</span><span>)</span> <span>{</span><span>if</span> <span>(</span><span>errors</span><span>.</span><span>hasErrors</span><span>())</span> <span>{</span><span>return</span><span>;</span><span>}</span><span>NovaCategoriaRequest</span> <span>request</span> <span>=</span> <span>(</span><span>NovaCategoriaRequest</span><span>)</span> <span>target</span><span>;</span><span>if</span> <span>(</span><span>categoriaRepository</span><span>.</span><span>existsByNome</span><span>(</span><span>request</span><span>.</span><span>nome</span><span>()))</span> <span>{</span><span>errors</span><span>.</span><span>rejectValue</span><span>(</span><span>"nome"</span><span>,</span> <span>null</span><span>,</span> <span>"Já existe uma categoria cadastrada com este nome: "</span> <span>+</span> <span>request</span><span>.</span><span>nome</span><span>());</span><span>}</span><span>}</span><span>}</span><span>@Component</span> <span>public</span> <span>class</span> <span>ProibeNomeDuplicadoCategoriaValidator</span> <span>implements</span> <span>Validator</span> <span>{</span> <span>private</span> <span>final</span> <span>CategoriaRepository</span> <span>categoriaRepository</span><span>;</span> <span>public</span> <span>ProibeNomeDuplicadoCategoriaValidator</span><span>(</span><span>CategoriaRepository</span> <span>categoriaRepository</span><span>)</span> <span>{</span> <span>this</span><span>.</span><span>categoriaRepository</span> <span>=</span> <span>categoriaRepository</span><span>;</span> <span>}</span> <span>@Override</span> <span>public</span> <span>boolean</span> <span>supports</span><span>(</span><span>Class</span><span><?></span> <span>clazz</span><span>)</span> <span>{</span> <span>return</span> <span>NovaCategoriaRequest</span><span>.</span><span>class</span><span>.</span><span>isAssignableFrom</span><span>(</span><span>clazz</span><span>);</span> <span>}</span> <span>@Override</span> <span>public</span> <span>void</span> <span>validate</span><span>(</span><span>Object</span> <span>target</span><span>,</span> <span>Errors</span> <span>errors</span><span>)</span> <span>{</span> <span>if</span> <span>(</span><span>errors</span><span>.</span><span>hasErrors</span><span>())</span> <span>{</span> <span>return</span><span>;</span> <span>}</span> <span>NovaCategoriaRequest</span> <span>request</span> <span>=</span> <span>(</span><span>NovaCategoriaRequest</span><span>)</span> <span>target</span><span>;</span> <span>if</span> <span>(</span><span>categoriaRepository</span><span>.</span><span>existsByNome</span><span>(</span><span>request</span><span>.</span><span>nome</span><span>()))</span> <span>{</span> <span>errors</span><span>.</span><span>rejectValue</span><span>(</span><span>"nome"</span><span>,</span> <span>null</span><span>,</span> <span>"Já existe uma categoria cadastrada com este nome: "</span> <span>+</span> <span>request</span><span>.</span><span>nome</span><span>());</span> <span>}</span> <span>}</span> <span>}</span>@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 camponome
, 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
:
<span>@RestController</span><span>public</span> <span>class</span> <span>CategoriasController</span> <span>{</span><span>private</span> <span>final</span> <span>EntityManager</span> <span>entityManager</span><span>;</span><span>private</span> <span>final</span> <span>ProibeNomeDuplicadoCategoriaValidator</span> <span>proibeNomeDuplicadoCategoriaValidator</span><span>;</span><span>public</span> <span>CategoriasController</span><span>(</span><span>final</span> <span>EntityManager</span> <span>entityManager</span><span>,</span><span>final</span> <span>ProibeNomeDuplicadoCategoriaValidator</span> <span>proibeNomeDuplicadoCategoriaValidator</span><span>)</span> <span>{</span><span>this</span><span>.</span><span>entityManager</span> <span>=</span> <span>entityManager</span><span>;</span><span>this</span><span>.</span><span>proibeNomeDuplicadoCategoriaValidator</span> <span>=</span> <span>proibeNomeDuplicadoCategoriaValidator</span><span>;</span><span>}</span><span>@InitBinder</span><span>public</span> <span>void</span> <span>init</span><span>(</span><span>WebDataBinder</span> <span>binder</span><span>)</span> <span>{</span><span>binder</span><span>.</span><span>addValidators</span><span>(</span><span>proibeNomeDuplicadoCategoriaValidator</span><span>);</span><span>}</span><span>@PostMapping</span><span>(</span><span>"/categorias"</span><span>)</span><span>@ResponseStatus</span><span>(</span><span>HttpStatus</span><span>.</span><span>OK</span><span>)</span><span>@Transactional</span><span>public</span> <span>String</span> <span>criarCategoria</span><span>(</span><span>@RequestBody</span> <span>@Valid</span> <span>NovaCategoriaRequest</span> <span>novaCategoriaRequest</span><span>)</span> <span>{</span><span>Categoria</span> <span>novaCategoria</span> <span>=</span> <span>novaCategoriaRequest</span><span>.</span><span>toModel</span><span>();</span><span>entityManager</span><span>.</span><span>persist</span><span>(</span><span>novaCategoria</span><span>);</span><span>return</span> <span>novaCategoria</span><span>.</span><span>toString</span><span>();</span><span>}</span><span>}</span><span>@RestController</span> <span>public</span> <span>class</span> <span>CategoriasController</span> <span>{</span> <span>private</span> <span>final</span> <span>EntityManager</span> <span>entityManager</span><span>;</span> <span>private</span> <span>final</span> <span>ProibeNomeDuplicadoCategoriaValidator</span> <span>proibeNomeDuplicadoCategoriaValidator</span><span>;</span> <span>public</span> <span>CategoriasController</span><span>(</span> <span>final</span> <span>EntityManager</span> <span>entityManager</span><span>,</span> <span>final</span> <span>ProibeNomeDuplicadoCategoriaValidator</span> <span>proibeNomeDuplicadoCategoriaValidator</span><span>)</span> <span>{</span> <span>this</span><span>.</span><span>entityManager</span> <span>=</span> <span>entityManager</span><span>;</span> <span>this</span><span>.</span><span>proibeNomeDuplicadoCategoriaValidator</span> <span>=</span> <span>proibeNomeDuplicadoCategoriaValidator</span><span>;</span> <span>}</span> <span>@InitBinder</span> <span>public</span> <span>void</span> <span>init</span><span>(</span><span>WebDataBinder</span> <span>binder</span><span>)</span> <span>{</span> <span>binder</span><span>.</span><span>addValidators</span><span>(</span><span>proibeNomeDuplicadoCategoriaValidator</span><span>);</span> <span>}</span> <span>@PostMapping</span><span>(</span><span>"/categorias"</span><span>)</span> <span>@ResponseStatus</span><span>(</span><span>HttpStatus</span><span>.</span><span>OK</span><span>)</span> <span>@Transactional</span> <span>public</span> <span>String</span> <span>criarCategoria</span><span>(</span><span>@RequestBody</span> <span>@Valid</span> <span>NovaCategoriaRequest</span> <span>novaCategoriaRequest</span><span>)</span> <span>{</span> <span>Categoria</span> <span>novaCategoria</span> <span>=</span> <span>novaCategoriaRequest</span><span>.</span><span>toModel</span><span>();</span> <span>entityManager</span><span>.</span><span>persist</span><span>(</span><span>novaCategoria</span><span>);</span> <span>return</span> <span>novaCategoria</span><span>.</span><span>toString</span><span>();</span> <span>}</span> <span>}</span>@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 oPOST
. -
entityManager.persist(novaCategoria)
salva a nova categoria no banco.
O que está acontecendo aqui?
-
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.
- O Spring chama métodos marcados com
-
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.
- O
-
binder.addValidators(proibeNomeDuplicadoCategoriaValidator);
- Esse trecho adiciona o validador
ProibeNomeDuplicadoCategoriaValidator
aoWebDataBinder
. - 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.
- Esse trecho adiciona o validador
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:
<span>{</span><span> </span><span>"nome"</span><span>:</span><span> </span><span>"Tecnologia"</span><span> </span><span>}</span><span> </span><span>{</span><span> </span><span>"nome"</span><span>:</span><span> </span><span>"Tecnologia"</span><span> </span><span>}</span><span> </span>{ "nome": "Tecnologia" }
Enter fullscreen mode Exit fullscreen mode
Resposta:
HTTP 200 OK{"id": 1,"nome": "Tecnologia"}HTTP 200 OK { "id": 1, "nome": "Tecnologia" }HTTP 200 OK { "id": 1, "nome": "Tecnologia" }
Enter fullscreen mode Exit fullscreen mode
Requisição Duplicada:
<span>{</span><span> </span><span>"nome"</span><span>:</span><span> </span><span>"Tecnologia"</span><span> </span><span>}</span><span> </span><span>{</span><span> </span><span>"nome"</span><span>:</span><span> </span><span>"Tecnologia"</span><span> </span><span>}</span><span> </span>{ "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"}]}HTTP 400 BAD REQUEST { "errors": [ { "field": "nome", "message": "Já existe uma categoria cadastrada com este nome: Tecnologia" } ] }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!
原文链接:Criando um Validador Customizado no Spring usando `Validator`
暂无评论内容