使用 Java 持久性的仅限 Web 应用程序中的设计选择

状态:在 Early Access 版本阶段
作者:Sean Brydon 及 Smitha Kangath

问题描述

新的 Java 持久性 API 提供了一种新方法来设计 Web 应用程序的模型层。包括 Java 持久性在内的 Java EE 5 规范的内容很多,其中涵盖了很多技术。由于开发者在设计使用 Java 持久性 API 的 Web 应用程序时面临很多选择,因此,本文简要介绍了一些关键的编程模型选择。并概述了使用实体管理器、实体和事务以及利用标注时的一些注意事项。

首先定义我们所说的“仅限 Web”应用程序的含义和适用范围是非常有用的。就仅限 Web 的应用程序而言,我们是指打包为 war 文件的应用程序(有时可能会打包为 ear 文件),其中不包含 EJB 模块,并且它被部署到与 Java EE 5 平台兼容的应用服务器上。本文将不介绍 EJB 方面的内容,如用作实体客户端的会话 Bean。仅限 Web 的应用程序包含 Web 组件,如使用 Java 持久性 API 的 Servlet。它还包含 Java EE 容器的所有可用服务,如 JNDI 命名服务、安全服务等。这是一种可移植的 Web 应用程序,能够在与 Java EE 5 兼容的任何应用服务器上运行。今后,我们将通过其他文档来介绍一些不同的应用程序体系结构,如使用 EJB 和 Java 持久性的 Web 应用程序、部署在 Web 容器中并且 Java 持久性位于 Java SE 环境中的 Web 应用程序(Java 持久性能够在没有 Java EE 容器的情况下运行)。请注意,对于仅限 Web 的应用程序,编程模型将不同于包含 EJB 模块的 Web 应用程序。本文概述的设计选择和限制仅适用于仅限 Web 的应用程序的编程模型。

以下是使用 Java 持久性 API 设计 Web 应用程序时需要考虑的一些问题:
由于这些选择以及很多其他内容都会影响应用程序的设计,因此,需要我们更周密地考虑这些问题,并了解在 Web 应用程序中使用 Java 持久性的编程模型。

解决方案

在持久性上下文内,实体实例及其生命周期是由实体管理器进行管理的。其中,实体管理器包含两种类型:容器管理的实体管理器或应用程序管理的实体管理器。对于仅限 Web 的应用程序来说,使用容器管理的实体管理器还是应用程序管理的实体管理器是编程模型的主要选择。为什么这种选择至关重要呢?一方面,应用程序管理的实体管理器需要在应用程序逻辑中执行更多的编码,因为应用程序必须以编程方式来管理实体管理器的生命周期。而另一方面,对于容器管理的实体管理器,实体管理器的生命周期是由 Java EE 容器透明管理的。但这一选择还是至关重要的,因为每种模型中都包含一些限制和需要权衡利弊的因素。表 1 总结了这两种不同的选择。
 
应用程序管理的实体管理器 容器管理的实体管理器
使用实体管理器工厂指定来获取实体管理器实例 使用 @PersistenceContext 标注或 JNDI 查找指定来获取实体管理器实例
必须使用扩展范围的持久性上下文 必须使用事务范围的持久性上下文
必须使用应用程序管理的事务 必须使用应用程序管理的事务
表 1:应用程序管理的和容器管理的实体管理器的特性

选择容器管理的或应用程序管理的实体管理器是在应用程序代码中完成的。如果应用程序代码使用 JNDI 查找或依赖关系注入(通过使用 @PersistenceContext 标注)来获取实体管理器实例,则选择结果为容器管理的实体管理器。如果应用程序代码使用 EntityManagerFactory.createEntityManager 方法来获取实体管理器,则选择结果为应用程序管理的实体管理器。

选择容器管理的或应用程序管理的实体管理器的一种影响是,每种实体管理器仅限于一种持久性上下文生命周期:事务范围的持久性上下文或扩展范围的持久性上下文。持久性上下文可以在事务范围内,也可以在跨多个事务的扩展范围内。对于仅限 Web 的应用程序,应用程序管理的实体管理器具有扩展范围;而容器管理的实体管理器具有事务范围。一个设计问题是,持久性上下文的生命周期范围应该是一个事务,还是跨多个事务。事务范围内的持久性上下文在维持当前 JTA 事务时存在;扩展的持久性上下文跨事务边界存在。这主要用于以下情况:您不希望像在会话中一样,将实体管理器在几个 HTTP 请求之间保持活动状态。如果应用程序必须使用扩展范围的事务,则它必须使用应用程序管理的实体管理器。请注意,对于使用 EJB 的 Web 应用程序,扩展范围的事务既可以用于容器管理的实体管理器,也可以用于应用程序管理的实体管理器。但在仅限 Web 的应用程序中,只能将扩展范围的事务用于应用程序管理的实体管理器。

在仅限 Web 的应用程序中,由于容器管理的实体管理器只能使用事务范围的持久性上下文,因此,不需要指定持久性上下文的类型。请注意,这不同于包含 EJB 的 Web 应用程序,后者可以通过 @PersistenceContext(type=PersistenceContextType.EXTENDED) 或 @PersistenceContext(type=PersistenceContextType.TRANSACTION) 将实体管理器指定为使用扩展范围的持久性上下文。在仅限 Web 的应用程序中,代码不应尝试使用标注来设置持久性上下文类型。

对于仅限 Web 的应用程序,应注意的一个限制是,只能使用应用程序管理的事务。包含 EJB 模块的 Web 应用程序(使用无态和有态会话 Bean 访问实体)可以使用容器管理的事务。容器管理的事务允许 Java EE 平台处理事务的生命周期。但对于仅限 Web 的应用程序,容器管理的事务不是一种选择。这意味着,将使用编程 API 来设置事务启动、提交和管理的界限。

现在,让我们看一下在仅限 Web 的应用程序中使用 Java 持久性的两种不同方式:使用应用程序管理的实体管理器和使用容器管理的实体管理器。

1. 使用应用程序管理的实体管理器

让我们看一下使用应用程序管理的实体管理器的应用程序设计。实体管理器工厂的通常用法是使用 PersistenceUnit 标注来注入一个实体管理器。例如,@PersistenceUnit(unitName="CatalogPu") EntityManagerFactory emf。然后,代码应保留该工厂以进行重用,直至使用完其实体管理器时为止。实体管理器工厂是线程安全的,可以将其保留在应用程序范围内;因此,建议使用应用程序管理的实体管理器的仅限 Web 应用程序将该工厂缓存在应用程序范围内,并在请求之间共享它。请注意,如果应用程序关闭某个实体管理器工厂,则认为它的所有实体管理器均处于关闭状态。

让我们看一下应用程序管理的实体管理器的实体管理器接口。该实体管理器接口包含一些方法,用于永久保存对象和执行查询等;还包含一些生命周期方法,它们是因为没有使用容器管理的实体管理器而必须由代码调用的方法。首先,让我们看一下,如何为应用程序管理的实体管理器获取实体管理器。在使用应用程序管理的实体管理器时,代码使用 EntityManagerFactory.createEntityManager() 方法来获取实体管理器。通过工厂来获取实体管理器还会指定由应用程序管理这些实体管理器。对于应用程序管理的实体管理器,不能使用注入或 JNDI 查找来获取实体管理器,而必须使用 EntityManagerFactory.createEntityManager()。

代码示例 1 说明了获取实体管理器工厂的正确方法。请注意,可以将引用 emf 保存为实体变量并在其他方法调用中重复使用。应用程序管理的实体管理器的使用是通过使用工厂获取实体管理器(通过 EntityManager em = emf.createEntityManager() 调用)来指定的。可随后使用实体管理器 API 永久保留或查询实体。

@PersistenceUnit private EntityManagerFactory emf;//declared as instance variable for re-use.
...
//this method uses entity manager to persist an entity Item.java
public void addItem(Item item){
  EntityManager em = emf.createEntityManager();
  try{
    ...
    em.persist(item);
  ...
  } finally {
    em.close();
  }

代码示例 1:获取和使用应用程序管理的实体管理器

应用程序代码还必须管理实体管理器的生命周期。使用应用程序管理的实体管理器时,代码使用 closeisOpenjoinTransaction 方法来管理实体管理器及其生命周期。应用程序在使用完实体管理器时,需要将其关闭。实体管理器不是线程安全的,因此不应以非线程安全的方式保留对实体管理器实例的引用。由于应用程序管理的实体管理器具有扩展范围的持久性上下文,因此,代码可以选择将实体管理器保持打开状态(针对几个请求),并在完成时将其关闭。应用程序管理的实体管理器还可以模拟事务范围的实体管理器的行为,并在创建实体管理器的同一方法结束时将其关闭。这样,扩展范围的持久性上下文可提供更大的灵活性,因为它们可以实现事务范围的持久性上下文的行为并且跨多个 HTTP 请求和多个事务。一种将实体管理器保持打开状态并使用扩展范围的持久性上下文的使用情况是:多页面表单可能使用几个表单提交来收集实体中的所有用户信息;在收集了所有信息后刷新所有实体并将其提交到数据库。

javax.persistence.EntityManager 接口中的某些方法要求必须在事务上下文中调用它们。如果没有事务以及持久性上下文在事务范围内,这些方法将抛出 TransactionRequiredException。这些方法对数据库中的数据执行写入或更新操作。开发者应该确保在调用这些方法之前具有事务上下文。这些方法是 persistmergeremoveflushrefresh 方法。

应用程序管理的实体管理器的另一种设计选择是,是使用 JTA 事务还是使用本地资源事务。仅限 Web 的应用程序必须使用应用程序管理的事务。这意味着,代码将使用编程 API 来设置事务启动和提交的界限。但对于应用程序管理的实体管理器,您还具有一些额外的灵活性,这些灵活性是容器管理的实体管理器所没有的。对于应用程序管理的实体管理器,应做出以下设计决策:是使用 JTA 实体管理器,还是使用资源本地实体管理器。JTA 实体管理器的事务是通过 JTA 控制的。应用程序管理的实体管理器可以是 JTA 实体管理器或资源本地实体管理器。 代码示例 2 比较了 JTA 应用程序管理的实体管理器的代码和资源本地应用程序管理的实体管理器的代码。

JTA 实体管理器代码 资源本地实体管理器代码
@PersistenceUnit EntityManagerFactory emf;
@Resource UserTransaction utx;
 
...
public void addItem(Item item){
  EntityManager em = emf.createEntityManager();
  try{
    utx.begin();
    em.joinTransaction();
    em.persist(item);
    utx.commit();
  } catch(Exception exe){
    System.out.println("Error persisting item: "+exe);
    try {
      utx.rollback();
    } catch (Exception e) {}
 } finally {
    em.close();
 }
}
...
@PersistenceUnit private EntityManagerFactory emf;
...
EntityManager em = emf.createEntityManager();
em.getTransaction().begin();
//call other entity manager operations like persist
...
em.getTransaction().commit();
em.close();
代码示例 2:比较 JTA 实体管理器和资源本地实体管理器的代码

通常,使用 JTA 是首选。使用 JTA 时,允许将多个操作划分到某事务中,并且这些操作在该事务中处于原子级别(最小级别)。使用资源本地实体管理器时,可能会略微提高一些性能,这对于以下情况可能非常有用:只执行一个操作的简单情况,或者显式地要求方法中的两个操作位于单独的事务中。

2. 使用容器管理的实体管理器

Web 应用程序可使用以下方法来指定它将使用容器管理的实体管理器:使用 JNDI 查找以获取实体管理器,或者使用 @PersistenceContext 标注来注入实体管理器;这会导致由 Java EE 5 容器来管理实体管理器。对于容器管理的实体管理器,应用程序代码不会与实体管理器工厂进行交互,而只使用注入或查找。通过使用容器管理的实体管理器,容器将调用操作的生命周期方法,如关闭实体管理器实例。这使代码变得更加简单。代码仅注入并使用实体管理器,在调用方法结束时,容器将关闭实体管理器。

请注意,实体管理器不是线程安全的,因此,代码不应尝试保留并重复使用实体管理器。相反,应在同一方法内创建和关闭实体管理器。例如,如果在 Servlet 中将某个实体管理器声明为实例变量,则多个并发请求可能会同时访问该实体管理器,从而导致非线程安全的行为。这种情况也有例外,例如,请求范围的 Web 组件(如使用请求范围指定的 JSF 受管 Bean)可能将实体管理器声明为实例变量,并且可能会使用注入来设置它的值。对于具有请求范围的 JSF 受管 Bean,由于在请求结束时将销毁对象,因此,不会重复使用实体管理器,因而实体管理器是线程安全的。但是,很少会出现这种情况,应考虑更具一般性的解决方案。由于无法在所有 Web 组件中使用标注来以线程安全的方式注入实体管理器,因此,在仅限 Web 的应用程序中使用容器管理的实体管理器会有一些麻烦。由于实体管理器不是线程安全的,因此,大多数 Web 组件应该在方法范围内声明实体管理器。但无法在方法内应用标注,因此,如果在方法内声明实体管理器,则无法使用注入。这意味着,必须使用 JNDI 查找来获取实体管理器实例。下面的示例代码说明了如何使用标注来声明依赖关系(可避免使用部署描述符),然后说明了如何使用 JNDI 查找来获取实体管理器,以及在仅限 Web 的应用程序(不包含 EJB 模块)中使用容器管理的实体管理器的线程安全方法。代码示例 3 展示了使用实体管理器的方法,指出了正确的线程安全方法和不正确的做法。

不正确的做法:非线程安全,因而这种方法不可取

正确的做法:线程安全。
在开头使用标注来声明 JNDI 依赖关系;使用 JNDI 查找来获取实体管理器。
public class MyServlet extends HttpServelet {

  @PersistenceContext EntityManager em;
 
  public void doGet( HttpServletRequest req,
     HttpServletResponse resp) throws ... {
    ...
    //note, a concurrent thread
    //could be using
the same
    //entity manager instance
    em.persist(item)
    
....some code  using the entity manager
  }
}
 


@PersistenceContext(name="foo", unitName="myPuName")
public class MyServlet extends HttpServelet {

  public void doGet(HttpServletRequest req,
                    HttpServletResponse resp) throws ... {
    ...
    //each doGet method gets its own 
    //
entity manager so this method-scoped 
    //
em is thread safe.
    //
also notice JNDI lookup to get em
    EntityManager em = (EntityManager) ic.lookup("java:comp/env/foo");
    em.persist(item)
    
....some code  using the entity manager
  }
}

代码示例 3:在仅限 Web 的应用程序(不包含 EJB 模块)中使用容器管理的实体管理器的线程安全方法

仅限 Web 的应用程序(不包含 EJB 模块)不能具有容器管理的扩展持久性上下文,因此,在使用容器管理的实体管理器时,只能使用事务范围的持久性上下文。这是因为,容器无法像在有态会话 Bean 中那样调用 close()。如果需要扩展范围的持久性上下文,请考虑使用应用程序管理的实体管理器,或者引入 EJB 模块并使用有态会话 Bean,这允许容器管理的实体管理器与扩展范围的持久性上下文共存。

按照 Java 持久性规范,容器管理的实体管理器必须是 JTA 实体管理器。JTA 实体管理器参与当前 JTA 事务。与应用程序管理的实体管理器不同,不能使用本地资源事务。这意味着,仅限 Web 的应用程序需要在应用程序代码中使用 JTA 来设置事务界限。代码示例 4 说明了将容器管理的实体管理器和 JTA 配合使用以设置事务界限的正确方法。

@PersistenceContext(name="foo", unitName="myPuName")
public class MyServlet extends HttpServelet {

@Resource UserTransaction utx;

  public void doGet(HttpServletRequest req,
                    HttpServletResponse resp) throws throws ServletException, IOException {

    EntityManager em = (EntityManager) ic.lookup("java:comp/env/foo");

    //get data from request
    String name = request.getParameter("item_name");
    ...
    //create new Item entity
    Item item = new Item();
    //set values of Item entity
    item.setName(name);
    ...
    utx.begin();
    em.persist(item); //persist and add new Item to database
    utx.commit();
    ...
}

代码示例 4:说明在仅限 Web 的应用程序中使用容器管理的实体管理器的正确方法

正如代码示例 4 中所示,容器管理的实体管理器的使用是相当简便的。由于实体管理器是由容器管理的,因此,不需要使用显式的调用来管理其生命周期。@Resource 标注注入了一个用于控制 JTA 事务的 UserTransaction 实例。UserTransaction 是线程安全的,因此,可以将其保存为由其他请求共享的实例变量。由于我们此处执行的是数据库写入操作,因此,我们仅在调用 em.persist() 之前启动事务并随后进行提交。容器管理实体管理器实例的生命周期,并在方法结束时将其关闭。请注意,Servlet 类具有标注 @PersistenceContext(name="foo", unitName="myPuName"),它用于声明对持久性上下文的依赖关系,然后在 Servlet.doGet 方法中使用 JNDI 查找并获取实体管理器(通过 EntityManager em = ic.lookup("java:comp/env/foo"))。

参考资料

以下是一些值得参考的资料:

© Sun Microsystems 2006。Java BluePrints Solutions Catalog 中的所有内容受版权保护,未经 Sun Microsystems 的明确书面许可,不得在其他产品中发布。