Introduction
Since Jakarta EE 10, especially with Jakarta Concurrency 3.0 and Jakarta Transactions 2.0, we can use CDI to substitute a @Stateless
EJB. Jakarta Concurrency 3.0 delivers the new @Asynchronous
annotation as well as the CronTrigger
helper to substitute EJB's @Asynchronous
and @Schedule
. Jakarta Transactions 2.0 delivers the new Transactional.TxType
enum to substitute EJB's TransactionAttributeType
. In case you wish to migrate away from EJB to CDI, or simply need to know the "canonical" CDI approach to transactional business service beans, then you may find this guideline helpful.
Migrating @Stateless EJB to CDI
Here's how the average stateless bean in EJB looks like (note: method arguments and return types are omitted for brevity):
package com.example;
import jakarta.ejb.EJB;
import jakarta.ejb.Stateless;
import jakarta.ejb.TransactionAttribute;
import jakarta.ejb.TransactionAttributeType;
import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext;
@Stateless
public class StatelessBeanInEJB {
@PersistenceContext
private EntityManager entityManager;
// The @TransactionAttribute(TransactionAttributeType.REQUIRED) annotation is optional; this is the default already.
public void transactionalMethod() {
// ...
}
@TransactionAttribute(TransactionAttributeType.NOT_SUPPORTED)
public void nonTransactionalMethod() {
// ...
}
@TransactionAttribute(TransactionAttributeType.REQUIRES_NEW)
public void independentTransactionalMethod() {
// ...
}
@TransactionAttribute(TransactionAttributeType.SUPPORTS)
public void optionalTransactionalMethod() {
// ...
}
}
And here's the equivalent in CDI, with help of the in Jakarta Transactions 2.0 introduced Transactional.TxType
enum, and demonstrating that you can perfectly fine continue using the EntityManager
the same way:
package com.example;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext;
import jakarta.transaction.Transactional;
import jakarta.transaction.Transactional.TxType;
@ApplicationScoped
public class StatelessBeanInCDI {
@PersistenceContext
private EntityManager entityManager;
@Transactional // The annotation value TxType.REQUIRED is optional; this is the default already.
public void transactionalMethod() {
// ...
}
@Transactional(TxType.NOT_SUPPORTED)
public void nonTransactionalMethod() {
// ...
}
@Transactional(TxType.REQUIRES_NEW)
public void independentTransactionalMethod() {
// ...
}
@Transactional(TxType.SUPPORTS)
public void optionalTransactionalMethod() {
// ...
}
}
Noted should be that @Stateless
has one more feature in EJB: it's pooled. There's no such equivalent in CDI and there is also not really a need for it as the stateless CDI bean has been marked @ApplicationScoped
and CDI instances are unsynchronized while EJB instances are synchronized. In case you need a business service bean which potentially holds state, and therefore you're forced to mark it @RequestScoped
, and you want to reduce the cost of construction, then you could consider using the @Pooled
annotation of the OmniServices utility library for this.
Another reason to use pooling would be "throttling", so that the back-end access is kind of secured behind a FIFO queue at business service bean level (even though it could also be throttled at HTTP server, JDBC connection pool, and DB level). There's also no such equivalent in CDI, but it might be good to know that the upcoming Jakarta Concurrency 3.2 for Jakarta EE 12 may according to issue 136 offer a @MaxConcurrency
annotation for that. My advice is, measuring is knowing. Hardware and JVM (garbage collection especially) capabilities have made so much progress since the introduction of EJB (1998!) that one should wonder whether throttling at business service level and/or reducing the constructor calls is still absolutely necessary these days.
Also noted should be that @Transactional
is also supported as a class-level annotation. So that opens the possibility to create a new CDI @Stereotype
annotation such as @Service
which basically combines @ApplicationScoped
and @Transactional
into a single annotation, much closer to the behavior of @Stateless
.
Migrating EJB @Asynchronous to CDI
Here's how the average @Asynchronous
methods in EJB look like:
package com.example;
import java.util.concurrent.Future;
import jakarta.ejb.AsyncResult;
import jakarta.ejb.Asynchronous;
import jakarta.ejb.EJB;
import jakarta.ejb.Stateless;
import jakarta.ejb.TransactionAttribute;
import jakarta.ejb.TransactionAttributeType;
import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext;
@Stateless
public class StatelessBeanInEJB {
@PersistenceContext
private EntityManager entityManager;
@Asynchronous
@TransactionAttribute(TransactionAttributeType.REQUIRES_NEW)
public Future<Void> asyncTransactionalMethod(YourEntity yourEntity) {
// ...
return new AsyncResult<>(null);
}
@Asynchronous
@TransactionAttribute(TransactionAttributeType.NOT_SUPPORTED)
public Future<YourEntity> asyncNonTransactionalMethod() {
// ...
return new AsyncResult<>(yourEntity);
}
}
And here's the equivalent in CDI, with help of the in Jakarta Concurrency 3.0 introduced @Asynchronous
annotation:
package com.example;
import java.util.concurrent.CompletableFuture;
import jakarta.enterprise.concurrent.Asynchronous;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext;
import jakarta.transaction.Transactional;
import jakarta.transaction.Transactional.TxType;
@ApplicationScoped
public class StatelessBeanInCDI {
@PersistenceContext
private EntityManager entityManager;
@Asynchronous
@Transactional(TxType.REQUIRES_NEW)
public CompletableFuture<Void> asyncTransactionalMethod(YourEntity yourEntity) {
// ...
return Asynchronous.Result.complete(null);
}
@Asynchronous
@Transactional(TxType.NOT_SUPPORTED)
public CompletableFuture<YourEntity> asyncNonTransactionalMethod() {
// ...
return Asynchronous.Result.complete(yourEntity);
}
}
Be careful with IDE autocomplete when importing the @Asynchronous
annotation! In order to get it to work in a CDI managed bean, it needs to come from the jakarta.enterprise.concurrent
package instead of the jakarta.ejb
package.
Migrating @Startup EJB to CDI
Here's how the average startup bean in EJB looks like:
package com.example;
import jakarta.annotation.PostConstruct;
import jakarta.ejb.Singleton;
import jakarta.ejb.Startup;
@Startup
@Singleton
public class StartupBeanInEJB {
@PostConstruct
public void init() {
// ...
}
}
And here's the equivalent in CDI, with help of the in CDI 4.0 introduced Startup
event:
package com.example;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.enterprise.event.Observes;
import jakarta.enterprise.event.Startup;
@ApplicationScoped
public class StartupBeanInCDI {
public void init(@Observes Startup startup) {
// ...
}
}
You need to keep in mind that the @ApplicationScoped
is not read-write locked, unlike @Singleton
, even though many people don't need it and simply unlock the @Singleton
via an additional @ConcurrencyManagement(BEAN)
annotation. In case read-write locking is actually a technical requirement, generally to avoid DB deadlocks at business service level, then you might want to consider using the @Lock
annotation of the OmniServices utility library for this. An equivalent of the @Lock
annotation is namely also lacking in CDI, but there's currently an open issue to include it in the upcoming Jakarta Concurrency 3.2 for Jakarta EE 12: issue 135.
In case the startup CDI bean happens to be part of a WAR instead of a JAR, and you happen to already use OmniFaces, then you could also use its @Eager
instead of @Observes Startup
.
Migrating EJB @Schedule to CDI
Here's how the average background task scheduler in EJB looks like:
package com.example;
import jakarta.ejb.Schedule;
import jakarta.ejb.Singleton;
@Singleton
public class ScheduledTasksBeanInEJB {
@Schedule(hour="0", minute="0", second="0", persistent=false)
public void someDailyJob() {
// ... runs daily at midnight
}
@Schedule(hour="*", minute="0", second="0", persistent=false)
public void someHourlyJob() {
// ... runs every hour of the day
}
@Schedule(hour="*", minute="*/15", second="0", persistent=false)
public void someQuarterlyJob() {
// ... runs every 15th minute of the hour
}
@Schedule(hour="*", minute="*", second="*/5", persistent=false)
public void someFiveSecondelyJob() {
// ... runs every 5th second of the minute
}
}
And here's the equivalent in CDI, with help of the in Jakarta Concurrency 3.0 introduced CronTrigger
helper:
package com.example;
import java.time.ZoneId;
import jakarta.annotation.Resource;
import jakarta.enterprise.concurrent.CronTrigger;
import jakarta.enterprise.concurrent.ManagedScheduledExecutorService;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.enterprise.event.Observes;
import jakarta.enterprise.event.Startup;
@ApplicationScoped
public class ScheduledTasksBeanInCDI {
@Resource
private ManagedScheduledExecutorService scheduler;
public void init(@Observes Startup startup) {
scheduler.schedule(this::someDailyJob, new CronTrigger(ZoneId.systemDefault()).hours("0").minutes("0").seconds("0"));
scheduler.schedule(this::someHourlyJob, new CronTrigger(ZoneId.systemDefault()).hours("*").minutes("0").seconds("0"));
scheduler.schedule(this::someQuarterlyJob, new CronTrigger(ZoneId.systemDefault()).hours("*").minutes("*/15").seconds("0"));
scheduler.schedule(this::someFiveSecondelyJob, new CronTrigger(ZoneId.systemDefault()).hours("*").minutes("*").seconds("*/5"));
}
public void someDailyJob() {
// ... runs daily at midnight
}
public void someHourlyJob() {
// ... runs every hour of the day
}
public void someQuarterlyJob() {
// ... runs every 15th minute of the hour
}
public void someFiveSecondelyJob() {
// ... runs every 5th second of the minute
}
}
Yeah, it's remarkably more verbose. Fortunately this will be improved in Jakarta Concurrency 3.1, part of Jakarta EE 11. You can then simply use @Asynchronous(runAt = @Schedule(...))
on the methods like below, theoretically:
package com.example;
import jakarta.enterprise.concurrent.Asynchronous;
import jakarta.enterprise.concurrent.Schedule;
import jakarta.enterprise.context.ApplicationScoped;
@ApplicationScoped
public class ScheduledTasksBeanInCDI {
@Asynchronous(runAt = @Schedule())
public void someDailyJob() {
// ... runs daily at midnight
}
@Asynchronous(runAt = @Schedule(hours = {}))
public void someHourlyJob() {
// ... runs every hour of the day
}
@Asynchronous(runAt = @Schedule(hours = {}, minutes = { 0, 15, 30, 45 }))
public void someQuarterlyJob() {
// ... runs every 15th minute of the hour
}
@Asynchronous(runAt = @Schedule(hours = {}, minutes = {}, seconds = { 0, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55 }))
public void someFiveSecondelyJob() {
// ... runs every 5th second of the minute
}
}
It also supports a cron expression string, following the rules of CronTrigger
API:
package com.example;
import jakarta.enterprise.concurrent.Asynchronous;
import jakarta.enterprise.concurrent.Schedule;
import jakarta.enterprise.context.ApplicationScoped;
@ApplicationScoped
public class ScheduledTasksBeanInCDI {
@Asynchronous(runAt = @Schedule(cron = "0 0 0 * * *"))
public void someDailyJob() {
// ... runs daily at midnight
}
@Asynchronous(runAt = @Schedule(cron = "0 0 * * * *"))
public void someHourlyJob() {
// ... runs every hour of the day
}
@Asynchronous(runAt = @Schedule(cron = "0 */15 * * * *"))
public void someQuarterlyJob() {
// ... runs every 15th minute of the hour
}
@Asynchronous(runAt = @Schedule(cron = "*/5 * * * * *"))
public void someFiveSecondelyJob() {
// ... runs every 5th second of the minute
}
}
To reiterate, you need to keep in mind that the @ApplicationScoped
is not read-write locked, unlike @Singleton
. In case that is a technical requirement, then you might want to consider using the @Lock
annotation of the OmniServices utility library for this.
How about @Stateful EJBs?
It has never had any use in web based applications, it was only useful in CORBA/RMI which is completely obsoleted by web services these days, so forget about it. You'd best migrate it to a stateless @ApplicationScoped
bean, if necessary in combination with a @SessionScoped
or even @ViewScoped
managed bean to keep track of client's stateful data.
3 comments:
Hi, I tried use the CDI startup event to replace the EJB 3 @Startup, but failed in WildFly 34, check here, https://wildfly.zulipchat.com/#narrow/channel/196266-wildfly-user/topic/.60.40Observes.20Startup.20event.60.20caused.20ContextNotActiveException
Any workarounds for @RolesAllowed? Probably could be done using @Inject SecurityContext and some code inside a method instead of annotations, like if(securityContext.isCallerInRole("Role name")) .. else throw YourOwnException
Here a link to the jakarta security idea for RolesAllowed in CDI: https://github.com/jakartaee/security/issues/295
Post a Comment