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