Salta al contenuto principale

Interceptor in Spring Boot: controllo totale sulle richieste HTTP

Profile picture for user luca77king

In un’applicazione Spring Boot, gli interceptor sono strumenti essenziali per intervenire in ogni fase del ciclo di vita di una richiesta HTTP. Permettono di eseguire logica prima che il controller gestisca la richiesta, subito dopo la gestione del controller e al termine della risposta. Questo li rende ideali per implementare funzionalità trasversali come logging, autenticazione, autorizzazione, caching, rate-limiting, gestione degli header e metriche di performance.

A differenza dei Filter, che operano a livello di servlet e intercettano tutte le richieste HTTP indipendentemente dal framework, gli interceptor agiscono a livello Spring MVC, cioè direttamente sul controller e sulla logica applicativa. Questo approccio consente di mantenere il codice dei controller pulito e modulare, concentrando logiche comuni in componenti centralizzati e riutilizzabili.

Da questo punto di vista, gli interceptor (insieme a filter e ad alcuni AOP advice come ResponseBodyAdvice) possono essere considerati una forma di middleware in Spring Boot. Sebbene Spring non utilizzi formalmente il termine “middleware” come fanno framework come Express.js o NestJS, la loro funzione è analoga: intercettare e manipolare le richieste e le risposte lungo il flusso dell’applicazione, applicando logica trasversale senza intaccare i controller. In pratica, gli interceptor sono il “middleware nativo di Spring MVC”, mentre i filter svolgono la stessa funzione a livello di servlet, più generale e ampio.

Metodi principali di un Interceptor

Gli interceptor si implementano generalmente tramite l’interfaccia HandlerInterceptor, che offre tre metodi principali. Il metodo preHandle viene eseguito prima del controller e permette di bloccare o lasciare passare la richiesta, utile per autenticazione, validazione o rate-limiting. Restituendo false, la richiesta viene interrotta, mentre true consente di proseguire verso il controller.

Il metodo postHandle viene chiamato dopo il controller ma prima della renderizzazione della view, permettendo di modificare la risposta o aggiungere attributi al modello. Infine, afterCompletion è eseguito dopo che la risposta è stata inviata, ideale per logging finale, cleanup di risorse o gestione centralizzata delle eccezioni. Questa struttura modulare consente di avere un controllo granulare e coerente sul comportamento delle richieste, evitando duplicazioni di codice nei controller.

@Component
public class LoggingInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse
response, Object handler) {
        System.out.println("[PreHandle] Request URI: " +
request.getRequestURI());
        return true;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse
response, Object handler,
                           ModelAndView modelAndView) {
        System.out.println("[PostHandle] Response status: " +
response.getStatus());
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse
response, Object handler,
                                Exception ex) {
        System.out.println("[AfterCompletion] Completed request for: " +
request.getRequestURI());
    }
}

Registrazione dell’Interceptor

Per rendere operativo un interceptor è necessario registrarlo tramite WebMvcConfigurer. La registrazione permette di specificare i percorsi da intercettare e quelli da escludere, offrendo un livello di controllo preciso. Questo è particolarmente utile per escludere risorse pubbliche o statiche, evitando overhead su richieste che non necessitano di intervento.

La registrazione centralizzata migliora anche la manutenibilità dell’applicazione. Invece di configurare interceptor in ogni controller, possiamo gestirli tutti in un unico punto, definendo l’ordine di esecuzione e le eccezioni. Questo approccio rende il codice più leggibile e coerente.

@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Autowired
    private LoggingInterceptor loggingInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(loggingInterceptor)
                .addPathPatterns("/**")
                .excludePathPatterns("/public/**");
    }
}

Caching tramite Interceptor

Gli interceptor possono essere sfruttati per implementare strategie di caching avanzate. Intercettando la richiesta prima che arrivi al controller, è possibile verificare se una risposta già calcolata esiste in cache e restituirla direttamente, evitando di eseguire logiche complesse più volte.

Questo approccio non solo riduce il carico sui controller, ma accelera anche le risposte per richieste frequenti, migliorando la scalabilità dell’applicazione. In scenari reali, la gestione del caching può diventare più sofisticata, ad esempio leggendo il body della risposta o utilizzando sistemi di cache distribuita come Redis.

@Component
public class CacheInterceptor implements HandlerInterceptor {

    private Map<String, String> cache = new ConcurrentHashMap<>();

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse
response, Object handler) throws IOException {
        String key = request.getRequestURI();
        if (cache.containsKey(key)) {
            response.getWriter().write(cache.get(key));
            return false;
        }
        return true;
    }
}

Rate-limiting con Interceptor

Un altro utilizzo comune degli interceptor è il rate-limiting, cioè limitare il numero di richieste da parte di un client in un determinato intervallo di tempo. Questo meccanismo è essenziale per proteggere l’applicazione da abusi o sovraccarichi, garantendo stabilità e sicurezza.

Il rate-limiting a livello di interceptor è semplice da implementare e centrale. Intercettando le richieste prima del controller, possiamo bloccare quelle troppo frequenti, rispondendo con un codice di errore appropriato senza coinvolgere la logica di business.

@Component
public class RateLimitingInterceptor implements HandlerInterceptor {

    private Map<String, Long> lastRequestTime = new ConcurrentHashMap<>();
    private static final long MIN_INTERVAL_MS = 1000;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse
response, Object handler) {
        String clientIp = request.getRemoteAddr();
        long now = System.currentTimeMillis();
        if (lastRequestTime.containsKey(clientIp) && (now -
lastRequestTime.get(clientIp)) < MIN_INTERVAL_MS) {
            response.setStatus(HttpServletResponse.SC_TOO_MANY_REQUESTS);
            return false;
        }
        lastRequestTime.put(clientIp, now);
        return true;
    }
}

Gestione di eccezioni personalizzate

Gli interceptor consentono di centralizzare la gestione degli errori, intercettando eccezioni non gestite dai controller. Questo evita di inserire blocchi try/catch in ogni endpoint, semplificando il codice e migliorando la manutenibilità.

Oltre al logging, è possibile inviare notifiche, salvare stacktrace su sistemi esterni o applicare politiche di fallback in modo uniforme. Questo approccio rende l’applicazione più robusta e coerente, garantendo che tutte le eccezioni vengano tracciate in maniera centralizzata.

@Component
public class ExceptionInterceptor implements HandlerInterceptor {

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse
response, Object handler,
                                Exception ex) {
        if (ex != null) {
            System.err.println("Exception caught: " + ex.getMessage());
        }
    }
}

Aggiunta di header personalizzati

Gli interceptor permettono anche di manipolare le risposte HTTP aggiungendo header globali. Questo è utile per sicurezza, versioning o tracciamento, senza dover modificare ogni controller.

In contesti aziendali, l’aggiunta centralizzata di header come X-App-Version o X-Frame-Options garantisce uniformità e riduce errori. Si può estendere questo concetto anche a header di caching, CORS o token di sicurezza, rendendo il middleware dell’applicazione modulare e riutilizzabile.

@Component
public class HeaderInterceptor implements HandlerInterceptor {

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse
response, Object handler,
                           ModelAndView modelAndView) {
        response.addHeader("X-App-Version", "1.0.0");
        response.addHeader("X-Frame-Options", "DENY");
    }
}

Interceptor e più livelli di applicazione

Spring Boot consente di registrare più interceptor in catena, creando una pipeline di gestione della richiesta. Ogni interceptor decide se proseguire (preHandle ritorna true) o bloccare la richiesta (preHandle ritorna false).

Questa architettura consente di separare le responsabilità: logging, autenticazione, rate-limiting, caching e header possono essere gestiti in moduli distinti. L’ordine di registrazione è cruciale, perché determina la sequenza di esecuzione e quindi l’effetto complessivo sulle richieste. Questo modello rende l’applicazione più modulare, testabile e scalabile.

Considerazioni finali

Gli interceptor sono strumenti fondamentali per applicare logiche trasversali in modo modulare e centralizzato. Permettono di monitorare e loggare richieste e risposte, applicare autenticazione e autorizzazione, gestire caching e rate-limiting, aggiungere header e manipolare risposte, e centralizzare la gestione delle eccezioni.

Quando progettati correttamente, rendono un’applicazione più sicura, coerente e facile da mantenere. La combinazione di interceptor con filtri, AOP e ResponseBodyAdvice offre un controllo completo sul ciclo di vita delle richieste HTTP, garantendo un middleware potente e flessibile in Spring Boot.