如何从 EJB 迁移到 CDI?

从 Jakarta EE 10 开始,尤其是 Jakarta Concurrency 3.0 和 Jakarta Transactions 2.0,我们可以使用 CDI 来替代@StatelessEJB。

Jakarta Concurrency 3.0 提供了新的@Asynchronous注释以及CronTrigger替代 EJB@Asynchronous和 的帮助程序@Schedule。

Jakarta Transactions 2.0 提供了新的Transactional.TxType枚举来替代 EJB TransactionAttributeType。

如果您希望从 EJB 迁移到 CDI,或者只是需要了解事务性业务服务 bean 的“规范”CDI 方法,那么您可能会发现本指南很有帮助。

将 @Stateless EJB 迁移到 CDI
以下是 EJB 中平均无状态 bean 的样子(注意:为简洁起见,省略了方法参数和返回类型):

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() {
       
// ...
    }
}

这里是 CDI 中的等效项,借助 Jakarta Transactions 2.0 中引入的Transactional.TxType枚举,并证明您可以完美地继续使用EntityManager相同的方式:

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() {
       
// ...
    }
}

值得注意的是,@StatelessEJB 中还有一个特性:池化。CDI 中没有这样的等价物,而且实际上也不需要它,因为无状态 CDI bean 已被标记,@ApplicationScoped并且 CDI 实例不同步,而 EJB 实例已同步。如果您需要一个可能保存状态的业务服务 bean,因此您不得不标记它@RequestScoped,并且您想降低构建成本,那么您可以考虑为此使用OmniServices@Pooled实用程序库的注释。

使用池的另一个原因是“节流”,这样后端访问在业务服务 bean 级别上就被 FIFO 队列所保护(尽管它也可以在 HTTP 服务器、JDBC 连接池和 DB 级别上被节流)。CDI 中也没有这样的等效项,但根据问题 136 ,即将推出的 Jakarta Concurrency 3.2 for Jakarta EE 12 可能会为此提供注释@MaxConcurrency,这可能是件好事。我的建议是,衡量就是了解。自 EJB(1998 年!)推出以来,硬件和 JVM(尤其是垃圾收集)功能取得了长足的进步,人们应该想知道,在业务服务级别进行节流和/或减少构造函数调用是否在当今仍然是绝对必要的。

还应注意的是,@Transactional还支持作为类级注释。因此,这为创建新的 CDI 注释提供了可能性,@Stereotype例如,@Service它基本上将@ApplicationScoped和合并@Transactional为单个注释,更接近于的行为@Stateless。

将 EJB @Asynchronous 迁移到 CDI
EJB 中的平均@Asynchronous方法如下所示:

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

以下是 CDI 中的等效项,借助 Jakarta Concurrency 3.0 引入的@Asynchronous注释:

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

导入注释时要小心 IDE 自动完成@Asynchronous!为了使其在 CDI 托管 bean 中工作,它需要来自jakarta.enterprise.concurrent包而不是jakarta.ejb包。

将 @Startup EJB 迁移到 CDI
EJB 中的平均启动 bean 如下所示:

package com.example;

import jakarta.annotation.PostConstruct;
import jakarta.ejb.Singleton;
import jakarta.ejb.Startup;

@Startup
@Singleton
public class StartupBeanInEJB {

    @PostConstruct
    public void init() {
        // ...
    }
}
以下是 CDI 中的等效功能,借助 CDI 4.0 引入的Startup事件:

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) {
       
// ...
    }
}

您需要记住,@ApplicationScoped与 不同, 不是读写锁定的@Singleton,尽管许多人不需要它,只需@Singleton通过附加@ConcurrencyManagement(BEAN)注释解锁 。如果读写锁定实际上是一项技术要求,通常是为了避免业务服务级别的数据库死锁,那么您可能需要考虑为此使用OmniServices@Lock实用程序库的注释。注释的等效项也缺少 CDI,但目前有一个未解决的问题,将其包含在即将推出的 Jakarta Concurrency 3.2 for Jakarta EE 12 中:问题 135。@Lock

如果启动 CDI bean 恰好是 WAR 而不是 JAR 的一部分,并且您恰好已经使用OmniFaces,那么您也可以使用它@Eager来代替@Observes Startup。

将 EJB @Schedule 迁移到 CDI
EJB 中的平均后台任务调度程序如下所示:

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

以下是 CDI 中的等效功能,借助 Jakarta Concurrency 3.0 引入的CronTrigger帮助程序:

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

是的,它非常冗长。幸运的是,Jakarta Concurrency 3.1(Jakarta EE 11 的一部分)将对此进行改进。@Asynchronous(runAt = @Schedule(...))理论上,您可以简单地使用以下方法:

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

它还支持 cron 表达式字符串,遵循 API 规则CronTrigger:

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

重申一下,您需要记住@ApplicationScoped,与 不同, 不是读写锁定的@Singleton。如果这是技术要求,那么您可能需要考虑使用OmniServices@Lock实用程序库的注释来实现这一点。

@Stateful EJB 怎么样?
它从未在基于 Web 的应用程序中派上用场,它只在 CORBA/RMI 中有用,而如今 Web 服务已经完全淘汰了 CORBA/RMI,所以忘掉它吧。最好将其迁移到无状态@ApplicationScopedbean,必要时结合@SessionScoped甚至@ViewScoped托管 bean 来跟踪客户端的状态数据。