No Spring, Filter não é a mesma coisa que Interceptor

Em aplicações Spring, é comum ver Filter e HandlerInterceptor sendo tratados como se fossem duas formas equivalentes de fazer a mesma coisa.

Os dois rodam durante uma requisição HTTP.

Os dois conseguem executar código antes do controller.

Os dois podem bloquear uma chamada.

Mas eles não ocupam o mesmo lugar no fluxo.

E essa diferença muda bastante o tipo de responsabilidade que cada um deveria assumir.

Um Filter está mais perto da infraestrutura Servlet.

Um HandlerInterceptor está mais perto do Spring MVC.

Quando essa fronteira é ignorada, aparecem sintomas estranhos: log duplicado, métrica incompleta, autenticação no ponto errado, interceptor que não roda em erro de mapeamento e filtro tentando depender de informação que ainda nem existe.


O Filter vem antes do Spring MVC decidir o handler

Um Filter faz parte da cadeia Servlet.

Ele recebe a requisição antes de ela chegar ao processamento MVC do Spring.

Isso significa que ele roda em uma camada mais externa.

Nesse ponto, a aplicação ainda não selecionou qual controller atenderá a chamada.

O Spring ainda não resolveu @PathVariable, ainda não converteu @RequestBody, ainda não executou validação e ainda não sabe, necessariamente, qual método será chamado.

Um filtro trabalha com a requisição HTTP em estado mais bruto:

@Component
public class CorrelationIdFilter extends OncePerRequestFilter {

    @Override
    protected void doFilterInternal(
        HttpServletRequest request,
        HttpServletResponse response,
        FilterChain filterChain
    ) throws ServletException, IOException {
        String correlationId = request.getHeader("X-Correlation-Id");

        if (correlationId == null || correlationId.isBlank()) {
            correlationId = UUID.randomUUID().toString();
        }

        response.setHeader("X-Correlation-Id", correlationId);
        filterChain.doFilter(request, response);
    }
}

Esse tipo de lógica combina bem com filtro porque ela não depende do controller escolhido.

Toda requisição deveria receber um correlation id.

Não importa se ela vai cair em um endpoint válido, em uma rota inexistente, em um erro de autenticação ou em uma resposta estática.

O ponto é justamente esse: o filtro é externo ao MVC.


O Interceptor participa do fluxo do Spring MVC

Um HandlerInterceptor entra em outro momento.

Ele é chamado dentro do fluxo do Spring MVC, normalmente depois que o Spring já identificou um handler para a requisição.

Por isso a assinatura de preHandle recebe um parâmetro chamado handler:

@Component
public class AuditoriaInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(
        HttpServletRequest request,
        HttpServletResponse response,
        Object handler
    ) {
        if (handler instanceof HandlerMethod handlerMethod) {
            String controller = handlerMethod.getBeanType().getSimpleName();
            String metodo = handlerMethod.getMethod().getName();

            request.setAttribute("handler", controller + "#" + metodo);
        }

        return true;
    }
}

Esse código depende de algo que um filtro não deveria assumir: a existência de um handler MVC.

O interceptor consegue saber qual método do controller será chamado.

Ele pode ler anotações do método.

Ele pode aplicar uma lógica diferente dependendo do endpoint.

Ele pode executar código antes do controller, depois da execução do controller e depois da conclusão da requisição.

Esse contexto é valioso.

Mas ele só existe porque o Spring MVC já entrou no jogo.


A diferença principal é a posição no pipeline

Uma forma prática de enxergar a ordem é esta:

Cliente HTTP
  -> Servidor Servlet
  -> Filter
  -> DispatcherServlet
  -> HandlerMapping
  -> HandlerInterceptor
  -> Controller
  -> HandlerInterceptor
  -> DispatcherServlet
  -> Filter
  -> Resposta HTTP

Esse desenho é simplificado, mas ajuda a fixar a ideia.

Filtro fica fora do Spring MVC.

Interceptor fica dentro do Spring MVC.

Por isso, a pergunta correta não é "qual dos dois eu prefiro?".

A pergunta correta é: "em que ponto do fluxo essa responsabilidade precisa existir?"

Se a lógica precisa acontecer antes de qualquer decisão do MVC, provavelmente é filtro.

Se a lógica precisa conhecer o handler, provavelmente é interceptor.


Segurança geralmente não é caso para Interceptor

Um erro comum é tentar implementar autenticação ou autorização global usando HandlerInterceptor.

À primeira vista, parece funcionar:

@Component
public class TokenInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(
        HttpServletRequest request,
        HttpServletResponse response,
        Object handler
    ) throws IOException {
        String token = request.getHeader("Authorization");

        if (token == null || token.isBlank()) {
            response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
            return false;
        }

        return true;
    }
}

Esse código pode bloquear chamadas para controllers.

Mas isso não significa que ele esteja no melhor lugar.

Autenticação é uma preocupação mais externa.

Ela normalmente deveria acontecer antes do Spring MVC tentar resolver controller, converter argumento ou executar lógica relacionada a endpoint.

É por isso que o Spring Security trabalha fortemente com uma cadeia de filtros.

Segurança precisa estar cedo no fluxo.

Ela precisa proteger a aplicação antes que o MVC assuma que a requisição está apta a ser processada.

Interceptor pode ser útil para algumas decisões relacionadas ao handler, mas não deveria ser tratado como substituto genérico da camada de segurança.


Log e métrica dependem do que você quer medir

Outro ponto que gera confusão é observabilidade.

Devo medir tempo de requisição com filtro ou interceptor?

Depende do que você quer medir.

Um filtro mede uma visão mais externa da chamada:

@Component
public class RequestTimingFilter extends OncePerRequestFilter {

    @Override
    protected void doFilterInternal(
        HttpServletRequest request,
        HttpServletResponse response,
        FilterChain filterChain
    ) throws ServletException, IOException {
        long inicio = System.nanoTime();

        try {
            filterChain.doFilter(request, response);
        } finally {
            long duracaoMs = (System.nanoTime() - inicio) / 1_000_000;
            log.info("{} {} {}ms",
                request.getMethod(),
                request.getRequestURI(),
                duracaoMs
            );
        }
    }
}

Esse tempo inclui mais coisas do pipeline.

Ele pode capturar requisições que nem chegaram ao controller.

Isso é bom para métricas de borda.

Já um interceptor pode ser melhor quando você quer associar a métrica ao handler MVC:

@Component
public class HandlerTimingInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(
        HttpServletRequest request,
        HttpServletResponse response,
        Object handler
    ) {
        request.setAttribute("inicioHandler", System.nanoTime());
        return true;
    }

    @Override
    public void afterCompletion(
        HttpServletRequest request,
        HttpServletResponse response,
        Object handler,
        Exception ex
    ) {
        Long inicio = (Long) request.getAttribute("inicioHandler");

        if (inicio == null) {
            return;
        }

        long duracaoMs = (System.nanoTime() - inicio) / 1_000_000;
        log.info("handler={} status={} duracao={}ms",
            handler,
            response.getStatus(),
            duracaoMs
        );
    }
}

Aqui a informação sobre o handler está disponível.

Mas a métrica já está dentro do recorte MVC.

Se a requisição falhar antes de existir handler, esse interceptor pode nem participar.

Isso não torna o interceptor ruim.

Só mostra que ele mede outra coisa.


Por que HTTP ganha métrica sem você configurar quase nada

Esse ponto também explica uma diferença comum em ferramentas de observabilidade.

Bibliotecas como OpenTelemetry, Micrometer e agentes de APM costumam conseguir medir requisições HTTP automaticamente.

Você sobe a aplicação, faz uma chamada para um endpoint e as métricas aparecem: duração, status, método, rota e quantidade de requisições.

Isso acontece porque existe uma borda padronizada.

Em uma aplicação web Servlet, toda requisição passa por pontos conhecidos: servidor, cadeia de filtros, DispatcherServlet e fluxo MVC.

Para uma biblioteca de métricas, é relativamente simples plugar instrumentação nessa entrada.

Ela não precisa conhecer sua regra de negócio.

Ela não precisa saber quais services existem.

Ela não precisa entender o domínio da aplicação.

Basta observar a borda HTTP.

Com listeners, a história muda.

Um listener não é uma borda HTTP padronizada.

Ele pode ser Kafka, RabbitMQ, SQS, JMS, scheduler, evento interno, fila proprietária ou qualquer outro mecanismo.

Cada tecnologia tem seu próprio ciclo de vida, seu próprio modelo de confirmação, seu próprio jeito de representar erro, retry, tópico, fila, partição, consumer group e mensagem.

Por isso, a instrumentação automática nem sempre sabe qual é a unidade correta de trabalho.

Em HTTP, uma requisição costuma virar uma operação clara.

Em um listener, a operação pode ser uma mensagem, um lote, uma tentativa de retry, uma partição inteira, uma transação de consumo ou uma chamada disparada por agendamento.

É por isso que métricas de request aparecem com pouco ou nenhum código, enquanto métricas de listeners frequentemente exigem configuração extra, dependência específica ou instrumentação manual.

Não é porque listener seja menos importante.

É porque ele não passa pela mesma porta única que uma requisição HTTP passa.

Quando você entende Filter e Interceptor, entende também por que observabilidade automática funciona tão bem na borda web: existe um pipeline comum para interceptar.

Fora dele, a ferramenta precisa de uma integração específica com o mecanismo que está disparando o trabalho.


Interceptor precisa ser registrado no MVC

Outra diferença prática: um Filter anotado como @Component normalmente entra na cadeia da aplicação.

Um HandlerInterceptor, por outro lado, precisa ser registrado na configuração MVC:

@Configuration
public class WebConfig implements WebMvcConfigurer {

    private final AuditoriaInterceptor auditoriaInterceptor;

    public WebConfig(AuditoriaInterceptor auditoriaInterceptor) {
        this.auditoriaInterceptor = auditoriaInterceptor;
    }

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

Esse registro permite controlar onde o interceptor será aplicado.

Você pode incluir e excluir padrões de rota.

Pode limitar o interceptor a uma parte da API.

Pode organizar regras por contexto MVC.

Isso reforça a natureza dele: interceptor é uma extensão do fluxo do Spring MVC, não uma peça genérica da cadeia Servlet.


Escolher errado cria efeitos difíceis de depurar

Quando uma responsabilidade vai para o ponto errado, o código pode até funcionar no caso feliz.

O problema aparece nos cantos.

Um interceptor usado como segurança pode não cobrir fluxos que não passam pelo handler esperado.

Um filtro usado para auditoria de endpoint pode não saber qual método do controller foi selecionado.

Um filtro que tenta ler o body pode atrapalhar a conversão posterior do @RequestBody se não tratar corretamente o stream.

Um interceptor que deveria gerar correlation id pode rodar tarde demais para logs emitidos por filtros anteriores.

Um log feito no interceptor pode não registrar erro de rota inexistente.

Um log feito no filtro pode registrar o caminho, mas não o nome do handler.

Esses comportamentos não são bugs aleatórios do Spring.

São consequência da posição escolhida no pipeline.


Uma regra prática

Use Filter quando a responsabilidade for externa ao Spring MVC:

  • autenticação e segurança de borda;
  • correlation id;
  • headers globais;
  • CORS, compressão e infraestrutura HTTP;
  • métricas de requisição em nível mais externo;
  • lógica que precisa rodar mesmo quando nenhum controller for encontrado.

Use HandlerInterceptor quando a responsabilidade depender do contexto MVC:

  • lógica baseada no controller ou método selecionado;
  • leitura de anotações do handler;
  • auditoria ligada a endpoints específicos;
  • métricas por handler;
  • regras aplicadas apenas a certos padrões de rota MVC.

Essa divisão não resolve todos os casos automaticamente.

Mas evita a confusão principal.

Filter e Interceptor não são sinônimos.

Eles respondem a momentos diferentes da mesma requisição.


O ponto que vale fixar

No Spring, Filter não é uma versão antiga de Interceptor.

E Interceptor não é um filtro com nome mais bonito.

Um está na cadeia Servlet.

O outro está no fluxo do Spring MVC.

O filtro enxerga a requisição antes de o MVC decidir quem vai tratá-la.

O interceptor enxerga a requisição quando o MVC já tem mais contexto sobre o handler.

Quando você entende essa diferença, para de escolher pela familiaridade da API e começa a escolher pela posição correta no fluxo.

E, em aplicações web, posição no fluxo muda comportamento.