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,@PermitAlle@DenyAllna 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,@PersistenceContexte@Resourcesão preenchidos antes de@PostConstructrodar. - Interceptação: callbacks de ciclo de vida e interceptors
@AroundInvokerodam em volta dos métodos. - Acesso remoto, agendamento e assincronia: views remotas, timers
@Schedulee métodos@Asynchronoussó 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:
| View | Declarada por | Semântica da chamada | Use quando |
|---|---|---|---|
| Sem interface | Nada (métodos públicos da classe) | Dentro da JVM, por referência | Quase sempre |
| Local | Interface @Local | Dentro da JVM, por referência | Você quer uma interface para testes ou várias implementações |
| Remota | Interface @Remote | Entre 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:
| Necessidade | Bean CDI | Session bean |
|---|---|---|
| Injeção, scopes, eventos, interceptors, decorators | Sim | Sim (via CDI) |
| Transações | @Transactional (JTA 1.2+) | @TransactionAttribute, REQUIRED por padrão em todos os métodos |
| Verificações declarativas de papéis | Programáticas via SecurityContext.isCallerInRole; @RolesAllowed em beans CDI só como extensão do fornecedor | @RolesAllowed em qualquer lugar |
| Pooling e limitação de instâncias | Não | Pool stateless |
| Interface remota | Não | @Remote |
| Consumo de mensagens | Não (exceto extensões do fornecedor) | Message-driven beans |
| Timers de calendário | Só programáticos (ManagedScheduledExecutorService); o Jakarta Concurrency 3.1 no EE 11 adiciona métodos assíncronos agendados | @Schedule, TimerService |
| Contexto de persistência estendido | Não | Bean stateful |
| Ordem de inicialização | Observer de @Initialized | @Startup + @DependsOn |
| Disponibilidade | Todos os profiles, inclusive o Core | Web 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 queREQUIRES_NEWou@RolesAllowedse apliquem. O proxy é ignorado. Injete o bean nele mesmo (@Inject OrderService self;) e chameself.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
WRITEpadrã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 = falsepara 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
EJBExceptionchega a quem chamou, a transação já está marcada para rollback; se você mesmo capturar a exceção, verifiquesessionContext.getRollbackOnly()ou lance uma@ApplicationExceptionno lugar. - Campos não serializáveis em beans stateful. A passivação lança exceção; marque-os como
transiente 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
@Lockpara permitir leituras concorrentes. - Prefira a view sem interface; use
@Remoteapenas entre processos, e prefira REST ou mensageria para essa fronteira. - Timers (
@Schedule,TimerService) e métodos@Asynchronoussã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
@Transactionalsubstitui 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.