Java EE / Jakarta EE
Enterprise Beans 4: session beans stateless, stateful y singleton
Intermedio10 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
Los session beans son los componentes de la capa de negocio de Jakarta EE: el contenedor los instancia, los agrupa en un pool, envuelve cada llamada en una transacción y un control de seguridad, y puede ejecutarlos de forma asíncrona o programada. Esta guía explica los beans stateless, stateful y singleton, sus vistas y ciclos de vida, los interceptores, los timers y el control de concurrencia, y después traza la línea entre lo que todavía necesita un enterprise bean y lo que un simple bean CDI hace igual de bien.
Lo que aprenderás
- Elegir entre session beans stateless, stateful y singleton según el caso de uso
- Exponer un bean mediante vistas sin interfaz, locales y remotas, y entender lo que cuesta cada una
- Usar los servicios del contenedor: transacciones, seguridad declarativa, interceptores, timers y métodos asíncronos
- Controlar la concurrencia de los singletons con @Lock y entender el pooling y la pasivación
- Decidir cuándo un bean CDI con @Transactional sustituye a un enterprise bean
Enterprise JavaBeans es la parte de la plataforma con peor reputación y con la mejor excusa: lo que la gente recuerda es la versión 2.x, con sus home interfaces, sus RemoteException obligatorias y cientos de líneas de XML por componente. La versión 3.0 (2006) lo sustituyó todo por clases normales y un puñado de anotaciones, y desde entonces la especificación (hoy Jakarta Enterprise Beans 4.0) es una forma compacta de decir «ejecuta esta clase dentro del contenedor y dale transacciones, seguridad, pooling y planificación». Entender qué te aporta eso, y qué ofrece ya CDI por su cuenta, te permite usar enterprise beans donde compensan y en ningún otro sitio.
Qué hace el contenedor por un session bean
Un session bean es una clase anotada con @Stateless, @Stateful o @Singleton. Nunca la instancias tú: lo hace el contenedor, y cada llamada de un cliente pasa por un proxy que el contenedor coloca delante de la instancia. Ahí es donde se enganchan los servicios de la plataforma:
- Transacciones: por defecto, cada método de negocio se ejecuta en una transacción JTA (
REQUIRED), que se inicia si no existe ninguna y se confirma cuando retorna el método más externo; una excepción runtime la revierte. La guía de transacciones trata los atributos en profundidad. - Seguridad:
@RolesAllowed,@PermitAlly@DenyAllen la clase o en el método se comprueban antes de la llamada;SessionContext.getCallerPrincipal()te dice quién llama. - Concurrencia: el contenedor garantiza acceso de un solo thread a las instancias stateless y stateful, de modo que el código del bean no necesita locks.
- Inyección: los campos
@Inject,@EJB,@PersistenceContexty@Resourcese rellenan antes de que se ejecute@PostConstruct. - Intercepción: los callbacks de ciclo de vida y los interceptores
@AroundInvokese ejecutan alrededor de los métodos. - Acceso remoto, planificación y asincronía: las vistas remotas, los timers
@Scheduley los métodos@Asynchronoussolo están disponibles en enterprise beans.
El precio es el proxy: llamar a un método sobre this se lo salta, y cada llamada gestionada tiene cierto coste adicional. Ninguna de las dos cosas es significativa en código normal, pero ambas explican toda una familia de bugs del tipo «mi anotación se ignora».
Session beans stateless
El caballo de batalla. Un bean stateless no guarda estado conversacional entre llamadas, así que el contenedor mantiene un pool de instancias y presta una a cada invocación. Dos llamadas consecutivas del mismo cliente pueden llegar a dos instancias distintas; por eso los campos de instancia solo deben contener recursos inyectados y configuración inmutable.
package shop.service;
import java.util.List;
import jakarta.annotation.security.RolesAllowed;
import jakarta.ejb.Stateless;
import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext;
@Stateless
public class OrderService {
@PersistenceContext(unitName = "shop")
EntityManager em; // container-managed, transaction-scoped
public List<Order> find(String customer, OrderStatus status) {
return em.createQuery("""
select o from Order o
where o.customer.name = :customer
and (:status is null or o.status = :status)
order by o.placedAt desc""", Order.class)
.setParameter("customer", customer)
.setParameter("status", status)
.getResultList();
}
@RolesAllowed("sales")
public Order place(Long customerId, List<OrderLine> lines) {
Customer customer = em.getReference(Customer.class, customerId);
Order order = new Order(customer, lines);
em.persist(order); // committed when this method returns normally
return order;
}
}
No hay interfaz, ni descriptor, ni transacción explícita. find y place se ejecutan cada uno dentro de una transacción iniciada por el contenedor, place solo pueden invocarlo usuarios con el rol sales, y el EntityManager está ligado a esa transacción. Esta es la vista sin interfaz (no-interface view) que introdujo EJB 3.1: los clientes inyectan la clase directamente.
@Inject OrderService orderService; // CDI injection works for session beans
@EJB OrderService orderService; // the older enterprise-bean-specific injection; still valid
El tamaño de los pools es una configuración de cada proveedor (strict-max-pool en WildFly, maxPoolSize en Open Liberty, etc.). Cuando el pool se agota, quienes llaman esperan, lo que convierte al pool en un regulador natural delante de una base de datos: dimensiónalo teniendo en cuenta el tamaño del pool de conexiones.
Session beans stateful
Un bean stateful guarda estado para un único cliente a lo largo de varias llamadas: un carrito de la compra, un asistente de varios pasos, un flujo que va acumulando datos antes de confirmar. El contenedor crea una instancia nueva cuando el cliente obtiene la primera referencia y la destruye cuando termina un método anotado con @Remove o cuando el bean expira (@StatefulTimeout).
import java.util.concurrent.TimeUnit;
import jakarta.ejb.Remove;
import jakarta.ejb.Stateful;
import jakarta.ejb.StatefulTimeout;
import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext;
import jakarta.persistence.PersistenceContextType;
@Stateful
@StatefulTimeout(value = 30, unit = TimeUnit.MINUTES)
public class Checkout {
@PersistenceContext(unitName = "shop", type = PersistenceContextType.EXTENDED)
EntityManager em; // extended: entities stay managed across calls
private Order draft;
public void start(Customer customer) { draft = new Order(customer); }
public void add(Product p, int qty) { draft.addLine(p, qty); }
@Remove
public Order confirm() {
em.persist(draft); // the extended context flushes here, inside the transaction
return draft;
}
@Remove
public void cancel() { draft = null; }
}
Dos cosas distinguen a un bean stateful de un bean CDI @SessionScoped. La primera, el contexto de persistencia extendido: las entidades cargadas en una llamada siguen gestionadas en la siguiente, sin merges, hasta que se confirma la transacción que cierra la conversación; es el único lugar de la plataforma donde eso es posible. La segunda, la pasivación: un bean stateful inactivo puede serializarse a disco para liberar memoria, así que sus campos deben ser serializables o transient, exactamente igual que en un bean de Faces view-scoped. En la práctica, los beans stateful son poco habituales: la mayor parte del estado conversacional vive en la capa web, y el contexto de persistencia extendido es la principal justificación que les queda.
Session beans singleton
Un singleton se instancia una sola vez por aplicación y lo comparten todos los clientes, lo que lo convierte en el sitio para cachés, tareas de arranque y coordinación. Como se comparte, el contenedor tiene que gestionar el acceso concurrente, y la especificación ofrece dos modos.
import jakarta.annotation.PostConstruct;
import jakarta.ejb.*;
@Singleton
@Startup // create at deployment, not at first use
@DependsOn("ConfigurationLoader") // order start-up relative to another singleton
@Lock(LockType.READ) // default for all methods: concurrent readers
public class ExchangeRates {
private Map<String, BigDecimal> rates = Map.of();
@PostConstruct
void load() { rates = fetchFromProvider(); }
public BigDecimal rate(String currency) { return rates.get(currency); } // READ lock
@Lock(LockType.WRITE) // exclusive: readers wait until refresh finishes
@Schedule(hour = "*", minute = "0", persistent = false)
public void refresh() { rates = fetchFromProvider(); }
@AccessTimeout(value = 5, unit = TimeUnit.SECONDS) // fail rather than block forever
@Lock(LockType.WRITE)
public void override(String currency, BigDecimal rate) { /* ... */ }
}
Con la concurrencia gestionada por el contenedor (la opción por defecto), los métodos @Lock(READ) se ejecutan en paralelo y los @Lock(WRITE) en exclusiva: el patrón de lock de lectura/escritura sin escribir ningún lock. El lock por defecto de un singleton es WRITE, que serializa todas las llamadas, así que declara READ a nivel de clase y marca los métodos que modifican estado. @ConcurrencyManagement(BEAN) desactiva la gestión del contenedor y te deja usar synchronized o java.util.concurrent por tu cuenta.
Los singletons @Startup son la respuesta de la plataforma a «ejecuta esto cuando arranque la aplicación»; combinados con @DependsOn permiten una inicialización ordenada. CDI 4.0 ofrece una alternativa que no necesita un enterprise bean: observar @Initialized(ApplicationScoped.class).
Vistas: sin interfaz, local y remota
Un bean expone una o varias vistas, y la vista determina cómo ven el proxy los clientes:
| Vista | Se declara con | Semántica de la llamada | Úsala cuando |
|---|---|---|---|
| Sin interfaz | Nada (métodos públicos de la clase) | Dentro de la JVM, por referencia | Casi siempre |
| Local | Interfaz @Local | Dentro de la JVM, por referencia | Quieres una interfaz para tests o varias implementaciones |
| Remota | Interfaz @Remote | Entre JVM, por valor (serializado) | Otro proceso tiene que llamar al bean |
En el código, las llamadas remotas tienen exactamente el mismo aspecto, pero se comportan de forma completamente distinta: cada argumento se copia, cada resultado se copia de vuelta, hay un viaje de ida y vuelta por la red, y los fallos aparecen como EJBException envolviendo la causa. Un cliente remoto fuera del servidor busca el bean por su nombre JNDI portable usando las bibliotecas cliente del proveedor:
var props = new Properties();
props.put(Context.INITIAL_CONTEXT_FACTORY, "org.wildfly.naming.client.WildFlyInitialContextFactory");
props.put(Context.PROVIDER_URL, "remote+http://app-server:8080");
var ctx = new InitialContext(props);
var orders = (OrderServiceRemote) ctx.lookup("ejb:/shop/OrderService!shop.service.OrderServiceRemote");
La costumbre de la época J2EE de partir una aplicación en una capa web que llama a una capa de negocio remota es casi siempre un error hoy en día: si dos procesos tienen que hablar, una frontera REST o de mensajería es más fácil de evolucionar, proteger y monitorizar. Enterprise Beans 4.0 hizo opcional la interoperabilidad CORBA/IIOP y eliminó por completo los entity beans de EJB 2.x y los endpoints JAX-RPC, así que los protocolos remotos que quedan son específicos de cada proveedor.
Callbacks de ciclo de vida e interceptores
Cualquier session bean puede anotar un método con @PostConstruct (después de la inyección, antes de la primera llamada de negocio) y @PreDestroy (antes de descartar la instancia); los beans stateful añaden @PrePassivate y @PostActivate. Son los lugares para abrir y cerrar los recursos que posee el bean.
Los interceptores envuelven los métodos de negocio y son el equivalente, en los enterprise beans, de los aspectos. Una clase interceptora declara un método @AroundInvoke que recibe un InvocationContext y debe llamar a proceed():
import jakarta.interceptor.AroundInvoke;
import jakarta.interceptor.Interceptor;
import jakarta.interceptor.InvocationContext;
@Timed @Interceptor @Priority(Interceptor.Priority.APPLICATION)
public class TimingInterceptor {
@AroundInvoke
Object time(InvocationContext ctx) throws Exception {
long start = System.nanoTime();
try {
return ctx.proceed();
} finally {
log.info("{}.{} took {} µs", ctx.getTarget().getClass().getSimpleName(),
ctx.getMethod().getName(), (System.nanoTime() - start) / 1000);
}
}
}
Aquí @Timed es un interceptor binding de CDI (una anotación que defines con @InterceptorBinding), de modo que cualquier bean o método anotado con @Timed recibe el interceptor. La antigua anotación @Interceptors(TimingInterceptor.class) de la especificación Enterprise Beans sigue funcionando, pero acopla el bean a la clase interceptora. Los interceptores se ejecutan dentro de la intercepción transaccional y de seguridad propia del contenedor, en el orden de @Priority, y se aplican tanto a beans CDI como a session beans: el modelo de interceptores es común.
Timers y métodos asíncronos
@Schedule declara un timer basado en calendario con campos al estilo cron (second, minute, hour, dayOfWeek, dayOfMonth, month, year, además de timezone). Los timers son persistentes por defecto: el contenedor los almacena y lanza las ejecuciones perdidas tras un reinicio, lo cual es justo lo que quieres para un proceso nocturno y justo lo que no quieres para refrescar una caché; de ahí el persistent = false de arriba. Para timers creados en tiempo de ejecución, inyecta TimerService y llama a createCalendarTimer o createIntervalTimer; el callback es un método anotado con @Timeout. En un clúster, los timers persistentes se disparan en un solo nodo; los no persistentes, en todos los nodos.
@Asynchronous hace que el contenedor ejecute un método en otro thread y devuelva el control de inmediato. Un método void es fire-and-forget; un método que devuelve Future<T> permite a quien llama recoger el resultado o cancelarlo:
@Asynchronous
public Future<Invoice> generateInvoice(Long orderId) {
Invoice invoice = render(orderId); // runs in its own transaction, on a container thread
return new AsyncResult<>(invoice);
}
El método asíncrono se ejecuta en un nuevo contexto transaccional (el de quien llama no se propaga), mientras que su contexto de seguridad sí se propaga. El ManagedExecutorService de Jakarta Concurrency cubre un terreno parecido para código que no está en un bean; @Asynchronous es la opción más sencilla cuando el trabajo ya vive en un bean, y los servidores de la época de CDI 4.0 también admiten el @Asynchronous de Jakarta Concurrency 3.0 en beans CDI, que devuelve un CompletableFuture.
¿Enterprise beans o beans CDI?
CDI llegó con Java EE 6 y ha absorbido casi todo lo que antes requería un enterprise bean. Así se decide hoy:
| Necesidad | Bean CDI | Session bean |
|---|---|---|
| Inyección, scopes, eventos, interceptores, decoradores | Sí | Sí (a través de CDI) |
| Transacciones | @Transactional (JTA 1.2+) | @TransactionAttribute, REQUIRED por defecto en todos los métodos |
| Comprobaciones de roles declarativas | Programáticas con SecurityContext.isCallerInRole; @RolesAllowed en beans CDI solo como extensión del proveedor | @RolesAllowed en todas partes |
| Pooling y limitación de instancias | No | Pool stateless |
| Interfaz remota | No | @Remote |
| Consumo de mensajes | No (salvo extensiones del proveedor) | Message-driven beans |
| Timers de calendario | Solo programáticos (ManagedScheduledExecutorService); Jakarta Concurrency 3.1 en EE 11 añade métodos asíncronos programados | @Schedule, TimerService |
| Contexto de persistencia extendido | No | Bean stateful |
| Orden de arranque | Observer de @Initialized | @Startup + @DependsOn |
| Disponibilidad | Todos los perfiles, incluido Core | Web Profile (Lite) o Platform |
Una regla razonable: escribe los servicios como beans CDI @ApplicationScoped con @Transactional, salvo que necesites algo de la columna de la derecha, en cuyo caso la anotación extra no cuesta nada. Las bases de código de la época de Java EE 6 y 7 tiran hacia el otro lado (@Stateless en todo) y no hay ninguna prisa por convertirlas: los dos modelos interoperan sin fricción.
Errores comunes
- Llamar a
this.otherMethod()esperando que se apliquenREQUIRES_NEWo@RolesAllowed. Se salta el proxy. Inyecta el bean en sí mismo (@Inject OrderService self;) y llama aself.otherMethod(). - Guardar estado en un bean stateless. Un campo que se asigna en un método y se lee en otro funcionará en los tests con una sola instancia y fallará bajo carga cuando el pool entregue otra instancia.
- Singletons con el lock
WRITEpor defecto. Todas las llamadas se serializan y el throughput se desploma con concurrencia. Pon@Lock(READ)en la clase. - Timers persistentes para trabajo volátil. Tras un reinicio, el contenedor repite cada ejecución perdida. Usa
persistent = falsepara refrescos y heartbeats. - Vistas remotas dentro de un mismo despliegue. Copiar argumentos para una llamada dentro de la misma JVM es puro coste adicional. Usa la vista sin interfaz o la local.
- Capturar la excepción runtime dentro del bean y retornar con normalidad. Cuando una
EJBExceptionllega a quien llama, la transacción ya está marcada para rollback; si la capturas tú, compruebasessionContext.getRollbackOnly()o lanza en su lugar una@ApplicationException. - Campos no serializables en beans stateful. La pasivación falla con una excepción; márcalos como
transienty reconstrúyelos en@PostActivate.
Puntos clave
- Un session bean es una clase gestionada por el contenedor detrás de un proxy; el proxy aporta transacciones, controles de seguridad, acceso de un solo thread, inyección e interceptores.
- Los beans stateless van en un pool y no guardan estado del cliente; los stateful guardan estado por cliente y pueden tener un contexto de persistencia extendido; los singletons se comparten y necesitan
@Lockpara permitir lecturas concurrentes. - Prefiere la vista sin interfaz; usa
@Remotesolo entre procesos, y para esa frontera prefiere REST o mensajería. - Los timers (
@Schedule,TimerService) y los métodos@Asynchronousson funcionalidades del contenedor que no tienen los objetos normales; haz los timers no persistentes salvo que haya que repetir las ejecuciones perdidas. - Los interceptores, los callbacks de ciclo de vida y la inyección se comparten con CDI; un bean CDI con
@Transactionalsustituye a la mayoría de los beans stateless. - Enterprise Beans 4.0 eliminó los entity beans de EJB 2.x y JAX-RPC; la especificación es estable y es poco probable que cambie más.
Fuentes y lecturas recomendadas
Preguntas frecuentes
- ¿Se siguen usando los EJB o los ha sustituido CDI?
- Se usan ambos. CDI se encarga de la inyección, los scopes, los eventos y, con @Transactional, de las transacciones, así que buena parte de la lógica de negocio nueva se escribe como beans CDI. Los enterprise beans siguen siendo la forma estándar de obtener, en un servidor Jakarta EE, interfaces remotas, consumidores message-driven, timers, métodos @Asynchronous, comprobaciones de roles declarativas y lógica de arranque en un singleton. La especificación está estable, no en evolución: la versión 4.0 no ha cambiado desde Jakarta EE 9.
- ¿Por qué se agrupa en un pool un bean stateless si no tiene estado?
- Porque crearlo no sale gratis: el contenedor tiene que hacer la inyección, resolver recursos y llamar a @PostConstruct. Mantener un pool de instancias ya inicializadas permite entregar una instancia a cada petición de inmediato y garantiza que solo un thread use cada instancia a la vez, así que tu código nunca necesita sincronización. El tamaño del pool limita la concurrencia, lo que además sirve como mecanismo de throttling.
- ¿Qué diferencia hay entre una interfaz local y una remota?
- Una vista local es una llamada dentro de la misma JVM: los argumentos se pasan por referencia y el coste extra es una llamada a través del proxy. Una vista remota (@Remote) está pensada para invocarse desde otra JVM mediante un protocolo propio del proveedor: argumentos y resultados se serializan y se pasan por valor, las excepciones llegan envueltas en EJBException y la llamada es varios órdenes de magnitud más lenta. Usa la vista remota solo cuando de verdad haya un segundo proceso.
- ¿Puedo llamar a un método de un session bean desde otro método del mismo bean y obtener una transacción nueva?
- No llamándolo directamente con this: se salta el proxy del contenedor, así que no se aplica ningún atributo transaccional, control de seguridad ni interceptor. Inyecta el bean en sí mismo a través de su interfaz de negocio (o con SessionContext.getBusinessObject) y llama a través de esa referencia.