Skip to content

3. 多层架构中的 JPA

为了研究 JPA API,我们采用了以下测试架构:

我们的测试程序是直接查询 JPA 层的控制台应用程序。通过这种方式,我们探索了 JPA 层的主要方法。我们是在所谓的“Java SE”(标准版)环境中进行开发的。JPA 既可在 Java SE 环境中运行,也可在 Java EE5(企业版)环境中运行。

现在,我们已经充分掌握了关系型/对象桥接配置以及 JPA 层方法的使用,我们将回归到更传统的多层架构:

我们将通过由[业务]层和[DAO]层组成的两层架构来访问[JPA]层。将使用Spring框架[7],随后配合JBoss EJB3容器[8],将这些层连接在一起。

我们之前提到,JPA 既可在 SE 环境中使用,也可在 EE5 环境中使用。Java EE5 环境为访问持久化数据提供了众多服务,包括连接池、事务管理器等。开发人员利用这些服务可能会带来好处。Java EE5 环境目前尚未被广泛采用(2007 年 5 月)。它目前可在 Sun Application Server 9.x(Glassfish)上使用。 应用服务器本质上是一种 Web 应用服务器。如果您使用 Swing 构建独立的图形化应用程序,则无法利用 EE 环境及其提供的服务。这确实是一个问题。我们开始看到“独立”的 EE 环境,即那些可以在应用服务器之外使用的环境。JBoss EJB3 就是如此,本文将使用该环境。

在 EE5 环境中,各层由称为 EJB(企业级 Java Bean)的对象实现。在 EE 的早期版本中,EJB(EJB 2.x)被认为难以实现和测试,且有时性能欠佳。 我们将 EJB 2.x 分为“实体”Bean 和“会话”Bean。简而言之,EJB 2.x“实体”Bean 对应于数据库表中的一行,而 EJB 2.x“会话”Bean 则是用于实现多层架构中 [业务] 和 [DAO] 层的对象。 针对使用 EJB 实现 分层架构的主要批评之一是,它们只能在 EJB 容器内使用,而 EJB 容器是 EE 环境提供的一项服务。这使得单元测试变得困难。因此,在上图中,对使用 EJB 构建的 [业务] 和 [DAO] 层进行单元测试,需要搭建一个应用服务器,这是一项相当繁琐的操作,实际上并不鼓励开发人员频繁进行测试。

Spring框架的诞生正是为了应对EJB2的复杂性。Spring在SE(简单企业)环境中提供了大量通常由EE(企业级)环境提供的服务。因此,在本文关注的“数据持久化”部分,Spring提供了应用程序所需的连接池和事务管理器。Spring的出现促进了单元测试文化的形成,使得单元测试的实施突然变得容易得多。 Spring 允许使用标准 Java 对象(POJO,即普通 Java 对象)来实现应用层,从而使其能够在其他场景中被复用。最后,它能够相当透明地集成众多第三方工具,尤其是像 Hibernate、iBatis 等持久化工具……

Java EE 5 的设计旨在弥补先前 EE 规范的不足。EJB 2.x 已演进为 EJB 3。这些是带有注解的 POJO,当它们位于 EJB 3 容器内时,这些注解会将其标记为特殊对象。在容器内,EJB 3 可以利用容器提供的服务(连接池、事务管理器等)。 在 EJB 3 容器之外,EJB 3 便成为一个标准的 Java 对象。其 EJB 注解将被忽略。

上文中,我们已将 Spring 和 JBoss EJB3 描绘为多层架构的一种可能的基础架构(框架)。正是这一基础架构将提供我们所需的服务:连接池和事务管理器。

  • 在 Spring 中,各层将通过 POJO 来实现。这些 POJO 将通过依赖注入访问 Spring 的服务(连接池、事务管理器):在构建这些 POJO 时,Spring 会注入它们所需的服务引用。
  • JBoss EJB3 是一个能够在应用服务器外部运行的 EJB 容器。其工作原理(从开发者的角度来看)与 Spring 的描述类似。我们几乎找不到两者的区别。

本文将以一个三层Web应用程序的示例作为结尾——该示例虽然简单,却极具代表性:

3.1. 示例 1:使用 Spring / JPA 实现 Person 实体

我们将第2.1节中讨论的Person实体集成到一个多层架构中,该架构中各层通过Spring进行集成,持久层由Hibernate实现。

本文假设读者对Spring有基本的了解。若非如此,您可以阅读以下文档,其中解释了依赖注入的概念,这是Spring的核心:

[ref3]: Spring IoC(控制反转)[http://tahe.developpez.com/java/springioc]。

3.1.1. Eclipse/Spring/Hibernate “ ” 项目

Eclipse 项目如下:

  • 在 [1] 中:Eclipse 项目。可在 [6] 中找到,位于教程 [5] 的示例中。我们将导入它。
  • 在 [2] 中:分层结构的 Java 代码,按包呈现:
    • [entities]:JPA 实体包
    • [dao]:数据访问层——基于 JPA 层
    • [service]:服务层,而非业务层。此处我们将使用容器的事务服务。
    • [tests]:包含测试程序。
  • 在 [3] 中:[jpa-spring] 库包含 Spring 所需的 JAR 文件(另见 [7] 和 [8])。
  • 在 [4] 中:[conf] 文件夹包含本教程中使用的各数据库管理系统(DBMS)的 Spring 配置文件。

3.1.2. JPA 实体

此处仅管理一个实体,即第2.1节中讨论的Person实体,其配置如下所示:


package entites;
 
...
@Entity
@Table(name="jpa01_hb_personne")
public class Personne {
 
    @Id
    @Column(name = "ID", nullable = false)
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Integer id;
 
    @Column(name = "VERSION", nullable = false)
    @Version
    private int version;
 
    @Column(name = "NOM", length = 30, nullable = false, unique = true)
    private String nom;
 
    @Column(name = "PRENOM", length = 30, nullable = false)
    private String prenom;
 
    @Column(name = "DATENAISSANCE", nullable = false)
    @Temporal(TemporalType.DATE)
    private Date datenaissance;
 
    @Column(name = "MARIE", nullable = false)
    private boolean marie;
 
    @Column(name = "NBENFANTS", nullable = false)
    private int nbenfants;
 
    // manufacturers
    public Personne() {
    }
 
    public Personne(String nom, String prenom, Date datenaissance, boolean marie,
            int nbenfants) {
...
    }
 
    // toString
    public String toString() {
        return String.format("[%d,%d,%s,%s,%s,%s,%d]", getId(), getVersion(),
                getNom(), getPrenom(), new SimpleDateFormat("dd/MM/yyyy")
                        .format(getDatenaissance()), isMarie(), getNbenfants());
    }
 
    // getters and setters
...
}

3.1.3. [ DAO] 层

[DAO] 层提供了以下 IDao 接口:


package dao;
 
import java.util.List;
 
import entites.Personne;
 
public interface IDao {
    // find a person via his/her login
    public Personne getOne(Integer id);
 
    // get all the people
    public List<Personne> getAll();
 
    // save a person
    public Personne saveOne(Personne personne);
 
    // update a person
    public Personne updateOne(Personne personne);
 
    // delete a person via his/her login
    public void deleteOne(Integer id);
 
    // get people whose name corresponds to a model
    public List<Personne> getAllLike(String modele);
 
}

该接口的 [Dao] 实现如下:


package dao;
 
import java.util.List;
 
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
 
import entites.Personne;
 
public class Dao implements IDao {
 
    @PersistenceContext
    private EntityManager em;
 
    // supprimer une personne via son identifiant
    public void deleteOne(Integer id) {
        Personne personne = em.find(Personne.class, id);
        if (personne == null) {
            throw new DaoException(2);
        }
        em.remove(personne);
    }
 
    @SuppressWarnings("unchecked")
    // obtenir toutes les personnes
    public List<Personne> getAll() {
        return em.createQuery("select p from Personne p").getResultList();
    }
 
    @SuppressWarnings("unchecked")
    // obtenir les personnes dont le nom correspond àun modèle
    public List<Personne> getAllLike(String modele) {
        return em.createQuery("select p from Personne p where p.nom like :modele")
                .setParameter("modele", modele).getResultList();
    }

    // obtenir une personne via son identifiant
    public Personne getOne(Integer id) {
        return em.find(Personne.class, id);
    }
 
    // sauvegarder une personne
    public Personne saveOne(Personne personne) {
        em.persist(personne);
        return personne;
    }
 
    // mettre à jour une personne
    public Personne updateOne(Personne personne) {
        return em.merge(personne);
    }
 
}
  • 首先,请注意 [Dao] 实现的简洁性。这是由于使用了 JPA 层,它处理了大部分数据访问工作。
  • 第 10 行:[Dao] 类实现了 [IDao] 接口
  • 第 13 行:[EntityManager] 对象将用于操作 JPA 持久化上下文。为方便起见,我们有时会将其直接称为持久化上下文。该持久化上下文将包含 Person 实体。
  • 第 12 行:代码中没有任何地方初始化了 [EntityManager em] 字段。它将在应用程序启动时由 Spring 进行初始化。正是第 12 行上的 JPA @PersistenceContext 注解,指示 Spring 将持久化上下文管理器注入到 em 中
  • 第 26–28 行:通过 JPQL 查询获取所有人员的列表。
  • 第 32–35 行:通过 JPQL 查询检索所有姓名符合特定模式的人员列表。
  • 第 38–40 行:使用 JPA API 的 `find` 方法检索具有指定 ID 的人员。如果该人员不存在,则返回指针。
  • 第 43–46 行:使用 JPA API 的 `persist` 方法将人员对象持久化。该方法使人员对象成为持久对象。
  • 第 49–51 行:使用 JPA API 的 `merge` 方法更新人员。只有当被更新的人员此前处于脱离状态时,此方法才有意义。该方法将以此方式创建的人员设为持久化。
  • 第 16–22 行:删除作为参数传递的 ID 对应的人员分为两个步骤:
    • 第 17 行:在持久化上下文中查找该 Person
    • 第 18–20 行:若未找到,则抛出错误代码为 2 的异常
    • 第 21 行:若找到该对象,则使用 JPA API 的 remove 方法将其从持久化上下文中移除。
  • 此时尚未体现的是,每个方法都将在由 [service] 层启动的事务中执行。

该应用程序拥有一个名为 [DaoException] 的自定义异常类型:


package dao;
 
@SuppressWarnings("serial")
public class DaoException extends RuntimeException {
 
    // error code
    private int code;
 
    public DaoException(int code) {
        super();
        this.code = code;
    }
 
    public DaoException(String message, int code) {
        super(message);
        this.code = code;
    }
 
    public DaoException(Throwable cause, int code) {
        super(cause);
        this.code = code;
    }
 
    public DaoException(String message, Throwable cause, int code) {
        super(message, cause);
        this.code = code;
    }
 
    // getter and setter
 
    public int getCode() {
        return code;
    }
 
    public void setCode(int code) {
        this.code = code;
    }
 
}
  • 第 4 行:[DaoException] 继承自 [RuntimeException]。因此,它属于编译器不要求我们使用 try/catch 代码块进行处理,也不要求在方法签名中包含的异常类型。 正因如此,[DaoException] 并未包含在 [IDao] 接口的 [deleteOne] 方法签名中。这使得该接口可以由抛出不同类型异常的类来实现,前提是该类也继承自 [RuntimeException]。
  • 为了区分可能发生的错误,我们在第 7 行使用了错误代码。第 14、19 和 24 行中的三个构造函数是父类 [RuntimeException] 的构造函数,我们向其中添加了一个参数:即我们要分配给该异常的错误代码。

3.1.4. [business/ 服务]层

[服务]层提供了以下[IService]接口:


package service;
 
import java.util.List;
 
import entites.Personne;
 
public interface IService {
    // find a person via his/her login
    public Personne getOne(Integer id);
 
    // get all the people
    public List<Personne> getAll();
 
    // save a person
    public Personne saveOne(Personne personne);
 
    // update a person
    public Personne updateOne(Personne personne);
 
    // delete a person via his/her login
    public void deleteOne(Integer id);
 
    // get people whose names match a model
    public List<Personne> getAllLike(String modele);
 
    // delete several people at once
    public void deleteArray(Personne[] personnes);
 
    // save several people at once
    public Personne[] saveArray(Personne[] personnes);
 
    // update several people at once
    public Personne[] updateArray(Personne[] personnes);
 
}
  • 第 8–24 行:[IService] 接口继承了 [IDao] 接口中的方法
  • 第 27 行:[deleteArray] 方法允许您在事务内删除一组人员:要么全部删除,要么一个都不删除。
  • 第 30 行和第 33 行:与 [deleteArray] 类似的方法,用于在事务内保存(第 30 行)或更新(第 33 行)一组人员。

[IService] 接口的 [Service] 实现如下:


package service;
 
...
 
// all class methods take place in a transaction
@Transactional
public class Service implements IService {
 
    // layer [dao]
    private IDao dao;
 
    public IDao getDao() {
        return dao;
    }
 
    public void setDao(IDao dao) {
        this.dao = dao;
    }
 
    // delete several people at once
    public void deleteArray(Personne[] personnes) {
        for (Personne p : personnes) {
            dao.deleteOne(p.getId());
        }
    }
 
    // delete a person via his/her login
    public void deleteOne(Integer id) {
        dao.deleteOne(id);
    }
 
    // get all the people
    public List<Personne> getAll() {
        return dao.getAll();
    }
 
    // get people whose names match a model
    public List<Personne> getAllLike(String modele) {
        return dao.getAllLike(modele);
    }
 
    // find a person via his/her login
    public Personne getOne(Integer id) {
        return dao.getOne(id);
    }
 
    // save several people at once
    public Personne[] saveArray(Personne[] personnes) {
        Personne[] personnes2 = new Personne[personnes.length];
        for (int i = 0; i < personnes.length; i++) {
            personnes2[i] = dao.saveOne(personnes[i]);
        }
        return personnes2;
    }
 
    // save a person
    public Personne saveOne(Personne personne) {
        return dao.saveOne(personne);
    }
 
    // update several people at once
    public Personne[] updateArray(Personne[] personnes) {
        Personne[] personnes2 = new Personne[personnes.length];
        for (int i = 0; i < personnes.length; i++) {
            personnes2[i] = dao.updateOne(personnes[i]);
        }
        return personnes2;
    }
 
    // update a person
    public Personne updateOne(Personne personne) {
        return dao.updateOne(personne);
    }
 
}
  • 第 6 行:Spring 的 @Transactional 注解表明该类中的所有方法都必须在事务范围内执行。事务将在方法开始执行前启动,并在执行结束后关闭。如果方法执行过程中发生了 [RuntimeException] 类型或其子类的异常,自动回滚将取消整个事务;否则,自动提交将确认该事务。请注意,Java 代码无需关注事务,它们由 Spring 负责管理。
  • 第 10 行:对 [dao] 层的引用。稍后我们将看到,该引用由 Spring 在应用程序启动时初始化。
  • [Service] 的方法仅调用第 10 行中 [IDao dao] 接口的方法。具体代码请读者自行查阅,其中并无特别难点。
  • 我们之前提到过,[Service] 的每个方法都在事务中运行。该事务与方法的执行线程相关联。在该线程内,[dao] 层的方法会被执行,这些方法将自动加入执行线程的事务中。例如,[deleteArray] 方法(第 21 行)需要 N 次调用 [dao] 层的 [deleteOne] 方法。 这 N 次调用将在 [deleteArray] 方法的执行线程内进行,因此处于同一事务中。因此,如果一切顺利,它们将全部提交;如果 [dao] 层中 [deleteOne] 方法的 N 次调用中的任何一次发生异常,则所有操作都将回滚。

3.1.5. 层级配置

[service]、[dao] 和 [JPA] 层的配置由上述两个文件处理:[META-INF/persistence.xml] 和 [spring-config.xml]。这两个文件必须位于应用程序的类路径中,因此它们被放置在 Eclipse 项目的 [src] 文件夹中。文件名 [spring-config.xml] 是任意的。

persistence.xml


<?xml version="1.0" encoding="UTF-8"?>
<persistence version="1.0" xmlns="http://java.sun.com/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_1_0.xsd">
    <persistence-unit name="jpa" transaction-type="RESOURCE_LOCAL" />
</persistence>
  • 第 4 行:该文件声明了一个名为 jpa 的持久化单元,它使用“本地”事务,即非由 EJB3 容器提供的事务。这些事务由 Spring 创建和管理,并在 [spring-config.xml] 文件中进行配置。

spring-config.xml


<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:tx="http://www.springframework.org/schema/tx"
    xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.0.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-2.0.xsd">
 
    <!-- application layers -->
    <bean id="dao" class="dao.Dao" />
    <bean id="service" class="service.Service">
        <property name="dao" ref="dao" />
    </bean>
 
    <!-- persistence layer JPA -->
    <bean id="entityManagerFactory"
        class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
        <property name="dataSource" ref="dataSource" />
        <property name="jpaVendorAdapter">
            <bean
                class="org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter">
                <!-- 
                    <property name="showSql" value="true" />
                -->
                <property name="databasePlatform"
                    value="org.hibernate.dialect.MySQL5InnoDBDialect" />
                <property name="generateDdl" value="true" />
            </bean>
        </property>
        <property name="loadTimeWeaver">
            <bean
                class="org.springframework.instrument.classloading.InstrumentationLoadTimeWeaver" />
        </property>
    </bean>
 
    <!-- data source DBCP -->
    <bean id="dataSource"
        class="org.apache.commons.dbcp.BasicDataSource"
        destroy-method="close">
        <property name="driverClassName" value="com.mysql.jdbc.Driver" />
        <property name="url" value="jdbc:mysql://localhost:3306/jpa" />
        <property name="username" value="jpa" />
        <property name="password" value="jpa" />
    </bean>
 
    <!-- transaction manager -->
    <tx:annotation-driven transaction-manager="txManager" />
    <bean id="txManager"
        class="org.springframework.orm.jpa.JpaTransactionManager">
        <property name="entityManagerFactory"
            ref="entityManagerFactory" />
    </bean>
 
    <!-- translation of exceptions -->
    <bean
        class="org.springframework.dao.annotation.PersistenceExceptionTranslationPostProcessor" />
 
    <!-- persistence annotations -->
    <bean
        class="org.springframework.orm.jpa.support.PersistenceAnnotationBeanPostProcessor" />
 
</beans>
  • 第 2-5 行:配置文件的根 <beans> 标签。我们在此不详细说明该标签的各项属性。请务必仔细复制粘贴,因为这些属性中的任何一个出现错误都可能导致难以排查的错误。
  • 第 8 行:"dao" Bean 指向 [dao.Dao] 类的实例。将创建一个单一实例(单例),并实现应用程序的 [dao] 层。
  • 第 9–11 行:[service] 层的实例化。“service” Bean 指向 [service.Service] 类的实例。将创建一个单一实例(单例),并实现应用程序的 [service] 层。 我们看到 [service.Service] 类有一个私有字段 [IDao dao]。该字段在第 10 行由第 8 行定义的 "dao" Bean 进行初始化。
  • 最终,第 8–11 行配置了 [dao] 和 [service] 层。我们稍后将看到它们何时以及如何被实例化。
  • 第 35–42 行:定义了一个数据源。在学习使用 Hibernate 处理 JPA 实体时,我们已经接触过数据源的概念:

上文中,[c3p0] 被称为“连接池”,其实也可以称为“数据源”。 数据源提供“连接池”服务。在 Spring 中,我们将使用 [c3p0] 以外的数据源,即来自 Apache Commons DBCP 项目 [http://jakarta.apache.org/commons/dbcp/] 的 [DBCP]。已将 [DBCP] 归档文件放置在 [jpa-spring] 用户库中:

 
  • 第 38–41 行:为了与目标数据库建立连接,数据源需要知道所使用的 JDBC 驱动程序(第 38 行)、数据库 URL(第 39 行)、连接用户名及其密码(第 40–41 行)。
  • 第 14–32 行:配置 JPA 层
  • 第 14–15 行:定义一个 [EntityManagerFactory] Bean,该 Bean 能够创建 [EntityManager] 对象以管理持久化上下文。实例化的类 [LocalContainerEntityManagerFactoryBean] 由 Spring 提供。其实例化需要若干参数,这些参数在第 16–31 行中定义。
  • 第 16 行:用于获取 DBMS 连接的数据源。这是第 35–42 行中定义的 [DBCP] 源。
  • 第 17–27 行:要使用的 JPA 实现
  • 第 18–26 行:定义 Hibernate(第 19 行)作为要使用的 JPA 实现
  • 第 23–24 行:Hibernate 必须与目标 DBMS 配合使用的 SQL 方言,本例中为 MySQL5。
  • 第 25 行:要求在应用程序启动时生成数据库(删除并创建)。
  • 第 28–31 行:定义一个“类加载器”。我无法清晰解释 JPA 层中 EntityManagerFactory 所使用的这个 Bean 的作用。不过,它涉及向运行应用程序的 JVM 传递一个归档文件的名称,该归档文件的内容将在应用程序启动时管理类加载。 此处,该归档文件为 [spring-agent.jar],位于 [jpa-spring] 用户库中(见上文)。我们将看到,Hibernate 不需要此代理,但 Toplink 需要。
  • 第 45–50 行:定义要使用的事务管理器
  • 第 45 行:表明事务将通过 Java 注解进行管理(也可以在 spring-config.xml 中声明)。具体而言,这指的是 [Service] 类(第 6 行)中的 @Transactional 注解。
  • 第 46–50 行:事务管理器
  • 第 47 行:事务管理器是由 Spring 提供的类
  • 第 48–49 行:Spring 的事务管理器需要知道管理 JPA 层的 EntityManagerFactory。这就是在第 14–32 行中定义的那个。
  • 第 57–58 行:定义管理 Java 代码中 Spring 持久化注解的类,例如 [dao.Dao] 类中的 @PersistenceContext 注解(第 12 行)。
  • 第 53–54 行:定义 Spring 类,该类专门管理 @Repository 注解。该注解使被标注的类能够将 DBMS 的 JDBC 驱动程序中的原生异常转换为 [DataAccessException] 类型的通用 Spring 异常。这种转换将原生 JDBC 异常封装在 [DataAccessException] 类型中,该类型包含多种子类:

Image

这种转换使客户端程序能够通用的方式处理异常,而无需考虑目标 DBMS。我们在 Java 代码中并未使用 @Repository 注解。因此,第 53–54 行是多余的。我们保留它们仅供参考。

至此,Spring 配置文件的配置工作已完成。该配置较为复杂,且许多方面仍不明确。此配置摘自 Spring 文档。幸运的是,将其适配到各种场景中通常只需进行两处修改:

  • 目标数据库:第 38–41 行。我们将提供一个 Oracle 示例。
  • JPA 实现:第 14–32 行。我们将提供一个 TopLink 示例。

3.1.6. 客户端程序 [ InitDB]

现在,我们将为上述架构编写第一个客户端:

[InitDB] 的代码如下:


package tests;
 
...
public class InitDB {
 
    // service layer
    private static IService service;
 
    // manufacturer
    public static void main(String[] args) throws ParseException {
        // application configuration
        ApplicationContext ctx = new ClassPathXmlApplicationContext("spring-config.xml");
        // service layer
        service = (IService) ctx.getBean("service");
        // empty the base
        clean();
        // fill it
        fill();
        // a visual check
        dumpPersonnes();
    }
 
    // table content display
    private static void dumpPersonnes() {
        System.out.format("[personnes]%n");
        for (Personne p : service.getAll()) {
            System.out.println(p);
        }
    }
 
    // table filling
    public static void fill() throws ParseException {
        // creating people
        Personne p1 = new Personne("p1", "Paul", new SimpleDateFormat("dd/MM/yy").parse("31/01/2000"), true, 2);
        Personne p2 = new Personne("p2", "Sylvie", new SimpleDateFormat("dd/MM/yy").parse("05/07/2001"), false, 0);
        // we save
        service.saveArray(new Personne[] { p1, p2 });
    }

    // deleting table items
    public static void clean() {
        for (Personne p : service.getAll()) {
            service.deleteOne(p.getId());
        }
    }
}
  • 第 12 行:使用 [spring-config.xml] 文件创建 [ApplicationContext ctx] 对象,该对象是该文件的内存表示。此时,[spring-config.xml] 中定义的 Bean 会被实例化。
  • 第 14 行:向应用上下文 ctx 请求 [service] 层的引用。我们知道该层由一个名为“service”的 Bean 表示。
  • 第 16 行:使用 clean 方法清空数据库(第 41–45 行):
    • 第 42–44 行:我们从持久化上下文中获取所有用户的列表,并循环遍历以逐一删除它们。您可能还记得,[spring-config.xml] 指定在应用程序启动时必须生成数据库。因此,在我们的情况下,由于初始数据库为空,调用 `clean` 方法是多余的。
  • 第 18 行:`fill` 方法用于填充数据库。该方法在第 32–38 行中定义:
    • 第 34–35 行:创建了两个人
    • 第 37 行:请求 [service] 层将它们持久化。
  • 第 20 行:`dumpPersonnes` 方法用于显示持久化的人员。该方法定义在第 24–29 行
    • 第 26–28 行:我们向 [service] 层请求所有持久化人员的列表,并在控制台上显示它们。

运行 [InitDB] 会产生以下结果:

1
2
3
[personnes]
[72,0,p1,Paul,31/01/2000,true,2]
[73,0,p2,Sylvie,05/07/2001,false,0]

3.1.7. 单元测试 [ TestNG]

第5.2.4节介绍了[TestNG]插件的安装方法。[TestNG]程序代码如下:


package tests;
 
....
public class TestNG {
 
    // service layer
    private IService service;
 
    @BeforeClass
    public void init() {
        // log
        log("init");
        // application configuration
        ApplicationContext ctx = new ClassPathXmlApplicationContext("spring-config.xml");
        // service layer
        service = (IService) ctx.getBean("service");
    }
 
    @BeforeMethod
    public void setUp() throws ParseException {
        // empty the base
        clean();
        // fill it
        fill();
    }
 
    // logs
    private void log(String message) {
        System.out.println("----------- " + message);
    }
 
    // table content display
    private void dump() {
        log("dump");
        System.out.format("[personnes]%n");
        for (Personne p : service.getAll()) {
            System.out.println(p);
        }
    }
 
    // table filling
    public void fill() throws ParseException {
        log("fill");
        // creating people
        Personne p1 = new Personne("p1", "Paul", new SimpleDateFormat("dd/MM/yy").parse("31/01/2000"), true, 2);
        Personne p2 = new Personne("p2", "Sylvie", new SimpleDateFormat("dd/MM/yy").parse("05/07/2001"), false, 0);
        // we save
        service.saveArray(new Personne[] { p1, p2 });
    }
 
    // deleting table items
    public void clean() {
        log("clean");
        for (Personne p : service.getAll()) {
            service.deleteOne(p.getId());
        }
    }
 
    @Test()
    public void test01() {
...
    }
...
}
  • 第 9 行:@BeforeClass 注解指定了用于初始化测试所需配置的方法。该方法在第一个测试运行之前执行。@AfterClass 注解(此处未使用)指定了在所有测试运行完毕后执行的方法。
  • 第 10–17 行:带有 @BeforeClass 注解的 init 方法使用 Spring 配置文件来实例化应用程序的各个层,并获取 [service] 层的引用。随后所有测试都将使用此引用。
  • 第 19 行:@BeforeMethod 注解指定在每个测试执行前运行的方法。@AfterMethod 注解(此处未使用)指定在每个测试执行后运行的方法。
  • 第 20–25 行:带有 @BeforeMethod 注解的 setUp 方法会清空数据库(清空操作见第 52–56 行),然后向其中插入两名人员(填充操作见第 42–49 行)。
  • 第 59 行:@Test 注解指定了一个待执行的测试方法。接下来我们将描述这些测试。

@Test()
    public void test01() {
        log("test1");
        dump();
        // list of persons
        List<Personne> personnes = service.getAll();
        assert 2 == personnes.size();
    }
 
    @Test()
    public void test02() {
        log("test2");
        // search for people by name
        List<Personne> personnes = service.getAllLike("p1%");
        assert 1 == personnes.size();
        Personne p1 = personnes.get(0);
        assert "Paul".equals(p1.getPrenom());
    }
 
    @Test()
    public void test03() throws ParseException {
        log("test3");
        // create a new person
        Personne p3 = new Personne("p3", "x", new SimpleDateFormat("dd/MM/yy").parse("05/07/2001"), false, 0);
        // we keep it
        service.saveOne(p3);
        // we ask for it again
        Personne loadedp3 = service.getOne(p3.getId());
        // we display it
        System.out.println(loadedp3);
        // check
        assert "p3".equals(loadedp3.getNom());
    }
  • 第 2–8 行:测试 01。请注意,在每个测试开始时,数据库中包含两名名为 p1 p2 的人员。
  • 第 6 行:我们请求人员列表
  • 第 7 行:我们验证返回列表中的人数是否为 2
  • 第 14 行:我们查询姓氏以 p1 开头的人员列表
  • 我们验证结果列表仅包含一个元素(第 15 行),且找到的该人的名字是“Paul”(第 17 行)
  • 第 24 行:创建一名名为 p3 的人员
  • 第 25 行:将其持久化
  • 第 28 行:从持久化上下文中检索该人以进行验证
  • 第 32 行:我们验证检索到的人员确实名为 p3

@Test()
    public void test04() throws ParseException {
        log("test4");
        // we load person p1
        List<Personne> personnes = service.getAllLike("p1%");
        Personne p1 = personnes.get(0);
        // we display it
        System.out.println(p1);
        // we check
        assert "p1".equals(p1.getNom());
        int version1 = p1.getVersion();
        // change the first name
        p1.setPrenom("x");
        // we save
        service.updateOne(p1);
        // recharge
        p1 = service.getOne(p1.getId());
        // we display it
        System.out.println(p1);
        // check that the version has been incremented
        assert (version1 + 1) == p1.getVersion();
 
    }
  • 第 5 行:我们获取人员 p1
  • 第 10 行:检查其姓名
  • 第 11 行:记录其版本号
  • 第13行:修改其名字
  • 第 15 行:保存更改
  • 第 17 行:再次请求用户 p1
  • 第 21 行:检查版本号是否增加了 1

@Test()
    public void test05() {
        log("test5");
        // we load person p2
        List<Personne> personnes = service.getAllLike("p2%");
        Personne p2 = personnes.get(0);
        // we display it
        System.out.println(p2);
        // we check
        assert "p2".equals(p2.getNom());
        // delete person p2
        service.deleteOne(p2.getId());
        // recharge it
        p2 = service.getOne(p2.getId());
        // check that a null pointer has been obtained
        assert null == p2;
        // table is displayed
        dump();
    }
  • 第 5 行:我们查询人员 p2
  • 第 10 行:检查其姓名
  • 第12行:删除该用户
  • 第14行:再次查询该用户
  • 第16行:我们检查是否未找到该用户

@Test()
    public void test06() throws ParseException {
        log("test6");
        // on crée un tableau de 2 personnes de même nom (enfreint la règle d'unicité du nom)
        Personne[] personnes = { new Personne("p3", "x", new SimpleDateFormat("dd/MM/yy").parse("31/01/2000"), true, 2),
                new Personne("p4", "x", new SimpleDateFormat("dd/MM/yy").parse("31/01/2000"), true, 2),
                new Personne("p4", "x", new SimpleDateFormat("dd/MM/yy").parse("31/01/2000"), true, 2)};
        // on sauvegarde ce tableau - on doit obtenir une exception et un rollback
        boolean erreur = false;
        try {
            service.saveArray(personnes);
        } catch (RuntimeException e) {
            erreur = true;
        }
        // dump
        dump();
        // vérifications
        assert erreur;
        // recherche personne de nom p3
        List<Personne> personnesp3 = service.getAllLike("p3%");
        assert 0 == personnesp3.size();
        // dump
        dump();
    }
  • 第 5 行:我们创建了一个包含三人的数组,其中两人名字相同,均为“p4”。这违反了 @Entity Person 实体中关于名字唯一性的规则:

    @Column(name = "NOM", length = 30, nullable = false, unique = true)
private String nom;
  • 第 11 行:将包含三人的数组放入持久化上下文中。此时添加第二个人 p4 应该会失败。由于 [saveArray] 方法是在事务内运行的,因此之前进行的任何插入操作都会被回滚。最终,不会有任何数据被添加。
  • 第 18 行:我们验证 [saveArray] 是否确实抛出了异常
  • 第 20–21 行:我们验证本应被添加的人员 p3 并未被添加。

@Test()
    public void test07() {
        log("test7");
        // test optimistic locking
        // we load person p1
        List<Personne> personnes = service.getAllLike("p1%");
        Personne p1 = personnes.get(0);
        // we display it
        System.out.println(p1);
        // increase the number of children
        int nbEnfants1 = p1.getNbenfants();
        p1.setNbenfants(nbEnfants1 + 1);
        // save p1
        Personne newp1 = service.updateOne(p1);
        assert (nbEnfants1 + 1) == newp1.getNbenfants();
        System.out.println(newp1);
        // we save a second time - we should have an exception because p1 no longer has the correct version
        // newp1 has it
        boolean erreur = false;
        try {
            service.updateOne(p1);
        } catch (RuntimeException e) {
            erreur = true;
        }
        // check
        assert erreur;
        // we increase the number of newp1 children
        int nbEnfants2 = newp1.getNbenfants();
        newp1.setNbenfants(nbEnfants2 + 1);
        // save newp1
        service.updateOne(newp1);
        // recharge
        p1 = service.getOne(p1.getId());
        // we check
        assert (nbEnfants1 + 2) == p1.getNbenfants();
        System.out.println(p1);
    }
  • 第 6 行:请求人员 p1
  • 第 12 行:我们将子女的数量增加 1
  • 第 14 行:我们在持久化上下文中更新人员 p1。[updateOne] 方法将新版本 newp1 p1 持久化。它与 p1 的区别在于其版本号,该版本号必须已递增。
  • 第 15 行:检查 newp1 的子节点数量。
  • 第 21 行:我们基于旧版本 p1 请求更新人员 p1。此时应抛出异常,因为 p1 并非人员 p1 的最新版本。最新版本是 newp1
  • 第 23 行:我们验证错误是否确实发生
  • 第 27–35 行:我们验证如果从最新版本 newp1 进行更新,则一切运行正常。

@Test()
    public void test08() {
        log("test8");
        // test rollback on updateArray
        // we load person p1
        List<Personne> personnes = service.getAllLike("p1%");
        Personne p1 = personnes.get(0);
        // we display it
        System.out.println(p1);
        // increase the number of children
        int nbEnfants1 = p1.getNbenfants();
        p1.setNbenfants(nbEnfants1 + 1);
        // save 2 modifications, the 2nd of which must fail (person incorrectly initialized)
        // because of the transaction, both must be cancelled
        boolean erreur = false;
        try {
            service.updateArray(new Personne[] { p1, new Personne() });
        } catch (RuntimeException e) {
            erreur = true;
        }
        // checks
        assert erreur;
        // we recharge person p1
        personnes = service.getAllLike("p1%");
        p1 = personnes.get(0);
        // her number of children must not have changed
        assert nbEnfants1 == p1.getNbenfants();
    }
  • 测试 8 与测试 6 类似:它检查对一个包含两人的数组执行 `updateArray` 操作时的回滚情况,其中第二个人尚未被正确初始化。从 JPA 的角度来看,对第二个人(该人尚未存在)执行的 `merge` 操作将生成一个 `insert` SQL 语句,该语句会因 `Person` 实体某些字段的 `nullable=false` 约束而失败。

@Test()
    public void test09() {
        log("test9");
        // test rollback on deleteArray
        // dump
        dump();
        // we load person p1
        List<Personne> personnes = service.getAllLike("p1%");
        Personne p1 = personnes.get(0);
        // we display it
        System.out.println(p1);
        // we make 2 deletions, the 2nd of which must fail (unknown person)
        // because of the transaction, both must be cancelled
        boolean erreur = false;
        try {
            service.deleteArray(new Personne[] { p1, new Personne() });
        } catch (RuntimeException e) {
            erreur = true;
        }
        // checks
        assert erreur;
        // we recharge person p1
        personnes = service.getAllLike("p1%");
        // check
        assert 1 == personnes.size();
        // dump
        dump();
    }
  • 测试 9 与前一个类似:它检查在包含两个人的数组上执行 `deleteArray` 操作时的回滚情况,其中第二个人并不存在。不过,在这种情况下,`[dao]` 层中的 `[deleteOne]` 方法会抛出异常。

// optimistic locking - multi-threaded access
    @Test()
    public void test10() throws Exception {
        // add a person
        Personne p3 = new Personne("X", "X", new SimpleDateFormat("dd/MM/yyyy").parse("01/02/2006"), true, 0);
        service.saveOne(p3);
        int id3 = p3.getId();
        // creation of N threads for updating the number of children
        final int N = 20;
        Thread[] taches = new Thread[N];
        for (int i = 0; i < taches.length; i++) {
            taches[i] = new ThreadMajEnfants("thread n° " + i, service, id3);
            taches[i].start();
        }
        // we wait for the end of threads
        for (int i = 0; i < taches.length; i++) {
            taches[i].join();
        }
        // we pick up the person
        p3 = service.getOne(id3);
        // she must have N children
        assert N == p3.getNbenfants();
        // delete person p3
        service.deleteOne(p3.getId());
        // check
        p3 = service.getOne(p3.getId());
        // we must have a null pointer
        assert p3 == null;
    }
  • 测试 10 的设计思路是启动 N 个线程(第 9 行),以并行方式增加某人的子女数量。我们希望验证版本号系统能否处理这种情况。该系统正是为此目的而设计的。
  • 第 5–6 行:创建并持久化了一个名为 p3 的人。该人初始时有 0 个子女。
  • 第 7 行:记录其标识符。
  • 第 9–14 行:我们并行启动 N 个线程,每个线程的任务是将 p3 的子女数量增加 1。
  • 第 16–18 行:等待所有线程完成
  • 第 20 行:我们查询人员 p3
  • 第 22 行:我们验证 p3 现在是否拥有 N 个子节点
  • 第 24 行:删除 p3

[ThreadMajEnfants] 线程如下:


package tests;
 
...
public class ThreadMajEnfants extends Thread {
    // thread name
    private String name;
 
    // reference on the [service] layer
    private IService service;
 
    // the id of the person we're going to work on
    private int idPersonne;
 
    // manufacturer
    public ThreadMajEnfants(String name, IService service, int idPersonne) {
        this.name = name;
        this.service = service;
        this.idPersonne = idPersonne;
    }
 
    // thread core
    public void run() {
        // follow-up
        suivi("lancé");
        // we loop until we have succeeded in incrementing by 1
        // person's number of children idPersonne
        boolean fini = false;
        int nbEnfants = 0;
        while (!fini) {
            // a copy of the idPersonne person is retrieved
            Personne personne = service.getOne(idPersonne);
            nbEnfants = personne.getNbenfants();
            // follow-up
            suivi("" + nbEnfants + " -> " + (nbEnfants + 1) + " pour la version " + personne.getVersion());
            // increments the number of children by 1
            personne.setNbenfants(nbEnfants + 1);
            // 10 ms wait to abandon processor
            try {
                // follow-up
                suivi("début attente");
                // we pause to let the processor
                Thread.sleep(10);
                // follow-up
                suivi("fin attente");
            } catch (Exception ex) {
                throw new RuntimeException(ex.toString());
            }
            // waiting complete - try to validate the copy
            // in the meantime, other threads may have modified the original
            try {
                // we try to modify the original
                service.updateOne(personne);
                // we passed - the original has been modified
                fini = true;
            } catch (javax.persistence.OptimisticLockException e) {
                // incorrect object version: exception ignored to start again
            } catch (org.springframework.transaction.UnexpectedRollbackException e2) {
                // with the occasional Spring exception
            } catch (RuntimeException e3) {
                // another type of exception - it is reassembled
                throw e3;
            }
        }
        // follow-up
        suivi("a terminé et passé le nombre d'enfants à " + (nbEnfants + 1));
    }
 
    // follow-up
    private void suivi(String message) {
        System.out.println(name + " [" + new Date().getTime() + "] : " + message);
    }
}
  • 第 15–19 行:构造函数存储其运行所需的信息:名称(第 16 行)、必须使用的 [service] 层引用(第 17 行),以及需要增加其子女数量的人员 p 的标识符(第 18 行)。
  • 第 22–66 行:由所有线程并行执行的 [run] 方法。
  • 第 29 行:线程反复尝试增加人物 p 的子女数量。只有成功时才会停止。
  • 第 31 行:查询人员 p
  • 第 36 行:在内存中增加其子女数
  • 第 38–47 行:暂停 10 毫秒。这将允许其他线程获取同一个人 p 的版本。因此,此时会有多个线程持有同一个人 p 的版本并试图修改它。这是期望的行为。
  • 第 52 行:暂停结束后,线程请求 [服务] 层将更改持久化。我们知道偶尔会发生异常,因此将该操作包裹在 try/catch 块中。
  • 第 55 行:测试表明我们会遇到 [javax.persistence.OptimisticLockException] 类型的异常。这是正常的:当线程在未获取 person p 的最新版本时尝试修改它,JPA 层会抛出此异常。该异常被忽略,以便线程可以重试操作直至成功。
  • 第 57 行:测试显示我们还会遇到 [org.springframework.transaction.UnexpectedRollbackException] 类型的异常。这令人困扰且出乎意料。 我 对此无法解释。尽管我们原本希望避免,但现在却不得不依赖 Spring。这意味着,如果我们在 JBoss EJB3 中运行应用程序,则需要修改线程的代码。此处同样忽略了 Spring 异常,以便线程能够重试递增操作。
  • 第 59 行:其他类型的异常会被传播到应用程序中。

运行 [TestNG] 时,我们得到以下结果:

Image

全部 10 个测试均成功通过。

测试 10 值得进一步说明,因为它能通过测试这件事本身带有某种神奇的色彩。让我们先回顾一下 [dao] 层的配置:


public class Dao implements IDao {
 
    @PersistenceContext
    private EntityManager em;
 
  • 第 4 行:使用 JPA 的 @PersistenceContext 注解将一个 [EntityManager] 对象注入到 em 字段中。 [dao] 层仅实例化一次。它是一个单例,供所有使用 JPA 层线程共享。因此,所有线程共享同一个 EntityManager em。这可以通过查看 [ThreadMajEnfants] 线程所调用的 [updateOne] 方法中 em 的值来验证:所有线程的该值均相同。

因此,人们可能会质疑:由 EntityManager em(对所有线程而言都是相同的)管理的不同线程的持久化对象,是否会相互混淆并引发冲突。在 ThreadMajEnfants 中可以找到可能发生的情况的一个示例:


        while (!fini) {
            // on récupère une copie de la personne d'idPersonne
            Personne personne = service.getOne(idPersonne);
            nbEnfants = personne.getNbenfants();
            // suivi
            suivi("" + nbEnfants + " -> " + (nbEnfants + 1) + " pour la version " + personne.getVersion());
            // incrémente de 1 le nbre d'enfants de la personne
            personne.setNbenfants(nbEnfants + 1);
            // attente de 10 ms pour abandonner le processeur
            try {
                // suivi
                suivi("début attente");
                // on s'interrompt pour laisser le processeur
                Thread.sleep(10);
                // suivi
                suivi("fin attente");
            } catch (Exception ex) {
                throw new RuntimeException(ex.toString());
}
  • 第 3 行:线程 T1 获取人 p
  • 第 8 行:它将 p 的子节点数量加 1
  • 第14行:线程T1暂停

线程 T2 接管并同样执行第 3 行代码:它请求与 T1 相同的 person p。如果这两个线程的持久化上下文相同,那么 person p(由于 T1 的操作,它已经存在于上下文中)应该会被返回给 T2。 事实上,[getOne] 方法使用了 JPA API 的 [EntityManager].find 方法,该方法仅在请求的对象不属于持久化上下文时才会访问数据库;否则,它会从持久化上下文中返回该对象。如果情况确实如此,T1 和 T2 将持有同一个 person p。此时 T2 会再次将 p 的子女数量增加 1(第 8 行)。 如果某个线程在暂停后成功更新了数据,那么 p 的子女数量就会增加 2,而不是预期的 1。 此时人们可能会预期,这 N 个线程将把 p 的子女数量设置为高于 N 的值。然而,实际情况并非如此。因此我们可以得出结论:T1 和 T2 持有的 p 并非同一引用。我们通过让各线程显示 p 的内存地址来验证这一点:它们各自显示的地址各不相同。

因此,似乎这些线程:

  • 共享同一个持久化上下文管理器(EntityManager)
  • 但各自拥有独立的持久化上下文。

这些只是推测,此处若能得到专家的意见将不胜感激。

3.1.8. 更换数据库管理系统

要更改数据库管理系统(DBMS),只需将 [src/spring-config.xml] 文件 [2] 替换为 [conf] 文件夹 [1] 中对应数据库管理系统的 [spring-config.xml] 文件即可。

例如,Oracle 的 [spring-config.xml] 文件如下所示:


<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:tx="http://www.springframework.org/schema/tx"
    xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.0.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-2.0.xsd">
 
...
    <bean id="entityManagerFactory" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
        <property name="dataSource" ref="dataSource" />
        <property name="jpaVendorAdapter">
            <bean class="org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter">
                <!-- 
                    <property name="showSql" value="true" />
                -->
                <property name="databasePlatform" value="org.hibernate.dialect.OracleDialect" />
                <property name="generateDdl" value="true" />
            </bean>
        </property>
        <property name="loadTimeWeaver">
            <bean class="org.springframework.instrument.classloading.InstrumentationLoadTimeWeaver" />
        </property>
    </bean>
 
    <!-- data source DBCP -->
    <bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
        <property name="driverClassName" value="oracle.jdbc.OracleDriver" />
        <property name="url" value="jdbc:oracle:thin:@localhost:1521:xe" />
        <property name="username" value="jpa" />
        <property name="password" value="jpa" />
    </bean>
...
</beans>

与之前用于 MySQL5 的同一文件相比,仅更改了寥寥数行:

  • 第 14 行:Hibernate 必须使用的 SQL 方言
  • 第25–28行:连接数据库管理系统(DBMS)的JDBC连接属性

建议读者使用其他数据库管理系统(DBMS)重复本文中针对 MySQL 5 描述的测试。

3.1.9. 更改 JPA 实现

让我们回到前几个测试的架构:

我们将 JPA/Hibernate 实现替换为 JPA/TopLink 实现。由于 TopLink 使用的库与 Hibernate 不相同,因此我们创建了一个新的 Eclipse 项目:

  • 在 [1] 中:Eclipse 项目。它与之前的完全相同。唯一的改动是配置文件 [spring-config.xml] [2] 以及 [jpa-toplink] 库,它替换了 [jpa-hibernate] 库。
  • 在 [3] 中:本教程的 examples 文件夹。在 [4] 中,待导入的 Eclipse 项目。

Toplink 的配置文件 [spring-config.xml] 如下所示:


<?xml version="1.0" encoding="UTF-8"?>
 
<!-- the JVM must be launched with the -javaagent:C:\data\2006-2007\eclipse\dvp-jpa\lib\spring\spring-agent.jar argument 
    (à remplacer par le chemin exact de spring-agent.jar)-->
 
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:tx="http://www.springframework.org/schema/tx"
    xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.0.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-2.0.xsd">
 
    <!-- application layers -->
    <bean id="dao" class="dao.Dao" />
    <bean id="service" class="service.Service">
        <property name="dao" ref="dao" />
    </bean>
 
    <bean id="entityManagerFactory" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
        <property name="dataSource" ref="dataSource" />
        <property name="jpaVendorAdapter">
            <bean class="org.springframework.orm.jpa.vendor.TopLinkJpaVendorAdapter">
                <!-- 
                    <property name="showSql" value="true" />
                -->
                <property name="databasePlatform" value="oracle.toplink.essentials.platform.database.MySQL4Platform" />
                <property name="generateDdl" value="true" />
            </bean>
        </property>
        <property name="loadTimeWeaver">
            <bean class="org.springframework.instrument.classloading.InstrumentationLoadTimeWeaver" />
        </property>
    </bean>
 
    <!-- data source DBCP -->
    <bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
        <property name="driverClassName" value="com.mysql.jdbc.Driver" />
        <property name="url" value="jdbc:mysql://localhost:3306/jpa" />
        <property name="username" value="jpa" />
        <property name="password" value="jpa" />
    </bean>
 
    <!-- transaction manager -->
    <tx:annotation-driven transaction-manager="txManager" />
    <bean id="txManager" class="org.springframework.orm.jpa.JpaTransactionManager">
        <property name="entityManagerFactory" ref="entityManagerFactory" />
    </bean>
 
    <!-- translation of exceptions -->
    <bean class="org.springframework.dao.annotation.PersistenceExceptionTranslationPostProcessor" />
 
    <!-- persistence -->
    <bean class="org.springframework.orm.jpa.support.PersistenceAnnotationBeanPostProcessor" />
 
</beans>

只需修改几行代码即可从 Hibernate 切换到 Toplink:

  • 第 19 行:JPA 的实现现在由 Toplink 负责
  • 第 23 行:[databasePlatform] 属性的值与 Hibernate 不同,这里使用的是 Toplink 特有的类名。该类名的查找方法已在第 2.1.15.2 节中说明。

就这样。请注意,使用 Spring 切换 DBMS 或 JPA 实现是多么简单。

不过,我们还没完全搞定。例如,当你运行 [InitDB] 时,会遇到一个难以理解的异常:


Exception in thread "main" org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'entityManagerFactory' defined in class path resource [spring-config.xml]: Invocation of init method failed; nested exception is java.lang.IllegalStateException: Must start with Java agent to use InstrumentationLoadTimeWeaver. See Spring documentation.
Caused by: java.lang.IllegalStateException: Must start with Java agent to use 
 

第 1 行的错误消息提示您查阅 Spring 文档。在那里,您将进一步了解 [spring-config.xml] 文件中一个不为人知的声明所起的作用:


    <bean id="entityManagerFactory" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
        <property name="dataSource" ref="dataSource" />
        <property name="jpaVendorAdapter">
            <bean class="org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter">
                <!-- 
                    <property name="showSql" value="true" />
                -->
                <property name="databasePlatform" value="org.hibernate.dialect.OracleDialect" />
                <property name="generateDdl" value="true" />
            </bean>
        </property>
        <property name="loadTimeWeaver">
            <bean class="org.springframework.instrument.classloading.InstrumentationLoadTimeWeaver" />
        </property>
</bean>

异常的第 1 行提到了一个名为 [InstrumentationLoadTimeWeaver] 的类,该类可在 Spring 配置文件的第 13 行找到。Spring 文档解释说,在某些情况下,该类对于加载应用程序的类是必要的,并且为了使其正常工作,JVM 必须与一个代理一起启动。该代理由 Spring 提供,名为 [spring-agent]:

  • [spring-agent.jar] 文件位于 <examples>/lib 文件夹中 [1]。该文件包含在 Spring 2.x 发行版中(参见第 5.11 节)。
  • 在 [3] 中,创建一个运行配置 [运行/运行...]
  • 在 [4] 中,创建一个 Java 运行配置(运行配置有多种类型)
  • 在 [5] 中,选择 [Main] 选项卡
  • 在 [6] 中,为该配置命名
  • 在 [7] 中,为与该配置关联的 Eclipse 项目命名(使用“浏览”按钮)
  • 在 [8] 中,为包含 [main] 方法的 Java 类命名(使用“浏览”按钮)
  • 在 [9] 中,转到 [Arguments] 选项卡。在那里,您可以指定两种类型的参数:
    • 在 [9] 中,传递给 [main] 方法的参数
    • 在 [10] 中,指定传递给将执行代码的 JVM 的参数。Spring 代理通过 JVM 参数 -javaagent:value 进行定义。该值为 [spring-agent.jar] 文件的路径。
  • 在 [11] 中:保存配置
  • 在 [12]:配置已创建
  • 在 [13]:运行配置

完成上述操作后,[InitDB] 将运行并产生与 Hibernate 相同的结果。对于 [TestNG],请按相同方式操作:

  • 在 [1] 中,创建一个运行配置 [运行/运行...]
  • 在 [2] 中,创建一个 TestNG 运行配置
  • 在 [3] 中,选择 [测试] 选项卡
  • 在 [4] 中,为配置命名
  • 在 [5] 中,为与该配置关联的 Eclipse 项目命名(使用“浏览”按钮)
  • 在 [6] 中,为测试类命名(使用“浏览”按钮)
  • 在 [7] 中,转到 [Arguments] 选项卡。
  • 在 [8] 中:为 JVM 设置 -javaagent 参数。
  • 在 [9] 中:保存配置
  • 在 [10] 中:配置已创建
  • 在 [11] 中:运行它

完成上述操作后,[TestNG] 运行并产生与 Hibernate 相同的结果。

3.2. 示例 2:JBoss EJB3/JPA 基于 Person 实体的

我们将使用与之前相同的示例,但在 EJB3 容器中运行,具体来说是 JBoss 的容器:

EJB3容器通常集成在应用服务器中。JBoss提供了一个“独立”的EJB3容器,可在应用服务器之外使用。我们将发现它提供的服务与Spring提供的服务类似。我们将尝试比较这些容器,看看哪一个最实用。

第5.12节介绍了JBoss EJB3容器的安装方法。

3.2.1. Eclipse / JBoss EJB3 / Hibernate 项目

Eclipse 项目结构如下:

  • 在 [1] 中:Eclipse 项目。可在 [6] 中找到,位于教程 [5] 的示例中。我们将导入它。
  • 在 [2] 中:分层结构的 Java 代码,按包呈现:
    • [entities]:JPA 实体包
    • [dao]:数据访问层——基于 JPA 层
    • [service]:服务层,而非业务层。我们将使用 EJB3 容器的事务服务。
    • [tests]:包含测试程序。
  • 在 [3] 中:[jpa-jbossejb3] 库包含 JBoss EJB3 所需的 JAR 文件(另见 [7] 和 [8])。
  • 在 [4] 中:[conf] 文件夹包含本教程中使用的各数据库管理系统(DBMS)的配置文件。每种数据库系统各有两个文件:[persistence.xml] 用于配置 JPA 层,[jboss-config.xml] 用于配置 EJB3 容器。

3.2.2. JPA 实体

此处仅管理一个实体:即第 3.1.2 节中讨论过的 Person 实体。

3.2.3. [DAO] 层

[DAO]层实现了第3.1.3节中描述的[IDao]接口。

该接口的 [Dao] 实现如下:


package dao;
 
...
@Stateless
public class Dao implements IDao {
 
    @PersistenceContext
    private EntityManager em;
 
    // delete a person via his/her login
    @TransactionAttribute(TransactionAttributeType.REQUIRED)
    public void deleteOne(Integer id) {
        Personne personne = em.find(Personne.class, id);
        if (personne == null) {
            throw new DaoException(2);
        }
        em.remove(personne);
    }
 
    // get all the people
    @TransactionAttribute(TransactionAttributeType.REQUIRED)
    public List<Personne> getAll() {
        return em.createQuery("select p from Personne p").getResultList();
    }
 
    // get people whose names match a model
    @TransactionAttribute(TransactionAttributeType.REQUIRED)
    public List<Personne> getAllLike(String modele) {
        return em.createQuery("select p from Personne p where p.nom like :modele")
                .setParameter("modele", modele).getResultList();
    }
 
    // find a person via his/her login
    @TransactionAttribute(TransactionAttributeType.REQUIRED)
    public Personne getOne(Integer id) {
        return em.find(Personne.class, id);
    }
 
    // save a person
    @TransactionAttribute(TransactionAttributeType.REQUIRED)
    public Personne saveOne(Personne personne) {
        em.persist(personne);
        return personne;
    }
 
    // update a person
    @TransactionAttribute(TransactionAttributeType.REQUIRED)
    public Personne updateOne(Personne personne) {
        return em.merge(personne);
    }
 
}
  • 这段代码在各个方面都与我们之前使用的 Spring 代码完全相同。唯一的变化在于 Java 注解,而这正是我们正在讨论的内容。
  • 第 4 行:@Stateless 注解将 [Dao] 类定义为无状态 EJB@Stateful 注解则将类定义为有状态 EJB。有状态 EJB 包含私有字段,其值必须在整个生命周期内被保留。 一个典型的例子是包含 Web 应用程序用户相关信息的类。该类的实例与特定用户相关联,当该用户请求的执行线程完成时,必须保留该实例,以便同一客户端的下一次请求能够使用它。 @Stateless EJB 没有状态。以同样的例子来说,在用户请求执行线程结束时,@Stateless EJB 会加入 @Stateless EJB 池,并可供其他用户的请求执行线程使用。
  • 对于开发人员而言,@Stateless EJB3 的概念与 Spring 单例类似,适用于相同的场景。
  • 第 7 行:@PersistenceContext 注解与 Spring 版本 [DAO] 层中遇到的注解相同。它指定了将保存 EntityManager 的字段,从而允许 [DAO] 层操作持久化上下文。
  • 第 11 行:应用于方法的 @TransactionAttribute 注解用于配置该方法的执行事务。该注解的可能取值如下:
    • TransactionAttributeType.REQUIRED:该方法必须在事务中执行。如果事务已启动,则该方法的持久化操作将在该事务中进行。否则,将创建并启动一个事务。
    • TransactionAttributeType.REQUIRES_NEW:该方法必须在新的事务中执行。此时将创建并启动该事务。
    • TransactionAttributeType.MANDATORY:该方法必须在现有事务中执行。如果不存在此类事务,则抛出异常。
    • TransactionAttributeType.NEVER:该方法绝不在事务中执行。
    • ...

该注解本可以直接添加到类本身上:


@Stateless
@TransactionAttribute(TransactionAttributeType.REQUIRED)
public class Dao implements IDao {

该属性随后将应用于该类的所有方法。

3.2.4. [业务/服务]层

[服务]层实现了第3.1.4节中讨论过的[IService]接口。[Service]对[IService]接口的实现与第3.1.4节中讨论的实现完全相同,但有三个例外:


 
@Stateless
@TransactionAttribute(TransactionAttributeType.REQUIRED)
public class Service implements IService {
 
    // layer [dao]
    @EJB
    private IDao dao;
 
    public IDao getDao() {
        return dao;
    }
 
    public void setDao(IDao dao) {
        this.dao = dao;
    }
 
  • 第 2 行:[Service] 类是一个无状态 EJB
  • 第 3 行:[Service] 类的所有方法都必须在事务内执行
  • 第 7–8 行:EJB 容器会将 [dao] 层中 EJB 的引用注入到第 8 行的 [IDao dao] 字段中。第 7 行上的 @EJB 注解请求了此注入。被注入的对象必须是 EJB。这与 Spring 存在关键差异,在 Spring 中,任何类型的对象都可以被注入到另一个对象中。

3.2.5. 各层的配置

[service]、[dao] 和 [JPA] 层的配置由以下文件处理:

  • [META-INF/persistence.xml] 用于配置 JPA 层
  • [jboss-config.xml] 配置 EJB3 容器。它使用 [default.persistence.properties、ejb3-interceptors-aop.xml、embedded-jboss-beans.xml、jndi.properties] 这些文件。这些文件随 JBoss EJB3 一起提供,并提供默认配置,通常无需修改。 开发人员只需关注 [jboss-config.xml] 文件

让我们来查看这两个配置文件:

persistence.xml


<persistence xmlns="http://java.sun.com/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://java.sun.com/xml/ns/persistence
    http://java.sun.com/xml/ns/persistence/persistence_1_0.xsd" version="1.0">
 
    <persistence-unit name="jpa">
 
        <!-- the JPA provider is Hibernate -->
        <provider>org.hibernate.ejb.HibernatePersistence</provider>
 
        <!-- the DataSource JTA managed by the Java EE5 environment -->
        <jta-data-source>java:/datasource</jta-data-source>
 
        <properties>
            <!-- search for JBA layer entities -->
            <property name="hibernate.archive.autodetection" value="class, hbm" />
 
            <!-- logs SQL Hibernate
                <property name="hibernate.show_sql" value="true"/>
                <property name="hibernate.format_sql" value="true"/>
                <property name="use_sql_comments" value="true"/>
            -->
 
            <!-- the type of SGBD managed -->
            <property name="hibernate.dialect" value="org.hibernate.dialect.MySQLInnoDBDialect" />
 
            <!-- recreate all tables (drop+create) when the persistence unit is deployed -->
            <property name="hibernate.hbm2ddl.auto" value="create" />
 
        </properties>
    </persistence-unit>
 
</persistence>

该文件与我们在研究 JPA 实体时遇到的文件类似。它配置了一个 Hibernate JPA 层。新增功能如下:

  • 第 5 行:JPA 持久化单元不再包含我们此前一直使用的 transaction-type 属性:

<persistence-unit name="jpa" transaction-type="RESOURCE_LOCAL" />

如果未指定值,transaction-type 属性的默认值为“JTA”(即 Java 事务 API),这表示事务管理器由 EJB 3 容器提供。与“RESOURCE_LOCAL”管理器相比,“JTA”管理器功能更强大:它能够管理跨越多个连接的事务。 使用 JTA,您可以在数据库 1 的连接 c1 上开启事务 t1,在数据库 2 的连接 c2 上开启事务 t2,并将 (t1,t2) 视为一个单一事务,其中要么所有操作都成功(提交),要么全部失败(回滚)。

在此,我们使用的是 JBoss EJB3 容器的 JTA 管理器。

  • 第 11 行:声明 JTA 管理器必须使用的数据源。该数据源以 JNDI(Java 命名和目录接口)名称的形式指定。该数据源在 [jboss-config.xml] 中定义。

jboss-config.xml


<?xml version="1.0" encoding="UTF-8"?>
 
<deployment xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="urn:jboss:bean-deployer bean-deployer_1_0.xsd"
    xmlns="urn:jboss:bean-deployer:2.0">
 
    <!-- factory of the DataSource -->
    <bean name="datasourceFactory" class="org.jboss.resource.adapter.jdbc.local.LocalTxDataSource">
        <!-- name JNDI of DataSource -->
        <property name="jndiName">java:/datasource</property>
 
        <!-- managed database -->
        <property name="driverClass">com.mysql.jdbc.Driver</property>
        <property name="connectionURL">jdbc:mysql://localhost:3306/jpa</property>
        <property name="userName">jpa</property>
        <property name="password">jpa</property>
 
        <!-- properties connection pool -->
        <property name="minSize">0</property>
        <property name="maxSize">10</property>
        <property name="blockingTimeout">1000</property>
        <property name="idleTimeout">100000</property>
 
        <!-- transaction manager, here JTA -->
        <property name="transactionManager">
            <inject bean="TransactionManager" />
        </property>
        <!-- hibernate cache manager -->
        <property name="cachedConnectionManager">
            <inject bean="CachedConnectionManager" />
        </property>
        <!-- properties instantiation JNDI ? -->
        <property name="initialContextProperties">
            <inject bean="InitialContextProperties" />
        </property>
    </bean>
 
    <!-- the DataSource is requested from a factory -->
    <bean name="datasource" class="java.lang.Object">
        <constructor factoryMethod="getDatasource">
            <factory bean="datasourceFactory" />
        </constructor>
    </bean>
 
</deployment>
  • 第 3 行:该文件的根标签是 <deployment>。此部署文件主要用于配置在 persistence.xml 中声明的 java:/datasource 数据源。
  • 该数据源由第 38 行中的 "datasource" Bean 定义。我们可以看到,数据源(第 40 行)是从第 7 行中由 "datasourceFactory" Bean 定义的 "factory" 获取的。要获取应用程序的数据源,客户端必须调用该工厂的 [getDatasource] 方法(第 39 行)。
  • 第 7 行:提供数据源的工厂是一个 JBoss 类。
  • 第 9 行:数据源的 JNDI 名称。该名称必须与 persistence.xml 文件中 <jta-data-source> 标签内声明的名称一致。实际上,JPA 层将使用此 JNDI 名称来请求数据源。
  • 第 12–15 行:更标准的内容:用于连接 DBMS 的 JDBC 属性
  • 第 18–21 行:JBoss EJB3 容器的内部连接池配置。
  • 第 24–26 行:JTA 管理器。第 25 行注入的 [TransactionManager] 类在 [embedded-jboss-beans.xml] 文件中定义。
  • 第 28–30 行:Hibernate 缓存,这是一个我们尚未涉及的概念。第 29 行注入的 [CachedConnectionManager] 类在 [embedded-jboss-beans.xml] 文件中定义。请注意,该配置现在依赖于 Hibernate,这将在我们想要迁移到 TopLink 时引发问题。
  • 第 32–34 行:JNDI 服务配置。

至此,我们完成了 JBoss EJB3 配置文件的讲解。该文件较为复杂,许多方面仍不明确。此配置摘自 [ref1]。不过,我们可以将其适配到其他数据库管理系统(jboss-config.xml 的第 12–15 行,persistence.xml 的第 24 行)。由于缺乏示例,无法实现向 TopLink 的迁移。

3.2.6. 客户端程序 [InitDB]

现在,我们将开始编写针对上述架构的第一个客户端:

[InitDB] 的代码如下:


package tests;
 
...
public class InitDB {
 
    // service layer
    private static IService service;
 
    // manufacturer
    public static void main(String[] args) throws ParseException, NamingException {
        // start the EJB3 JBoss container
        // configuration files ejb3-interceptors-aop.xml and embedded-jboss-beans.xml are used
        EJB3StandaloneBootstrap.boot(null);
 
        // Creating application-specific beans
        EJB3StandaloneBootstrap.deployXmlResource("META-INF/jboss-config.xml");
 
        // Deploy all EJBs found on classpath (slow, scans all)
        // EJB3StandaloneBootstrap.scanClasspath();
 
        // deploy all EJB found in the application classpath
        EJB3StandaloneBootstrap.scanClasspath("bin".replace("/", File.separator));

        // The JNDI context is initialized. The jndi.properties file is used
        InitialContext initialContext = new InitialContext();
 
        // service layer instantiation
        service = (IService) initialContext.lookup("Service/local");
        // empty the base
        clean();
        // fill it
        fill();
        // a visual check
        dumpPersonnes();
        // we stop the Ejb container
        EJB3StandaloneBootstrap.shutdown();
 
    }
 
    // table content display
    private static void dumpPersonnes() {
        System.out.format("[personnes]-------------------------------------------------------------------%n");
        for (Personne p : service.getAll()) {
            System.out.println(p);
        }
    }
 
    // table filling
    public static void fill() throws ParseException {
        // creating people
        Personne p1 = new Personne("p1", "Paul", new SimpleDateFormat("dd/MM/yy").parse("31/01/2000"), true, 2);
        Personne p2 = new Personne("p2", "Sylvie", new SimpleDateFormat("dd/MM/yy").parse("05/07/2001"), false, 0);
        // we save
        service.saveArray(new Personne[] { p1, p2 });
    }
 
    // deleting table items
    public static void clean() {
        for (Personne p : service.getAll()) {
            service.deleteOne(p.getId());
        }
    }
}
  • 在[ref1]中找到了启动 JBoss EJB3 容器的方法。
  • 第 13 行:启动容器。[EJB3StandaloneBootstrap] 是该容器的类。
  • 第 16 行:将由 [jboss-config.xml] 配置的部署单元部署到容器中:设置 JTA 管理器、数据源、连接池、Hibernate 缓存和 JNDI 服务。
  • 第 22 行:指示容器扫描 Eclipse 项目的 bin 文件夹以定位 EJB。来自 [service] 和 [dao] 层的 EJB 将被容器查找并管理。
  • 第 25 行:初始化一个 JNDI 上下文。我们将使用它来定位 EJB。
  • 第 28 行:向 JNDI 服务请求与 [service] 层中的 [Service] 类对应的 EJB。EJB 可以通过本地或网络访问。此处,所查找 EJB 的名称“Service/local”指代 [service] 层中的 [Service] 类,用于本地访问。
  • 现在,应用程序已部署,我们获得了对 [service] 层的引用。这与 Spring 版本中 [InitDB] 代码第 11 行之后的情况相同。因此,两个版本中都出现了相同的代码。

public class InitDB {
 
    // service layer
    private static IService service;
 
    // manufacturer
    public static void main(String[] args) throws ParseException {
        // application configuration
        ApplicationContext ctx = new ClassPathXmlApplicationContext("spring-config.xml");
        // service layer
        service = (IService) ctx.getBean("service");
        // empty the base
        clean();
        // fill it
        fill();
        // a visual check
        dumpPersonnes();
    }
...
  • 第 36 行 (JBoss EJB3):停止 EJB3 容器。

运行 [InitDB] 会得到以下结果:

16:07:00,781  INFO LocalTxDataSource:117 - Bound datasource to JNDI name 'java:/datasource'
...
16:07:01,171  INFO Version:94 - Hibernate EntityManager 3.2.0.CR1
...
16:07:01,296  INFO Ejb3Configuration:94 - Processing PersistenceUnitInfo [
    name: jpa
    ...]
16:07:01,312  INFO Ejb3Configuration:94 - found EJB3 Entity bean: entites.Personne
...
16:07:01,375  INFO Configuration:94 - Reading mappings from resource: META-INF/orm.xml
16:07:01,375  INFO Ejb3Configuration:94 - [PersistenceUnit: jpa] no META-INF/orm.xml found
16:07:01,421  INFO AnnotationBinder:94 - Binding entity from annotated class: entites.Personne
16:07:01,468  INFO EntityBinder:94 - Bind entity entites.Personne on table jpa01_hb_personne
...
16:07:01,859  INFO SettingsFactory:94 - RDBMS: MySQL, version: 5.0.41-community-nt
16:07:01,859  INFO SettingsFactory:94 - JDBC driver: MySQL-AB JDBC Driver, version: mysql-connector-java-5.0.5 ( $Date: 2007-03-01 00:01:06 +0100 (Thu, 01 Mar 2007) $, $Revision: 6329 $ )
16:07:01,890  INFO Dialect:94 - Using dialect: org.hibernate.dialect.MySQLInnoDBDialect
16:07:01,890  INFO TransactionFactoryFactory:94 - Transaction strategy: org.hibernate.ejb.transaction.JoinableCMTTransactionFactory
...
16:07:02,234  INFO SchemaExport:94 - Running hbm2ddl schema export
16:07:02,234  INFO SchemaExport:94 - exporting generated schema to database
16:07:02,343  INFO SchemaExport:94 - schema export complete
...
16:07:02,562  INFO EJBContainer:479 - STARTED EJB: dao.Dao ejbName: Dao
...
16:07:02,593  INFO EJBContainer:479 - STARTED EJB: service.Service ejbName: Service
...
[personnes]-------------------------------------------------------------------
[1,0,p1,Paul,31/01/2000,true,2]
[2,0,p2,Sylvie,05/07/2001,false,0]

建议读者查阅这些日志。其中包含有关 EJB3 容器运行情况的有趣信息。

3.2.7. 单元测试 [TestNG]

[TestNG] 程序代码如下:


package tests;
 
...
public class TestNG {
 
    // service layer
    private IService service = null;
 
    @BeforeClass
    public void init() throws NamingException, ParseException {
        // log
        log("init");
        // start the EJB3 JBoss container
        // configuration files ejb3-interceptors-aop.xml and embedded-jboss-beans.xml are used
        EJB3StandaloneBootstrap.boot(null);
 
        // Creating application-specific beans
        EJB3StandaloneBootstrap.deployXmlResource("META-INF/jboss-config.xml");
 
        // Deploy all EJBs found on classpath (slow, scans all)
        // EJB3StandaloneBootstrap.scanClasspath();
 
        // deploy all EJB found in the application classpath
        EJB3StandaloneBootstrap.scanClasspath("bin".replace("/", File.separator));
 
        // The JNDI context is initialized. The jndi.properties file is used
        InitialContext initialContext = new InitialContext();
 
        // service layer instantiation
        service = (IService) initialContext.lookup("Service/local");
        // empty the base
        clean();
        // fill it
        fill();
        // a visual check
        dumpPersonnes();
    }
 
    @AfterClass
    public void terminate() {
        // log
        log("terminate");
        // Shutdown EJB container
        EJB3StandaloneBootstrap.shutdown();
    }
 
    @BeforeMethod
    public void setUp() throws ParseException {
...
    }
 
...
}
  • init 方法(第 10–37 行)用于设置测试所需的环境,其中使用了 [InitDB] 中先前解释过的代码。
  • terminate 方法(第 40–45 行)在测试结束时执行(由于 @AfterClass 注解),用于停止 EJB3 容器(第 44 行)。
  • 其余部分与 Spring 版本完全一致。

测试通过:

Image

3.2.8. 更改数据库管理系统

要更改数据库管理系统(DBMS),只需将 [META-INF] 文件夹 [2] 中的内容替换为 [conf] 文件夹 [1] 中 DBMS 文件夹的内容即可。以 SQL Server 为例:

[persistence.xml] 文件内容如下:


<persistence xmlns="http://java.sun.com/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://java.sun.com/xml/ns/persistence
    http://java.sun.com/xml/ns/persistence/persistence_1_0.xsd" version="1.0">
 
    <persistence-unit name="jpa">
 
        <!-- the JPA provider is Hibernate -->
        <provider>org.hibernate.ejb.HibernatePersistence</provider>
 
        <!-- the DataSource JTA managed by the Java EE5 environment -->
        <jta-data-source>java:/datasource</jta-data-source>
 
        <properties>
            <!-- search for JBA layer entities -->
            <property name="hibernate.archive.autodetection" value="class, hbm" />
 
            <!-- logs SQL Hibernate
                <property name="hibernate.show_sql" value="true"/>
                <property name="hibernate.format_sql" value="true"/>
                <property name="use_sql_comments" value="true"/>
            -->
 
            <!-- the type of SGBD managed -->
            <property name="hibernate.dialect" value="org.hibernate.dialect.SQLServerDialect" />
 
            <!-- recreate all tables (drop+create) when the persistence unit is deployed -->
            <property name="hibernate.hbm2ddl.auto" value="create" />
 
        </properties>
    </persistence-unit>
 
</persistence>

只有一行发生了变化:

  • 第 24 行:Hibernate 必须使用的 SQL 方言

SQL Server 的 [jboss-config.xml] 文件如下:


<?xml version="1.0" encoding="UTF-8"?>
 
<deployment xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="urn:jboss:bean-deployer bean-deployer_1_0.xsd"
    xmlns="urn:jboss:bean-deployer:2.0">
 
    <!-- factory of the DataSource -->
    <bean name="datasourceFactory" class="org.jboss.resource.adapter.jdbc.local.LocalTxDataSource">
        <!-- name JNDI of DataSource -->
        <property name="jndiName">java:/datasource</property>
 
        <!-- managed database -->
        <property name="driverClass">com.microsoft.sqlserver.jdbc.SQLServerDriver</property>
        <property name="connectionURL">jdbc:sqlserver://localhost\\SQLEXPRESS:1246;databaseName=jpa</property>
        <property name="userName">jpa</property>
        <property name="password">jpa</property>
 
        <!-- properties connection pool -->
    ...
    </bean>
 
</deployment>

仅第 12–15 行发生了变化:它们指定了新 JDBC 连接的特性。

建议读者使用其他数据库管理系统(DBMS)重复针对 MySQL5 描述的测试。

3.2.9. 更改 JPA 实现

如上所述,我们尚未找到任何将 JBoss EJB3 容器与 TopLink 结合使用的示例。截至本文撰写之时(2007 年 6 月),我仍然不知道这种配置是否可行。

3.3. 其他示例

让我们总结一下针对“Person”实体所做的工作。我们构建了三种架构来运行相同的测试:

1 - Spring/Hibernate 实现方案

2 - Spring/TopLink 实现

3 - 一个 JBoss EJB3 / Hibernate 实现

本教程中的示例使用了这三种架构,以及教程第一部分中涉及的其他实体:

分类 - 文章

  • 在 [1] 中:Spring/Hibernate 版本
  • 在 [2] 中:Spring / TopLink 版本
  • 在 [3] 中:JBoss EJB3 / Hibernate 版本

人员 - 地址 - 活动

  • 在 [1] 中:Spring/Hibernate 版本
  • 在 [2] 中:Spring/TopLink 版本
  • 在 [3] 中:JBoss EJB3 / Hibernate 版本

这些示例并未引入任何新的架构概念。它们仅仅适用于需要管理多个实体,且实体之间存在一对多多对多关系的场景——而使用 Person 实体的示例中并不存在这种情况。

3.4. 示例 3:Web 应用中的 Spring / JPA

3.4.1. 概述

在此,我们将重新审视以下文档中介绍的应用程序:

[ref4]: Java 中 MVC Web 开发的基础 [http://tahe.developpez.com/java/baseswebmvc/]。

本文介绍了Java中MVC Web开发的基础知识。为理解下文的示例,读者应熟悉这些基础知识。该Web应用程序将使用Tomcat服务器。其在Eclipse中的安装和使用方法详见第5.3节

该应用程序最初是基于 iBatis/SQLMap 工具 [http://ibatis.apache.org/] 构建的 [DAO] 层开发的,该工具负责关系型数据库到对象的映射。我们将直接用 JPA 替换 iBatis。应用程序架构如下:

我们将要编写的Web应用程序将允许我们通过四种操作来管理一组人员:

  • 查看组内成员列表
  • 向组中添加成员
  • 修改组内成员
  • 从组中移除成员

这四项基本操作在数据库表中很常见。以下屏幕截图 展示了应用程序向用户显示的页面。

 

3.4.2. Eclipse 项目

该应用程序的Eclipse项目如下:

  • 在 [1] 中:Web 项目。这是一个类型为 [动态 Web 项目] [2] 的 Eclipse 项目。它位于教程示例的 [3] 文件夹中的 [4] 位置。我们将导入该项目。
  • 在 [5] 中:[服务、DAO、JPA] 层的源代码和配置。我们保留第 3.1.1 节中讨论的 Eclipse 项目 [hibernate-spring-people-business-dao] 中的现有 [DAO、实体、服务] 组件。我们仅开发 [Web] 层,此处由 [web] 包表示。 此外,我们将保留该项目中的配置文件 [persistence.xml, spring-config.xml],但会改用 Postgres 数据库管理系统,这导致 [spring-config.xml] 中出现以下更改:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans" 
...
    <bean id="entityManagerFactory" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
        <property name="dataSource" ref="dataSource" />
        <property name="jpaVendorAdapter">
...
                <property name="databasePlatform" value="org.hibernate.dialect.PostgreSQLDialect" />
...
        </property>
    ...
    </bean>
 
    <!-- data source DBCP -->
    <bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
        <property name="driverClassName" value="org.postgresql.Driver" />
        <property name="url" value="jdbc:postgresql:jpa" />
        <property name="username" value="jpa" />
        <property name="password" value="jpa" />
    </bean>
....
</beans>

第 8 行和第 16–19 行已针对 Postgres 进行了调整。

  • 在 [6] 中:[WebContent] 文件夹包含项目的 JSP 页面以及必要的库文件。这些内容在 [8] 中列出
  • 该应用程序可与多种数据库管理系统(DBMS)配合使用。只需修改 [spring-config.xml] 文件即可。[conf] 文件夹 [7] 中包含针对各种 DBMS 适配的 [spring-config.xml] 文件。

3.4.3. [web] 层

本应用程序采用以下多层架构:

[Web] 层将向用户提供界面,以便他们管理人员组:

  • 群组成员列表
  • 将人员添加到组中
  • 编辑组内成员
  • 从组中移除成员

为此,它将依赖于[服务]层,而[服务]层又会调用[DAO]层。我们已经介绍了由[Web]层管理的界面(第3.4.1节)。为了描述Web层,我们将依次介绍以下内容:

  • 其配置
  • 其视图
  • 其控制器
  • 部分测试

3.4.3.1. Web 应用程序配置

让我们来看看 Eclipse 项目的架构:

 
  • 在 [web] 包中,我们可以找到 Web 应用程序控制器:即 [Application] 类。
  • 应用程序的 JSP/JSTL 页面位于 [WEB-INF/views] 目录下。
  • [WEB-INF/lib] 文件夹包含应用程序所需的第三方库。这些库可在 [Web App Libraries] 文件夹中查看。

[web.xml]


[web.xml]文件是Web服务器用于加载应用程序的文件。其内容如下:


<?xml version="1.0" encoding="UTF-8"?>
<web-app id="WebApp_ID" version="2.4" xmlns="http://java.sun.com/xml/ns/j2ee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd">
    <display-name>spring-jpa-hibernate-personnes-crud</display-name>
    <!--  ServletPersonne -->
    <servlet>
        <servlet-name>personnes</servlet-name>
        <servlet-class>web.Application</servlet-class>
        <init-param>
            <param-name>urlEdit</param-name>
            <param-value>/WEB-INF/vues/edit.jsp</param-value>
        </init-param>
        <init-param>
            <param-name>urlErreurs</param-name>
            <param-value>/WEB-INF/vues/erreurs.jsp</param-value>
        </init-param>
        <init-param>
            <param-name>urlList</param-name>
            <param-value>/WEB-INF/vues/list.jsp</param-value>
        </init-param>
    </servlet>
    <!--  Mapping ServletPersonne-->
    <servlet-mapping>
        <servlet-name>personnes</servlet-name>
        <url-pattern>/do/*</url-pattern>
    </servlet-mapping>
    <!--  welcome files -->
    <welcome-file-list>
        <welcome-file>index.jsp</welcome-file>
    </welcome-file-list>
    <!--  Unexpected error page -->
    <error-page>
        <exception-type>java.lang.Exception</exception-type>
        <location>/WEB-INF/vues/exception.jsp</location>
    </error-page>
</web-app>
  • 第 23-26 行:URL [/do/*] 将由 [people] Servlet 处理
  • 第 7-8 行:[personnes] Servlet 是 [Application] 类的实例,该类我们将自行实现。
  • 第 9-20 行:定义三个参数 [urlList, urlEdit, urlErrors],用于标识 [list, edit, errors] 视图的 JSP 页面的 URL。
  • 第 28-30 行:该应用程序有一个默认入口页面 [index.jsp],位于 Web 应用程序文件夹的根目录下。
  • 第 32–35 行:该应用程序有一个默认错误页面,当 Web 服务器遇到应用程序未处理的异常时,将显示该页面。
    • 第 37 行:<exception-type> 标签指定了由 <error-page> 指令处理的异常类型;此处为 [java.lang.Exception] 类型及其子类型,即所有异常。
    • 第 38 行:<location> 标签指定在发生 <exception-type> 定义的类型异常时要显示的 JSP 页面。如果该页面包含以下指令,则发生的异常可在该页面中通过名为 exception 的对象获取:

<%@ page isErrorPage="true" %>
  • (续)
    • 如果 <exception-type> 指定类型 T1,而类型为 T2(未从 T1 派生)的异常被向上传播至 Web 服务器,服务器会向客户端发送一个专有异常页面,该页面通常用户体验不佳。因此,[web.xml] 文件中的 <error-page> 标签至关重要。

[index.jsp]


当用户未指定 URL(即此处的 [/spring-jpa-hibernate-personnes-crud])而直接请求应用上下文时,将显示此页面。其内容如下:


<%@ page language="java" pageEncoding="ISO-8859-1" contentType="text/html;charset=ISO-8859-1"%>
<%@ taglib uri="/WEB-INF/c.tld" prefix="c" %>
 
<c:redirect url="/do/list"/>

[index.jsp] 将客户端重定向(第 4 行)至 URL [/do/list]。该 URL 显示该组中的人员列表。

3.4.3.2. 应用程序的 JSP/JSTL 页面


视图 [ list.jsp]


它用于显示人员列表:

Image

其代码如下:


<%@ page language="java" pageEncoding="ISO-8859-1" contentType="text/html;charset=ISO-8859-1"%>
<%@ taglib uri="/WEB-INF/c.tld" prefix="c" %>
<%@ taglib uri="/WEB-INF/taglibs-datetime.tld" prefix="dt" %>
 
<html>
    <head>
        <title>MVC - Personnes</title>
    </head>
    <body background="<c:url value="/ressources/standard.jpg"/>">
            <c:if test="${erreurs!=null}">
                <h3>Les erreurs suivantes se sont produites :</h3>
                <ul>
                    <c:forEach items="${erreurs}" var="erreur">
                        <li><c:out value="${erreur}"/></li>
                    </c:forEach>
                </ul>
            <hr>
        </c:if>
        <h2>Liste des personnes</h2>
        <table border="1">
            <tr>
                <th>Id</th>
                <th>Version</th>
                <th>Pr&eacute;nom</th>
                <th>Nom</th>
                <th>Date de naissance</th>
                <th>Mari&eacute;</th>
                <th>Nombre d'enfants</th>
                <th></th>
            </tr>
            <c:forEach var="personne" items="${personnes}">
                <tr>
                    <td><c:out value="${personne.id}"/></td>
                    <td><c:out value="${personne.version}"/></td>
                    <td><c:out value="${personne.prenom}"/></td>
                    <td><c:out value="${personne.nom}"/></td>
                    <td><dt:format pattern="dd/MM/yyyy">${personne.datenaissance.time}</dt:format></td>
                    <td><c:out value="${personne.marie}"/></td>
                    <td><c:out value="${personne.nbenfants}"/></td>
                    <td><a href="<c:url value="/do/edit?id=${personne.id}"/>">Modifier</a></td>
                    <td><a href="<c:url value="/do/delete?id=${personne.id}"/>">Supprimer</a></td>
                </tr>
            </c:forEach>
        </table>
        <br>
        <a href="<c:url value="/do/edit?id=-1"/>">Ajout</a>
    </body>
</html>
  • 该视图在其模型中接收两个元素:
    • 与 [Person] 对象列表 [List] 关联的 [people] 元素:一个人员列表。
    • 可选的 [errors] 元素,关联一个 [List] 类型的 [String] 对象列表:错误消息列表。
  • 第 31–43 行:我们遍历 ${people} 列表,以显示一个包含该组中人员的 HTML 表格。
  • 第 40 行:使用当前人员的 [id] 字段设置 [Edit] 链接指向的 URL,以便与 URL [/do/edit] 关联的控制器知道要编辑哪个人。
  • 第 41 行:[Delete] 链接也采用了同样的处理方式。
  • 第 37 行:为了以 DD/MM/YYYY 格式显示该用户的出生日期,我们使用了 Apache [Jakarta Taglibs] 项目 [DateTime] 标签库中的 <dt> 标签:

Image

该标签库的描述文件在第 3 行中定义。

  • 第 46 行:用于添加新人员的 [Add] 链接指向 URL [/do/edit],与第 40 行的 [Edit] 链接相同。[id] 参数的值为 -1,表示这是添加操作而非编辑操作。
  • 第 10–18 行:如果模板中包含 ${errors} 元素,则会显示其中包含的错误信息。

[ edit.jsp] 页面


该页面用于显示添加新人员或修改现有人员的表单:

[edit.jsp] 视图的代码如下:


<%@ page language="java" pageEncoding="ISO-8859-1" contentType="text/html;charset=ISO-8859-1"%>
<%@ taglib uri="/WEB-INF/c.tld" prefix="c" %>
<%@ taglib uri="/WEB-INF/taglibs-datetime.tld" prefix="dt" %>
 
<html>
    <head>
        <title>MVC - Personnes</title>
    </head>
    <body background="../ressources/standard.jpg">
        <h2>Ajout/Modification d'une personne</h2>
        <c:if test="${erreurEdit!=''}">
            <h3>Echec de la mise à jour :</h3>
          L'erreur suivante s'est produite : ${erreurEdit}
            <hr>
        </c:if>
        <form method="post" action="<c:url value="/do/validate"/>">
            <table border="1">
                <tr>
                    <td>Id</td>
                    <td>${id}</td>
                </tr>
                <tr>
                    <td>Version</td>
                    <td>${version}</td>
                </tr>
                <tr>
                    <td>Pr&eacute;nom</td>
                    <td>
                        <input type="text" value="${prenom}" name="prenom" size="20">
                    </td>
                    <td>${erreurPrenom}</td>
                </tr>
                <tr>
                    <td>Nom</td>
                    <td>
                        <input type="text" value="${nom}" name="nom" size="20">
                    </td>
                    <td>${erreurNom}</td>
                </tr>
                <tr>
                <td>Date de naissance (JJ/MM/AAAA)</td>
                    <td>
                        <input type="text" value="${datenaissance}" name="datenaissance">
                    </td>
                    <td>${erreurDateNaissance}</td>
                </tr>
                <tr>
                    <td>Mari&eacute;</td>
                    <td>
                        <c:choose>
                            <c:when test="${marie}">
                                <input type="radio" name="marie" value="true" checked>Oui
                                <input type="radio" name="marie" value="false">Non
                            </c:when>
                            <c:otherwise>
                                <input type="radio" name="marie" value="true">Oui
                                <input type="radio" name="marie" value="false" checked>Non
                            </c:otherwise>
                        </c:choose>
                    </td>
                </tr>
                <tr>
                    <td>Nombre d'enfants</td>
                    <td>
                        <input type="text" value="${nbenfants}" name="nbenfants">
                    </td>
                    <td>${erreurNbEnfants}</td>
                </tr>
            </table>
            <br>
            <input type="hidden" value="${id}" name="id">
      <input type="hidden" value="${version}" name="version">
            <input type="submit" value="Valider">
            <a href="<c:url value="/do/list"/>">Annuler</a>
        </form>
    </body>
</html>

此视图显示了一个用于添加新人员或更新现有人员的表单。从现在起,为简化表述,我们将统一使用术语 [更新]。 [提交]按钮(第73行)会向URL [/do/validate](第16行)发起POST请求。如果POST请求失败,则重新显示[edit.jsp]视图并附带发生的错误信息;否则,显示[list.jsp]视图。

  • [edit.jsp] 视图(无论是在 GET 请求还是 POST 请求失败时都会显示)在其模型中接收以下元素:
属性
GET
POST
id
要更新
相同
版本
其版本
相同
名字
名字
输入的姓名
他的/她的姓氏
已输入的姓氏
出生日期
他的出生日期
输入的出生日期
已婚
婚姻状况
已婚
子女人数
子女人数
已输入的子女数量
错误编辑
一条错误消息,指出在POST时
或修改操作触发了
。若无错误,则为空。
errorFirstName
为空
表示名字不正确——否则为空
lastNameError
为空
表示姓氏不正确——否则为空
birthDateError
为空
表示出生日期不正确——否则为空
errorNumberOfChildren
为空
表示子女数量不正确——否则为空
  • 第 11-15 行:如果表单 POST 提交失败,将返回 [errorEdit!=''] 并显示错误信息。
  • 第 16 行:表单将提交至 URL [/do/validate]
  • 第 20 行:显示模板中的 [id] 元素
  • 第 24 行:显示模板中的 [version] 元素
  • 第 26-32 行:输入人员的名字:
    • 当表单初次显示(GET)时,${firstName} 显示更新后的 [Person] 对象中 [firstName] 字段的当前值,而 ${firstNameError} 为空。
    • 若在 POST 操作后发生错误,将再次显示已输入的值 ${firstName} 以及任何错误信息 ${firstNameError}
  • 第 33-39 行:输入人员的姓
  • 第 40–46 行:输入人员的出生日期
  • 第 47–61 行:使用单选按钮输入人员的婚姻状况。[Person] 对象的 [married] 字段值用于确定应选中两个单选按钮中的哪一个。
  • 第 62-68 行:输入该人的子女数
  • 第 71 行:一个名为 [id] 的隐藏 HTML 字段,其值等于正在更新的该人的 [id] 字段,若为新增则为 -1,若为修改则为其他值。
  • 第 72 行:一个名为 [version] 的隐藏 HTML 字段,其值等于正在更新的该人的 [id] 字段。
  • 第 73 行:表单的 [提交] 按钮
  • 第 74 行:返回人员列表的链接。该链接标记为 [Cancel],因为它允许您在不提交表单的情况下退出表单。

[ exception.jsp] 页面


该页面用于显示一条消息,表明应用程序未处理的异常已发生并已传播到 Web 服务器。

例如,让我们尝试删除一个在组中不存在的用户:

[exception.jsp] 视图的代码如下:


<%@ page language="java" pageEncoding="ISO-8859-1" contentType="text/html;charset=ISO-8859-1"%>
<%@ taglib uri="/WEB-INF/c.tld" prefix="c" %>
<%@ page isErrorPage="true" %>
 
<%
  response.setStatus(200);
%>
 
<html>
    <head>
        <title>MVC - Personnes</title>
    </head>
    <body background="<c:url value="/ressources/standard.jpg"/>">
        <h2>MVC - personnes</h2>
        L'exception suivante s'est produite :
        <%= exception.getMessage()%>
        <br><br>
        <a href="<c:url value="/do/list"/>">Retour &agrave; la liste</a>
    </body>
</html>
  • 该视图在其模板中接收一个键,即 [exception] 元素,该元素代表被 Web 服务器拦截的异常。为了让 Web 服务器将此元素包含到 JSP 页面模板中,页面必须在第 3 行定义了该标签。
  • 第 6 行:响应的 HTTP 状态码被设置为 200。这是响应中的第一个 HTTP 头。200 状态码告知客户端其请求已得到满足。 通常,服务器响应中会包含一个 HTML 文档。本例中也是如此。如果响应的 HTTP 状态码未设置为 200,此处将显示 500 值,这意味着发生了错误。实际上,当 Web 服务器拦截到未处理的异常时,会将其视为异常情况,并通过 500 代码发出信号。 不同浏览器对 HTTP 500 状态码的响应方式各异:Firefox 会显示随响应附带的 HTML 文档,而 IE 则会忽略该文档并显示其自身的页面。这就是我们用 200 状态码替换 500 状态码的原因。
  • 第 16 行:显示异常文本
  • 第 18 行:向用户提供返回人员列表的链接

[ -errors .jsp] 视图


该视图用于显示报告应用程序初始化错误的页面,即在控制器 Servlet 的 [init] 方法执行过程中检测到的错误。例如,这可能是 [web.xml] 文件中缺少某个参数,如下例所示:

Image

[errors.jsp] 页面的代码如下:


<%@ page language="java" contentType="text/html; charset=ISO-8859-1"
    pageEncoding="ISO-8859-1"%>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<%@ taglib uri="/WEB-INF/c.tld" prefix="c" %>
 
<html>
    <head>
      <title>MVC - Personnes</title>
  </head>
  <body>
      <h2>Les erreurs suivantes se sont produites</h2>
    <ul>
            <c:forEach var="erreur" items="${erreurs}">
                <li>${erreur}</li>
            </c:forEach>
    </ul>
  </body>
</html>

该页面在其模板中接收一个 [errors] 元素,这是一个包含 [String] 对象的 [ArrayList];这些是错误消息。它们由第 13–15 行的循环显示出来。

3.4.3.3. 应用程序控制器

[Application] 控制器定义在 [web] 包中:

Image


结构 控制器结构与初始化


[Application] 控制器的骨架如下:


package web;
 
...
 
 
@SuppressWarnings("serial")
public class Application extends HttpServlet {
    // instance parameters
    private String urlErreurs = null;
    private ArrayList erreursInitialisation = new ArrayList<String>();
    private String[] paramètres = { "urlList", "urlEdit", "urlErreurs" };
    private Map params = new HashMap<String, String>();
 
    // service
    private IService service = null;
 
    // init
    @SuppressWarnings("unchecked")
    public void init() throws ServletException {
        // retrieve servlet initialization parameters
        ServletConfig config = getServletConfig();
        // other initialization parameters are processed
        String valeur = null;
        for (int i = 0; i < paramètres.length; i++) {
            // parameter value
            valeur = config.getInitParameter(paramètres[i]);
            // present parameter?
            if (valeur == null) {
                // we note the error
                erreursInitialisation.add("Le paramètre [" + paramètres[i] + "] n'a pas été initialisé");
            } else {
                // parameter value is stored
                params.put(paramètres[i], valeur);
            }
        }
        // the [errors] view url has a special treatment
        urlErreurs = config.getInitParameter("urlErreurs");
        if (urlErreurs == null)
            throw new ServletException("Le paramètre [urlErreurs] n'a pas été initialisé");
        // application configuration
        ApplicationContext ctx = new ClassPathXmlApplicationContext("spring-config.xml");
        // service layer
        service = (IService) ctx.getBean("service");
        // empty the base
        clean();
        // fill it
        try {
            fill();
        } catch (ParseException e) {
            throw new ServletException(e);
        }
    }
 
    // table filling
    public void fill() throws ParseException {
        // creating people
        Personne p1 = new Personne("p1", "Paul", new SimpleDateFormat("dd/MM/yy").parse("31/01/2000"), true, 2);
        Personne p2 = new Personne("p2", "Sylvie", new SimpleDateFormat("dd/MM/yy").parse("05/07/2001"), false, 0);
        // we save
        service.saveArray(new Personne[] { p1, p2 });
    }
 
    // deleting table items
    public void clean() {
        for (Personne p : service.getAll()) {
            service.deleteOne(p.getId());
        }
    }
 
    // GET
    @SuppressWarnings("unchecked")
    public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
...
    }
 
    // display list of persons
    private void doListPersonnes(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
...
    }
 
    // modify / add a person
    private void doEditPersonne(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
...
    }
 
    // deleting a person
    private void doDeletePersonne(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
...
    }
 
    // validation modification / addition of a person
    public void doValidatePersonne(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
...
    }
 
    // display pre-filled form
    private void showFormulaire(HttpServletRequest request, HttpServletResponse response, String erreurEdit) throws ServletException, IOException {
    ...
    }
 
    // post
    public void doPost(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
        // we hand over to GET
        doGet(request, response);
    }
 
}
  • 第 21–34 行:我们获取 [web.xml] 文件中指定的参数。
  • 第 37–39 行:[urlErrors] 参数必须存在,因为它指定了能够显示任何初始化错误的 [errors] 视图的 URL。如果不存在,应用程序将通过抛出 [ServletException] 异常(第 39 行)而终止。该异常将传播到 Web 服务器,并由 [web.xml] 文件中的 <error-page> 标签进行处理。 因此将显示 [exception.jsp] 视图:

Image

上方的 [返回列表] 链接处于非活动状态。只要应用程序未被修改并重新加载,点击该链接就会返回相同的响应。正如我们之前所见,这对处理其他类型的异常非常有用。

  • 第 40–43 行:使用 Spring 配置文件获取 [service] 层的引用。控制器初始化后,其方法将拥有指向 [service] 层的 [service] 引用(第 15 行),并利用该引用执行用户请求的操作。这些操作将被 [doGet] 方法拦截,并由控制器中的特定方法进行处理:
Url
HTTP 方法
控制器方法
/do/list
GET
doListPeople
/do/edit
GET
doEditPerson
/do/validate
POST
doValidatePerson
/do/delete
GET
doDeletePerson

[doGet] 方法


此方法的目的是将用户请求的操作处理路由到正确的方法。其代码如下:


// GET
    @SuppressWarnings("unchecked")
    public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
 
        
// check how the servlet was initialized
        if (erreursInitialisation.size() != 0) {
            // we hand over to the error page
            request.setAttribute("erreurs", erreursInitialisation);
            getServletContext().getRequestDispatcher(urlErreurs).forward(request, response);
            // end
            return;
        }
        // retrieve the request sending method
        String méthode = request.getMethod().toLowerCase();
        // retrieve the action to be executed
        String action = request.getPathInfo();
        // action?
        if (action == null) {
            action = "/list";
        }
        // execution action
        if (méthode.equals("get") && action.equals("/list")) {
            // list of persons
            doListPersonnes(request, response);
            return;
        }
        if (méthode.equals("get") && action.equals("/delete")) {
            // deleting a person
            doDeletePersonne(request, response);
            return;
        }
        if (méthode.equals("get") && action.equals("/edit")) {
            // presentation form add / modify a person
            doEditPersonne(request, response);
            return;
        }
        if (méthode.equals("post") && action.equals("/validate")) {
            // validation form add / modify a person
            doValidatePersonne(request, response);
            return;
        }
        // other cases
        doListPersonnes(request, response);
    }
  • 第 7–13 行:我们检查初始化错误列表是否为空。如果不是,则显示 [errors(errors)] 视图,该视图将报告错误。
  • 第 15 行:我们获取客户端用于发送请求的 [get] 或 [post] 方法。
  • 第 17 行:从请求中获取 [action] 参数的值。
  • 第 23–27 行:处理 [GET /do/list] 请求,该请求用于获取人员列表。
  • 第 28–32 行:处理 [GET /do/delete] 请求,该请求用于删除某人。
  • 第 33–37 行:处理 [GET /do/edit] 请求,该请求用于更新人员表单。
  • 第 38–42 行:处理 [POST /do/validate] 请求,该请求用于验证更新的用户。
  • 第 44 行:如果请求的操作不属于前五种,则将其视为 [GET /do/list] 请求。

[doListPersonnes] 方法


该方法处理 [GET /do/list] 请求,该请求用于获取人员列表:

Image

其代码如下:


    // display list of persons
    private void doListPersonnes(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        // the [list] view model
        request.setAttribute("personnes", service.getAll());
        // list] view display
        getServletContext().getRequestDispatcher((String) params.get("urlList")).forward(request, response);
}
  • 第 4 行:我们从 [服务] 层请求该组中的人员列表,并将其存储在模型中,键名为 "people"。
  • 第 6 行:显示第 3.4.3.2 节描述的 [list.jsp] 视图。

[doDeletePerson] 方法


该方法处理 [GET /do/delete?id=XX] 请求,该请求用于删除 id=XX 的用户。URL [/do/delete?id=XX] 即 [list.jsp] 视图中 [Delete] 链接的地址:

Image

其代码如下:


...
<html>
    <head>
        <title>MVC - Personnes</title>
    </head>
    <body background="<c:url value="/ressources/standard.jpg"/>">
...
            <c:forEach var="personne" items="${personnes}">
                <tr>
...
                    <td><a href="<c:url value="/do/edit?id=${personne.id}"/>">Modifier</a></td>
                    <td><a href="<c:url value="/do/delete?id=${personne.id}"/>">Supprimer</a></td>
                </tr>
            </c:forEach>
        </table>
        <br>
        <a href="<c:url value="/do/edit?id=-1"/>">Ajout</a>
    </body>
</html>

第 12 行显示了 [删除] 链接的 URL [/do/delete?id=XX]。处理此 URL 的 [doDeletePerson] 方法必须删除 id=XX 的用户,然后显示该组中更新后的人员列表。其代码如下:


// deleting a person
    private void doDeletePersonne(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        // retrieve the person's id
        int id = Integer.parseInt(request.getParameter("id"));
        // we delete the person
        service.deleteOne(id);
        // redirects to the list of persons
        response.sendRedirect("list");
    }
  • 第 4 行:正在处理的 URL 格式为 [/do/delete?id=XX]。我们从 [id] 参数中提取 [XX] 的值。
  • 第 6 行:我们请求 [service] 层根据获取的 ID 删除该用户。 我们不进行任何验证。如果要删除的用户不存在,[dao]层会抛出一个异常,该异常会向上传播至[service]层。我们在此控制器中也不进行处理。因此,该异常将向上传播至Web服务器,根据配置,Web服务器将显示第3.4.3.2节中描述的[exception.jsp]页面:

Image

  • 第 9 行:如果删除成功(未抛出异常),客户端将被重定向到相对 URL [list]。由于刚刚处理的 URL 是 [/do/delete],因此重定向 URL 将是 [/do/list]。浏览器将执行 [GET /do/list] 请求,从而显示人员列表。

[doEditPerson] 方法


该方法处理 [GET /do/edit?id=XX] 请求,该请求用于获取用于更新 id=XX 的人员的表单。URL [/do/edit?id=XX] 用于 [list.jsp] 视图中的 [编辑] 和 [添加] 链接:

Image

其代码如下:


...
<html>
    <head>
        <title>MVC - Personnes</title>
    </head>
    <body background="<c:url value="/ressources/standard.jpg"/>">
...
            <c:forEach var="personne" items="${personnes}">
                <tr>
...
                    <td><a href="<c:url value="/do/edit?id=${personne.id}"/>">Modifier</a></td>
                    <td><a href="<c:url value="/do/delete?id=${personne.id}"/>">Supprimer</a></td>
                </tr>
            </c:forEach>
        </table>
        <br>
        <a href="<c:url value="/do/edit?id=-1"/>">Ajout</a>
    </body>
</html>

在第 11 行,我们可以看到 [编辑] 链接的 URL [/do/edit?id=XX],而在第 17 行,则是 [添加] 链接的 URL [/do/edit?id=-1]。doEditPersonne 方法必须显示 id=XX 人员的编辑表单,如果是添加操作,则应显示一个空表单。

  • 上文中的 [1] 表示添加表单,[2] 表示编辑表单。

[doEditPerson] 方法的代码如下:


// modify / add a person
    private void doEditPersonne(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        // retrieve the person's id
        int id = Integer.parseInt(request.getParameter("id"));
        // addition or modification?
        Personne personne = null;
        if (id != -1) {
            // modification - the person to be modified is retrieved
            personne = service.getOne(id);
            request.setAttribute("id", personne.getId());
            request.setAttribute("version", personne.getVersion());
        } else {
            // add - create an empty person
            personne = new Personne();
            request.setAttribute("id", -1);
            request.setAttribute("version", -1);
        }
        // we put the [Person] object in the user's session
        request.getSession().setAttribute("personne", personne);
        // and in the view model [edit]
        request.setAttribute("erreurEdit", "");
        request.setAttribute("prenom", personne.getPrenom());
        request.setAttribute("nom", personne.getNom());
        Date dateNaissance = personne.getDatenaissance();
        if (dateNaissance != null) {
            request.setAttribute("datenaissance", new SimpleDateFormat("dd/MM/yyyy").format(dateNaissance));
        } else {
            request.setAttribute("datenaissance", "");
        }
        request.setAttribute("marie", personne.isMarie());
        request.setAttribute("nbenfants", personne.getNbenfants());
        // view display [edit]
        getServletContext().getRequestDispatcher((String) params.get("urlEdit")).forward(request, response);
    }
  • 该 GET 请求的目标 URL 格式为 [/do/edit?id=XX]。在第 4 行,我们获取 [id] 的值。随后有两种情况:
    1. 如果 id 不等于 -1,则表示更新操作,我们需要显示一个预先填入了待更新人员信息的表单。在第 9 行,该人员信息是从 [service] 层获取的。
    2. 如果 id 等于 -1,则表示新增操作,必须显示一个空表单。为此,在第 14 行创建了一个空人员对象。
    3. 在两种情况下,都会初始化第3.4.3.2节所述的[edit.jsp]页面模板中的[id, version]元素。
  • 生成的 [Person] 对象被放入 [edit.jsp] 页面模板中。该模板包含以下元素:[errorEdit, id, version, firstName, errorFirstName, lastName, errorLastName, birthDate, errorBirthDate, married, numberOfChildren, errorNumberOfChildren]。 这些元素在第 19–31 行被初始化,但值为空字符串的元素除外 [erreurPrenom, erreurNom, erreurDateNaissance, erreurNbEnfants]。 我们知道,如果这些元素在模板中缺失,JSTL 库将显示空字符串作为其值。尽管 [errorEdit] 元素的值也是空字符串,但它仍被初始化,因为 [edit.jsp] 页面中对其值进行了检查。
  • 模型准备就绪后,控制权将传递至 [edit.jsp] 页面的第 33 行,该行将生成 [edit] 视图。

[doValidatePersonne] 方法


该方法处理 [POST /do/validate] 请求,用于验证更新表单。此 POST 请求由 [Validate] 按钮触发

Image

让我们回顾一下上文视图中 HTML 表单的输入元素:


<form method="post" action="<c:url value="/do/validate"/>">
...
                        <input type="text" value="${nom}" name="nom" size="20">
...
                        <input type="text" value="${datenaissance}" name="datenaissance">
...
                        <c:choose>
                            <c:when test="${marie}">
                                <input type="radio" name="marie" value="true" checked>Oui
                                <input type="radio" name="marie" value="false">Non
                            </c:when>
                            <c:otherwise>
                                <input type="radio" name="marie" value="true">Oui
                                <input type="radio" name="marie" value="false" checked>Non
                            </c:otherwise>
                        </c:choose>
...
                        <input type="text" value="${nbenfants}" name="nbenfants">
...
            <input type="hidden" value="${id}" name="id">
      <input type="hidden" value="${version}" name="version">
            <input type="submit" value="Valider">
            <a href="<c:url value="/do/list"/>">Annuler</a>
        </form>

该 POST 请求包含参数 [first_name, last_name, date_of_birth, married_to, number_of_children, id],并发送至 URL [/do/validate](第 1 行)。该请求由以下 [doValidatePerson] 方法处理:


// validation modification / addition of a person
    public void doValidatePersonne(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        // retrieve posted items
        boolean formulaireErroné = false;
        boolean erreur;
        // first name
        String prenom = request.getParameter("prenom").trim();
        // valid first name?
        if (prenom.length() == 0) {
            // we note the error
            request.setAttribute("erreurPrenom", "Le prénom est obligatoire");
            formulaireErroné = true;
        }
        // the name
        String nom = request.getParameter("nom").trim();
        // valid first name?
        if (nom.length() == 0) {
            // we note the error
            request.setAttribute("erreurNom", "Le nom est obligatoire");
            formulaireErroné = true;
        }
        // date of birth
        Date datenaissance = null;
        try {
            datenaissance = new SimpleDateFormat("dd/MM/yyyy").parse(request.getParameter("datenaissance").trim());
        } catch (ParseException e) {
            // we note the error
            request.setAttribute("erreurDateNaissance", "Date incorrecte");
            formulaireErroné = true;
        }
        // marital status
        boolean marie = Boolean.parseBoolean(request.getParameter("marie").trim());
        // number of children
        int nbenfants = 0;
        erreur = false;
        try {
            nbenfants = Integer.parseInt(request.getParameter("nbenfants").trim());
            if (nbenfants < 0) {
                erreur = true;
            }
        } catch (NumberFormatException ex) {
            // we note the error
            erreur = true;
        }
        // wrong number of children?
        if (erreur) {
            // we report the error
            request.setAttribute("erreurNbEnfants", "Nombre d'enfants incorrect");
            formulaireErroné = true;
        }
        // pERSON ID
        int id = Integer.parseInt(request.getParameter("id"));
        // is the form incorrect?
        if (formulaireErroné) {
            // redisplay the form with error messages
            showFormulaire(request, response, "");
            // finish
            return;
        }
        // the form is correct - we update the person who has been placed in the session
        // with information sent by the customer
        Personne personne = (Personne)request.getSession().getAttribute("personne");
        personne.setDatenaissance(datenaissance);
        personne.setMarie(marie);
        personne.setNbenfants(nbenfants);
        personne.setNom(nom);
        personne.setPrenom(prenom);
        // persistence
        try {
            if (id == -1) {
                // creation
                service.saveOne(personne);
            } else {
                // update
                service.updateOne(personne);
            }
        } catch (DaoException ex) {
            // redisplay the form with the error message
            showFormulaire(request, response, ex.getMessage());
            // finish
            return;
        }
        // redirects to the list of persons
        response.sendRedirect("list");
    }
 
    // display pre-filled form
    private void showFormulaire(HttpServletRequest request, HttpServletResponse response, String erreurEdit) throws ServletException, IOException {
        // prepare the view model [edit]
        request.setAttribute("erreurEdit", erreurEdit);
        request.setAttribute("id", request.getParameter("id"));
        request.setAttribute("version", request.getParameter("version"));
        request.setAttribute("prenom", request.getParameter("prenom").trim());
        request.setAttribute("nom", request.getParameter("nom").trim());
        request.setAttribute("datenaissance", request.getParameter("datenaissance").trim());
        request.setAttribute("marie", request.getParameter("marie"));
        request.setAttribute("nbenfants", request.getParameter("nbenfants").trim());
        // view display [edit]
        getServletContext().getRequestDispatcher((String) params.get("urlEdit")).forward(request, response);
    }
  • 第 7–13 行:检索 POST 请求中的 [firstName] 参数并验证其有效性。如果参数不正确,则将 [firstNameError] 元素初始化为一条错误消息,并将其放入请求属性中。
  • 第 15–21 行:对 [lastName] 参数执行相同的处理流程
  • 第 23–30 行:对 [dateOfBirth] 参数应用相同的处理流程
  • 第 32 行:我们获取 [marie] 参数。原则上,由于该参数来自单选按钮的值,因此我们不对其进行有效性检查。话虽如此,但没有任何机制能阻止程序发送一个带有虚假 [marie] 参数的 [POST /.../do/validate] 请求。 因此,我们应当验证该参数的有效性。在此,我们依赖于异常处理机制:若控制器未自行处理异常,则会触发 [exception.jsp] 页面的显示。因此,若第 32 行将 [marie] 参数转换为布尔值失败,将抛出异常,导致 [exception.jsp] 页面被发送至客户端。这种行为符合我们的预期。
  • 第 34–50 行:我们获取 [nbenfants] 参数并检查其值。
  • 第 52 行:我们获取 [id] 参数,但不检查其值
  • 第 54–59 行:如果表单无效,则将其重新显示,并附带之前生成的错误消息
  • 第 62–67 行:如果表单有效,则使用表单字段创建一个新的 [Person] 对象
  • 第 69–82 行:保存该人员。保存操作可能会失败。在多用户环境中,待修改的人员可能已被删除或已被他人修改。这种情况下,[dao] 层将抛出异常,我们在此处进行处理。
  • 第 84 行:若未发生异常,则将客户端重定向至 URL [/do/list] 以显示该组的新状态。
  • 第 79 行:若保存过程中发生异常,我们将请求重新显示初始表单,并将其异常的错误消息(第 3 个参数)传递给表单。

[showFormulaire] 方法(第 88–97 行)使用输入的值(request.getParameter(" ... ")构建 [edit.jsp] 页面所需的模型。请注意,错误消息已由 [doValidatePersonne] 方法放入模型中。第 99 行显示 [edit.jsp] 页面。

3.4.4. 测试 Web 应用程序

第 3.4.1 节中已介绍了一些测试。我们建议读者再次运行这些测试。在此,我们展示了一些额外的屏幕截图,以说明多用户环境中的数据访问冲突情况:

[Firefox] 将作为用户 U1 的浏览器。用户 U1 请求 URL [http://localhost:8080/spring-jpa-hibernate-personnes-crud/do/list]:

Image

[IE7] 将作为用户 U2 的浏览器。用户 U2 请求相同的 URL:

Image

用户 U1 开始编辑人员 [p2] 的记录:

Image

用户 U2 也进行了同样的操作:

Image

用户 U1 进行修改并提交:

用户 U2 也进行了同样的操作:

用户 U2 通过表单上的 [返回列表] 链接返回用户列表:

Image

他们找到了由 U1 修改过的 [Lemarchand](已婚,有 2 个孩子)。p2 的版本号已发生变化。现在 U2 删除了 [p2]:

U1 仍保留自己的列表,并希望再次编辑 [p2]:

U1 点击 [返回列表] 链接查看情况:

Image

他发现 [p2] 确实已不再出现在列表中……

3.4.5. 版本 2

我们对前一个版本稍作修改,改用 [service, dao, jpa] 层的归档文件,而不是它们的源代码:

  • 在 [1] 中:新的 Eclipse 项目。请注意,[service、dao、entities] 包已不复存在。这些内容已被封装在位于 [WEB-INF/lib] 目录下的 [service-dao-jpa-personne.jar] 归档文件 [2] 中。
  • 项目文件夹位于 [4]。我们将导入该项目。

无需进行其他操作。当启动新的 Web 应用程序并请求人员列表时,我们将收到以下响应:

 

Hibernate 无法找到 [Person] 实体。要解决此问题,我们必须在 [persistence.xml] 中显式声明受管理的实体:


<?xml version="1.0" encoding="UTF-8"?>
<persistence version="1.0"
    xmlns="http://java.sun.com/xml/ns/persistence"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_1_0.xsd">
    <persistence-unit name="jpa" transaction-type="RESOURCE_LOCAL">
        <class>entites.Personne</class>
    </persistence-unit>
</persistence>
  • 第 7 行:声明了 Person 实体。

完成此操作后,异常便消失了:

 

3.4.6. 更改 JPA 实现

  • 在 [1] 中:新的 Eclipse 项目
  • 在 [2] 中:TopLink 库已取代 Hibernate 库
  • 项目文件夹位于 [4]。我们将导入它。

更改 JPA 实现仅需对 [spring-config.xml] 文件进行少量修改。其他内容均保持不变。对 [spring-config.xml] 文件所做的修改已在第 3.1.9 节中进行过说明:


<?xml version="1.0" encoding="UTF-8"?>
 
<!-- the JVM must be launched with the -javaagent:C:\data\2006-2007\eclipse\dvp-jpa\lib\spring\spring-agent.jar argument 
    (à remplacer par le chemin exact de spring-agent.jar)-->
 
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
...
    <bean id="entityManagerFactory" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
        <property name="dataSource" ref="dataSource" />
        <property name="jpaVendorAdapter">
            <bean class="org.springframework.orm.jpa.vendor.TopLinkJpaVendorAdapter">
...    
            <property name="databasePlatform" value="oracle.toplink.essentials.platform.database.MySQL4Platform" />
...
    </bean>
...
</beans>

只需修改几行代码即可从 Hibernate 切换到 Toplink:

  • 第 11 行:JPA 实现现在由 Toplink 负责
  • 第 13 行:[databasePlatform] 属性的值与 Hibernate 不同,这里使用的是 Toplink 特有的类名。该类名的查找方法已在第 2.1.15.2 节中说明。

就这样。请注意,使用 Spring 切换 DBMS 或 JPA 实现是多么简单。不过,我们还没完全完成。运行应用程序时,会发生一个异常:

 

这与第3.1.9节中遇到并描述的问题相同。通过使用Spring代理启动JVM即可解决此问题。为此,请修改Tomcat的启动配置:

  • 在 [1] 中:我们选择了 [运行 / 运行...] 选项来修改 Tomcat 的配置
  • 在 [2] 中:我们选择了 [参数] 选项卡
  • 在 [3] 中:我们按照第 3.1.9 节的说明添加了 -javaagent 参数。

完成这一步后,我们就可以请求获取人员列表:

Image

3.5. 其他示例

我们本希望展示一个Web示例,其中将Spring容器替换为第3.2节中讨论的JBoss EJB3容器:

  • 在[1]中:Eclipse项目
  • 在[3]中:其在示例文件夹中的位置。我们将导入它。

我们复用了第3.2节中描述的配置文件 [jboss-config.xml, persistence.xml],然后将 [Application.java] 控制器中的 [init] 方法修改如下:


// init
    @SuppressWarnings("unchecked")
    public void init() throws ServletException {
        try {
            // retrieve servlet initialization parameters
            ServletConfig config = getServletConfig();
            // other initialization parameters are processed
            String valeur = null;
            for (int i = 0; i < paramètres.length; i++) {
                // parameter value
                valeur = config.getInitParameter(paramètres[i]);
                // present parameter?
                if (valeur == null) {
                    // we note the error
                    erreursInitialisation.add("Le paramètre [" + paramètres[i] + "] n'a pas été initialisé");
                } else {
                    // parameter value is stored
                    params.put(paramètres[i], valeur);
                }
            }
            // the [errors] view url has a special treatment
            urlErreurs = config.getInitParameter("urlErreurs");
            if (urlErreurs == null)
                throw new ServletException("Le paramètre [urlErreurs] n'a pas été initialisé");
            // application configuration
            // start the EJB3 JBoss container
            // configuration files ejb3-interceptors-aop.xml and embedded-jboss-beans.xml are used
            EJB3StandaloneBootstrap.boot(null);
 
            // Creating application-specific beans
            EJB3StandaloneBootstrap.deployXmlResource("META-INF/jboss-config.xml");
 
            // deploy all EJB found in the application classpath
            //EJB3StandaloneBootstrap.scanClasspath("WEB-INF/classes".replace("/", File.separator));
            EJB3StandaloneBootstrap.scanClasspath();
 
            // The JNDI context is initialized. The jndi.properties file is used
            InitialContext initialContext = new InitialContext();
 
            // service layer instantiation
            service = (IService) initialContext.lookup("Service/local");
            // empty the base
            clean();
            // fill it
            fill();
        } catch (Exception e) {
            throw new ServletException(e);
        }
    }
  • 第 28–38 行:启动 EJB3 容器。这将取代 Spring 容器。
  • 第 41 行:我们请求应用程序 [服务] 层的引用。

乍看之下,这些就是所需的全部更改。但在执行时,会出现以下错误:

 

我无法确定问题究竟出在哪里。Tomcat 报告的异常似乎表明,系统向 JNDI 服务请求了一个名为“TransactionManager”的对象,但该服务未能识别该对象。 解决此问题的方法就留给读者自行探索了。如果找到了解决方案,将会将其纳入本文档。