Componentes Fundamentais do Java Web
No ecossistema Java Web, a arquitetura baseia-se em três pilares principais: Servlets, Filters (Filtros) e Listeners (Observadores). O Servlet é a espinha dorsal desse modelo, atuando como o componente central para o processamento de requisições. Filtros e Listeners complementam essa arquitetura, lidando com interceptação de requisições e eventos de ciclo de vida, respectivamente.
O Papel do Servlet na Arquitetura
Um Servlet é essencialmente um programa Java executado dentro de um contêiner web (como Apache Tomcat ou Jetty). Ele funciona como uma camada intermediária entre as requisições HTTP originadas pelos clientes e os recursos do servidor, como bancos de dados ou lógica de negócios. O contêiner web é responsável por analisar a URL da requisição, roteá-la para o Servlet apropriado e gerenciar todo o ciclo de vida do componente.
A Interface Servlet e seus Métodos
Para criar um Servlet do zero, é necessário implementar a interface javax.servlet.Servlet (ou jakarta.servlet.Servlet nas versões mais recentes). Esta interface dita o contrato que o contêiner web utilizará para gerenciar o componente.
| Método | Frequência de Execução | Responsabilidade Principal |
|---|---|---|
init(ServletConfig) |
Uma única vez | Preparar o ambiente, carregar configurações e inicializar recursos pesados (ex: pools de conexão). |
service(Request, Response) |
A cada requisição | Ponto de entrada para a lógica de negócios. O contêiner invoca este método em uma thread separada para cada solicitação. |
destroy() |
Uma única vez | Limpar recursos, fechar conexões e realizar tarefas de desligamento antes da instância ser removida da memória. |
getServletConfig() |
Sob demanda | Retornar o objeto de configuração específico da instância. |
getServletInfo() |
Raramnete | Fornecer metadados descritivos sobre o componente (autor, versão). |
Implementação de Baixo Nível
Abaixo, um exemplo de implementação direta da interface base. Note que o contêiner é o único responsável por instanciar e gerenciar este objeto.
package br.com.exemplo.web;
import javax.servlet.*;
import java.io.IOException;
public class EndpointBasico implements Servlet {
private ServletConfig configLocal;
@Override
public void init(ServletConfig config) throws ServletException {
this.configLocal = config;
System.out.println("Fase de Inicialização: Configurando recursos do endpoint.");
String nomeServlet = config.getServletName();
String paramCustom = config.getInitParameter("configEspecifica");
System.out.println("Servlet: " + nomeServlet + " | Param: " + paramCustom);
}
@Override
public ServletConfig getServletConfig() {
return this.configLocal;
}
@Override
public void service(ServletRequest req, ServletResponse res)
throws ServletException, IOException {
System.out.println("Processando requisição na thread: " + Thread.currentThread().getName());
// Lógica de negócios e manipulação de resposta
}
@Override
public String getServletInfo() {
return "EndpointBasico v2.0";
}
@Override
public void destroy() {
System.out.println("Fase de Destruição: Liberando recursos alocados.");
}
}
Evolução para HttpServlet
Na prática, raramente implementamos a interface Servlet diretamente. A API fornece a classe abstrata HttpServlet, que já lida com a comlpexidade do protocolo HTTP. Ela sobrescreve o método service para inspecionar o verbo HTTP (GET, POST, PUT, DELETE) e delegar a execução para métodos específicos como doGet e doPost.
package br.com.exemplo.web;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
public class ProcessadorHttp extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws IOException {
resp.setContentType("application/json");
resp.setCharacterEncoding("UTF-8");
resp.getWriter().write("{\"status\": \"sucesso\", \"metodo\": \"GET\"}");
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp)
throws IOException {
// Processamento de dados submetidos via POST
resp.setStatus(HttpServletResponse.SC_CREATED);
}
}
Configuração e Mapeamento de Rotas
Historicamente, o mapeamento era feito exclusivamente via arquivo web.xml. Embora ainda suportado e útil para configurações globais, a especificação Servlet 3.0+ introduziu o uso de anotações, simplificando o desenvolvimento.
Abordagem via web.xml
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee" version="4.0">
<!-- Parâmetro Global -->
<context-param>
<param-name>ambiente</param-name>
<param-value>producao</param-value>
</context-param>
<servlet>
<servlet-name>processadorHttp</servlet-name>
<servlet-class>br.com.exemplo.web.ProcessadorHttp</servlet-class>
<init-param>
<param-name>timeout</param-name>
<param-value>5000</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>processadorHttp</servlet-name>
<url-pattern>/api/processar</url-pattern>
</servlet-mapping>
</web-app>
Regras de Prioridade de URLs
O contêiner web avalia os padrões de URL (url-pattern) seguindo uma hierarquia estrita para resolver conflitos:
- Correspondência Exata:
/api/usuarios(Maior prioridade) - Correspondência de Diretório:
/api/* - Correspondência de Extensão:
*.json(Menor prioridade)
Atenção: Combinar diretório e extensão (ex: /api/*.json) é inválido e causará falhas na inicialização do contêiner.
Gerenciamento de Contexto e Escopo
Compreender a diferença entre ServletConfig e ServletContext é crucial para o gerenciamento de estado na aplicação.
- ServletConfig: Escopo local. Contém parâmetros de inicialização exclusivos para um Servlet específico.
- ServletContext: Escopo global. Representa a aplicação web inteira. É utilizado para compartilhar dados entre todos os Servlets, acessar recursos do servidor e ler parâmetros de contexto globais.
Compartilhamento de Dados Globais
O ServletContext atua como um repositório compartilhado. Um caso de uso comum é a criação de contadores globais ou caches de aplicação.
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
ServletContext contextoGlobal = getServletContext();
// Sincronização necessária para evitar condições de corrida em variáveis compartilhadas
synchronized (contextoGlobal) {
Integer acessos = (Integer) contextoGlobal.getAttribute("totalAcessos");
if (acessos == null) {
acessos = 0;
}
contextoGlobal.setAttribute("totalAcessos", ++acessos);
resp.getWriter().write("Total de acessos à aplicação: " + acessos);
}
}
Resolução de Caminhos e Leitura de Recursos
Ao manipular arquivos dentro do servidor, é vital distinguir entre caminhos de cliente e caminhos de servidor:
- Caminho do Cliente (Navegador): A barra
/aponta para a raiz do servidor HTTP (ex:http://localhost:8080/). - Caminho do Servidor (Contêiner): A barra
/aponta para a raiz da aplicação web implantada (context root).
Para ler arquivos de configuração, a abordagem mais robusta e desacoplada do contêiner web é utilizar o ClassLoader:
// Leitura segura de arquivos no classpath (ex: src/main/resources)
InputStream fluxoConfig = this.getClass().getClassLoader()
.getResourceAsStream("configuracoes.properties");
Concorrência e Segurança de Threads
Por padrão, o contêiner web cria apenas uma única instância de cada classe Servlet e a reutiliza para atender a todas as requisições. Para lidar com múltiplos usuários simultâneos, o contêiner aloca uma thread distinta do seu pool para executar o método service (ou doGet/doPost) na mesma instância.
Isso implica que Servlets não são thread-safe por natureza. A regra de ouro para evitar condições de corrida e corrupção de dados é:
- Nunca utilize variáveis de instância (atirbutos da classe) para armazenar dados específicos de uma requisição ou estado mutável.
- Utilize exclusivamente variáveis locais dentro dos métodos de serviço, pois elas são alocadas na pilha (stack) de cada thread, garantindo isolamento total.
- Evite sincronizar os métodos do Servlet com a palavra-chave
synchronized, pois isso transformará sua aplicação em um gargalo de thread única, destruindo a escalabilidade.