27

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

1. 创建一个请求范围的 CDI bean,它具有对 an 的引用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);
    }
}

在这个例子中 anEntityManager将在创建时创建,并在销毁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 请求中创建一个 which,但是关闭EntityManager?处理完HTTP请求后也会关闭吗?我知道@PersistanceContext注释注入了 container-managed EntityManager。这意味着EntityManager当客户端 bean 被销毁时 an 将被关闭。在这种情况下,什么是客户端 bean?是 ,ApplicationScopedBean在应用程序停止之前永远不会被销毁,还是EntityManagerProducer? 有什么建议吗?

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

4

4 回答 4

42

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

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

org.jboss.weld.exceptions.DefinitionException: WELD-001502: 资源生产者字段 [资源生产者字段 [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 容器责任”部分:

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

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

这里已经证明,错误地使用 CDI 生产者和处置器,1) 非线程安全的实体管理器可能会泄露给许多线程,并且 2) 处置器不起作用;使实体管理器保持打开状态。发生的事情是容器吞下的 IllegalStateException,没有留下任何痕迹(生成了一个神秘的日志条目,上面写着“销毁实例时出错”)。

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

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

于 2015-05-02T21:39:02.327 回答
1

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

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

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

于 2018-06-15T10:23:03.413 回答
0

您应该使用@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();
        }
    }

}
于 2014-07-19T19:46:23.157 回答
-1

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

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

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

于 2013-10-17T16:01:28.687 回答