Java EE / Jakarta EE

Arquitetura do Jakarta EE: contêineres, perfis e servidores de aplicação

Iniciante14 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

O Jakarta EE é um conjunto de especificações que um servidor de aplicação implementa para que seu código conte com serviços gerenciados em vez de montá-los por conta própria. Ao final deste guia, você saberá explicar o modelo de contêineres, citar as especificações que compõem o Jakarta EE 10 e seus perfis, entender o que mudou com a passagem de javax.* para jakarta.*, empacotar corretamente um WAR ou um EAR e escolher entre GlassFish, WildFly, Open Liberty, Payara ou um simples contêiner de servlets.

O que você vai aprender

  • Descrever o modelo multicamadas e o papel do contêiner web e do contêiner de enterprise beans
  • Listar as principais especificações do Jakarta EE 10 e o que os perfis Platform, Web e Core incluem
  • Explicar a história do J2EE ao Jakarta EE e a migração de javax.* para jakarta.*
  • Empacotar uma aplicação como WAR ou EAR e entender os nomes JNDI portáveis
  • Escolher um servidor de aplicação ou runtime para um projeto novo ou existente

Toda aplicação de servidor que não seja trivial precisa do mesmo conjunto de serviços: uma porta de entrada HTTP, um jeito de conversar com um banco de dados dentro de uma transação, um modelo de segurança, um lugar para rodar tarefas em segundo plano e alguma forma de mensageria. O Jakarta EE é a resposta padronizada a essa lista na plataforma Java. Você escreve componentes com base em um conjunto de APIs publicadas; um servidor de aplicação implementa essas APIs e fornece os serviços de runtime e, em troca, quase não sobra código de infraestrutura para você escrever. Entender essa divisão de responsabilidades — o que o contêiner faz por você e o que ele espera de você — é a base de tudo o que vem a seguir neste tema, e também explica boa parte de como o Spring e o MicroProfile foram projetados.

Do J2EE ao Jakarta EE

A plataforma mudou de nome duas vezes e os nomes ainda causam confusão, então vale a pena fixar a linha do tempo.

VersãoAnoConteúdo relevante
J2EE 1.2 – 1.41999 – 2003Servlets, JSP, EJB 1.x/2.x com interfaces home e descritores XML, JMS, JTA, JAX-RPC
Java EE 52006Anotações substituem a maioria dos descritores, EJB 3.0, Java Persistence API (JPA) 1.0, JSF 1.2
Java EE 62009CDI 1.0, EJB 3.1 (visão sem interface, singletons), JAX-RS 1.1, Bean Validation, o Web Profile
Java EE 72013API simplificada do JMS 2.0, WebSocket, JSON-P, Batch, Concurrency Utilities, @Transactional do JTA
Java EE 82017JSON-B, Java EE Security API 1.0, Servlet 4.0 com push HTTP/2, JAX-RS 2.1
Jakarta EE 82019As mesmas APIs do Java EE 8, agora sob a governança da Eclipse Foundation; pacotes javax.*
Jakarta EE 9 / 9.12020 / 2021Namespace dos pacotes alterado para jakarta.*; remoção de APIs antigas já podadas; suporte ao Java 11
Jakarta EE 102022Primeira versão com novos recursos sob a Eclipse: CDI 4.0 com CDI Lite, Core Profile, Servlet 6, Faces 4, Persistence 3.1, REST 3.1
Jakarta EE 112025Java 17 como base mínima, Jakarta Data 1.0, nova poda de APIs legadas

A troca de nome aconteceu porque, em 2017, a Oracle transferiu as especificações e o código de referência para a Eclipse Foundation, mas ficou com a marca Java. O prefixo de pacote javax fazia parte dessa marca, então a comunidade não teve escolha a não ser renomear todos os pacotes. É por isso que o Jakarta EE 9 é uma versão que quebra compatibilidade sem nenhum recurso novo: javax.servlet.http.HttpServletRequest virou jakarta.servlet.http.HttpServletRequest, javax.persistence.Entity virou jakarta.persistence.Entity, e assim por diante em todas as especificações. Os namespaces XML dos descritores de implantação mudaram ao mesmo tempo, de http://xmlns.jcp.org/xml/ns/javaee para https://jakarta.ee/xml/ns/jakartaee.

Este guia tem como alvo o Jakarta EE 10 e usa jakarta.* do começo ao fim. Ao ler material mais antigo — e a maior parte do código Java corporativo em produção foi escrita para o Java EE 6, 7 ou 8 —, troque o prefixo mentalmente: de resto, os nomes de classes e métodos são os mesmos.

O modelo de camadas e contêineres

O Jakarta EE descreve uma aplicação como um conjunto de componentes distribuídos em camadas lógicas. As camadas são conceituais: duas delas muitas vezes compartilham o mesmo processo e, na maioria das implantações modernas, todas as camadas do lado do servidor ficam em uma única instância do servidor de aplicação.

  • Camada cliente — um navegador, um app mobile, outro serviço chamando um endpoint REST ou um cliente Java desktop. Para este último caso a plataforma ainda define um contêiner de cliente de aplicação, mas ele é pouco usado hoje.
  • Camada web — servlets, páginas Faces, recursos Jakarta REST e endpoints WebSocket. Esses componentes recebem as requisições, as traduzem em chamadas à camada de negócio e montam a resposta.
  • Camada de negócio — enterprise beans e beans CDI que implementam os casos de uso, rodam dentro de transações e aplicam as regras de segurança.
  • Camada de sistemas de informação corporativos (EIS) — bancos de dados relacionais, brokers de mensagens, mainframes e outros back-ends acessados via JDBC, JMS ou resource adapters de conectores (JCA).

O que torna isso mais do que um diagrama de arquitetura em camadas é o contêiner: o runtime que hospeda um tipo de componente e fica entre o componente e o mundo externo. Como seu servlet ou bean é instanciado e invocado pelo contêiner, ele pode:

  • injetar dependências (@Inject, @EJB, @PersistenceContext, @Resource) antes que seu código rode;
  • iniciar e confirmar uma transação em torno de cada método de negócio;
  • verificar se quem chama tem o papel exigido;
  • manter instâncias em pool, gerenciar o acesso das threads e passivar o estado ocioso;
  • aplicar interceptors e callbacks de ciclo de vida (@PostConstruct, @PreDestroy).

O contêiner web hospeda servlets, filtros, listeners, páginas JSP, views Faces, recursos REST e endpoints WebSocket. O contêiner de enterprise beans hospeda session beans e message-driven beans. Os dois compartilham o contêiner CDI para injeção de dependências e escopos contextuais, além de serviços da plataforma como a nomeação JNDI, as transações JTA e o subsistema de segurança. A consequência prática: nunca instancie um componente gerenciado com new. Um new OrderService() é um objeto comum, sem campos injetados, sem transação e sem contexto de segurança; só as instâncias criadas pelo contêiner têm tudo isso.

O que o servidor realmente fornece

Um servidor de aplicação é o produto que implementa as especificações. Os fornecedores competem em ferramentas de administração, clustering, desempenho e suporte, não nas APIs contra as quais seu código compila. Entre os serviços que todo servidor compatível precisa oferecer estão:

ServiçoEspecificaçãoPara que você usa
NomeaçãoJNDI (parte do Java SE)Localizar data sources, destinos JMS e beans pelo nome
TransaçõesJakarta TransactionsFronteiras ACID declarativas ou programáticas, incluindo XA entre vários recursos
PersistênciaJakarta PersistenceMapeamento objeto-relacional com um EntityManager gerenciado pelo contêiner
MensageriaJakarta MessagingFilas e tópicos, message-driven beans
WebServlet, Pages, Faces, REST, WebSocketTratamento de requisições HTTP e renderização
Injeção e escoposCDI, Dependency InjectionLigação de componentes, escopos request/session/application, eventos
SegurançaJakarta Security, Authentication, AuthorizationMecanismos de autenticação, identity stores, verificação de papéis
ConcorrênciaJakarta ConcurrencyExecutors gerenciados e tarefas agendadas que preservam o contexto do contêiner
IntegraçãoConnectors (JCA), MailResource adapters para sistemas legados, envio de e-mail

Recursos como data sources são configurados no servidor e expostos ao seu código por nomes JNDI, de modo que o mesmo WAR roda em teste e em produção com credenciais diferentes e sem rebuild. @DataSourceDefinition permite que uma aplicação declare seu próprio data source, o que é conveniente para demos, mas coloca credenciais no código.

import jakarta.annotation.Resource;
import jakarta.enterprise.context.ApplicationScoped;
import javax.sql.DataSource;

@ApplicationScoped
public class HealthCheck {

    // O contêiner resolve este nome JNDI no momento da injeção.
    @Resource(lookup = "java:/jdbc/OrdersDS")
    DataSource dataSource;

    public boolean databaseReachable() {
        try (var connection = dataSource.getConnection()) {
            return connection.isValid(2);
        } catch (Exception e) {
            return false;
        }
    }
}

Perfis: Platform, Web e Core

Nem toda aplicação precisa de tudo isso, e exigir um servidor de plataforma completa para uma pequena aplicação web foi um dos motivos pelos quais o Java EE perdeu espaço para stacks mais leves nos anos 2000. Desde o Java EE 6 a plataforma define perfis, e o Jakarta EE 10 tem três:

  • Jakarta EE Platform — tudo.
  • Web Profile — a camada web mais o que uma aplicação web típica precisa por trás: Servlet, Pages, Expression Language, Faces, CDI, Bean Validation, Persistence, Transactions, REST, JSON-P/JSON-B, WebSocket, Security, Concurrency (incluída no perfil no EE 10) e EJB Lite: session beans locais, transações gerenciadas pelo contêiner, interceptors, segurança declarativa e timers não persistentes, mas sem interfaces remotas, sem message-driven beans e sem JMS.
  • Core Profile — novidade do EE 10, voltado para frameworks de microsserviços e compilação ahead-of-time: CDI Lite, REST, JSON-P, JSON-B, Annotations, Dependency Injection e Interceptors. O MicroProfile é construído sobre esse perfil.

A escolha afeta sua dependência Maven. Compile contra o artefato de API do perfil escolhido e deixe o servidor fornecê-lo em tempo de execução:

<dependency>
  <groupId>jakarta.platform</groupId>
  <artifactId>jakarta.jakartaee-web-api</artifactId>  <!-- ou jakarta.jakartaee-api para a plataforma completa -->
  <version>10.0.0</version>
  <scope>provided</scope>
</dependency>

O escopo provided faz diferença. Se você empacotar as classes da API — ou, pior, uma implementação como o Hibernate — dentro de um WAR implantado em um servidor que já as traz, vai ter conflitos de carregamento de classes difíceis de diagnosticar.

Empacotamento e implantação

As aplicações Jakarta EE são implantadas como arquivos com um layout interno fixo, e o contêiner lê tanto as anotações quanto os descritores XML opcionais para descobrir os componentes.

  • WAR (web archive) — WEB-INF/classes para suas classes, WEB-INF/lib para as bibliotecas, WEB-INF/web.xml (opcional desde o Servlet 3.0), WEB-INF/beans.xml (opcional no CDI 4, que por padrão descobre os beans anotados), META-INF/persistence.xml dentro de classes para o JPA. Desde o Java EE 6 um WAR pode conter enterprise beans diretamente; é de longe o empacotamento mais comum hoje.
  • EJB JAR — apenas enterprise beans, com um META-INF/ejb-jar.xml opcional.
  • EAR (enterprise archive) — um invólucro que contém WARs, EJB JARs, resource adapters (RAR) e bibliotecas compartilhadas em lib/, descrito por META-INF/application.xml. Use-o só quando você realmente precisar que vários módulos compartilhem bibliotecas e sejam implantados como uma unidade; ele complica o carregamento de classes e a maioria dos times já não precisa dele.

O cabeçalho de um web.xml do Servlet 6.0 mostra o namespace e a versão atuais:

<web-app xmlns="https://jakarta.ee/xml/ns/jakartaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="https://jakarta.ee/xml/ns/jakartaee
                             https://jakarta.ee/xml/ns/jakartaee/web-app_6_0.xsd"
         version="6.0">
  <session-config>
    <session-timeout>30</session-timeout>
  </session-config>
</web-app>

Os descritores sobrescrevem as anotações, e esse é o jeito previsto de alterar configurações específicas de cada ambiente — uma restrição de segurança, um parâmetro de inicialização de servlet, um timeout de sessão — sem mexer no código-fonte.

Nomes JNDI portáveis

Desde o Java EE 6, todo session bean recebe nomes JNDI padronizados, então você pode localizá-lo de qualquer lugar sem sintaxe específica de fornecedor:

java:global/<app-name>/<module-name>/<bean-name>[!<fully-qualified-interface>]
java:app/<module-name>/<bean-name>
java:module/<bean-name>

Para um bean OrderService em shop.war implantado de forma isolada, o nome global é java:global/shop/OrderService. O namespace java:comp/env guarda as entradas de ambiente do próprio componente (pontos de injeção @Resource), e java:comp/UserTransaction e java:comp/DefaultDataSource são entradas padrão que todo servidor fornece. A injeção tornou raros os lookups explícitos, mas você ainda vai encontrá-los em código antigo e em clientes remotos.

Escolhendo um servidor ou runtime

Todos os produtos compatíveis com o Jakarta EE 10 passam pelo mesmo Technology Compatibility Kit, então o comportamento das APIs é o mesmo; as diferenças são operacionais.

ProdutoPerfilObservações
Eclipse GlassFish 7PlatformA implementação compatível usada para ratificar as especificações; boa para aprender, menos comum em produção
WildFly (27+)PlatformO servidor comunitário da Red Hat, base do JBoss EAP; também oferece um feature pack "preview" e empacotamento como bootable JAR
Open LibertyPlatformO runtime modular da IBM; os recursos são ativados por aplicação, inicialização rápida, base do WebSphere Liberty
Payara Server / MicroPlatformDerivado do GlassFish com suporte comercial; o Payara Micro roda um WAR pela linha de comando
Apache TomEE 10Variantes Web Profile / PlatformTomcat mais OpenEJB, OpenJPA/Hibernate, ActiveMQ, CXF
Apache Tomcat 10.1 / Jetty 12Apenas Servlet, Pages, EL, WebSocketNão são servidores Jakarta EE; o resto você traz como bibliotecas

Na prática, a escolha depende do que seu time de operações já roda e mantém atualizado, de você precisar ou não de suporte comercial (JBoss EAP, WebSphere Liberty e Payara Enterprise são as edições com suporte de WildFly, Open Liberty e Payara) e de como você implanta: todos esses servidores podem ser embutidos em uma imagem de contêiner junto com a aplicação, e o bootable JAR do WildFly ou o Payara Micro geram um único executável, bem parecido com um jar do Spring Boot. Se você só usa servlets, JPA e REST, um runtime Web Profile — ou o Tomcat mais bibliotecas — pode ser a resposta pragmática.

Onde entram o Spring e o MicroProfile

O Spring nasceu em 2003 como reação ao EJB 2.x, cujas interfaces home, exceções checked obrigatórias e descritores XML tornavam trabalhosas as coisas simples. O Java EE 5 e 6 absorveram a maior parte das ideias do Spring — anotações, injeção de dependências, componentes como objetos simples — no EJB 3 e no CDI. Hoje a relação é mais simbiótica do que competitiva: o Spring Framework 6 e o Spring Boot 3 dependem das APIs do Jakarta EE 9+ para a camada de servlets, JPA, Bean Validation, JMS e JTA, e apenas fornecem seu próprio contêiner em vez de depender de um servidor de aplicação. Se você entende as especificações, entende boa parte do que o Spring autoconfigura; o tema Spring cobre esse lado.

O MicroProfile é um conjunto irmão de especificações da Eclipse que acrescenta, sobre o Core Profile, o que serviços cloud-native precisam — configuração, health checks, métricas, OpenAPI, um cliente REST, tolerância a falhas, propagação de JWT — e é implementado por Open Liberty, Payara, WildFly, Quarkus e Helidon. O que você sabe de CDI e Jakarta REST vale para todos eles.

Migrando de javax para jakarta

Como a renomeação do Jakarta EE 9 é mecânica, as ferramentas conseguem fazer a maior parte do trabalho:

  1. Atualize o runtime e a dependência da API

    Migre para um servidor Jakarta EE 9.1+ (ou Tomcat 10.1+, Jetty 11+) e troque a dependência jakarta.jakartaee-api (ou as dependências por especificação) para uma versão 9.1 ou 10.
  2. Reescreva imports e descritores

    Rode o Eclipse Transformer ou uma receita do OpenRewrite sobre a árvore de código-fonte. Eles reescrevem os imports javax.* dos pacotes afetados, os namespaces XML e nomes de propriedades como javax.persistence.jdbc.url, sem mexer em pacotes do Java SE como javax.sql e javax.naming, que nunca mudaram de lugar.
  3. Atualize todas as bibliotecas que usam as APIs

    Hibernate 6, Jersey 3, RESTEasy 6, Mojarra 4, EclipseLink 4 e Spring 6 são as linhas com o namespace Jakarta. Uma única dependência transitiva que ainda importe javax.servlet vai falhar em tempo de execução com NoClassDefFoundError, então vasculhe a árvore de dependências.
  4. Procure APIs removidas

    O Servlet 6.0 removeu tipos que já estavam depreciados havia muito tempo, como SingleThreadModel e HttpSessionContext; o Faces 4.0 removeu @ManagedBean e as views JSP; o Enterprise Beans 4.0 abandonou os entity beans do EJB 2.x e os endpoints JAX-RPC. O código que os usa precisa ser reescrito, não apenas reimportado.

O guia de migração do Tomcat descreve uma variante da mesma abordagem: o Tomcat 10 consegue transformar WARs legados na hora, o que dá fôlego, mas não deve ser o plano de longo prazo.

Erros comuns

  • Empacotar uma implementação que o servidor já fornece. Colocar hibernate-core ou uma implementação de Jakarta REST dentro de um WAR para WildFly ou Liberty gera duas cópias das mesmas classes em class loaders diferentes e erros como ClassCastException: OrderEntity cannot be cast to OrderEntity. Use o escopo provided para as APIs e deixe o servidor fornecer as implementações, a menos que o servidor documente como substituir a versão dele.
  • Misturar artefatos javax e jakarta. Um projeto migrado pela metade compila e depois falha na implantação. Vasculhe a árvore de dependências, não só os seus próprios imports.
  • Tratar as camadas como físicas. Separar a camada web e a de negócio em servidores diferentes, ligados por chamadas remotas a enterprise beans, era moda na era do J2EE e quase sempre é mais lento e mais difícil de operar do que um único processo. Mantenha-as juntas, a não ser que você tenha medições que justifiquem o contrário.
  • Configurar recursos na aplicação. URLs de conexão fixas em persistence.xml ou em @DataSourceDefinition funcionam no notebook e depois vazam credenciais para o controle de versão. Defina data sources e destinos JMS no servidor e referencie-os pelo nome JNDI.
  • Usar um EAR por hábito. Um único WAR é mais simples de construir, implantar e depurar. Recorra a um EAR só quando você tiver vários módulos que precisam ser versionados juntos.

Principais pontos

  • O Jakarta EE é um conjunto de especificações; um servidor de aplicação as implementa e roda seus componentes dentro de contêineres que injetam dependências, gerenciam transações e aplicam a segurança.
  • A linhagem é J2EE, depois Java EE 5–8, depois Jakarta EE 8 (mesmas APIs), 9 (javax.* vira jakarta.*, sem recursos novos), 10 (primeira versão com recursos novos) e 11.
  • O Jakarta EE 10 define três perfis — Platform, Web e Core — para que você adote só o que precisa; o Web Profile atende à maioria das aplicações web.
  • Compile contra o artefato jakarta.jakartaee-api ou jakarta.jakartaee-web-api com escopo provided e deixe o servidor fornecer as implementações.
  • Empacote como WAR sempre que possível; use nomes JNDI portáveis java:global e recursos definidos no servidor em vez de configuração fixa no código.
  • GlassFish, WildFly, Open Liberty, Payara e TomEE são implementações compatíveis; Tomcat e Jetty implementam apenas a família Servlet.
  • O Spring e o MicroProfile se apoiam nas mesmas especificações, então o conhecimento se transfere nos dois sentidos.

Fontes e leituras complementares

Perguntas frequentes

Jakarta EE é a mesma coisa que Java EE?
É a continuação do Java EE sob a Eclipse Foundation. O Jakarta EE 8 é funcionalmente idêntico ao Java EE 8 e ainda usa os pacotes javax.*. A partir do Jakarta EE 9, os pacotes passaram para jakarta.*, e é por isso que bibliotecas e servidores escritos para o Java EE 8 não rodam sem alterações em um servidor Jakarta EE 9 ou posterior.
Preciso de um servidor de aplicação ou o Tomcat basta?
Tomcat e Jetty implementam apenas as especificações de Servlet, JSP, EL e WebSocket. Se você precisa que o próprio contêiner forneça CDI, JPA, JTA, JMS, enterprise beans ou Jakarta REST, use um servidor Web Profile ou da Platform completa, como WildFly, Open Liberty, Payara, GlassFish ou TomEE. Frameworks como o Spring Boot trazem suas próprias implementações de várias dessas especificações e, por isso, conseguem rodar no Tomcat.
Qual a diferença entre o Web Profile e a Platform completa?
O Web Profile é um subconjunto voltado para aplicações web: servlets, Faces, CDI, JPA, JTA, Bean Validation, Jakarta REST, JSON, WebSocket, Security, Concurrency e um conjunto reduzido de recursos de enterprise beans chamado EJB Lite. A Platform completa acrescenta mensageria (JMS), conectores, Mail, Batch, beans remotos e message-driven, entre outros. O Core Profile, novidade do Jakarta EE 10, é ainda menor e mira runtimes de microsserviços.
Um projeto novo deve usar Jakarta EE ou Spring Boot?
Os dois são opções viáveis. O Jakarta EE oferece padrões neutros em relação a fornecedores, um runtime gerenciado e menos código de framework na sua aplicação; o Spring Boot oferece um ecossistema maior, um ritmo de releases mais rápido e um empacotamento autônomo mais simples. O próprio Spring 6 é construído sobre as APIs do Jakarta EE 9+ (Servlet, JPA, Bean Validation), então as especificações que você aprende aqui se aplicam diretamente.