Il Kobayashi Maru di Java: Gestione Globale delle Eccezioni
Per chi ha saltato le lezioni dell’Accademia della Flotta Stellare, il Kobayashi Maru è un’esercitazione progettata come uno “scenario senza vittoria”. L’obiettivo non è vincere, ma vedere come gestisci un fallimento inevitabile. Nel mondo dell’Ingegneria Backend, il nostro scenario senza vittoria è l’Eccezione Non Gestita.
Passi settimane ad architettare un servizio bellissimo e pulito. Usi i Record, ottimizzi le query SQL, applichi i principi SOLID. E poi, il giorno del rilascio in produzione, un utente invia un JSON malformato e la tua API vomita una Stack Trace di 50 righe direttamente nella console del suo browser. È brutto, è poco professionale ed espone la tua logica interna al mondo.

Il modo dilettantesco di gestire questo problema è disseminare la tua codebase di blocchi try-catch. Avvolgi ogni metodo del Controller in una coperta di sicurezza, duplicando codice e rendendo la tua logica illeggibile.
Ma poiché stiamo usando Spring Boot, possiamo fare quello che ha fatto il Capitano Kirk: possiamo riprogrammare la simulazione. Non dobbiamo combattere l’eccezione in ogni metodo, possiamo catturarla globalmente prima che esca dall’airlock.
La Strategia: @ControllerAdvice
Spring fornisce un’annotazione chiamata @ControllerAdvice. Pensala come un intercettore globale. Si posiziona sopra tutti i tuoi controller, osservando qualsiasi errore che risale in superficie. Se un’eccezione viene lanciata da qualsiasi parte della tua applicazione, questo componente la cattura, la formatta e restituisce una risposta JSON educata invece di un errore grezzo.
Implementiamo un meccanismo di gestione degli errori centralizzato e pulito.
Passo 1: Definire l’Errore Standard
Innanzitutto, smettiamo di restituire Stringhe o Mappe casuali. Abbiamo bisogno di un contratto. Poiché siamo su Java 21, usiamo un record per creare una struttura dati immutabile e concisa per i nostri errori.
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: Il Gestore Globale
Ora avviene la magia. Creiamo una classe annotata con @ControllerAdvice. All’interno, definiamo metodi annotati con @ExceptionHandler per ogni problema specifico che vogliamo catturare.
Di solito li separo in due categorie: Errori Logici Attesi (come “Utente Non Trovato”) e L’Inaspettato (NullPointers, crash del Database).
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 {
// Scenario 1: L'utente ha chiesto qualcosa che non esiste.
// Restituiamo un 404 (Non Trovato), non un 500 (Errore del Server).
@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);
}
// Scenario 2: Validazione della Logica di Business (es. "Fondi Insufficienti")
@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);
}
// Scenario 3: Il "Kobayashi Maru" (Tutto il resto)
// Questo cattura NullPointers, errori SQL e qualsiasi cosa non abbiamo previsto.
@ExceptionHandler(Exception.class)
public ResponseEntity<ApiError> handleGenericException(
Exception ex,
HttpServletRequest request
) {
// Loggiamo la vera stack trace internamente per poterla sistemare dopo
// Ma NON la mostriamo MAI al client.
ex.printStackTrace();
ApiError error = new ApiError(
HttpStatus.INTERNAL_SERVER_ERROR.value(),
"Si è verificato un errore imprevisto. Contatta l'assistenza.",
request.getRequestURI()
);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(error);
}
}
Perché Questo è Importante
Facendo così, disaccoppi la gestione degli errori dalla logica di business. Le tue classi Service possono semplicemente throw new EntityNotFoundException("Utente id 5 non trovato"), e non devono preoccuparsi degli status HTTP o della formattazione JSON. Il GlobalExceptionHandler si occupa della traduzione.
Trasforma un crash caotico in un atterraggio controllato. Il client riceve un JSON pulito che spiega cosa è successo, e il tuo codice rimane pulito, leggibile e concentrato sul percorso felice.
Non hai necessariamente “vinto” (l’errore è comunque avvenuto), ma hai cambiato le regole dell’ingaggio per assicurarti di non perdere.