Monday, October 28, 2024

How to migrate from EJB to CDI?

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.