Java EE / Jakarta EE

Enterprise Beans 4: session beans stateless, stateful e singleton

Intermediário10 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

Session beans são os componentes da camada de negócio do Jakarta EE: o contêiner os instancia, mantém em pool, envolve cada chamada em uma transação e em uma verificação de segurança, e pode executá-los de forma assíncrona ou agendada. Este guia explica os beans stateless, stateful e singleton, suas views e ciclos de vida, interceptors, timers e controle de concorrência, e depois traça a linha entre o que ainda precisa de um enterprise bean e o que um simples bean CDI faz tão bem quanto.

O que você vai aprender

  • Escolher entre session beans stateless, stateful e singleton para cada caso de uso
  • Expor um bean por meio de views sem interface, local e remota, e entender o custo de cada uma
  • Usar os serviços do contêiner: transações, segurança declarativa, interceptors, timers e métodos assíncronos
  • Controlar a concorrência em beans singleton com @Lock e entender pooling e passivação
  • Decidir quando um bean CDI com @Transactional substitui um enterprise bean

Enterprise JavaBeans é a parte da plataforma com a pior reputação e a melhor desculpa: o que as pessoas lembram é a versão 2.x, com suas home interfaces, RemoteExceptions obrigatórias e centenas de linhas de XML por componente. A versão 3.0 (2006) trocou tudo isso por classes comuns e um punhado de anotações, e desde então a especificação (hoje Jakarta Enterprise Beans 4.0) é uma forma compacta de dizer "rode esta classe dentro do contêiner e dê a ela transações, segurança, pooling e agendamento". Entender o que isso traz, e o que o CDI já oferece sozinho, permite usar enterprise beans onde eles se pagam e em nenhum outro lugar.

O que o contêiner faz por um session bean

Um session bean é uma classe anotada com @Stateless, @Stateful ou @Singleton. Você nunca a instancia: quem faz isso é o contêiner, e toda chamada de um cliente passa por um proxy que o contêiner coloca na frente da instância. É nesse proxy que os serviços da plataforma se encaixam:

  • Transações: por padrão, todo método de negócio roda em uma transação JTA (REQUIRED), iniciada se não houver nenhuma e confirmada quando o método mais externo retorna; uma exceção runtime faz o rollback. O guia de transações aborda os atributos em detalhe.
  • Segurança: @RolesAllowed, @PermitAll e @DenyAll na classe ou no método são verificados antes da chamada; SessionContext.getCallerPrincipal() informa quem está chamando.
  • Concorrência: o contêiner garante acesso por uma única thread às instâncias stateless e stateful, então o código do bean não precisa de locks.
  • Injeção: os campos @Inject, @EJB, @PersistenceContext e @Resource são preenchidos antes de @PostConstruct rodar.
  • Interceptação: callbacks de ciclo de vida e interceptors @AroundInvoke rodam em volta dos métodos.
  • Acesso remoto, agendamento e assincronia: views remotas, timers @Schedule e métodos @Asynchronous só estão disponíveis em enterprise beans.

O preço é o proxy: chamar um método em this passa por fora dele, e toda chamada gerenciada tem algum overhead. Nenhum dos dois é significativo em código normal, mas ambos explicam toda uma categoria de bugs do tipo "minha anotação está sendo ignorada".

Session beans stateless

O burro de carga. Um bean stateless não guarda estado conversacional entre chamadas, então o contêiner mantém um pool de instâncias e empresta uma para cada invocação. Duas chamadas seguidas do mesmo cliente podem cair em duas instâncias diferentes; por isso, campos de instância servem apenas para recursos injetados e configuração imutável.

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

Não há interface, nem descritor, nem transação explícita. find e place rodam, cada um, dentro de uma transação iniciada pelo contêiner, place só pode ser chamado por usuários com o papel sales, e o EntityManager fica vinculado a essa transação. Essa é a view sem interface (no-interface view) introduzida no EJB 3.1: os clientes injetam a classe diretamente.

@Inject OrderService orderService;      // CDI injection works for session beans
@EJB   OrderService orderService;       // the older enterprise-bean-specific injection; still valid

O tamanho dos pools é uma configuração de cada fornecedor (strict-max-pool no WildFly, maxPoolSize no Open Liberty e assim por diante). Quando o pool se esgota, quem chama fica esperando, o que faz do pool um throttle natural na frente de um banco de dados: dimensione-o levando em conta o tamanho do pool de conexões.

Session beans stateful

Um bean stateful guarda estado para um único cliente ao longo de várias chamadas: um carrinho de compras, um assistente em várias etapas, um fluxo que acumula dados antes de confirmar. O contêiner cria uma instância nova quando o cliente obtém a primeira referência e a destrói quando termina um método anotado com @Remove ou quando o 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; }
}

Duas coisas diferenciam um bean stateful de um bean CDI @SessionScoped. Primeiro, o contexto de persistência estendido: entidades carregadas em uma chamada continuam gerenciadas na seguinte, sem merges, até o commit da transação que encerra a conversa; é o único lugar da plataforma em que isso é possível. Segundo, a passivação: um bean stateful ocioso pode ser serializado em disco para liberar memória, então seus campos precisam ser serializáveis ou transient, exatamente como em um bean Faces view-scoped. Na prática, beans stateful são raros: a maior parte do estado conversacional fica na camada web, e o contexto de persistência estendido é a principal justificativa que lhes resta.

Session beans singleton

Um singleton é instanciado uma única vez por aplicação e compartilhado por todos os clientes, o que faz dele o lugar para caches, tarefas de inicialização e coordenação. Como ele é compartilhado, o contêiner precisa gerenciar o acesso concorrente, e a especificação oferece dois 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) { /* ... */ }
}

Com a concorrência gerenciada pelo contêiner (o padrão), métodos @Lock(READ) rodam em paralelo e métodos @Lock(WRITE) rodam com exclusividade: o padrão de lock de leitura/escrita sem escrever nenhum lock. O lock padrão de um singleton é WRITE, que serializa todas as chamadas; por isso, declare READ no nível da classe e marque os métodos que alteram estado. @ConcurrencyManagement(BEAN) desliga o controle do contêiner e deixa você usar synchronized ou java.util.concurrent por conta própria.

Os singletons @Startup são a resposta da plataforma para "rode isto quando a aplicação subir"; combinados com @DependsOn, permitem uma inicialização ordenada. O CDI 4.0 oferece uma alternativa que não exige enterprise bean: observar @Initialized(ApplicationScoped.class).

Views: sem interface, local e remota

Um bean expõe uma ou mais views, e a view determina como o proxy aparece para os clientes:

ViewDeclarada porSemântica da chamadaUse quando
Sem interfaceNada (métodos públicos da classe)Dentro da JVM, por referênciaQuase sempre
LocalInterface @LocalDentro da JVM, por referênciaVocê quer uma interface para testes ou várias implementações
RemotaInterface @RemoteEntre JVMs, por valor (serializado)Outro processo precisa chamar o bean

No código, chamadas remotas têm a mesma cara, mas se comportam de maneira completamente diferente: cada argumento é copiado, cada resultado é copiado de volta, acontece uma ida e volta pela rede, e as falhas aparecem como EJBException encapsulando a causa. Um cliente remoto fora do servidor localiza o bean pelo seu nome JNDI portável usando as bibliotecas cliente do fornecedor:

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");

O hábito da era J2EE de dividir uma aplicação em uma camada web que chama uma camada de negócio remota é quase sempre um erro hoje em dia: se dois processos precisam conversar, uma fronteira REST ou de mensageria é mais fácil de evoluir, proteger e monitorar. O Enterprise Beans 4.0 tornou opcional a interoperabilidade CORBA/IIOP e removeu de vez os entity beans do EJB 2.x e os endpoints JAX-RPC, então os protocolos remotos que sobraram são específicos de cada fornecedor.

Callbacks de ciclo de vida e interceptors

Todo session bean pode anotar um método com @PostConstruct (depois da injeção, antes da primeira chamada de negócio) e @PreDestroy (antes de a instância ser descartada); os beans stateful acrescentam @PrePassivate e @PostActivate. É nesses pontos que se abrem e fecham os recursos que pertencem ao bean.

Os interceptors envolvem os métodos de negócio e são o equivalente, no mundo dos enterprise beans, aos aspectos. Uma classe interceptor declara um método @AroundInvoke que recebe um InvocationContext e precisa chamar 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);
        }
    }
}

Aqui, @Timed é um interceptor binding do CDI (uma anotação que você define com @InterceptorBinding), então qualquer bean ou método anotado com @Timed recebe o interceptor. A antiga anotação @Interceptors(TimingInterceptor.class) da especificação Enterprise Beans ainda funciona, mas acopla o bean à classe do interceptor. Os interceptors rodam dentro da interceptação de transação e de segurança do próprio contêiner, na ordem de @Priority, e se aplicam tanto a beans CDI quanto a session beans: o modelo de interceptors é compartilhado.

Timers e métodos assíncronos

@Schedule declara um timer baseado em calendário com campos no estilo cron (second, minute, hour, dayOfWeek, dayOfMonth, month, year, além de timezone). Os timers são persistentes por padrão: o contêiner os armazena e dispara as execuções perdidas depois de um restart, o que é exatamente o que você quer para um job noturno e exatamente o que você não quer para atualizar um cache; daí o persistent = false acima. Para timers criados em tempo de execução, injete TimerService e chame createCalendarTimer ou createIntervalTimer; o callback é um método anotado com @Timeout. Em um cluster, timers persistentes disparam em um único nó; os não persistentes, em todos os nós.

@Asynchronous faz o contêiner executar um método em outra thread e retornar imediatamente. Um método void é fire-and-forget; um método que retorna Future<T> permite que quem chamou colete o resultado ou cancele:

@Asynchronous
public Future<Invoice> generateInvoice(Long orderId) {
    Invoice invoice = render(orderId);          // runs in its own transaction, on a container thread
    return new AsyncResult<>(invoice);
}

O método assíncrono roda em um novo contexto de transação (o de quem chamou não é propagado), enquanto o contexto de segurança é propagado. O ManagedExecutorService do Jakarta Concurrency cobre um terreno parecido para código fora de beans; @Asynchronous é a opção mais simples quando o trabalho já está em um bean, e servidores da geração do CDI 4.0 também suportam o @Asynchronous do Jakarta Concurrency 3.0 em beans CDI, que retorna um CompletableFuture.

Enterprise beans ou beans CDI?

O CDI chegou no Java EE 6 e absorveu quase tudo o que antes exigia um enterprise bean. A decisão hoje:

NecessidadeBean CDISession bean
Injeção, scopes, eventos, interceptors, decoratorsSimSim (via CDI)
Transações@Transactional (JTA 1.2+)@TransactionAttribute, REQUIRED por padrão em todos os métodos
Verificações declarativas de papéisProgramáticas via SecurityContext.isCallerInRole; @RolesAllowed em beans CDI só como extensão do fornecedor@RolesAllowed em qualquer lugar
Pooling e limitação de instânciasNãoPool stateless
Interface remotaNão@Remote
Consumo de mensagensNão (exceto extensões do fornecedor)Message-driven beans
Timers de calendárioSó programáticos (ManagedScheduledExecutorService); o Jakarta Concurrency 3.1 no EE 11 adiciona métodos assíncronos agendados@Schedule, TimerService
Contexto de persistência estendidoNãoBean stateful
Ordem de inicializaçãoObserver de @Initialized@Startup + @DependsOn
DisponibilidadeTodos os profiles, inclusive o CoreWeb Profile (Lite) ou Platform

Uma regra razoável: escreva serviços como beans CDI @ApplicationScoped com @Transactional, a menos que você precise de um recurso da coluna da direita, caso em que a anotação extra não custa nada. Bases de código da era Java EE 6 e 7 pendem para o outro lado (@Stateless em tudo) e não há pressa nenhuma em convertê-las: os dois modelos interoperam sem atrito.

Erros comuns

  • Chamar this.otherMethod() esperando que REQUIRES_NEW ou @RolesAllowed se apliquem. O proxy é ignorado. Injete o bean nele mesmo (@Inject OrderService self;) e chame self.otherMethod().
  • Guardar estado em um bean stateless. Um campo preenchido em um método e lido em outro vai funcionar nos testes com uma única instância e falhar sob carga, quando o pool entregar outra instância.
  • Singletons com o lock WRITE padrão. Todas as chamadas são serializadas e o throughput despenca sob concorrência. Coloque @Lock(READ) na classe.
  • Timers persistentes para trabalho volátil. Depois de um restart, o contêiner repete cada disparo perdido. Use persistent = false para atualizações periódicas e heartbeats.
  • Views remotas dentro de um mesmo deploy. Copiar argumentos para uma chamada dentro da mesma JVM é puro overhead. Use a view sem interface ou a local.
  • Capturar a exceção runtime dentro do bean e retornar normalmente. Quando uma EJBException chega a quem chamou, a transação já está marcada para rollback; se você mesmo capturar a exceção, verifique sessionContext.getRollbackOnly() ou lance uma @ApplicationException no lugar.
  • Campos não serializáveis em beans stateful. A passivação lança exceção; marque-os como transient e reconstrua-os em @PostActivate.

Principais pontos

  • Um session bean é uma classe gerenciada pelo contêiner por trás de um proxy; o proxy fornece transações, verificações de segurança, acesso por uma única thread, injeção e interceptors.
  • Beans stateless ficam em pool e não guardam estado do cliente; beans stateful guardam estado por cliente e podem ter um contexto de persistência estendido; singletons são compartilhados e precisam de @Lock para permitir leituras concorrentes.
  • Prefira a view sem interface; use @Remote apenas entre processos, e prefira REST ou mensageria para essa fronteira.
  • Timers (@Schedule, TimerService) e métodos @Asynchronous são recursos do contêiner indisponíveis para objetos comuns; torne os timers não persistentes, a menos que as execuções perdidas precisem ser repetidas.
  • Interceptors, callbacks de ciclo de vida e injeção são compartilhados com o CDI; um bean CDI com @Transactional substitui a maioria dos beans stateless.
  • O Enterprise Beans 4.0 removeu os entity beans do EJB 2.x e o JAX-RPC; a especificação é estável e dificilmente vai mudar de novo.

Fontes e leituras complementares

Perguntas frequentes

Os EJBs ainda são usados ou o CDI os substituiu?
Os dois são usados. O CDI cuida de injeção, scopes, eventos e, com @Transactional, de transações, então boa parte da lógica de negócio nova é escrita como beans CDI. Os enterprise beans continuam sendo a forma padrão de ter, em um servidor Jakarta EE, interfaces remotas, consumidores message-driven, timers, métodos @Asynchronous, verificações declarativas de papéis e lógica de inicialização em um singleton. A especificação está estável, não em evolução: a versão 4.0 não mudou desde o Jakarta EE 9.
Por que um bean stateless fica em pool se ele não tem estado?
Porque criá-lo não é de graça: o contêiner precisa fazer a injeção, resolver recursos e chamar @PostConstruct. Manter um pool de instâncias já inicializadas permite entregar uma instância a cada requisição de imediato e garante que apenas uma thread use cada instância por vez, então seu código nunca precisa de sincronização. O tamanho do pool limita a concorrência, o que também funciona como um throttle útil.
Qual a diferença entre uma interface local e uma remota?
Uma view local é uma chamada dentro da mesma JVM: os argumentos são passados por referência e o overhead é o de uma chamada via proxy. Uma view remota (@Remote) foi feita para ser chamada de outra JVM por um protocolo específico do fornecedor: argumentos e resultados são serializados e passados por valor, as exceções vêm encapsuladas em EJBException e a chamada é ordens de grandeza mais lenta. Use a remota só quando existir de fato um segundo processo.
Posso chamar um método de um session bean a partir de outro método do mesmo bean e obter uma nova transação?
Não chamando diretamente via this: o proxy do contêiner é ignorado, então nenhum atributo de transação, verificação de segurança ou interceptor é aplicado. Injete o bean nele mesmo pela sua interface de negócio (ou use SessionContext.getBusinessObject) e chame por essa referência.
Arquitetura do Jakarta EE: contêineres, perfis e servidores de aplicaçãoComo o Jakarta EE se organiza: camadas, contêineres, especificações, perfis, empacotamento e a troca de javax por jakarta, e como escolher um servidor.Iniciante14 min de leituraJakarta Transactions (JTA 2): transações gerenciadas pelo contêiner, propagação e XAComo as transações JTA funcionam em um servidor Jakarta EE: isolamento, propagação do @Transactional, regras de rollback, UserTransaction e commit XA.Intermediário11 min de leituraMessage-driven beans e Jakarta Messaging (JMS 3.1) no contêinerConsuma filas e topics com message-driven beans, envie mensagens com JMSContext e acerte transações, reentrega, seletores e assinaturas duráveis.Intermediário10 min de leituraJakarta Persistence (JPA 3.1): entidades, mapeamento e contexto de persistênciaComo o JPA mapeia classes em tabelas: unidade de persistência, EntityManager, estados da entidade, IDs, relacionamentos, fetch, cascata, herança e locking.Intermediário10 min de leituraSpringInversão de controle e injeção de dependência no SpringO que o container do Spring faz, como diferem a injeção por construtor, setter e campo, e como ligar beans com configuração Java e component scan.Iniciante9 min de leituraSpringProgramação orientada a aspectos com Spring AOP e AspectJComo o Spring AOP aplica comportamento transversal com proxies: aspectos, pointcuts, tipos de advice, proxies JDK vs. CGLIB e a armadilha da autoinvocação.Intermediário9 min de leitura