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:
- 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. - Recursos caros e thread-safe (um
DataSource, um template já processado, um bean CDI injetado) ficam em campos inicializados noinitou por injeção. - 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.
getParameterretorna o primeiro valor; usegetParameterValuespara nomes repetidos. ChamargetParameternumPOSTlê 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
@MultipartConfigno servlet (ou o equivalente noweb.xml); aíreq.getPart("file")retorna umPartcom um stream, um tamanho e o nome do arquivo enviado. ConfiguremaxFileSizeemaxRequestSizecom 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
HttpOnlyeSecurepara que scripts não consigam lê-lo e ele nunca trafegue em texto claro. Configure isso noweb.xmlem<session-config><cookie-config>ou programaticamente viaServletContext.getSessionCookieConfig(). - Defina
SameSite. O Servlet 6.0 finalmente adicionouCookie.setAttribute("SameSite", "Lax")e oSessionCookieConfig.setAttributecorrespondente, 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
getParameterougetInputStream, o corpo já foi consumido. Se um filtro precisar dele, envolva a requisição numHttpServletRequestWrapperque 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.xmlos 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 noweb.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
servicenuma 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.xmlo 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.xmlquando 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
SameSitepor 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.