El Kobayashi Maru de Java: Gestió Global d’Excepcions
Per aquells que es van saltar les classes de l’Acadèmia de la Flota Estel·lar, el Kobayashi Maru és un exercici d’entrenament dissenyat com un “escenari sense victòria”. L’objectiu no és guanyar, sinó veure com gestiones un fracàs inevitable. En el món de l’Enginyeria de Backend, el nostre escenari sense victòria és la Excepció No Controlada.
Passes setmanes arquitecturant un servei bonic i net. Utilitzes Records, optimitzes les teves consultes SQL, apliques principis SOLID. I després, el dia de posar-lo en producció, un usuari envia un JSON mal format, i la teva API vomita un Stack Trace de 50 línies directament a la seva consola del navegador. És lleig, és poc professional i exposa la teva lògica interna al món.

La manera amateur de gestionar això és omplir la base de codi amb blocs try-catch. Envoltes cada mètode del Controlador amb una manta de seguretat, duplicant codi i fent la teva lògica il·legible.
Però com que estem utilitzant Spring Boot, podem fer el que va fer el Capità Kirk: podem reprogramar la simulació. No hem de lluitar contra l’excepció en cada mètode, podem capturar-la globalment abans que surti mai de l’escotilla.
L’Estratègia: @ControllerAdvice
Spring proporciona una anotació anomenada @ControllerAdvice. Pensa-hi com un interceptor global. Se situa per sobre de tots els teus controladors, observant qualsevol error que brolli. Si es llança una excepció en qualsevol lloc de la teva aplicació, aquest component la captura, la formata i retorna una resposta JSON educada en lloc de l’error en brut.
Implementem un mecanisme net i centralitzat de gestió d’errors.
Pas 1: Defineix l’Error Estàndard
Primer, deixem de retornar cadenes o mapes aleatoris. Necessitem un contracte. Com que estem en Java 21, utilitzem un record per crear una estructura de dades immutable i concisa per als nostres errors.
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);
}
}
Pas 2: El Gestor Global
Ara és quan passa la màgia. Creem una classe anotada amb @ControllerAdvice. A dins, definim mètodes anotats amb @ExceptionHandler per a cada problema específic que vulguem capturar.
Normalment els separo en dues categories: Errors Lògics Esperats (com “Usuari No Trobat”) i L’Inesperat (NullPointers, caigudes de Base de Dades).
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 {
// Escenari 1: L'usuari ha demanat alguna cosa que no existeix.
// Retornem un 404 (No Trobat), no un 500 (Error del 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);
}
// Escenari 2: Validació de Lògica de Negoci (p. ex., "Fons Insuficients")
@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);
}
// Escenari 3: El "Kobayashi Maru" (Tot la resta)
// Això captura NullPointers, errors SQL i qualsevol cosa que no haguem previst.
@ExceptionHandler(Exception.class)
public ResponseEntity<ApiError> handleGenericException(
Exception ex,
HttpServletRequest request
) {
// Registra el stack trace real internament per poder arreglar-ho després
// Però MAI el mostris al client.
ex.printStackTrace();
ApiError error = new ApiError(
HttpStatus.INTERNAL_SERVER_ERROR.value(),
"S'ha produït un error inesperat. Si us plau, contacta amb suport.",
request.getRequestURI()
);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(error);
}
}
Per què Això És Important
Fent-ho així, desacoples la gestió d’errors de la lògica de negoci. Les teves classes de Servei poden simplement fer throw new EntityNotFoundException("Usuari id 5 no trobat"), i no han de preocupar-se pels estats HTTP o el format JSON. El GlobalExceptionHandler s’encarrega de la traducció.
Converteix un caos en un aterratge controlat. El client rep un JSON net que explica què ha passat, i el teu codi roman net, llegible i centrat en el camí feliç.
No has “guanyat” necessàriament (l’error encara ha passat), però has canviat les regles del joc per assegurar-te que no perds.