A validação de parâmetros de entrada em aplicações Java é uma tarefa comum e essencial. Frequentemente, antes de executar a lógica de negócio de um método, é necessário verificar a validade dos argumentos recebidos, como a ausência de valores nulos ou a conformidade com formatos esperados. Tradicionalmente, isso envolvia uma série de verificações condicionais (if-else), o que resultava em código menos legível e elegante. Para padronizar e simplificar esse processo, a Java Community Process (JCP) definiu a Java Validation API (também conhecida como JSR 303).
É importante notar que a Java Validation API define apenas um contrato (uma especificação), e não uma implementação concreta. Diversas bibliotecas fornecem essa implementação. Abaixo, exploramos duas das mais utilizadas.
Implementações da Java Validation API
Hibernate Validator
Esta é uma implementação popular da API de validação, desenvolvida pela equipe do Hibernate. Para validar objetos complexos (POJOs), você pode utilizar a anotação @Valid em conjunto com as anotações de validação padrão. No entanto, o Hibernate Validator puro não permite a aplicação direta de anotações como @NotNull ou @Min em parâmetros individuais de métodos.
<dependency>
<groupId>org.hibernate.validator</groupId>
<artifactId>hibernate-validator</artifactId>
<version>6.0.20.Final</version>
</dependency>
Spring Boot Starter Validation
O Spring Boot oferece uma abstração e extensão sobre o Hibernate Validator através do starter spring-boot-starter-validation. Alternativamente, se você já utiliza spring-boot-starter-web, a dependência de validação já está incluída. O Spring introduz a anotação @Validated, que pode ser aplicada tanto a nível de classe quanto para marcar parâmetros de métodos. Sua principal vantagem é o suporte à validação de parâmetros individuais, desde que a anotação @Validated seja aplicada na classe controladora.
A anotação @Validated pode substituir completamente o uso de @Valid, oferecendo maior flexibilidade.
<!-- Se você já usa web starter, a validação já está incluída -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Ou adicione explicitamente -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
Analisando as dependências, tanto starter-web quanto starter-validation dependem de hibernate-validator, que por sua vez depende da validation-api. Anotações comuns como @NotBlank, @NotNull e @Min são provenientes do pacote javax.validation.constraints da validation-api. O uso de anotações específicas do Hibernate Validator não é mais recomendado em favor das anotações padronizadas pela JSR.
Exemplo de Uso com Grupos de Validação
Considere os seguintes requisitos:
- Registro de Usuário: Nome, idade e e-mail não podem ser vazios.
- Atualização de Usuário: Nome, idade, e-mail e ID do usuário não podem ser vazios.
- Consulta de Usuário: Recebe apenas o ID do usuário como parâmetro individual.
Definição de Grupos de Validação
Para diferenciar as regras de validação entre registro e atualização, podemos usar grupos:
/**
* Grupo de validação para operações de adição.
*/
public interface ValidacaoAdicao {
}
/**
* Grupo de validação para operações de atualização.
*/
public interface ValidacaoAtualizacao {
}
Objeto de Requisição do Usuário
Definimos o DTO (Data Transfer Object) com as anotações de validação, especificando os grupos quando necessário.
import javax.validation.constraints.Email;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import lombok.Builder;
import lombok.Data;
/**
* Objeto de requisição para operações de usuário.
*/
@Data
@Builder
public class UserRequest implements java.io.Serializable {
private static final long serialVersionUID = -2655536314774756670L;
/**
* Identificador único do usuário.
*/
@NotNull(message = "ID do usuário não pode ser nulo", groups = {ValidacaoAtualizacao.class})
@Min(value = 1, message = "ID do usuário deve ser positivo", groups = {ValidacaoAtualizacao.class})
private Long userId;
/**
* Idade do usuário.
*/
@NotNull(message = "Idade não pode ser nula", groups = {ValidacaoAtualizacao.class, ValidacaoAdicao.class})
@Min(value = 1, message = "Idade deve ser pelo menos 1", groups = {ValidacaoAtualizacao.class, ValidacaoAdicao.class})
private Integer age;
/**
* Nome do usuário.
*/
@NotBlank(message = "Nome não pode ser vazio", groups = {ValidacaoAtualizacao.class, ValidacaoAdicao.class})
private String name;
/**
* Endereço de e-mail do usuário.
*/
@NotBlank(message = "E-mail não pode ser vazio", groups = {ValidacaoAtualizacao.class, ValidacaoAdicao.class})
@Email(message = "Formato de e-mail inválido")
private String email;
/**
* Apelido do usuário.
*/
private String nickName;
}
Controlador (Controller)
No controlador, usamos @Validated na classe e nas chamadas de método para especificar os grupos de validação e coletar os resultados através de BindingResult.
import org.springframework.validation.BindingResult;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotNull;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
/**
* Controlador para operações de usuário.
*/
@RestController
@Validated // Habilita a validação de parâmetros individuais nesta classe
public class UserController {
/**
* Registra um novo usuário.
* @param userRequest Dados do usuário a serem registrados.
* @param bindingResult Resultado da validação.
* @return Mensagens de erro, se houver.
*/
@PostMapping("/user/register")
public String registerUser(@Validated(ValidacaoAdicao.class) @RequestBody UserRequest userRequest, BindingResult bindingResult) {
return processValidationErrors(bindingResult);
}
/**
* Obtém informações de um usuário por ID.
* @param userId O ID do usuário a ser buscado.
* @param bindingResult Resultado da validação.
* @return Mensagens de erro, se houver.
*/
@PostMapping("/user/get")
public String getUser(@NotNull(message = "ID do usuário é obrigatório") @Min(value = 1, message = "ID do usuário deve ser positivo") Long userId, BindingResult bindingResult) {
// Lógica para obter o usuário seria colocada aqui
return processValidationErrors(bindingResult);
}
/**
* Atualiza um usuário existente.
* @param userRequest Dados atualizados do usuário.
* @param bindingResult Resultado da validação.
* @return Mensagens de erro, se houver.
*/
@PostMapping("/user/update")
public String updateUser(@Validated(ValidacaoAtualizacao.class) @RequestBody UserRequest userRequest, BindingResult bindingResult) {
return processValidationErrors(bindingResult);
}
/**
* Processa e retorna os erros de validação.
* @param bindingResult O resultado da validação.
* @return Uma string contendo as mensagens de erro concatenadas, ou uma string vazia se não houver erros.
*/
private String processValidationErrors(BindingResult bindingResult) {
if (bindingResult.hasErrors()) {
return bindingResult.getFieldErrors().stream()
.map(fieldError -> fieldError.getDefaultMessage())
.collect(Collectors.joining(", "));
}
return ""; // Indica sucesso
}
}
Tratamento Centralizado de Exceções de Validação
A abordagem anterior exige a passagem de BindingResult em cada método do controlador. Para uma solução mais limpa, podemos centralizar o tratamento das exceções de validação usando @RestControllerAdvice.
import org.springframework.validation.BindException;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;
import javax.validation.ConstraintViolation;
import javax.validation.ConstraintViolationException;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
/**
* Handler global para exceções de validação e outros erros relacionados a requisições.
*/
@RestControllerAdvice
public class GlobalExceptionHandler {
/**
* Trata exceções de validação para argumentos de método não válidos (@RequestBody).
* @param exception A exceção lançada.
* @return Uma resposta padronizada com os erros.
*/
@ExceptionHandler(MethodArgumentNotValidException.class)
public ApiResponse handleMethodArgumentNotValidException(MethodArgumentNotValidException exception) {
return formatValidationErrors(exception.getBindingResult());
}
/**
* Trata exceções de quando o tipo do argumento do método não corresponde.
* @param exception A exceção lançada.
* @return Uma resposta padronizada com o erro.
*/
@ExceptionHandler(MethodArgumentTypeMismatchException.class)
public ApiResponse handleMethodArgumentTypeMismatchException(MethodArgumentTypeMismatchException exception) {
String errorMessage = String.format("Parâmetro '%s' com valor inválido '%s'. Esperado tipo %s.",
exception.getName(),
exception.getValue(),
exception.getRequiredType().getSimpleName());
return new ApiResponse(400, errorMessage, null);
}
/**
* Trata exceções de vinculação de parâmetros (geralmente para parâmetros de formulário ou query).
* @param exception A exceção lançada.
* @return Uma resposta padronizada com os erros.
*/
@ExceptionHandler(value = BindException.class)
public ApiResponse handleBindException(BindException exception) {
return formatValidationErrors(exception.getBindingResult());
}
/**
* Trata exceções de violação de restrições para parâmetros individuais.
* Usado quando @Validated é aplicado a parâmetros de método diretamente.
* @param exception A exceção lançada.
* @return Uma resposta padronizada com os erros.
*/
@ExceptionHandler(value = ConstraintViolationException.class)
public ApiResponse handleConstraintViolationException(ConstraintViolationException exception) {
return formatConstraintViolations(exception.getConstraintViolations());
}
/**
* Formata os erros de validação de um BindingResult em uma resposta padronizada.
* @param result O BindingResult contendo os erros.
* @return Um objeto ApiResponse com as mensagens de erro.
*/
private ApiResponse formatValidationErrors(BindingResult result) {
List<string> errors = result.getFieldErrors().stream()
.map(fieldError -> String.format("%s: %s", fieldError.getField(), fieldError.getDefaultMessage()))
.collect(Collectors.toList());
return new ApiResponse(400, "Erros de validação", errors);
}
/**
* Formata um conjunto de ConstraintViolations em uma resposta padronizada.
* @param violations O conjunto de violações de restrição.
* @return Um objeto ApiResponse com as mensagens de erro.
*/
private ApiResponse formatConstraintViolations(Set extends ConstraintViolation> violations) {
List<string> errors = violations.stream()
.map(ConstraintViolation::getMessage)
.collect(Collectors.toList());
return new ApiResponse(400, "Erros de validação", errors);
}
// Assumindo que você tem uma classe base de resposta como esta:
// public class ApiResponse {
// private int status;
// private String message;
// private Object data;
//
// public ApiResponse(int status, String message, Object data) {
// this.status = status;
// this.message = message;
// this.data = data;
// }
// // Getters e Setters...
// }
}
</string></string>
Com o GlobalExceptionHandler configurado, não é mais necessário passar BindingResult nos métodos do controlador. As exceções de validação serão capturadas e tratadsa automaticamente, retornando uma resposta padronizada ao cliente.