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
- Tratamento Global de Exceções com
@RestControllerAdvice+@ExceptionHandler - Sistema de Exceções de Negócio Personalizado:
BusinessException/ResourceNotFoundException/ValidationException - Design de DTO de Resposta de Erro: code / message / timestamp / details
- Tratamento Especial de
MethodArgumentNotValidExceptionpara Erros de Validação - Logging de Exceções e Geração de ID de Rastreamento de Erros
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:
@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
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
@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:
// Execução bem-sucedida
4. Sistema de Exceções de Negócio Personalizado
(1) Hierarquia de Classes de Exceção
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
// 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:
// Execução bem-sucedida
5. Design de DTO de Resposta de Erro
(1) Princípios de Design do ErrorResponse
(1) ▶ Exemplos: ErrorResponse e ValidationErrorResponse
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:
// 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
@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:
// Execução bem-sucedida
{
"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
@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:
// 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
// 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
📖 Resumo
@RestControllerAdvice+@ExceptionHandlerTratam todas as exceções do Controller de forma unificada- Hierarquia de exceção personalizada:
BusinessExceptionserve como classe base, com subclasses distinguindo diferentes cenários de negócio ErrorResponsecontém cinco campos:code,message,timestamp,traceIdedetails- Tratamento especial de erros de validação: Extrair detalhes de erro em nível de campo no array
details - Usar WARN e ERROR para distinguir entre exceções de negócio e exceções de sistema no log de exceções
- traceId vincula respostas a logs, facilitando a solução de problemas
📝 Exercícios
-
Exercício Básico (Dificuldade: ⭐): Implemente um
GlobalExceptionHandlerparaOrderFlowpara tratarResourceNotFoundExceptioneBusinessException, e retornar um formatoErrorResponsepadronizado. -
Exercício Avançado (Dificuldade: ⭐⭐): Adicione um handler para
MethodArgumentNotValidExceptione extraia detalhes de erro em nível de campo. Implemente geração detraceIde use-o no log. -
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.



