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, @PermitAll y @DenyAll en 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, @PersistenceContext y @Resource se rellenan antes de que se ejecute @PostConstruct.
  • Intercepción: los callbacks de ciclo de vida y los interceptores @AroundInvoke se ejecutan alrededor de los métodos.
  • Acceso remoto, planificación y asincronía: las vistas remotas, los timers @Schedule y los métodos @Asynchronous solo 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:

VistaSe declara conSemántica de la llamadaÚsala cuando
Sin interfazNada (métodos públicos de la clase)Dentro de la JVM, por referenciaCasi siempre
LocalInterfaz @LocalDentro de la JVM, por referenciaQuieres una interfaz para tests o varias implementaciones
RemotaInterfaz @RemoteEntre 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:

NecesidadBean CDISession bean
Inyección, scopes, eventos, interceptores, decoradoresSíSí (a través de CDI)
Transacciones@Transactional (JTA 1.2+)@TransactionAttribute, REQUIRED por defecto en todos los métodos
Comprobaciones de roles declarativasProgramáticas con SecurityContext.isCallerInRole; @RolesAllowed en beans CDI solo como extensión del proveedor@RolesAllowed en todas partes
Pooling y limitación de instanciasNoPool stateless
Interfaz remotaNo@Remote
Consumo de mensajesNo (salvo extensiones del proveedor)Message-driven beans
Timers de calendarioSolo programáticos (ManagedScheduledExecutorService); Jakarta Concurrency 3.1 en EE 11 añade métodos asíncronos programados@Schedule, TimerService
Contexto de persistencia extendidoNoBean stateful
Orden de arranqueObserver de @Initialized@Startup + @DependsOn
DisponibilidadTodos los perfiles, incluido CoreWeb 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 apliquen REQUIRES_NEW o @RolesAllowed. Se salta el proxy. Inyecta el bean en sí mismo (@Inject OrderService self;) y llama a self.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 WRITE por 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 = false para 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 EJBException llega a quien llama, la transacción ya está marcada para rollback; si la capturas tú, comprueba sessionContext.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 transient y 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 @Lock para permitir lecturas concurrentes.
  • Prefiere la vista sin interfaz; usa @Remote solo entre procesos, y para esa frontera prefiere REST o mensajería.
  • Los timers (@Schedule, TimerService) y los métodos @Asynchronous son 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 @Transactional sustituye 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.
Arquitectura de Jakarta EE: contenedores, perfiles y servidores de aplicacionesCómo se organiza Jakarta EE: capas, contenedores, especificaciones, perfiles, empaquetado y el cambio de javax a jakarta, y cómo elegir un servidor.Principiante14 min de lecturaJakarta Transactions (JTA 2): transacciones gestionadas por contenedor, propagación y XACómo funcionan las transacciones JTA en un servidor Jakarta EE: aislamiento, propagación de @Transactional, reglas de rollback, UserTransaction y commit XA.Intermedio11 min de lecturaMessage-driven beans y Jakarta Messaging (JMS 3.1) en el contenedorConsume colas y topics con message-driven beans, envía mensajes con JMSContext y domina transacciones, reentregas, selectores y suscripciones durables.Intermedio10 min de lecturaJakarta Persistence (JPA 3.1): entidades, mapeo y contexto de persistenciaCómo mapea JPA las clases a tablas: unidad de persistencia, EntityManager, estados, identificadores, relaciones, fetch, cascadas, herencia y bloqueo.Intermedio11 min de lecturaSpringInversión de control e inyección de dependencias en SpringQué hace el contenedor de Spring, en qué se diferencian la inyección por constructor, setter y campo, y cómo conectar beans con configuración Java y escaneo.Principiante9 min de lecturaSpringProgramación orientada a aspectos con Spring AOP y AspectJCómo aplica Spring AOP comportamiento transversal con proxies: aspectos, pointcuts, tipos de advice, proxies JDK o CGLIB y la trampa de la autoinvocación.Intermedio9 min de lectura