404 Not Found

404 Not Found


nginx

Tratamento Global de Exceções

O tratamento global de exceções padroniza o formato das respostas de erro da API—os clientes não precisam mais lidar com uma grande variedade de formatos de erro, tornando a depuração e integração mais eficientes.

1. O Que Você Vai Aprender


2. Uma História Real de um Desenvolvedor Front-End

(1) Ponto de Dor: Formatos de resposta de erro inconsistentes

Bob é um desenvolvedor front-end que teve problemas ao integrar com a API do OrderFlow: alguns endpoints retornavam 404 com texto puro "Not Found", alguns retornavam 500 com uma página de erro HTML, alguns retornavam uma exceção de negócio {"error": "xxx"}, e outros retornavam {"message": "xxx"}. Ele tinha que escrever lógica de tratamento de erro diferente para cada endpoint, resultando em blocos try-catch espalhados pelo código—e frequentemente os esquecia, fazendo a página ficar em branco.

(2) Solução com @RestControllerAdvice

Um handler de exceção unificado garante que todas as respostas de erro sigam um formato consistente:

JAVA
@RestControllerAdvice
public class GlobalExceptionHandler {
    @ExceptionHandler(ResourceNotFoundException.class)
    public ResponseEntity<ErrorResponse> handleNotFound(ResourceNotFoundException ex) {
        return ResponseEntity.status(HttpStatus.NOT_FOUND)
            .body(new ErrorResponse("NOT_FOUND", ex.getMessage(), Instant.now()));
    }
}

(3) Resultado

Depois que Alice implementou o tratamento global de exceções, todos os formatos de resposta de erro foram padronizados para {code, message, timestamp, details}, e o front-end de Bob precisou de apenas uma única função de tratamento de erro unificada, resultando em um aumento de cinco vezes na eficiência de integração.


3. O Mecanismo @RestControllerAdvice

(1) Processo de Despacho de Tratamento de Exceções

100%
graph TD
    A[Controller lança Exceção] --> B{Spring DispatcherServlet}
    B --> C["@RestControllerAdvice<br/>Escaneia @ExceptionHandler"]
    C --> D{Corresponde Tipo de Exceção?}
    D -->|Sim| E["Executar @ExceptionHandler<br/>Retornar ErrorResponse"]
    D -->|Não| F["Spring Padrão<br/>Resposta de Erro"]
    E --> G["Cliente recebe<br/>JSON Consistente"]
    F --> H["Cliente recebe<br/>Resposta inconsistente"]
Anotação Finalidade Localização
@RestControllerAdvice Classe de Tratamento Global de Exceções Na classe
@ExceptionHandler Tratar Tipos de Exceção Especificados No método
@ResponseStatus Especificar um código de status de resposta na classe de exceção ou método

(1) ▶ Exemplo: Handler de Exceção Global Básico

JAVA
@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(ResourceNotFoundException.class)
    public ResponseEntity<ErrorResponse> handleResourceNotFound(
            ResourceNotFoundException ex) {
        ErrorResponse error = new ErrorResponse(
            "RESOURCE_NOT_FOUND", ex.getMessage(), Instant.now(), null);
        return ResponseEntity.status(HttpStatus.NOT_FOUND).body(error);
    }

    @ExceptionHandler(BusinessException.class)
    public ResponseEntity<ErrorResponse> handleBusiness(
            BusinessException ex) {
        ErrorResponse error = new ErrorResponse(
            "BUSINESS_ERROR", ex.getMessage(), Instant.now(), null);
        return ResponseEntity.status(HttpStatus.UNPROCESSABLE_ENTITY).body(error);
    }

    @ExceptionHandler(Exception.class)
    public ResponseEntity<ErrorResponse> handleGeneral(Exception ex) {
        ErrorResponse error = new ErrorResponse(
            "INTERNAL_ERROR", "An unexpected error occurred", Instant.now(), null);
        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(error);
    }
}

Saída:

TEXT
// Execução bem-sucedida

4. Sistema de Exceções de Negócio Personalizado

(1) Hierarquia de Classes de Exceção

100%
graph TD
    A[RuntimeException] --> B[BusinessException<br/>Exceção de negócio base]
    B --> C[ResourceNotFoundException<br/>404 Not Found]
    B --> D[InsufficientStockException<br/>422 Violação de Regra de Negócio]
    B --> E[OrderStateException<br/>422 Transição de Estado Inválida]
    A --> F[ValidationException<br/>400 Bad Request]

(1) ▶ Exemplo: Classe de Exceção Personalizada

JAVA
// Exceção de negócio base
public class BusinessException extends RuntimeException {
    private final String errorCode;

    public BusinessException(String errorCode, String message) {
        super(message);
        this.errorCode = errorCode;
    }

    public String getErrorCode() { return errorCode; }
}

// Recurso não encontrado
public class ResourceNotFoundException extends BusinessException {
    public ResourceNotFoundException(String resource, Long id) {
        super("RESOURCE_NOT_FOUND",
            resource + " not found with id: " + id);
    }
}

// Estoque insuficiente
public class InsufficientStockException extends BusinessException {
    public InsufficientStockException(Long productId, int available, int requested) {
        super("INSUFFICIENT_STOCK",
            String.format("Product %d: available=%d, requested=%d",
                productId, available, requested));
    }
}

// Estado de pedido inválido
public class OrderStateException extends BusinessException {
    public OrderStateException(Long orderId, String current, String target) {
        super("INVALID_ORDER_STATE",
            String.format("Order %d: cannot transition from %s to %s",
                orderId, current, target));
    }
}

Saída:

TEXT
// Execução bem-sucedida

5. Design de DTO de Resposta de Erro

(1) Princípios de Design do ErrorResponse

(1) ▶ Exemplos: ErrorResponse e ValidationErrorResponse

JAVA
public record ErrorResponse(
    String code,
    String message,
    Instant timestamp,
    String traceId,
    List<FieldError> details
) {
    public record FieldError(
        String field,
        String message,
        Object rejectedValue
    ) {}
}

// Métodos de fábrica de conveniência
public class ErrorResponse {
    public static ErrorResponse of(String code, String message, String traceId) {
        return new ErrorResponse(code, message, Instant.now(), traceId, null);
    }

    public static ErrorResponse withDetails(String code, String message,
            String traceId, List<FieldError> details) {
        return new ErrorResponse(code, message, Instant.now(), traceId, details);
    }
}

Saída:

TEXT
// Execução bem-sucedida
Campo Tipo Descrição
code String Código de erro (legível por máquina)
message String Mensagem de erro (legível por humanos)
timestamp Instant Momento da Ocorrência
traceId String ID de Rastreamento (Log Associado)
details List Detalhes de erro em nível de campo (para erros de validação)

6. Tratamento Especial de Erros de Validação

(1) ▶ Exemplo: Tratando MethodArgumentNotValidException

JAVA
@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<ErrorResponse> handleValidation(
            MethodArgumentNotValidException ex,
            HttpServletRequest request) {
        String traceId = generateTraceId();

        List<ErrorResponse.FieldError> details = ex.getBindingResult()
            .getFieldErrors().stream()
            .map(fe -> new ErrorResponse.FieldError(
                fe.getField(),
                fe.getDefaultMessage() != null ? fe.getDefaultMessage() : "Invalid value",
                fe.getRejectedValue()
            ))
            .toList();

        ErrorResponse error = ErrorResponse.withDetails(
            "VALIDATION_ERROR",
            "Input validation failed",
            traceId,
            details
        );

        log.warn("Validation failed [traceId={}]: {}", traceId, details);
        return ResponseEntity.badRequest().body(error);
    }
}

Saída:

TEXT
// Execução bem-sucedida
💻 Saída:

JSON
{
  "code": "VALIDATION_ERROR",
  "message": "Input validation failed",
  "timestamp": "2024-01-15T10:00:00Z",
  "traceId": "abc-123-def",
  "details": [
    {"field": "quantity", "message": "Quantity must be at least 1", "rejectedValue": 0},
    {"field": "customerEmail", "message": "Invalid email format", "rejectedValue": "abc"}
  ]
}

7. Logs de Exceções e Trace IDs

(1) ▶ Exemplo: Um handler de exceção completo com ID de rastreamento

JAVA
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(ResourceNotFoundException.class)
    public ResponseEntity<ErrorResponse> handleNotFound(
            ResourceNotFoundException ex,
            HttpServletRequest request) {
        String traceId = generateTraceId();
        log.warn("Resource not found [traceId={}]: {}", traceId, ex.getMessage());
        return ResponseEntity.status(HttpStatus.NOT_FOUND)
            .body(ErrorResponse.of(ex.getErrorCode(), ex.getMessage(), traceId));
    }

    @ExceptionHandler(InsufficientStockException.class)
    public ResponseEntity<ErrorResponse> handleInsufficientStock(
            InsufficientStockException ex,
            HttpServletRequest request) {
        String traceId = generateTraceId();
        log.warn("Insufficient stock [traceId={}]: {}", traceId, ex.getMessage());
        return ResponseEntity.status(HttpStatus.CONFLICT)
            .body(ErrorResponse.of(ex.getErrorCode(), ex.getMessage(), traceId));
    }

    @ExceptionHandler(BusinessException.class)
    public ResponseEntity<ErrorResponse> handleBusiness(
            BusinessException ex,
            HttpServletRequest request) {
        String traceId = generateTraceId();
        log.warn("Business error [traceId={}]: {}", traceId, ex.getMessage());
        return ResponseEntity.status(HttpStatus.UNPROCESSABLE_ENTITY)
            .body(ErrorResponse.of(ex.getErrorCode(), ex.getMessage(), traceId));
    }

    @ExceptionHandler(Exception.class)
    public ResponseEntity<ErrorResponse> handleGeneral(
            Exception ex,
            HttpServletRequest request) {
        String traceId = generateTraceId();
        log.error("Unexpected error [traceId={}]", traceId, ex);
        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
            .body(ErrorResponse.of("INTERNAL_ERROR",
                "An unexpected error occurred. TraceId: " + traceId, traceId));
    }

    private String generateTraceId() {
        return UUID.randomUUID().toString().replace("-", "").substring(0, 16);
    }
}

Saída:

TEXT
// Execução bem-sucedida
Tipo de Erro Código de Status HTTP Nível de Log Descrição
ResourceNotFoundException 404 WARN Recurso não existe; problema do cliente
InsufficientStockException 409 WARN Conflito de Negócio
BusinessException 422 WARN Violação de regra de negócio
MethodArgumentNotValidException 400 WARN Falha na validação de entrada
Exception 500 ERROR Erro inesperado; precisa ser investigado

8. Exemplo Abrangente: Sistema Completo de Tratamento de Exceções do OrderFlow

JAVA
// ErrorResponse.java
package com.orderflow.exception;

import java.time.Instant;
import java.util.List;

public record ErrorResponse(
    String code, String message, Instant timestamp,
    String traceId, List<FieldError> details
) {
    public record FieldError(String field, String message, Object rejectedValue) {}

    public static ErrorResponse of(String code, String message, String traceId) {
        return new ErrorResponse(code, message, Instant.now(), traceId, null);
    }

    public static ErrorResponse withDetails(String code, String message,
            String traceId, List<FieldError> details) {
        return new ErrorResponse(code, message, Instant.now(), traceId, details);
    }
}

// Hierarquia BusinessException
public class BusinessException extends RuntimeException {
    private final String errorCode;
    public BusinessException(String errorCode, String message) {
        super(message); this.errorCode = errorCode;
    }
    public String getErrorCode() { return errorCode; }
}

public class ResourceNotFoundException extends BusinessException {
    public ResourceNotFoundException(String resource, Long id) {
        super("RESOURCE_NOT_FOUND", resource + " not found with id: " + id);
    }
}

public class InsufficientStockException extends BusinessException {
    public InsufficientStockException(Long productId, int avail, int req) {
        super("INSUFFICIENT_STOCK",
            "Product " + productId + ": available=" + avail + ", requested=" + req);
    }
}

// GlobalExceptionHandler.java
package com.orderflow.exception;

import jakarta.servlet.http.HttpServletRequest;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.*;
import java.util.*;

@RestControllerAdvice
public class GlobalExceptionHandler {
    private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class);

    @ExceptionHandler(ResourceNotFoundException.class)
    public ResponseEntity<ErrorResponse> handleNotFound(ResourceNotFoundException ex) {
        String tid = tid();
        log.warn("[{}] {}", tid, ex.getMessage());
        return ResponseEntity.status(HttpStatus.NOT_FOUND)
            .body(ErrorResponse.of(ex.getErrorCode(), ex.getMessage(), tid));
    }

    @ExceptionHandler(InsufficientStockException.class)
    public ResponseEntity<ErrorResponse> handleStock(InsufficientStockException ex) {
        String tid = tid();
        log.warn("[{}] {}", tid, ex.getMessage());
        return ResponseEntity.status(HttpStatus.CONFLICT)
            .body(ErrorResponse.of(ex.getErrorCode(), ex.getMessage(), tid));
    }

    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<ErrorResponse> handleValidation(MethodArgumentNotValidException ex) {
        String tid = tid();
        List<ErrorResponse.FieldError> details = ex.getBindingResult()
            .getFieldErrors().stream()
            .map(f -> new ErrorResponse.FieldError(f.getField(),
                f.getDefaultMessage() != null ? f.getDefaultMessage() : "", f.getRejectedValue()))
            .toList();
        log.warn("[{}] Validation failed: {}", tid, details);
        return ResponseEntity.badRequest()
            .body(ErrorResponse.withDetails("VALIDATION_ERROR", "Validation failed", tid, details));
    }

    @ExceptionHandler(Exception.class)
    public ResponseEntity<ErrorResponse> handleGeneral(Exception ex) {
        String tid = tid();
        log.error("[{}] Unexpected error", tid, ex);
        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
            .body(ErrorResponse.of("INTERNAL_ERROR", "Unexpected error. Ref: " + tid, tid));
    }

    private String tid() {
        return UUID.randomUUID().toString().replace("-", "").substring(0, 16);
    }
}

❓ Perguntas Frequentes

P Qual é a diferença entre @ControllerAdvice e @RestControllerAdvice?
R @RestControllerAdvice = @ControllerAdvice + @ResponseBody. Em projetos de API REST, sempre use @RestControllerAdvice; valores de retorno de métodos são automaticamente serializados para JSON.
P Qual é a ordem de correspondência do @ExceptionHandler?
R O Spring seleciona a correspondência de tipo de exceção mais específica. Se handlers para BusinessException e Exception estiverem registrados, o handler de BusinessException tem precedência quando uma BusinessException é lançada.
P Como múltiplas classes @RestControllerAdvice podem coexistir?
R Você pode usar @Order para controlar prioridade. @Order(Ordered.HIGHEST_PRECEDENCE) tem precedência na correspondência. Diferentes classes Advice podem tratar diferentes tipos de exceções.
P O ambiente de produção deve retornar um stack trace de exceção?
R Não, não deve. O ambiente de produção deve retornar apenas códigos de erro e mensagens genéricas; não deve expor sua implementação interna. Informações de stack trace são registradas apenas em log. Para exceções inesperadas, retorne um traceId para que operações possam localizar o problema através dos logs.
P Como tratar exceções do Spring Security?
R Exceções do Spring Security (como AccessDeniedException) são lançadas dentro da cadeia de filtros e não passam pelo @RestControllerAdvice. Você precisa personalizar o AuthenticationEntryPoint e o AccessDeniedHandler.
P Qual é a relação entre traceId e MDC?
R traceId é usado para associar logs ao cliente na resposta, enquanto MDC é usado dentro do framework de logging para associar todos os logs da mesma requisição. Recomenda-se usar o mesmo valor para ambos; defina MDC.put("traceId", id) no Filter.

📖 Resumo


📝 Exercícios

  1. Exercício Básico (Dificuldade: ⭐): Implemente um GlobalExceptionHandler para OrderFlow para tratar ResourceNotFoundException e BusinessException, e retornar um formato ErrorResponse padronizado.

  2. Exercício Avançado (Dificuldade: ⭐⭐): Adicione um handler para MethodArgumentNotValidException e extraia detalhes de erro em nível de campo. Implemente geração de traceId e use-o no log.

  3. Desafio (Dificuldade: ⭐⭐⭐): Crie um Servlet Filter que gere um traceID no ponto de entrada da requisição e o defina no MDC, para que todos os logs incluam automaticamente o traceID. O GlobalExceptionHandler lê o traceID do MDC para habilitar rastreamento de logs de ponta a ponta.

Web-Tutorial.com

Equipe Técnica Web-Tutorial

Uma plataforma de tutoriais mantida por diversos desenvolvedores. Cada tutorial é escrito e revisado por profissionais da área correspondente. Trabalhamos para manter nosso conteúdo preciso e confiável — se encontrar algum problema, avise-nos.

100%