O Kobayashi Maru do Java: Tratamento Global de Exceções
Para quem pulou as aulas da Academia da Frota Estelar, o Kobayashi Maru é um exercício de treinamento projetado como um “cenário sem vitória”. O objetivo não é vencer, é ver como você lida com uma falha inevitável. No mundo da Engenharia de Backend, nosso cenário sem vitória é a Exceção Não Tratada.
Você passa semanas arquitetando um serviço bonito e limpo. Você usa Records, otimiza suas consultas SQL, aplica princípios SOLID. E então, no dia da produção, um usuário envia um JSON malformado, e sua API vomita um Stack Trace de 50 linhas diretamente no console do navegador dele. É feio, é pouco profissional e expõe sua lógica interna para o mundo.

A maneira amadora de lidar com isso é espalhar blocos try-catch por toda a sua base de código. Você envolve cada método do Controller em um cobertor de segurança, duplicando código e tornando sua lógica ilegível.
Mas como estamos usando Spring Boot, podemos fazer o que o Capitão Kirk fez: podemos reprogramar a simulação. Não precisamos combater a exceção em cada método, podemos capturá-la globalmente antes que ela sequer deixe a escotilha.
A Estratégia: @ControllerAdvice
O Spring fornece uma anotação chamada @ControllerAdvice. Pense nela como um interceptador global. Ela fica acima de todos os seus controllers, observando qualquer erro que suba. Se uma exceção for lançada em qualquer lugar da sua aplicação, este componente a captura, formata e retorna uma resposta JSON educada em vez de um erro bruto.
Vamos implementar um mecanismo de tratamento de erros limpo e centralizado.
Passo 1: Definir o Erro Padrão
Primeiro, paramos de retornar Strings ou Maps aleatórios. Precisamos de um contrato. Como estamos no Java 21, vamos usar um record para criar uma estrutura de dados imutável e concisa para nossos erros.
package me.doismiu.api.exception;
import java.time.LocalDateTime;
public record ApiError(
LocalDateTime timestamp,
int status,
String error,
String path
) {
public ApiError(int status, String error, String path) {
this(LocalDateTime.now(), status, error, path);
}
}
Passo 2: O Manipulador Global
Agora, a mágica acontece. Criamos uma classe anotada com @ControllerAdvice. Dentro dela, definimos métodos anotados com @ExceptionHandler para cada problema específico que queremos capturar.
Eu geralmente os separo em duas categorias: Erros Lógicos Esperados (como “Usuário Não Encontrado”) e Os Inesperados (NullPointers, falhas de Banco de Dados).
package me.doismiu.api.exception;
import jakarta.persistence.EntityNotFoundException;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
@ControllerAdvice
public class GlobalExceptionHandler {
// Cenário 1: O usuário pediu algo que não existe.
// Retornamos um 404 (Não Encontrado), não um 500 (Erro do Servidor).
@ExceptionHandler(EntityNotFoundException.class)
public ResponseEntity<ApiError> handleEntityNotFound(
EntityNotFoundException ex,
HttpServletRequest request
) {
ApiError error = new ApiError(
HttpStatus.NOT_FOUND.value(),
ex.getMessage(),
request.getRequestURI()
);
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(error);
}
// Cenário 2: Validação de Lógica de Negócio (ex: "Fundos Insuficientes")
@ExceptionHandler(IllegalArgumentException.class)
public ResponseEntity<ApiError> handleBadRequest(
IllegalArgumentException ex,
HttpServletRequest request
) {
ApiError error = new ApiError(
HttpStatus.BAD_REQUEST.value(),
ex.getMessage(),
request.getRequestURI()
);
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(error);
}
// Cenário 3: O "Kobayashi Maru" (Todo o resto)
// Isso captura NullPointers, erros de SQL e qualquer coisa que não prevíamos.
@ExceptionHandler(Exception.class)
public ResponseEntity<ApiError> handleGenericException(
Exception ex,
HttpServletRequest request
) {
// Registramos o stack trace real internamente para podermos corrigi-lo depois
// Mas NUNCA o mostramos ao cliente.
ex.printStackTrace();
ApiError error = new ApiError(
HttpStatus.INTERNAL_SERVER_ERROR.value(),
"Ocorreu um erro inesperado. Por favor, entre em contato com o suporte.",
request.getRequestURI()
);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(error);
}
}
Por Que Isso Importa
Ao fazer isso, você desacopla o tratamento de erros da lógica de negócio. Suas classes de Service podem simplesmente throw new EntityNotFoundException("Usuário id 5 não encontrado"), e elas não precisam se preocupar com status HTTP ou formatação JSON. O GlobalExceptionHandler cuida da tradução.
Isso transforma uma queda caótica em um pouso controlado. O cliente recebe um JSON limpo explicando o que aconteceu, e seu código permanece limpo, legível e focado no caminho feliz.
Você não necessariamente “venceu” (o erro ainda aconteceu), mas você mudou as regras do jogo para garantir que não perca.