Arquitetura e Ciclo de Vida de Interceptors no Spring MVC

Conceitos Fundamentais e Aplicações

Os Interceptors (Interceptadores) no ecossistema Spring são componentes de nível de framework projetados para interceptar e manipular requisições HTTP em diferentes estágios de seu processamento. Diferente dos Servlet Filters, que operam no nível da especificação Java EE, os Interceptors estão profunadmente integrados ao contexto do Spring MVC, permitindo o acesso a beans gerenciados e metadados específicos do handler.

As aplicações mais comuns incluem:

  • Autenticação e Autorização: Validação de tokens JWT ou chaves de API antes que a requisição atinja o controller.
  • Auditoria e Logs: Registro de metadados da requisição, como IP de origem, URI e parâmetros.
  • Métricas de Performence: Cálculo do tempo de execução de endpoints para identificação de gargalos.
  • Manipulação de Contexto: Injeção de dados transversais (como tenant ID ou locale) no ThreadLocal ou nos atributos da requisição.

Ciclo de Vida da Requisição

O fluxo de execução de um interceptor é dividido em três fases distintas, orquestradas pelo DispatcherServlet:

  1. preHandle: Executado antes da invocação do método do controller. Retornar false aborta o fluxo.
  2. postHandle: Executado após o controller processar a lógica, mas antes da renderização da视图 (View) ou da serialização da resposta JSON.
  3. afterCompletion: Executado após a resposta ser totalmente processada e enviada ao cliente, ou imediatamente se uma exceção não tratada ocorrer nas fases anteriores. Ideal para limpeza de recursos.

Implementação Prática

Para ilustrar a implementação, criaremos um interceptor que valida uma chave de API customizada e, em caso de falha, retorna uma resposta JSON estruturada utilizando o ObjectMapper do Jackson.


import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.http.MediaType;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Map;

public class ApiKeyValidationInterceptor implements HandlerInterceptor {

    private final ObjectMapper objectMapper;
    private final String expectedApiKey;

    public ApiKeyValidationInterceptor(ObjectMapper objectMapper, String expectedApiKey) {
        this.objectMapper = objectMapper;
        this.expectedApiKey = expectedApiKey;
    }

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        String providedKey = request.getHeader("X-Api-Key");
        
        if (providedKey == null || !providedKey.equals(expectedApiKey)) {
            response.setStatus(HttpServletResponse.SC_FORBIDDEN);
            response.setContentType(MediaType.APPLICATION_JSON_VALUE);
            response.setCharacterEncoding("UTF-8");
            
            Map<String, String> errorPayload = Map.of(
                "code", "INVALID_API_KEY",
                "message", "Acesso negado. Chave de API inválida ou ausente."
            );
            
            response.getWriter().write(objectMapper.writeValueAsString(errorPayload));
            return false; // Interrompe a cadeia de execução
        }
        
        request.setAttribute("requestStartTime", System.currentTimeMillis());
        return true;
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        Long startTime = (Long) request.getAttribute("requestStartTime");
        if (startTime != null) {
            long duration = System.currentTimeMillis() - startTime;
            // Lógica para enviar métricas para um sistema de monitoramento (ex: Prometheus, Micrometer)
            System.out.printf("Endpoint %s processado em %d ms%n", request.getRequestURI(), duration);
        }
    }
}

Configuração e Registro

Os interceptors devem ser registrados em uma classe de configuração que implemente WebMvcConfigurer. É crucial definir padrões de inclusão e exclusão para evitar a interceptação de rotas públicas ou recursos estáticos.


import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class WebMvcSetup implements WebMvcConfigurer {

    private final ApiKeyValidationInterceptor apiKeyInterceptor;

    public WebMvcSetup(ApiKeyValidationInterceptor apiKeyInterceptor) {
        this.apiKeyInterceptor = apiKeyInterceptor;
    }

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(apiKeyInterceptor)
                .addPathPatterns("/api/v1/**")       // Aplica apenas às rotas da API v1
                .excludePathPatterns(
                    "/api/v1/health",                // Exclui health checks
                    "/api/v1/public/**"              // Exclui endpoints públicos
                );
    }
}

Padrões Avançados e Comportamento em Cadeia

Quando múltiplos interceptors são registrados, eles formam uma cadeia de responsabilidade. A ordem de execução é estritamente definida:

  • preHandle: Executado na ordem de registro (FIFO - First In, First Out).
  • postHandle e afterCompletion: Executados na ordem inversa de registro (LIFO - Last In, First Out).

Se um preHandle retornar false, os preHandle subsequentes não são chamados, mas os afterCompletion dos interceptors que já retornaram true serão executados em ordem inversa para garantir a limpeza de recursos.

Suporte a Requisições Assíncronas:
Para endpoints que retornam Callable ou DeferredResult, o interceptor padrão pode não se comportar como esperado após a conclusão da thread assíncrona. Nesses casos, deve-se implementar a interface AsyncHandlerInterceptor, que fornece o método afterConcurrentHandlingStarted.

Anatomia do DispatcherServlet

Para compreender verdadeiramente o mecanismo, é necessário analisar como o DispatcherServlet orquestra a execução. O método central doDispatch foi refatorado abaixo em uma representação simplificada para destacar a lógica de interação com o HandlerExecutionChain.


// Representação simplificada da lógica interna do DispatcherServlet
protected void processHttpRequest(HttpServletRequest req, HttpServletResponse res) {
    HandlerExecutionChain executionChain = null;
    Exception dispatchError = null;

    try {
        // 1. Localiza o Handler e seus Interceptores associados
        executionChain = locateExecutionChain(req);
        if (executionChain == null) {
            handleNoHandlerFound(req, res);
            return;
        }

        // 2. Localiza o Adapter adequado para o Handler (ex: RequestMappingHandlerAdapter)
        HandlerAdapter adapter = locateAdapter(executionChain.getTargetHandler());

        // 3. Executa a cadeia de preHandle
        if (!executionChain.invokePreHandle(req, res)) {
            return; // Cadeia interrompida por um interceptor
        }

        // 4. Invoca o método do Controller
        ModelAndView result = adapter.handle(req, res, executionChain.getTargetHandler());

        // 5. Executa a cadeia de postHandle
        executionChain.invokePostHandle(req, res, result);

        // 6. Renderiza a视图 ou serializa o JSON
        renderResult(result, req, res);

    } catch (Exception ex) {
        dispatchError = ex;
    } finally {
        // 7. Garante a execução do afterCompletion independentemente de exceções
        if (executionChain != null) {
            executionChain.invokeAfterCompletion(req, res, dispatchError);
        }
    }
}

A mágica da ordem de execução (FIFO/LIFO) e do tratamento de interrupções reside na classe HandlerExecutionChain. Abaixo está a estrutura lógica que controla essas transições:


public class HandlerExecutionChain {
    private final Object targetHandler;
    private final List<HandlerInterceptor> interceptors;
    private int successfulPreHandleIndex = -1;

    // Execução sequencial (FIFO)
    public boolean invokePreHandle(HttpServletRequest req, HttpServletResponse res) throws Exception {
        for (int i = 0; i < this.interceptors.size(); i++) {
            HandlerInterceptor current = this.interceptors.get(i);
            if (!current.preHandle(req, res, this.targetHandler)) {
                this.successfulPreHandleIndex = i;
                // Aborta e limpa os que já passaram
                invokeAfterCompletion(req, res, null);
                return false;
            }
        }
        this.successfulPreHandleIndex = this.interceptors.size() - 1;
        return true;
    }

    // Execução inversa (LIFO)
    public void invokePostHandle(HttpServletRequest req, HttpServletResponse res, ModelAndView mv) throws Exception {
        for (int i = this.interceptors.size() - 1; i >= 0; i--) {
            this.interceptors.get(i).postHandle(req, res, this.targetHandler, mv);
        }
    }

    // Execução inversa (LIFO) apenas para os que tiveram preHandle bem-sucedido
    public void invokeAfterCompletion(HttpServletRequest req, HttpServletResponse res, Exception ex) {
        for (int i = this.successfulPreHandleIndex; i >= 0; i--) {
            try {
                this.interceptors.get(i).afterCompletion(req, res, this.targetHandler, ex);
            } catch (Throwable t) {
                // Log de erro interno do framework, não propaga a exceção
                LoggerFactory.getLogger(HandlerExecutionChain.class)
                           .error("Falha crítica no afterCompletion do interceptor", t);
            }
        }
    }
}

Tags: spring-boot spring-mvc handlerinterceptor java dispatcher-servlet

Publicado em 6-21 21:10