Capítulo 04: Validação com Jakarta Validation (@Valid) e Exception Handling Global
Especialização em Backend com Java & Spring Boot • Spring Boot 3 & Java 21 LTS • Spring Data JPA, Security, Microsserviços e Cloud
🗺️ Mapa Conceitual do Tópico
flowchart TD
A["@RequestBody + @Valid"] --> B["Jakarta Bean Validation (@NotBlank, @Positive, @Email)"]
B -->|Falha| C["MethodArgumentNotValidException"]
B -->|Sucesso| D["Service Layer"]
C --> E["@RestControllerAdvice + @ExceptionHandler"]
E --> F["ProblemDetail (RFC 7807)"]
subgraph ARQ["Arquitetura do Capítulo"]
G["ConstraintValidator Customizado"]
H["Validation Groups (Create vs Update)"]
I["Tratamento Global Centralizado"]
end
E --> ARQ
style A fill:#e1f5fe,stroke:#03a9f4,stroke-width:2px
style B fill:#fff3e0,stroke:#ff9800,stroke-width:2px
style C fill:#ede7f6,stroke:#7e57c2,stroke-width:2px
style D fill:#e8f5e9,stroke:#4caf50,stroke-width:2px
style E fill:#fce4ec,stroke:#e91e63,stroke-width:2px
style F fill:#f3e5f5,stroke:#9c27b0,stroke-width:2px
🏛️ 1. Fundamentos Técnicos: Bean Validation, Exceções Globais e RFC 7807
Como a validação declarativa é acionada. O Spring Boot inclui o Hibernate Validator (implementação de referência da especificação Jakarta Bean Validation) via spring-boot-starter-validation. Anotar um DTO com @NotBlank, @Size, @Positive, @Email etc. só tem efeito quando o parâmetro do Controller é anotado com @Valid (ou @Validated) — é esse gatilho que faz o Spring MVC interceptar a requisição antes de o método executar e rodar todas as constraints. Se qualquer uma falhar, o Spring lança automaticamente MethodArgumentNotValidException, contendo a lista de erros de campo — o método do Controller nunca chega a ser executado com dados inválidos.
Tratamento global com @RestControllerAdvice. Sem um handler central, cada Controller precisaria de blocos try/catch repetidos, e o cliente receberia stack traces ou mensagens inconsistentes. @RestControllerAdvice é um interceptador transversal a todos os Controllers da aplicação: métodos anotados com @ExceptionHandler(TipoDeExcecao.class) capturam a exceção especificada de qualquer lugar da aplicação e produzem uma resposta padronizada.
Padrão RFC 7807 (ProblemDetail). Em vez de cada equipe inventar seu próprio formato de erro JSON, a RFC 7807 define uma estrutura padrão (type, title, status, detail, instance) para erros HTTP. O Spring 6 / Boot 3 oferecem a classe ProblemDetail nativamente — extensível com propriedades customizadas (setProperty("errors", listaDeErros)):
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ProblemDetail> handleValidacao(MethodArgumentNotValidException ex) {
ProblemDetail problema = ProblemDetail.forStatus(HttpStatus.BAD_REQUEST);
problema.setTitle("Erro de Validação");
problema.setProperty("erros", ex.getFieldErrors().stream()
.map(fe -> fe.getField() + ": " + fe.getDefaultMessage()).toList());
return ResponseEntity.badRequest().body(problema);
}
Validadores customizados. Quando as constraints prontas não bastam (ex.: validar dígitos verificadores de CPF), cria-se uma anotação própria com @Constraint(validatedBy = ...) e uma classe que implementa ConstraintValidator<Anotacao, TipoDoCampo>, com a regra em isValid().
Validation Groups. Um mesmo DTO frequentemente serve para criação (POST) e atualização (PUT), mas as regras diferem — por exemplo, id é proibido no POST e obrigatório no PUT. Em vez de duplicar o DTO, usam-se interfaces marcadoras (OnCreate, OnUpdate) no atributo groups de cada constraint, e o Controller especifica qual grupo validar via @Validated(OnCreate.class).
💻 2. Código de Demonstração Corporativo
package com.empresa.api.usuario.dto;
import jakarta.validation.constraints.*;
public record UsuarioRequestDTO(
@NotBlank(message = "Nome é obrigatório") String nome,
@Email(message = "E-mail inválido") @NotBlank String email,
@Positive @Max(120) int idade
) {}
package com.empresa.api.exception;
import org.springframework.http.HttpStatus;
import org.springframework.http.ProblemDetail;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ProblemDetail> handleValidacao(MethodArgumentNotValidException ex) {
ProblemDetail problema = ProblemDetail.forStatus(HttpStatus.BAD_REQUEST);
problema.setTitle("Erro de Validação de Campos");
problema.setDetail("Um ou mais campos da requisição são inválidos.");
problema.setProperty("erros", ex.getFieldErrors().stream()
.map(fe -> fe.getField() + ": " + fe.getDefaultMessage())
.toList());
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(problema);
}
@ExceptionHandler(RecursoNaoEncontradoException.class)
public ResponseEntity<ProblemDetail> handleNaoEncontrado(RecursoNaoEncontradoException ex) {
ProblemDetail problema = ProblemDetail.forStatusAndDetail(HttpStatus.NOT_FOUND, ex.getMessage());
problema.setTitle("Recurso Não Encontrado");
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(problema);
}
}
package com.empresa.api.exception;
public class RecursoNaoEncontradoException extends RuntimeException {
public RecursoNaoEncontradoException(String mensagem) {
super(mensagem);
}
}
// Validador customizado: @CpfValido
package com.empresa.api.validacao;
import jakarta.validation.Constraint;
import jakarta.validation.Payload;
import java.lang.annotation.*;
@Documented
@Constraint(validatedBy = CpfValidator.class)
@Target({ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
public @interface CpfValido {
String message() default "CPF inválido";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
package com.empresa.api.validacao;
import jakarta.validation.ConstraintValidator;
import jakarta.validation.ConstraintValidatorContext;
public class CpfValidator implements ConstraintValidator<CpfValido, String> {
@Override
public boolean isValid(String cpf, ConstraintValidatorContext context) {
if (cpf == null) return false;
String digitos = cpf.replaceAll("\\D", "");
return digitos.length() == 11 && !digitos.chars().allMatch(c -> c == digitos.charAt(0));
// Em produção: calcular os dígitos verificadores completos (módulo 11).
}
}