使用 CDI 在 Java EE 应用程序中获取对 EntityManager 的引用

2022-09-01 15:41:34

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

1. 创建一个请求范围的 CDI Bean,其中包含对 的引用,然后将该 Bean 注入到应用程序范围的 CDI Bean 中。EntityManager

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

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

2. 创建一个生成 实例的生产者,然后将该实例注入到应用程序范围的 CDI Bean 中。EntityManagerEntityManager

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

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

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


答案 1

你几乎和你的CDI制作人在一起。唯一的问题是,您应该使用生产者方法而不是生产者字段。

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

org.jboss.weld.exceptions.DefinitionException: WELD-001502: Resource producer field [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;
    }
}

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

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

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

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

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

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

遗憾的是,许多人确实使用一种方法来关闭容器管理的实体管理器。当Oracle提供的官方Java EE 7教程以及CDI规范本身使用处理器来关闭容器管理的实体管理器时,谁能责怪他们。这是完全错误的,无论您将调用放在何处,在处置器方法中还是在其他地方,调用都会抛出一个。甲骨文的例子是两者中最大的罪人,它宣布生产者类是.正如我们所了解到的,这有可能将底层实体管理器引用暴露给许多不同的线程。@DisposesEntityManager.close()IllegalStateException@javax.inject.Singleton

这里已经证明,通过错误地使用CDI生产者和处理器,1)非线程安全的实体管理器可能会泄漏到许多线程,2)处理器没有效果;使实体管理器保持打开状态。发生的事情是容器吞没的非法状态异常,没有留下任何痕迹(一个神秘的日志条目说有一个“错误破坏实例”)。

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

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


答案 2

我理解你的问题。但这不是一个真正的问题。不要搞砸 CDI 声明的包含类的作用域,这将传播属性的作用域,期望那些使用 @Inject的属性!

@Inject将根据实现类的 CDI 声明的剩余性计算其引用。因此,您可能有一个内部包含@Inject EntityManager em 的 Applicationscoped 类,但是每个控制流都会找到自己对 disjount em 对象的 em 事务引用,因为后面的实现类的 EntityManager CDI 声明。

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


推荐