Java EE / Jakarta EE

Jakarta Servlet 6: servlets, filtros, sessões e a camada web

Iniciante10 min de leituraAtualizado

Esta página foi traduzida do inglês com a ajuda de IA e conferida com o original. Ler a versão em inglês

Servlets são a API HTTP de mais baixo nível do Jakarta EE e a base sobre a qual Faces, Jakarta REST e Spring MVC são construídos. Este guia explica como o contêiner mapeia requisições para servlets, o ciclo de vida e o modelo de threads, como ler requisições e escrever respostas, e como filtros, listeners, sessões e processamento assíncrono se encaixam, com exemplos que compilam com o Servlet 6.0.

O que você vai aprender

  • Explicar o ciclo de vida de um servlet e por que ele precisa ser thread-safe
  • Mapear URLs para servlets e filtros com anotações ou com web.xml
  • Ler parâmetros, cabeçalhos, corpos e uploads multipart, e escrever respostas com o encoding correto
  • Usar HttpSession e cookies corretamente e conhecer suas implicações de segurança
  • Aplicar filtros, listeners e processamento assíncrono a preocupações transversais

Toda requisição HTTP que chega a uma aplicação Jakarta EE passa pelo contêiner de servlets, não importa se o código que a atende no fim é uma página Faces, um recurso Jakarta REST, um controller Spring ou um servlet simples. A especificação Servlet define como o contêiner recebe uma requisição, escolhe o componente que vai tratá-la, expõe a requisição e a resposta como objetos Java, acompanha as sessões e permite que você intercepte o fluxo. Num projeto moderno, é raro escrever um método doGet à mão, mas quando cair na sua mesa um bug de ordem de filtros, um problema de encoding de caracteres ou um apontamento de session fixation, é nessa camada que está a resposta.

O contêiner e o ciclo da requisição

Um contêiner de servlets (Tomcat, Jetty, o Undertow dentro do WildFly, o motor web do Open Liberty e assim por diante) escuta numa porta, faz o parsing do HTTP e, para cada requisição, monta um HttpServletRequest e um HttpServletResponse. Em seguida, percorre as aplicações web implantadas para achar aquela cujo context path corresponde (/shop para shop.war), escolhe o servlet cujo URL pattern melhor casa com o restante do caminho, executa os filtros aplicáveis e, por fim, chama o método service do servlet numa thread do seu pool. Quando esse método retorna, a resposta é confirmada (committed) e a thread volta para o pool.

Os URL patterns seguem regras de correspondência fixas, aplicadas nesta ordem: correspondência exata (/orders/export), prefixo de caminho mais longo (/orders/*), extensão (*.xhtml) e, por último, o servlet padrão (/). O servlet padrão serve os arquivos estáticos do WAR; o pattern /* é diferente de / e captura tudo, inclusive recursos estáticos — um erro de configuração clássico.

Escrevendo um servlet

Um servlet é uma classe que estende jakarta.servlet.http.HttpServlet e sobrescreve os métodos correspondentes aos métodos HTTP que atende. Desde o Servlet 3.0 ele é registrado com uma anotação; o web.xml é opcional.

package shop.web;

import java.io.IOException;
import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

@WebServlet(urlPatterns = "/orders/*", loadOnStartup = 1)
public class OrderServlet extends HttpServlet {

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp)
            throws ServletException, IOException {
        String id = req.getPathInfo();            // "/42" for GET /shop/orders/42
        if (id == null || id.length() < 2) {
            resp.sendError(HttpServletResponse.SC_BAD_REQUEST, "order id required");
            return;
        }
        resp.setContentType("application/json");
        resp.setCharacterEncoding("UTF-8");        // must precede getWriter()
        resp.getWriter().printf("{\"id\":%s,\"status\":\"NEW\"}", id.substring(1));
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp)
            throws ServletException, IOException {
        String sku = req.getParameter("sku");     // form field or query parameter
        int qty = Integer.parseInt(req.getParameter("qty"));
        // ... create the order ...
        resp.setStatus(HttpServletResponse.SC_CREATED);
        resp.setHeader("Location", req.getContextPath() + "/orders/43");
    }
}

A classe base direciona GET para doGet, POST para doPost e assim por diante; a implementação padrão de cada um retorna 405 Method Not Allowed, então métodos não suportados são tratados corretamente sem código extra. HEAD é derivado automaticamente de doGet. OPTIONS e TRACE também têm implementações padrão — muitos times desativam o TRACE no nível do servidor por questões de segurança.

Ciclo de vida e threads

O contêiner instancia o servlet uma única vez (na primeira requisição, ou na inicialização quando loadOnStartup está definido), chama init(ServletConfig) uma vez, depois chama service para cada requisição, possivelmente a partir de dezenas de threads ao mesmo tempo, e por fim chama destroy() quando a aplicação é removida do servidor. Daí vêm três consequências:

  1. Um servlet não deve guardar estado da requisição em campos. Um campo de instância private Order current; é uma condição de corrida que só aparece sob carga.
  2. Recursos caros e thread-safe (um DataSource, um template já processado, um bean CDI injetado) ficam em campos inicializados no init ou por injeção.
  3. Desde o Servlet 3.0, servlets de uma aplicação com CDI habilitado são alvos de injeção, então @Inject OrderService service; funciona — mas o servlet em si não é um bean CDI e não tem escopo CDI.

A antiga interface SingleThreadModel, que pedia ao contêiner para serializar as chamadas, foi depreciada no Servlet 2.4 e finalmente removida na 6.0; ela nunca resolveu o problema, porque sessões e atributos de contexto continuavam compartilhados.

Requisição e resposta na prática

O HttpServletRequest fornece o método, a URI em partes (getContextPath(), getServletPath(), getPathInfo(), getQueryString()), os cabeçalhos, os cookies, os parâmetros e o corpo. Alguns pontos causam bugs com frequência:

  • Os parâmetros juntam a query string e um corpo codificado como formulário. getParameter retorna o primeiro valor; use getParameterValues para nomes repetidos. Chamar getParameter num POST lê e consome o corpo, então depois não dá mais para lê-lo como stream.
  • O encoding de um corpo de formulário é ISO-8859-1 por padrão se a requisição não informar o charset, o que os navegadores costumam omitir. Defina o padrão uma vez para a aplicação inteira em vez de chamar setCharacterEncoding("UTF-8") em cada servlet:
<!-- web.xml, Servlet 4.0+ -->
<request-character-encoding>UTF-8</request-character-encoding>
<response-character-encoding>UTF-8</response-character-encoding>
  • Uploads multipart exigem @MultipartConfig no servlet (ou o equivalente no web.xml); aí req.getPart("file") retorna um Part com um stream, um tamanho e o nome do arquivo enviado. Configure maxFileSize e maxRequestSize com limites razoáveis — por padrão, não há limite.
  • Atributos de requisição (setAttribute/getAttribute) são a área de rascunho de cada requisição, usada para passar dados de um filtro ou servlet para uma JSP ou view Faces para a qual se faz forward.

Do lado da resposta, a ordem importa: status, cabeçalhos e content type precisam ser definidos antes de a resposta ser confirmada, o que acontece assim que o buffer enche ou você faz flush. Depois disso, sendRedirect e setHeader são ignorados silenciosamente ou lançam IllegalStateException. Redirecionamentos (sendRedirect) devolvem ao cliente um 302 com uma nova URL; forwards (req.getRequestDispatcher("/WEB-INF/views/order.jsp").forward(req, resp)) transferem o controle para outro recurso dentro do servidor sem que o cliente saiba. Coloque as views sob WEB-INF para que não possam ser requisitadas diretamente.

O Servlet 6.0 adiciona getRequestId(), getProtocolRequestId() (o id do stream HTTP/2) e getServletConnection(), úteis para correlacionar linhas de log com a camada de rede.

Filtros e listeners

Um filtro envolve o processamento das requisições que casam com ele. Ele roda antes do servlet, pode decidir se a cadeia continua e enxerga a resposta na volta. Filtros são a ferramenta certa para verificações de autenticação, logging de requisições, compressão, cabeçalhos CORS e de segurança, e para envolver a requisição ou a resposta a fim de mudar seu comportamento.

import java.io.IOException;
import jakarta.servlet.*;
import jakarta.servlet.annotation.WebFilter;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

@WebFilter(urlPatterns = "/*")
public class SecurityHeadersFilter implements Filter {

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {
        var resp = (HttpServletResponse) response;
        resp.setHeader("X-Content-Type-Options", "nosniff");
        resp.setHeader("Referrer-Policy", "strict-origin-when-cross-origin");
        long start = System.nanoTime();
        try {
            chain.doFilter(request, response);   // hand over to the next filter or the servlet
        } finally {
            var req = (HttpServletRequest) request;
            System.out.printf("%s %s -> %d in %d ms%n", req.getMethod(), req.getRequestURI(),
                    resp.getStatus(), (System.nanoTime() - start) / 1_000_000);
        }
    }
}

Dois detalhes pegam muita gente. Primeiro, filtros declarados com anotações não têm ordem definida; se a ordem importa (autenticação antes de logging, por exemplo), declare-os no web.xml, onde rodam na ordem de declaração, ou use um único filtro que delegue. Segundo, por padrão um filtro só é invocado em dispatches REQUEST; adicione dispatcherTypes = {DispatcherType.REQUEST, DispatcherType.FORWARD} se ele também precisar ver requisições encaminhadas por forward, e DispatcherType.ERROR para a renderização de páginas de erro.

Listeners reagem a eventos do contêiner. ServletContextListener (@WebListener) executa código na subida e na parada da aplicação, o lugar tradicional para inicializações pontuais antes de existir o evento CDI @Observes @Initialized(ApplicationScoped.class). HttpSessionListener conta sessões ou faz a limpeza quando elas expiram, e ServletRequestListener pode montar um contexto por requisição, como um MDC de logging.

Sessões e cookies

HTTP não tem estado; HttpSession oferece estado no servidor, indexado por um identificador que o contêiner envia ao navegador, quase sempre como um cookie chamado JSESSIONID. req.getSession() cria uma sessão se não houver nenhuma; req.getSession(false) retorna null em vez de criar uma, que é o que você quer em código que só lê. Atributos guardados na sessão precisam ser Serializable se o servidor persistir ou replicar sessões num cluster.

As regras de segurança em torno de sessões estão bem estabelecidas, e a API de servlets dá suporte a todas elas:

  • Gere um novo id depois do login com req.changeSessionId() (Servlet 3.1+) para impedir session fixation.
  • Marque o cookie como HttpOnly e Secure para que scripts não consigam lê-lo e ele nunca trafegue em texto claro. Configure isso no web.xml em <session-config><cookie-config> ou programaticamente via ServletContext.getSessionCookieConfig().
  • Defina SameSite. O Servlet 6.0 finalmente adicionou Cookie.setAttribute("SameSite", "Lax") e o SessionCookieConfig.setAttribute correspondente, eliminando a necessidade de configurações específicas de cada servidor.
  • Mantenha o timeout curto e invalide a sessão no logout com session.invalidate().
  • Mantenha as sessões pequenas. Estado de sessão é memória no servidor e, num cluster, tráfego de rede a cada alteração. Guardar grafos de entidades ou conteúdo de arquivos na sessão é a causa mais comum de pressão de memória em aplicações web desse tipo.

A reescrita de URL (;jsessionid=… anexado aos links) era a alternativa para clientes sem cookies; o Servlet 6.0 removeu a API isRequestedSessionIdFromUrl, e você deve desativar o mecanismo com <tracking-mode>COOKIE</tracking-mode>, porque ids em URLs vazam por logs e por cabeçalhos Referer.

Processamento assíncrono

Por padrão, uma thread do contêiner fica presa durante toda a requisição. Quando o trabalho é lento porque espera por outra coisa — uma chamada remota, uma consulta longa ao banco, uma mensagem que ainda não chegou —, essa thread é desperdiçada. O Servlet 3.0 introduziu o AsyncContext para que a requisição possa ficar estacionada e ser concluída depois, a partir de outra thread:

import java.io.IOException;
import jakarta.annotation.Resource;
import jakarta.enterprise.concurrent.ManagedExecutorService;
import jakarta.servlet.AsyncContext;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.*;

@WebServlet(urlPatterns = "/report", asyncSupported = true)
public class ReportServlet extends HttpServlet {

    @Resource                                     // Jakarta Concurrency: a container-managed executor
    ManagedExecutorService executor;

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) {
        AsyncContext ctx = req.startAsync();
        ctx.setTimeout(30_000);
        executor.submit(() -> {
            String report = buildReport();        // slow work off the request thread
            try {
                ctx.getResponse().setContentType("text/plain;charset=UTF-8");
                ctx.getResponse().getWriter().write(report);
            } catch (IOException ignored) {
            } finally {
                ctx.complete();                   // releases the request; forgetting this leaks it
            }
        });
    }
}

Todos os filtros da cadeia também precisam declarar asyncSupported = true; caso contrário, startAsync lança uma exceção. O Servlet 3.1 adicionou I/O não bloqueante (ReadListener/WriteListener) para fazer streaming de corpos grandes sem bloquear uma thread; o @Suspended AsyncResponse e os Server-Sent Events do Jakarta REST são construídos sobre esses recursos, então normalmente você os usa por meio dessa API.

Tratamento de erros e restrições de segurança

Exceções não tratadas e chamadas a sendError são mapeadas para páginas de erro declaradas no web.xml, por código de status ou tipo de exceção:

<error-page>
  <error-code>404</error-code>
  <location>/WEB-INF/errors/not-found.jsp</location>
</error-page>
<error-page>
  <exception-type>java.lang.Throwable</exception-type>
  <location>/WEB-INF/errors/failure.jsp</location>
</error-page>

Sem elas, o contêiner renderiza a própria página, que em alguns servidores inclui um stack trace — um vazamento de informação que você deve fechar antes de ir para produção.

A segurança declarativa também fica no web.xml: elementos <security-constraint> associam URL patterns e métodos HTTP a papéis, e <login-config> escolhe um método de autenticação (FORM, BASIC ou um mecanismo de autenticação do Jakarta Security). Uma restrição com <auth-constraint/> e nenhum papel nega acesso a todos, e uma com <user-data-constraint>CONFIDENTIAL</user-data-constraint> força HTTPS. As verificações programáticas usam req.isUserInRole("admin"), req.getUserPrincipal() e, desde o Servlet 3.0, req.login, req.logout e req.authenticate. Atenção: as restrições só cobrem os métodos que você lista — ou todos, se você não listar nenhum — e omitir um método historicamente abriu brechas para contornar a proteção; por isso, prefira não listar nenhum (proteger tudo) e usar <http-method-omission> de forma deliberada.

Páginas, Expression Language e o destino das views

Jakarta Server Pages (JSP) e a Jakarta Standard Tag Library ainda fazem parte da plataforma e ainda funcionam, mas são uma tecnologia de view legada: são compiladas para servlets, misturam marcação e lógica com facilidade demais, e o Faces 4.0 não as aceita mais como camada de view. Já a Expression Language (EL 5.0), popularizada pelo JSP, continua bem viva e é usada pelo Faces e pelos beans @Named do CDI. Para novas interfaces, você normalmente vai escolher entre Jakarta Faces, para UIs de componentes renderizadas no servidor, e um front-end JavaScript conversando com endpoints Jakarta REST; ambos, assim como o Spring MVC, são servlets no fim das contas.

Erros comuns

  • Campos de instância guardando dados da requisição. O sintoma é um usuário, de vez em quando, ver os dados de outro. Correção: apenas variáveis locais e atributos de requisição.
  • Escrever na resposta antes de decidir o status. Depois que os bytes são enviados, não dá mais para redirecionar nem mudar cabeçalhos. Calcule primeiro, escreva por último e mantenha o buffer de resposta grande o bastante para páginas pequenas.
  • Ler o corpo duas vezes. Depois de getParameter ou getInputStream, o corpo já foi consumido. Se um filtro precisar dele, envolva a requisição num HttpServletRequestWrapper que guarde os bytes em cache.
  • Mapeamento /* num servlet da aplicação. Ele engole CSS, imagens e os mapeamentos dos servlets do Faces ou do REST. Use um prefixo (/api/*) ou caminhos exatos.
  • Contar com a ordem das anotações para os filtros. Declare no web.xml os filtros sensíveis à ordem.
  • Sessões inchadas e falta de HttpOnly/Secure/SameSite. Isso aparece em todo relatório de teste de invasão; configure o cookie uma vez no web.xml.
  • Esquecer o AsyncContext.complete(). A requisição fica pendurada até o timeout assíncrono disparar e o contêiner registra um aviso; o cliente recebe um erro depois de muita demora.

Principais pontos

  • O contêiner de servlets faz o parsing do HTTP, associa a URL a um servlet, executa a cadeia de filtros e chama service numa thread do pool; Faces, Jakarta REST e Spring MVC rodam todos sobre isso.
  • Uma única instância de servlet atende todas as requisições em paralelo, então não guarde estado em campos.
  • Defina status e cabeçalhos antes de escrever o corpo; use forward para dispatch interno e redirect quando o cliente precisar ver uma nova URL.
  • Configure uma vez no web.xml o encoding de requisições e respostas, os atributos do cookie de sessão, as páginas de erro e as restrições de segurança; use anotações para os próprios servlets, filtros e listeners.
  • Filtros cuidam de comportamentos transversais; declare-os no web.xml quando a ordem importar.
  • O processamento assíncrono libera threads do contêiner durante trabalho lento, mas todos os filtros da cadeia precisam suportá-lo e você deve sempre chamar complete().
  • O Servlet 6.0 removeu APIs legadas, adicionou suporte a SameSite por meio de atributos de cookie e incluiu identificadores de requisição e de conexão.

Fontes e leituras complementares

Perguntas frequentes

Uma única instância de servlet é compartilhada entre todas as requisições?
Sim. O contêiner cria uma única instância por declaração de servlet e chama o método service dela a partir de muitas threads ao mesmo tempo. Tudo o que fica num campo de instância é estado compartilhado: mantenha os dados de cada requisição em variáveis locais ou atributos da requisição, e deixe qualquer campo compartilhado imutável ou devidamente sincronizado.
Ainda preciso do web.xml?
Não para declarar servlets, filtros e listeners: desde o Servlet 3.0 as anotações cuidam disso. O web.xml continua sendo o lugar para o que as anotações não conseguem expressar ou que muda conforme o ambiente: restrições de segurança, páginas de erro, timeout de sessão, arquivos de boas-vindas, encodings padrão e a ordem de filtros declarados em bibliotecas diferentes.
O que mudou no Servlet 6.0 em relação ao 5.0?
O Servlet 6.0 removeu APIs depreciadas havia anos — SingleThreadModel, HttpSessionContext, HttpUtils e os métodos de id de sessão na URL —, adicionou Cookie.setAttribute para atributos como SameSite, incluiu identificadores de requisição e informações da conexão, e endureceu o tratamento de URIs para que caminhos ambíguos, como barras codificadas, sejam rejeitados por padrão.
Devo escrever servlets puros ou usar um framework?
Para código de aplicação, use Jakarta REST para APIs e Faces ou um framework front-end para interfaces; ambos rodam sobre a API de servlets e cuidam para você de roteamento, conversão e negociação de conteúdo. Escreva servlets e filtros diretamente para questões de infraestrutura, como autenticação, logging, endpoints de health check e integração com sistemas legados.