Java EE / Jakarta EE

Arquitectura de Jakarta EE: contenedores, perfiles y servidores de aplicaciones

Principiante14 min de lecturaActualizado

Esta página se tradujo del inglés con ayuda de IA y se cotejó con el original. Leer la versión en inglés

Jakarta EE es un conjunto de especificaciones que un servidor de aplicaciones implementa para que tu código se apoye en servicios gestionados en lugar de montarlos por su cuenta. Al terminar esta guía sabrás explicar el modelo de contenedores, nombrar las especificaciones que forman Jakarta EE 10 y sus perfiles, entender qué cambió con el paso de javax.* a jakarta.*, empaquetar correctamente un WAR o un EAR y elegir entre GlassFish, WildFly, Open Liberty, Payara o un simple contenedor de servlets.

Lo que aprenderás

  • Describir el modelo multicapa y el papel del contenedor web y del contenedor de enterprise beans
  • Enumerar las principales especificaciones de Jakarta EE 10 y qué incluyen los perfiles Platform, Web y Core
  • Explicar la historia de J2EE a Jakarta EE y la migración de javax.* a jakarta.*
  • Empaquetar una aplicación como WAR o EAR y entender los nombres JNDI portables
  • Elegir un servidor de aplicaciones o un runtime para un proyecto nuevo o existente

Cualquier aplicación de servidor que no sea trivial necesita el mismo conjunto de servicios: una entrada HTTP, una forma de hablar con una base de datos dentro de una transacción, un modelo de seguridad, un sitio donde ejecutar trabajo en segundo plano y algún tipo de mensajería. Jakarta EE es la respuesta estandarizada a esa lista en la plataforma Java. Escribes componentes contra un conjunto de API publicadas; un servidor de aplicaciones implementa esas API y aporta los servicios de ejecución, y a cambio tu código apenas contiene fontanería de infraestructura. Entender este reparto de responsabilidades —qué hace el contenedor por ti y qué espera de ti— es la base de todo lo demás en este tema, y además explica buena parte del diseño de Spring y MicroProfile.

De J2EE a Jakarta EE

La plataforma ha cambiado de nombre dos veces y los nombres todavía generan confusión, así que conviene fijar la cronología.

VersiónAñoContenido destacado
J2EE 1.2 – 1.41999 – 2003Servlets, JSP, EJB 1.x/2.x con interfaces home y descriptores XML, JMS, JTA, JAX-RPC
Java EE 52006Las anotaciones sustituyen a la mayoría de los descriptores, EJB 3.0, Java Persistence API (JPA) 1.0, JSF 1.2
Java EE 62009CDI 1.0, EJB 3.1 (vista sin interfaz, singletons), JAX-RS 1.1, Bean Validation, el Web Profile
Java EE 72013API simplificada de JMS 2.0, WebSocket, JSON-P, Batch, Concurrency Utilities, @Transactional de JTA
Java EE 82017JSON-B, Java EE Security API 1.0, Servlet 4.0 con push de HTTP/2, JAX-RS 2.1
Jakarta EE 82019Las mismas API que Java EE 8, ahora gobernadas por la Eclipse Foundation; paquetes javax.*
Jakarta EE 9 / 9.12020 / 2021El espacio de nombres de los paquetes cambia a jakarta.*; se eliminan las API antiguas podadas; soporte de Java 11
Jakarta EE 102022Primera versión con novedades bajo Eclipse: CDI 4.0 con 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, nueva poda de API heredadas

El cambio de nombre se produjo porque Oracle transfirió en 2017 las especificaciones y el código de referencia a la Eclipse Foundation, pero se quedó con la marca Java. El prefijo de paquete javax formaba parte de esa marca, así que la comunidad no tuvo más remedio que renombrar todos los paquetes. Por eso Jakarta EE 9 es una versión que rompe la compatibilidad sin aportar ninguna novedad: javax.servlet.http.HttpServletRequest pasó a ser jakarta.servlet.http.HttpServletRequest, javax.persistence.Entity pasó a ser jakarta.persistence.Entity, y así con todas las especificaciones. Los espacios de nombres XML de los descriptores de despliegue cambiaron al mismo tiempo, de http://xmlns.jcp.org/xml/ns/javaee a https://jakarta.ee/xml/ns/jakartaee.

Esta guía se centra en Jakarta EE 10 y usa jakarta.* en todo momento. Cuando leas material más antiguo —y la mayor parte del código Java empresarial en producción se escribió para Java EE 6, 7 u 8—, sustituye mentalmente el prefijo: por lo demás, los nombres de clases y métodos son los mismos.

El modelo de capas y contenedores

Jakarta EE describe una aplicación como un conjunto de componentes repartidos en capas lógicas. Las capas son conceptuales: dos de ellas suelen compartir un mismo proceso, y en la mayoría de los despliegues modernos todas las capas del lado servidor viven en una única instancia del servidor de aplicaciones.

  • Capa cliente — un navegador, una app móvil, otro servicio que llama a un endpoint REST o un cliente Java de escritorio. Para este último caso la plataforma sigue definiendo un contenedor de cliente de aplicación, aunque hoy apenas se usa.
  • Capa web — servlets, páginas Faces, recursos Jakarta REST y endpoints WebSocket. Estos componentes reciben las peticiones, las traducen en llamadas a la capa de negocio y generan la respuesta.
  • Capa de negocio — enterprise beans y beans CDI que implementan los casos de uso, se ejecutan dentro de transacciones y aplican las reglas de seguridad.
  • Capa de sistemas de información empresarial (EIS) — bases de datos relacionales, brokers de mensajes, mainframes y otros back-ends a los que se llega mediante JDBC, JMS o resource adapters de conectores (JCA).

Lo que convierte esto en algo más que un diagrama de arquitectura por capas es el contenedor: el runtime que aloja un tipo de componente y se sitúa entre el componente y el mundo exterior. Como es el contenedor quien instancia e invoca tu servlet o tu bean, puede:

  • inyectar dependencias (@Inject, @EJB, @PersistenceContext, @Resource) antes de que se ejecute tu código;
  • iniciar y confirmar una transacción alrededor de cada método de negocio;
  • comprobar que quien llama tiene el rol necesario;
  • mantener un pool de instancias, gestionar el acceso de los threads y pasivar el estado inactivo;
  • aplicar interceptores y callbacks de ciclo de vida (@PostConstruct, @PreDestroy).

El contenedor web aloja servlets, filtros, listeners, páginas JSP, vistas Faces, recursos REST y endpoints WebSocket. El contenedor de enterprise beans aloja los session beans y los message-driven beans. Ambos comparten el contenedor CDI para la inyección de dependencias y los scopes contextuales, además de servicios de plataforma como el servicio de nombres JNDI, las transacciones JTA y el subsistema de seguridad. La consecuencia práctica: nunca instancies un componente gestionado con new. Un new OrderService() es un objeto normal y corriente, sin campos inyectados, sin transacción y sin contexto de seguridad; solo las instancias creadas por el contenedor los tienen.

Lo que aporta realmente el servidor

Un servidor de aplicaciones es el producto que implementa las especificaciones. Los proveedores compiten en herramientas de administración, clustering, rendimiento y soporte, no en las API contra las que compila tu código. Entre los servicios que todo servidor compatible debe ofrecer están:

ServicioEspecificaciónPara qué lo usas
Servicio de nombresJNDI (parte de Java SE)Buscar por nombre data sources, destinos JMS y beans
TransaccionesJakarta TransactionsLímites ACID declarativos o programáticos, incluido XA entre varios recursos
PersistenciaJakarta PersistenceMapeo objeto-relacional con un EntityManager gestionado por el contenedor
MensajeríaJakarta MessagingColas y topics, message-driven beans
WebServlet, Pages, Faces, REST, WebSocketGestión de peticiones HTTP y renderizado
Inyección y scopesCDI, Dependency InjectionConectar componentes, scopes de request/session/application, eventos
SeguridadJakarta Security, Authentication, AuthorizationMecanismos de autenticación, identity stores, comprobación de roles
ConcurrenciaJakarta ConcurrencyExecutors gestionados y tareas programadas que conservan el contexto del contenedor
IntegraciónConnectors (JCA), MailResource adapters para sistemas heredados, envío de correo

Los recursos como los data sources se configuran en el servidor y se exponen a tu código mediante nombres JNDI, de modo que el mismo WAR funciona en pruebas y en producción con credenciales distintas y sin recompilar. @DataSourceDefinition permite que una aplicación declare su propio data source, algo cómodo para demos pero que mete las credenciales en el código.

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

@ApplicationScoped
public class HealthCheck {

    // El contenedor resuelve este nombre JNDI en el momento de la inyección.
    @Resource(lookup = "java:/jdbc/OrdersDS")
    DataSource dataSource;

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

Perfiles: Platform, Web y Core

No todas las aplicaciones necesitan todo eso, y exigir un servidor de plataforma completa para una pequeña aplicación web fue una de las razones por las que Java EE perdió terreno frente a stacks más ligeros en los años 2000. Desde Java EE 6 la plataforma define perfiles, y Jakarta EE 10 tiene tres:

  • Jakarta EE Platform — todo.
  • Web Profile — la capa web más lo que una aplicación web típica necesita detrás: Servlet, Pages, Expression Language, Faces, CDI, Bean Validation, Persistence, Transactions, REST, JSON-P/JSON-B, WebSocket, Security, Concurrency (incorporada al perfil en EE 10) y EJB Lite: session beans locales, transacciones gestionadas por el contenedor, interceptores, seguridad declarativa y timers no persistentes, pero sin interfaces remotas, sin message-driven beans y sin JMS.
  • Core Profile — nuevo en EE 10 y pensado para frameworks de microservicios y compilación ahead-of-time: CDI Lite, REST, JSON-P, JSON-B, Annotations, Dependency Injection e Interceptors. MicroProfile se construye sobre este perfil.

La elección afecta a tu dependencia de Maven. Compila contra el artefacto de API del perfil al que apuntas y deja que el servidor lo proporcione en tiempo de ejecución:

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

El scope provided importa. Si empaquetas las clases de la API —o, peor aún, una implementación como Hibernate— dentro de un WAR desplegado en un servidor que ya las incluye, tendrás conflictos de carga de clases muy difíciles de diagnosticar.

Empaquetado y despliegue

Las aplicaciones Jakarta EE se despliegan como archivos con una estructura interna fija, y el contenedor lee tanto las anotaciones como los descriptores XML opcionales para descubrir los componentes.

  • WAR (web archive) — WEB-INF/classes para tus clases, WEB-INF/lib para las librerías, WEB-INF/web.xml (opcional desde Servlet 3.0), WEB-INF/beans.xml (opcional en CDI 4, que por defecto descubre los beans anotados), META-INF/persistence.xml dentro de classes para JPA. Desde Java EE 6 un WAR puede contener enterprise beans directamente; es, con diferencia, el empaquetado más habitual hoy en día.
  • EJB JAR — solo enterprise beans, con un META-INF/ejb-jar.xml opcional.
  • EAR (enterprise archive) — un envoltorio que contiene WAR, EJB JAR, resource adapters (RAR) y librerías compartidas en lib/, descrito por META-INF/application.xml. Úsalo solo cuando de verdad necesites que varios módulos compartan librerías y se desplieguen como una unidad; complica la carga de clases y la mayoría de los equipos ya no lo necesitan.

La cabecera de un web.xml de Servlet 6.0 muestra el espacio de nombres y la versión actuales:

<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>

Los descriptores prevalecen sobre las anotaciones, y esa es la forma prevista de cambiar ajustes propios de cada entorno —una restricción de seguridad, un parámetro de inicialización de un servlet, un timeout de sesión— sin tocar el código fuente.

Nombres JNDI portables

Desde Java EE 6 cada session bean recibe nombres JNDI estándar, así que puedes buscarlo desde cualquier sitio sin sintaxis específica del proveedor:

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

Para un bean OrderService en shop.war desplegado de forma independiente, el nombre global es java:global/shop/OrderService. El espacio de nombres java:comp/env contiene las entradas de entorno propias del componente (puntos de inyección @Resource), y java:comp/UserTransaction y java:comp/DefaultDataSource son entradas estándar que todo servidor proporciona. La inyección ha hecho que los lookups explícitos sean poco frecuentes, pero aún te los encontrarás en código antiguo y en clientes remotos.

Elegir un servidor o un runtime

Todos los productos compatibles con Jakarta EE 10 superan el mismo Technology Compatibility Kit, así que el comportamiento de las API es el mismo; las diferencias son operativas.

ProductoPerfilNotas
Eclipse GlassFish 7PlatformLa implementación compatible usada para ratificar las especificaciones; buena para aprender, menos habitual en producción
WildFly (27+)PlatformEl servidor comunitario de Red Hat, base de JBoss EAP; ofrece además un feature pack "preview" y empaquetado como bootable JAR
Open LibertyPlatformEl runtime modular de IBM; las funcionalidades se activan por aplicación, arranque rápido, base de WebSphere Liberty
Payara Server / MicroPlatformDerivado de GlassFish con soporte comercial; Payara Micro ejecuta un WAR desde la línea de comandos
Apache TomEE 10Variantes Web Profile / PlatformTomcat más OpenEJB, OpenJPA/Hibernate, ActiveMQ, CXF
Apache Tomcat 10.1 / Jetty 12Solo Servlet, Pages, EL, WebSocketNo son servidores Jakarta EE; el resto lo aportas tú como librerías

En la práctica, la elección depende de lo que tu equipo de operaciones ya ejecuta y parchea, de si necesitas soporte comercial (JBoss EAP, WebSphere Liberty y Payara Enterprise son las ediciones con soporte de WildFly, Open Liberty y Payara) y de cómo despliegas: todos estos servidores se pueden incluir en una imagen de contenedor junto con la aplicación, y el bootable JAR de WildFly o Payara Micro generan un único ejecutable, muy parecido a un jar de Spring Boot. Si solo usas servlets, JPA y REST, un runtime Web Profile —o Tomcat más librerías— puede ser la respuesta pragmática.

Dónde encajan Spring y MicroProfile

Spring nació en 2003 como reacción a EJB 2.x, cuyas interfaces home, excepciones checked obligatorias y descriptores XML convertían lo sencillo en laborioso. Java EE 5 y 6 absorbieron la mayoría de las ideas de Spring —anotaciones, inyección de dependencias, componentes como objetos simples— en EJB 3 y CDI. Hoy la relación es más simbiótica que competitiva: Spring Framework 6 y Spring Boot 3 dependen de las API de Jakarta EE 9+ para la capa de servlets, JPA, Bean Validation, JMS y JTA, y simplemente aportan su propio contenedor en lugar de depender de un servidor de aplicaciones. Si entiendes las especificaciones, entiendes buena parte de lo que Spring autoconfigura; el tema de Spring cubre esa parte.

MicroProfile es un conjunto hermano de especificaciones de Eclipse que añade sobre el Core Profile lo que necesitan los servicios cloud-native —configuración, health checks, métricas, OpenAPI, un cliente REST, tolerancia a fallos, propagación de JWT— y lo implementan Open Liberty, Payara, WildFly, Quarkus y Helidon. Lo que sepas de CDI y Jakarta REST te sirve en todos ellos.

Migrar de javax a jakarta

Como el renombrado de Jakarta EE 9 es mecánico, las herramientas pueden hacer la mayor parte del trabajo:

  1. Actualiza el runtime y la dependencia de la API

    Pasa a un servidor Jakarta EE 9.1+ (o Tomcat 10.1+, Jetty 11+) y cambia la dependencia jakarta.jakartaee-api (o las de cada especificación) a una versión 9.1 o 10.
  2. Reescribe imports y descriptores

    Ejecuta Eclipse Transformer o una receta de OpenRewrite sobre el árbol de código fuente. Reescriben los imports javax.* de los paquetes afectados, los espacios de nombres XML y nombres de propiedades como javax.persistence.jdbc.url, sin tocar los paquetes de Java SE como javax.sql y javax.naming, que nunca se movieron.
  3. Actualiza todas las librerías que usan las API

    Hibernate 6, Jersey 3, RESTEasy 6, Mojarra 4, EclipseLink 4 y Spring 6 son las líneas con el espacio de nombres de Jakarta. Una sola dependencia transitiva que siga importando javax.servlet fallará en tiempo de ejecución con NoClassDefFoundError, así que revisa el árbol de dependencias.
  4. Busca API eliminadas

    Servlet 6.0 eliminó tipos declarados obsoletos hacía mucho, como SingleThreadModel y HttpSessionContext; Faces 4.0 eliminó @ManagedBean y las vistas JSP; Enterprise Beans 4.0 prescindió de los entity beans de EJB 2.x y de los endpoints JAX-RPC. El código que los usa hay que reescribirlo, no basta con cambiar los imports.

La guía de migración de Tomcat describe una variante del mismo enfoque: Tomcat 10 puede transformar al vuelo los WAR heredados, lo que te da margen pero no debería ser el plan a largo plazo.

Errores comunes

  • Empaquetar una implementación que el servidor ya incluye. Meter hibernate-core o una implementación de Jakarta REST dentro de un WAR para WildFly o Liberty produce dos copias de las mismas clases en class loaders distintos y errores como ClassCastException: OrderEntity cannot be cast to OrderEntity. Usa el scope provided para las API y deja que el servidor aporte las implementaciones, salvo que el servidor documente cómo sustituir su versión.
  • Mezclar artefactos javax y jakarta. Un proyecto migrado a medias compila y luego falla en el despliegue. Revisa el árbol de dependencias, no solo tus propios imports.
  • Tratar las capas como físicas. Separar la capa web y la de negocio en servidores distintos conectados por llamadas remotas a enterprise beans estuvo de moda en la época de J2EE, y casi siempre es más lento y más difícil de operar que un único proceso. Mantenlas juntas salvo que tengas mediciones que justifiquen lo contrario.
  • Configurar los recursos en la aplicación. Las URL de conexión fijadas en persistence.xml o en @DataSourceDefinition funcionan en un portátil y luego filtran credenciales al control de versiones. Define los data sources y los destinos JMS en el servidor y haz referencia a ellos por su nombre JNDI.
  • Usar un EAR por costumbre. Un único WAR es más fácil de construir, desplegar y depurar. Recurre a un EAR solo cuando tengas varios módulos que deban versionarse juntos.

Ideas clave

  • Jakarta EE es un conjunto de especificaciones; un servidor de aplicaciones las implementa y ejecuta tus componentes dentro de contenedores que inyectan dependencias, gestionan transacciones y aplican la seguridad.
  • El linaje es J2EE, luego Java EE 5–8, luego Jakarta EE 8 (mismas API), 9 (javax.* pasa a jakarta.*, sin novedades), 10 (primera versión con novedades) y 11.
  • Jakarta EE 10 define tres perfiles —Platform, Web y Core— para que apuntes solo a lo que necesitas; el Web Profile cubre la mayoría de las aplicaciones web.
  • Compila contra el artefacto jakarta.jakartaee-api o jakarta.jakartaee-web-api con scope provided y deja que el servidor aporte las implementaciones.
  • Empaqueta como WAR siempre que puedas; usa nombres JNDI portables java:global y recursos definidos en el servidor en lugar de configuración fija en el código.
  • GlassFish, WildFly, Open Liberty, Payara y TomEE son implementaciones compatibles; Tomcat y Jetty solo implementan la familia de Servlet.
  • Spring y MicroProfile se basan en las mismas especificaciones, así que el conocimiento se transfiere en ambos sentidos.

Fuentes y lecturas recomendadas

Preguntas frecuentes

¿Jakarta EE es lo mismo que Java EE?
Es la continuación de Java EE bajo la Eclipse Foundation. Jakarta EE 8 es funcionalmente idéntico a Java EE 8 y sigue usando los paquetes javax.*. A partir de Jakarta EE 9 los paquetes pasaron a jakarta.*, y por eso las librerías y servidores escritos para Java EE 8 no funcionan sin cambios en un servidor Jakarta EE 9 o posterior.
¿Necesito un servidor de aplicaciones o me basta con Tomcat?
Tomcat y Jetty solo implementan las especificaciones de Servlet, JSP, EL y WebSocket. Si necesitas que el propio contenedor te dé CDI, JPA, JTA, JMS, enterprise beans o Jakarta REST, usa un servidor Web Profile o de Platform completa como WildFly, Open Liberty, Payara, GlassFish o TomEE. Frameworks como Spring Boot traen sus propias implementaciones de varias de esas especificaciones y por eso pueden ejecutarse sobre Tomcat.
¿Qué diferencia hay entre el Web Profile y la Platform completa?
El Web Profile es un subconjunto pensado para aplicaciones web: servlets, Faces, CDI, JPA, JTA, Bean Validation, Jakarta REST, JSON, WebSocket, Security, Concurrency y un conjunto reducido de funcionalidades de enterprise beans llamado EJB Lite. La Platform completa añade mensajería (JMS), conectores, Mail, Batch, beans remotos y message-driven, entre otras cosas. El Core Profile, nuevo en Jakarta EE 10, es aún más pequeño y está orientado a runtimes de microservicios.
¿Un proyecto nuevo debería usar Jakarta EE o Spring Boot?
Ambas opciones son viables. Jakarta EE te da estándares neutrales respecto al proveedor, un runtime gestionado y menos código de framework en tu aplicación; Spring Boot te da un ecosistema más amplio, un ritmo de versiones más rápido y un empaquetado autónomo más sencillo. El propio Spring 6 se basa en las API de Jakarta EE 9+ (Servlet, JPA, Bean Validation), así que las especificaciones que aprendes aquí se aplican directamente.