【问题标题】:Getting a reference to EntityManager in Java EE applications using CDI使用 CDI 在 Java EE 应用程序中获取对 EntityManager 的引用
【发布时间】:2026-02-06 21:25:01
【问题描述】:

我正在使用 Java EE 7。我想知道将 JPA EntityManager 注入 应用程序范围 CDI bean 的正确方法是什么。您不能只使用 @PersistanceContext 注释注入它,因为 EntityManager 实例不是线程安全的。假设我们希望在每个 HTTP 请求处理开始时创建 EntityManager,并在处理 HTTP 请求后关闭。我想到了两个选择:

1。 创建一个请求范围的 CDI bean,它具有对 EntityManager 的引用,然后将该 bean 注入应用程序范围的 CDI bean。

import javax.enterprise.context.RequestScoped;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;

@RequestScoped
public class RequestScopedBean {

    @PersistenceContext
    private EntityManager entityManager;

    public EntityManager getEntityManager() {
        return entityManager;
    }
}

import javax.enterprise.context.ApplicationScoped;
import javax.inject.Inject;

@ApplicationScoped
public class ApplicationScopedBean {

    @Inject
    private RequestScopedBean requestScopedBean;

    public void persistEntity(Object entity) {
        requestScopedBean.getEntityManager().persist(entity);
    }
}

在此示例中,EntityManager 将在创建 RequestScopedBean 时创建,并在 RequestScopedBean 销毁时关闭。现在我可以将注入移动到某个抽象类,以将其从ApplicationScopedBean 中删除。

2。 创建一个产生EntityManager 实例的生产者,然后将EntityManager 实例注入应用程序范围的CDI bean。

import javax.enterprise.context.RequestScoped;
import javax.enterprise.inject.Produces;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;

public class EntityManagerProducer {

    @PersistenceContext
    @Produces
    @RequestScoped
    private EntityManager entityManager;
}

import javax.enterprise.context.ApplicationScoped;
import javax.inject.Inject;
import javax.persistence.EntityManager;

@ApplicationScoped
public class ApplicationScopedBean {

    @Inject
    private EntityManager entityManager;

    public void persistEntity(Object entity) {
        entityManager.persist(entity);
    }
}

在此示例中,我们还将有一个 EntityManager,它会在每个 HTTP 请求中创建,但是 关闭 EntityManager 呢?处理完HTTP请求后也会关闭吗?我知道@PersistanceContext 注释注入了容器管理的EntityManager。这意味着当客户端 bean 被销毁时,EntityManager 将被关闭。在这种情况下,什么是客户端 bean?是ApplicationScopedBean,在应用程序停止之前永远不会被销毁,还是EntityManagerProducer?有什么建议吗?

我知道我可以使用无状态 EJB 代替应用程序范围的 bean,然后通过 @PersistanceContext 注释注入 EntityManager,但这不是重点 :)

【问题讨论】:

    标签: java jpa cdi entitymanager java-ee-7


    【解决方案1】:

    您的 CDI 制作人几乎是正确的。唯一的问题是您应该使用生产者方法而不是生产者字段。

    如果您使用 Weld 作为 CDI 容器(GlassFish 4.1 和 WildFly 8.2.0),那么您在生产者字段上组合 @Produces@PersistenceContext@RequestScoped 的示例应该在部署期间抛出此异常:

    org.jboss.weld.exceptions.DefinitionException: WELD-001502: 资源生产者字段 [Resource Producer Field [EntityManager] 与 限定符 [@Any @Default] 声明为 [[BackedAnnotatedField] @Produces @RequestScoped @PersistenceContext com.somepackage.EntityManagerProducer.entityManager]] 必须是 @Dependent 作用域

    事实证明,当使用生产者字段查找 Java EE 资源时,容器不需要支持除 @Dependent 之外的任何其他范围。

    CDI 1.2,第 3.7 节。资源:

    容器不需要支持其他范围的资源 比@Dependent。便携式应用程序不应定义资源 具有@Dependent 以外的任何范围。

    这句话是关于生产者字段的。使用生产者方法查找资源是完全合法的:

    public class EntityManagerProducer {
    
        @PersistenceContext    
        private EntityManager em;
    
        @Produces
        @RequestScoped
        public EntityManager getEntityManager() {
            return em;
        }
    }
    

    首先,容器将实例化生产者,并将容器管理的实体管理器引用注入em 字段。然后容器将调用您的生产者方法并将他返回的内容包装在请求范围的 CDI 代理中。此 CDI 代理是您的客户端代码在使用 @Inject 时得到的。因为生产者类是@Dependent(默认),所以底层容器管理的实体管理器引用将不会被任何其他生成的 CDI 代理共享。每次另一个请求需要实体管理器时,都会实例化一个生产者类的新实例,并将一个新的实体管理器引用注入到生产者中,而生产者又包装在一个新的 CDI 代理中。

    为了技术上正确,将资源注入到字段em 的底层和未命名容器被允许重用旧实体管理器(请参阅 JPA 2.1 规范中的脚注,“7.9.1 容器责任”部分,第 357 页)。但到目前为止,我们尊重 JPA 要求的编程模型。

    在前面的示例中,标记EntityManagerProducer @Dependent 或@RequestScoped 无关紧要。使用 @Dependent 在语义上更正确。但是,如果您在生产者类上设置比请求范围更广的范围,您就有可能将底层实体管理器引用暴露给许多线程,我们都知道这不是一件好事。底层实体管理器实现可能是线程本地对象,但可移植应用程序不能依赖实现细节。

    CDI 不知道如何关闭您放入请求绑定上下文的任何内容。最重要的是,容器管理的实体管理器不能被应用程序代码关闭。

    JPA 2.1,“7.9.1 容器职责”部分:

    如果应用程序容器必须抛出 IllegalStateException 在容器管理的实体管理器上调用 EntityManager.close。

    不幸的是,很多人确实使用@Disposes 方法来关闭容器管理的实体管理器。当Oracle提供的官方Java EE 7 tutorial以及CDI specification本身使用disposer关闭容器管理的实体管理器时,谁能责怪他们。这是完全错误的,对EntityManager.close() 的调用将抛出IllegalStateException,无论您将该调用放在何处、在处理程序方法或其他地方。 Oracle 示例通过将生产者类声明为@javax.inject.Singleton,是两者中最大的罪魁祸首。正如我们所知,这种风险会将底层实体管理器引用暴露给许多不同的线程。

    已经证明here 错误地使用 CDI 生产者和处置器,1) 非线程安全的实体管理器可能会泄漏到许多线程,并且 2) 处置器无效;使实体管理器保持打开状态。发生的事情是容器吞下的 IllegalStateException,没有留下任何痕迹(一个神秘的日志条目显示“销毁实例时出错”)。

    通常,使用 CDI 查找容器管理的实体管理器不是一个好主意。该应用程序很可能只使用@PersistenceContext 并对此感到满意。但在您的示例中,规则总是有例外,在处理应用程序管理的实体管理器的生命周期时,CDI 也可以用于抽象出 EntityManagerFactory

    要全面了解如何获取容器管理的实体管理器以及如何使用 CDI 查找实体管理器,您可能需要阅读 thisthis

    【讨论】:

    • 老实说,为什么 SpringBoot 更好、更容易;-) 进行切换...
    • 是否有任何关于回滚作为同一调用请求的一部分的相互依赖的 DML 的最佳实践的文档或信息?例如。最后一个失败时回滚 3 个插入调用? Order Header(insert #1)、Order Details (insert #2) 和 Order Payment(insert #3) 订单支付 (insert #3) 失败,我们需要回滚之前的 3 个插入,但这些插入是一部分不同的存储库/DAO。
    【解决方案2】:

    我了解您的问题。但这不是真的。不要搞乱包含类的 CDI 声明范围,这将传播属性的范围,除了那些使用 @Inject'ion 的属性!

    @Inject'ed 将根据实现类的 CDI 声明计算它们的引用。所以你可能有一个 Applicationscoped 类,里面有一个 @Inject EntityManager em,但是每个控制流都会找到它自己的对一个不相交的 em 对象的 em 事务引用,因为后面的实现类的 EntityManager CDI 声明。

    您的代码的错误之处在于,您提供了一个内部 getEntityManager() 访问方法。不要传递 Injected 对象,如果需要,只需 @Inject it 。

    【讨论】:

      【解决方案3】:

      您应该使用@Dispose 注解来关闭EntityManager,如下例所示:

      @ApplicationScoped
      public class Resources {
      
          @PersistenceUnit
          private EntityManagerFactory entityManagerFactory;
      
          @Produces
          @Default
          @RequestScoped
          public EntityManager create() {
              return this.entityManagerFactory.createEntityManager();
          }
      
          public void dispose(@Disposes @Default EntityManager entityManager) {
              if (entityManager.isOpen()) {
                  entityManager.close();
              }
          }
      
      }
      

      【讨论】:

      • 您正在创建一个应用程序管理的实体管理器(必须由应用程序代码关闭)。问题是关于容器管理的实体管理器。
      【解决方案4】:

      你可以注入保存的EntityManagerFactory,它是线程保存

      @PersistenceUnit(unitName = "myUnit")
      private EntityManagerFactory entityManagerFactory;
      

      然后您可以从 entityManagerFactory 中检索 EntityManager。

      【讨论】:

      • 我可以这样做,然后在应用程序范围 bean 的每个方法中从工厂获取 EntityManager。但它会是一个容器管理的EntityManager吗?我不这么认为。我不想自己管理EntityManager 和事务的生命周期。
      • @FlyingDumpling 你用的是什么版本的java EE?如果在 Java EE 7 之前,CDI bean 中没有事务支持。看到这个回复:*.com/questions/17838221/…
      • @AndreiI 对不起,我没有提到,我使用的是 Java EE 7