Lingue disponibili:

Il Kobayashi Maru di Java: Gestione Globale delle Eccezioni

Questo post è stato originariamente scritto in inglese. La traduzione potrebbe non riflettere il 100% delle idee originali dell'autore.

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.

Eccezione!

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.