Skip to content

3. JPA 在多层架构中的应用

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

我们的测试程序是直接调用JPA层的控制台应用程序。在此过程中,我们发现了JPA层的主要方法。 当时我们处于所谓的“Java SE”(标准版)环境中。JPA 既可在 Java SE 环境中运行,也可在 Java EE5(企业版)环境中运行。

现在,我们既掌握了关系/对象桥接的配置,也熟悉了 JPA 层的方法的使用,因此我们将回归到更经典的多层架构:

将通过 [metier] 和 [dao] 组成的两层架构访问 [JPA] 层。 将使用 Spring 框架 [7],以及 JBoss 和 [8] 的容器 EJB3 来连接这些层。

我们前面提到,JPA可在SE和EE5环境中使用。 Java 环境 EE5 提供了许多与持久化数据访问相关的服务,特别是连接池、事务管理器等。对于开发人员来说,利用这些服务可能会很有帮助。 Java 环境 EE5 目前尚未广泛应用(截至 2007 年 5 月)。该环境目前可在 Sun Application Server 9.x(Glassfish)上找到。应用服务器本质上是一个 Web 应用服务器。 如果构建的是基于Swing的独立图形应用程序,则无法使用EE环境及其提供的服务。这确实是一个问题。目前已开始出现可独立运行的EE和c.a.d环境。 这些环境可在应用服务器之外使用。本文将使用的 JBoss 和 EJB3 便是此类环境。

在 EE5 环境中,各层由称为 EJB(企业 Java Bean)的对象实现。 在 EE 的早期版本中,EJB(EJB、2.x)被认为难以实现、难以测试,且有时性能不佳。 EJB2.x“实体”与EJB2.x“会话”有所区别。 简而言之,EJB2.x“实体”是数据库表中某行数据的映射,而EJB2.x“会话”则是用于实现多层架构中[metier]、 [dao] 层。 针对使用 EJB 实现的层级结构,主要批评之一是它们仅能在 EJB 容器内使用,而该容器是由 EE 环境提供的服务。这使得单元测试难以进行。 因此,在上图所示架构中, 针对基于 EJB 构建的 [metier] 和 [dao] 层的单元测试,将需要部署一个应用服务器,这操作相当繁琐,难以激励开发人员频繁进行测试。

Spring框架的诞生正是为了应对EJB2的复杂性。Spring在SE环境中提供了大量通常由EE环境提供的服务。 因此,在我们此处关注的“数据持久化”部分,Spring 提供了应用程序所需的连接池和事务管理器。Spring 的出现促进了单元测试文化的兴起,使得单元测试的实施变得容易得多。 Spring 允许通过经典的 Java 对象(POJO,即 Plain Old/Ordinary Java Object)来实现应用程序的各层,从而支持在其他场景中复用这些对象。最后,它以相当透明的方式集成了众多第三方工具,特别是诸如 Hibernate、Ibatis 等持久化工具。

Java EE5 的设计旨在弥补先前 EE 规范中的不足。 EJB 和 2.x 已更名为 EJB3。 这些是带有注释标记的 POJOs,当它们位于 EJB3 容器内时,这些注释会使其成为特殊对象。 在此容器中,EJB3 将能够利用容器的服务(连接池、事务管理器等)。 在 EJB3 容器之外,EJB3 便成为一个普通的 Java 对象。其 EJB 注解将被忽略。

上文中,我们将 Spring 和 JBoss EJB3 作为多层架构的潜在基础设施(框架)进行了展示。正是这一基础设施将提供我们所需的服务:连接池和事务管理器。

  • 在 Spring 框架下,各层将通过 POJOs 实现。这些组件将通过向 POJOs 注入依赖来访问 Spring 的服务(连接池、事务管理器):在构建这些组件时,Spring 会向其注入所需服务的引用。
  • JBoss EJB3 是一个可在应用服务器外部运行的 EJB 容器。其工作原理(对开发者而言)与 Spring 的描述类似。两者之间差异甚微。

本文最后将通过一个三层Web应用程序示例作为结尾,该示例虽然简单,但具有代表性:

3.1. 示例 1:Spring / JPA 与 Personne 实体

我们采用第2.1节中探讨的Personne实体,将其集成到多层架构中,其中各层通过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 代码按包呈现:
    • [entites]:实体包 JPA
    • [dao]:数据访问层——基于 JPA 层
    • [service]:更侧重服务而非业务逻辑的层。该层将使用容器的事务服务。
    • [tests]:包含测试程序。
  • 在 [3] 中:[jpa-spring] 库汇集了 Spring 所需的 JAR 文件(另见 [7] 和 [8])。
  • 在 [4] 中:文件夹 [conf] 汇集了本教程中使用的每个 SGBD 的 Spring 配置文件。

3.1.2. 实体 JPA

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


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;

    // 构造函数
    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());
    }

    //  获取器和设置器
...
}

3.1.3. [dao]层

[dao] 层具有以下 IDao 接口:


package dao;

import java.util.List;

import entites.Personne;

public interface IDao {
    // 通过 ID 获取人员
    public Personne getOne(Integer id);

    // 获取所有人员
    public List<Personne> getAll();

    // 保存联系人
    public Personne saveOne(Personne personne);

    // 更新联系人
    public Personne updateOne(Personne personne);

    // 通过 ID 删除联系人
    public void deleteOne(Integer id);

    // 获取姓名符合特定模式的人员
    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;

    // 通过 ID 删除人员
    public void deleteOne(Integer id) {
        Personne personne = em.find(Personne.class, id);
        if (personne == null) {
            throw new DaoException(2);
        }
        em.remove(personne);
    }

    @SuppressWarnings("unchecked")
    // 获取所有人员
    public List<Personne> getAll() {
        return em.createQuery("select p from Personne p").getResultList();
    }

    @SuppressWarnings("unchecked")
    // 获取姓名符合特定模式的人员
    public List<Personne> getAllLike(String modele) {
        return em.createQuery("select p from Personne p where p.nom like :modele")
                .setParameter("modele", modele).getResultList();
    }

    // 通过ID获取某人
    public Personne getOne(Integer id) {
        return em.find(Personne.class, id);
    }

    // 保存人员
    public Personne saveOne(Personne personne) {
        em.persist(personne);
        return personne;
    }

    // 更新人员
    public Personne updateOne(Personne personne) {
        return em.merge(personne);
    }

}
  • 首先,值得注意的是 [Dao] 的实现非常简单。这得益于使用了 JPA 层,该层承担了数据访问的主要工作。
  • 第 10 行:类 [Dao] 实现了接口 [IDao]
  • 第 13 行:类型为 [EntityManager] 的对象,将用于操作持久化上下文 JPA。 出于习惯,我们有时会将其与持久化上下文本身混为一谈。持久化上下文将包含 Personne 实体。
  • 第 12 行:代码中任何地方都没有初始化字段 [EntityManager em]。它将在应用程序启动时由 Spring 进行初始化。 正是第12行的注解 JPA @PersistenceContext 要求 Spring 将持久化上下文管理器注入到 em 中。
  • 第 26-28 行:通过 JPQL 查询获取所有人员的列表。
  • 第 32-35 行:通过 JPQL 查询获取所有姓名符合特定模式的人员列表。
  • 第38-40行:通过API JPA的find方法获取具有该标识符的人员。如果该人员不存在,则返回一个null指针。
  • 第43-46行:通过API JPA的persist方法将人员数据持久化。该方法将人员数据持久化。
  • 第49-51行:通过API JPA的merge方法更新人员。只有当被更新的人员此前处于未关联状态时,此方法才有意义。该方法将新创建的人员设为持久化。
  • 第16-22行:删除作为参数传递的个人ID的操作分为两个步骤:
    • 第 17 行:在持久化上下文中搜索该人员
    • 第18-20行:若未找到,则抛出错误代码为2的异常
    • 第21行:若找到该对象,则使用API JPA中的remove方法将其从持久化上下文中移除。
  • 目前尚未显示的是,每个方法都将在由 [service] 层启动的事务中执行。

该应用程序拥有自己的异常类型,名为 [DaoException]:


package dao;

@SuppressWarnings("serial")
public class DaoException extends RuntimeException {

    // 错误代码
    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 和 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. [metier / service] 层

[service] 层提供以下 [IService] 接口:


package service;

import java.util.List;

import entites.Personne;

public interface IService {
    // 通过 ID 获取联系人
    public Personne getOne(Integer id);

    // 获取所有人员
    public List<Personne> getAll();

    // 保存人员
    public Personne saveOne(Personne personne);

    // 更新用户
    public Personne updateOne(Personne personne);

    // 通过 ID 删除某人
    public void deleteOne(Integer id);

    // 获取姓名符合特定模式的人员
    public List<Personne> getAllLike(String modele);

    // 批量删除人员
    public void deleteArray(Personne[] personnes);

    // 一次性保存多个人员
    public Personne[] saveArray(Personne[] personnes);

    // 批量更新人员信息
    public Personne[] updateArray(Personne[] personnes);

}
  • 第 8-24 行:接口 [IService] 继承了接口 [IDao] 的方法
  • 第 27 行:方法 [deleteArray] 允许在单次事务中删除一组人员:要么全部删除,要么不删除任何人。
  • 第 30 行和第 33 行:与 [deleteArray] 类似的方法,用于在事务中保存(第 30 行)或更新(第 33 行)一组人员。

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


package service;

...

// 该类的所有方法都在事务中执行
@Transactional
public class Service implements IService {

    // [dao] 层
    private IDao dao;

    public IDao getDao() {
        return dao;
    }

    public void setDao(IDao dao) {
        this.dao = dao;
    }

    // 一次删除多人
    public void deleteArray(Personne[] personnes) {
        for (Personne p : personnes) {
            dao.deleteOne(p.getId());
        }
    }

    // 通过 ID 删除某人
    public void deleteOne(Integer id) {
        dao.deleteOne(id);
    }

    // 获取所有人员
    public List<Personne> getAll() {
        return dao.getAll();
    }

    // 获取姓名符合特定模式的用户
    public List<Personne> getAllLike(String modele) {
        return dao.getAllLike(modele);
    }

    // 通过 ID 获取某人
    public Personne getOne(Integer id) {
        return dao.getOne(id);
    }

    // 批量保存多人
    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;
    }

    // 保存一个联系人
    public Personne saveOne(Personne personne) {
        return dao.saveOne(personne);
    }

    // 一次更新多个人员
    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;
    }

    // 更新单个联系人
    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 行)将执行 [dao] 层中的方法 [deleteOne] N 次。 这 N 次执行将在方法 [deleteArray] 的执行线程内进行,因此属于同一事务。 因此,如果一切顺利,它们将被全部提交(commit);如果 [dao] 层中的 [deleteOne] 方法的 N 次执行中的任何一次发生异常,则它们将被全部回滚(rollback)。

3.1.5. 层配置

[service]、[dao] 和 [JPA] 层的配置由上述两个文件 [META-INF/persistence.xml] 和 [spring-config.xml] 负责。 这两个文件必须位于应用程序的 classpath 目录中,这也解释了为何它们位于 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 的持久化单元,该单元使用“本地”事务 c.a.d,这些事务并非由容器 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">

    <!-- 应用层 -->
    <bean id="dao" class="dao.Dao" />
    <bean id="service" class="service.Service">
        <property name="dao" ref="dao" />
    </bean>

    <!-- 持久化层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>

    <!-- 数据源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>

    <!-- 事务管理器 -->
    <tx:annotation-driven transaction-manager="txManager" />
    <bean id="txManager"
        class="org.springframework.orm.jpa.JpaTransactionManager">
        <property name="entityManagerFactory"
            ref="entityManagerFactory" />
    </bean>

    <!-- 异常转换 -->
    <bean
        class="org.springframework.dao.annotation.PersistenceExceptionTranslationPostProcessor" />

    <!-- 持久化注解 -->
    <bean
        class="org.springframework.orm.jpa.support.PersistenceAnnotationBeanPostProcessor" />

</beans>
  • 第 2-5 行:配置文件的根标签 <beans>。我们不对此标签的各个属性进行详细说明。请务必进行复制/粘贴操作,因为这些属性中的任何一个出现错误都可能导致难以理解的错误。
  • 第 8 行:bean "dao" 是对类 [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行:定义了一个数据源。我们在研究JPA实体与Hibernate的结合时,已经接触过数据源的概念:

在上文中,名为“连接池”的 [c3p0] 本可称为“数据源”。数据源提供“连接池”服务。 在 Spring 中,我们将使用 [c3p0] 以外的数据源。该数据源是来自 Apache Commons 项目的 [DBCP](DBCP [http://jakarta.apache.org/commons/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 行:用于获取 SGBD 连接的数据源。即第 35-42 行定义的 [DBCP] 数据源。
  • 第 17-27 行:要使用的 JPA 实现
  • 第18-26行:将Hibernate(第19行)定义为要使用的JPA实现
  • 第 23-24 行:Hibernate 需配合目标 SGBD(此处为 MySQL5)使用的方言 SQL。
  • 第 25 行:要求在应用程序启动时生成数据库(drop 和 create)。
  • 第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 的事务管理器需要知道 EntityManagerFactory,该类负责管理 JPA 层。该类在第 14-32 行中定义。
  • 第57-58行:定义了管理Java代码中Spring持久化注解的类,例如类[dao.Dao](第12行)中的@PersistenceContext注解
  • 第 53-54 行:定义了 Spring 类,该类主要管理 @Repository 注解,使带有此注解的类能够将 SGBD Jdbc 驱动程序的原生异常转换为 [DataAccessException] 类型的 Spring 通用异常。 该转换将原生 Jdbc 异常封装在 [DataAccessException] 类型中,该类型包含多个子类:

Image

这种转换使客户端程序能够以泛型方式处理异常,无论目标SGBD为何。我们在Java代码中未使用@Repository注解,因此第53-54行是多余的。保留它们仅出于说明目的。

至此,Spring 配置文件的讲解已告一段落。该文件结构复杂,许多内容仍显晦涩。此配置文件摘自 Spring 文档。所幸,将其适配于不同场景通常只需进行两处修改:

  • 目标数据库的配置:第 38-41 行。我们将以 Oracle 为例。
  • JPA 实现的配置:第 14-32 行。我们将以 Toplink 为例进行说明。

3.1.6. 客户端程序 [InitDB]

我们将着手编写上述架构的第一个客户端:

[InitDB]的代码如下:


package tests;

...
public class InitDB {

    // 服务层
    private static IService service;

    // 构造器
    public static void main(String[] args) throws ParseException {
        // 应用程序配置
        ApplicationContext ctx = new ClassPathXmlApplicationContext("spring-config.xml");
        // 服务层
        service = (IService) ctx.getBean("service");
        // 清空数据库
        clean();
        // 填充数据库
        fill();
        // 目视检查
        dumpPersonnes();
    }

    // 显示表内容
    private static void dumpPersonnes() {
        System.out.format("[personnes]%n");
        for (Personne p : service.getAll()) {
            System.out.println(p);
        }
    }

    // 填充表
    public static void fill() throws ParseException {
        // 创建人员
        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);
        // 保存
        service.saveArray(new Personne[] { p1, p2 });
    }

    // 删除表中的元素
    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 行:通过第 41-45 行中的 clean 方法清空数据库:
    • 第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] 单元测试

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


package tests;

....
public class TestNG {

    // 服务层
    private IService service;

    @BeforeClass
    public void init() {
        // 日志
        log("init");
        // 应用程序配置
        ApplicationContext ctx = new ClassPathXmlApplicationContext("spring-config.xml");
        // 服务层
        service = (IService) ctx.getBean("service");
    }

    @BeforeMethod
    public void setUp() throws ParseException {
        // 清空数据库
        clean();
        // 填充数据库
        fill();
    }

    // 日志
    private void log(String message) {
        System.out.println("----------- " + message);
    }

    // 显示表格内容
    private void dump() {
        log("dump");
        System.out.format("[personnes]%n");
        for (Personne p : service.getAll()) {
            System.out.println(p);
        }
    }

    // 表格填充
    public void fill() throws ParseException {
        log("fill");
        // 创建人员
        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);
        // 并保存
        service.saveArray(new Personne[] { p1, p2 });
    }

    // 删除表中的数据
    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 会清空数据库(clean 第 52-56 行),然后向其中插入两名人员(fill 第 42-49 行)。
  • 第 59 行:注解 @Test 指定了一个待执行的测试方法。接下来我们将描述这些测试。

@Test()
    public void test01() {
        log("test1");
        dump();
        // 人员列表
        List<Personne> personnes = service.getAll();
        assert 2 == personnes.size();
    }

    @Test()
    public void test02() {
        log("test2");
        // 按姓名搜索人员
        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");
        // 创建新人员
        Personne p3 = new Personne("p3", "x", new SimpleDateFormat("dd/MM/yy").parse("05/07/2001"), false, 0);
        // 保存该人员
        service.saveOne(p3);
        // 重新查询
        Personne loadedp3 = service.getOne(p3.getId());
        // 显示该人员
        System.out.println(loadedp3);
        // 验证
        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");
        // 加载人员 p1
        List<Personne> personnes = service.getAllLike("p1%");
        Personne p1 = personnes.get(0);
        // 显示
        System.out.println(p1);
        // 正在核对
        assert "p1".equals(p1.getNom());
        int version1 = p1.getVersion();
        // 修改名字
        p1.setPrenom("x");
        // 保存
        service.updateOne(p1);
        // 重新加载
        p1 = service.getOne(p1.getId());
        // 显示
        System.out.println(p1);
        // 验证版本号是否已递增
        assert (version1 + 1) == p1.getVersion();

    }
  • 第 5 行:查询人员 p1
  • 第10行:验证其姓名
  • 第11行:记录其版本号
  • 第13行:修改其名字
  • 第15行:保存修改
  • 第17行:再次查询用户p1
  • 第21行:验证其版本号是否增加了1

@Test()
    public void test05() {
        log("test5");
        // 加载第2个人
        List<Personne> personnes = service.getAllLike("p2%");
        Personne p2 = personnes.get(0);
        // 显示
        System.out.println(p2);
        // 进行验证
        assert "p2".equals(p2.getNom());
        // 删除人员 p2
        service.deleteOne(p2.getId());
        // 重新加载
        p2 = service.getOne(p2.getId());
        // 验证是否获得空指针
        assert null == p2;
        // 显示表
        dump();
    }
  • 第5行:请求人员p2
  • 第10行:验证其姓名
  • 第12行:将其删除
  • 第14行:再次查询该用户
  • 第16行:验证未找到该用户

@Test()
    public void test06() throws ParseException {
        log("test6");
        // 创建一个包含两名同名人员的数组(违反了姓名唯一性规则)
        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)};
        // 保存该表 - 应触发异常并回滚
        boolean erreur = false;
        try {
            service.saveArray(personnes);
        } catch (RuntimeException e) {
            erreur = true;
        }
        // 转储
        dump();
        // 验证
        assert erreur;
        // 查询名为 p3 的人员
        List<Personne> personnesp3 = service.getAllLike("p3%");
        assert 0 == personnesp3.size();
        // 转储
        dump();
    }
  • 第5行:创建一个包含三人的数组,其中两人名字相同为“p4”。这违反了@Entity Personne的名称唯一性规则:

    @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");
        // 乐观锁测试
        // 正在加载人员 p1
        List<Personne> personnes = service.getAllLike("p1%");
        Personne p1 = personnes.get(0);
        // 显示该人员
        System.out.println(p1);
        // 增加其子女数量
        int nbEnfants1 = p1.getNbenfants();
        p1.setNbenfants(nbEnfants1 + 1);
        // 保存 p1
        Personne newp1 = service.updateOne(p1);
        assert (nbEnfants1 + 1) == newp1.getNbenfants();
        System.out.println(newp1);
        // 第二次保存 - 此时应抛出异常,因为 p1 已不再是正确版本
        // 是 newp1 持有它
        boolean erreur = false;
        try {
            service.updateOne(p1);
        } catch (RuntimeException e) {
            erreur = true;
        }
        // 验证
        assert erreur;
        // 增加 newp1 的子节点数量
        int nbEnfants2 = newp1.getNbenfants();
        newp1.setNbenfants(nbEnfants2 + 1);
        // 保存 newp1
        service.updateOne(newp1);
        // 重新加载
        p1 = service.getOne(p1.getId());
        // 正在验证
        assert (nbEnfants1 + 2) == p1.getNbenfants();
        System.out.println(p1);
    }
  • 第 6 行:查询人员 p1
  • 第 12 行:将其子女数量增加 1
  • 第14行:在持久化上下文中更新人员p1。方法[updateOne]将p1的新版本newp1持久化。它与p1的区别在于其版本号,该版本号应已递增。
  • 第15行:验证newp1的子女数量。
  • 第21行:尝试基于旧版本p1再次更新人员p1。此时应抛出异常,因为p1并非人员p1的最新版本。最新版本是newp1
  • 第23行:验证错误是否确实发生
  • 第27-35行:验证如果从最新版本newp1进行更新,则一切正常。

@Test()
    public void test08() {
        log("test8");
        // 对 updateArray 进行回滚测试
        // 加载人员 p1
        List<Personne> personnes = service.getAllLike("p1%");
        Personne p1 = personnes.get(0);
        // 显示
        System.out.println(p1);
        // 增加其子女数量
        int nbEnfants1 = p1.getNbenfants();
        p1.setNbenfants(nbEnfants1 + 1);
        // 保存 2 项修改,其中第 2 项必须失败(人员初始化错误)
        // 由于事务的原因,这两项操作都必须被回滚
        boolean erreur = false;
        try {
            service.updateArray(new Personne[] { p1, new Personne() });
        } catch (RuntimeException e) {
            erreur = true;
        }
        // 验证
        assert erreur;
        // 重新加载 p1 对象
        personnes = service.getAllLike("p1%");
        p1 = personnes.get(0);
        // 其子女数量不应发生变化
        assert nbEnfants1 == p1.getNbenfants();
    }
  • 测试8与测试6类似:它检查rollbackupdateArray上的运行情况,后者操作一个包含两个人的数组,其中第二个元素未被正确初始化。 从 JPA 的角度来看,对尚未存在的第二个人执行合并操作,将生成一个 SQL insert 操作,该操作将因 nullable=false 约束(存在于 Personne 实体的某些字段上)而失败。

@Test()
    public void test09() {
        log("test9");
        // 对 deleteArray 进行回滚测试
        // 转储
        dump();
        // 正在加载人员 p1
        List<Personne> personnes = service.getAllLike("p1%");
        Personne p1 = personnes.get(0);
        // 显示该人员
        System.out.println(p1);
        // 执行两次删除操作,其中第二次必须失败(人员未知)
        // 由于事务关系,这两条操作必须被撤销
        boolean erreur = false;
        try {
            service.deleteArray(new Personne[] { p1, new Personne() });
        } catch (RuntimeException e) {
            erreur = true;
        }
        // 进行验证
        assert erreur;
        // 为 p1 用户充值
        personnes = service.getAllLike("p1%");
        // 验证
        assert 1 == personnes.size();
        // 转储
        dump();
    }
  • 测试 9 与前一个类似:它检查 rollback deleteArray 上的运行情况,该方法作用于一个包含两人的数组,其中第二个人不存在。 但在这种情况下,[dao] 层中的 [deleteOne] 方法会抛出异常。

// 乐观锁定 - 多线程访问
    @Test()
    public void test10() throws Exception {
        // 添加一个人员
        Personne p3 = new Personne("X", "X", new SimpleDateFormat("dd/MM/yyyy").parse("01/02/2006"), true, 0);
        service.saveOne(p3);
        int id3 = p3.getId();
        // 创建 N 个线程以更新子女数量
        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();
        }
        // 等待线程结束
        for (int i = 0; i < taches.length; i++) {
            taches[i].join();
        }
        // 获取该人员
        p3 = service.getOne(id3);
        // 该人员应有 N 个子女
        assert N == p3.getNbenfants();
        // 删除 p3 人员
        service.deleteOne(p3.getId());
        // 验证
        p3 = service.getOne(p3.getId());
        // 必须有一个空指针
        assert p3 == null;
    }
  • 测试 10 的思路是启动 N 个线程(第 9 行),以并行方式递增某人的子女数量。我们希望验证版本号系统能否很好地应对这种情况。该系统正是为此而设计的。
  • 第5-6行:创建名为p3的人员并将其持久化。该人员初始时没有子女。
  • 第7行:记录其标识符。
  • 第9-14行:并行启动N个线程,每个线程负责将p3的子女数量增加1。
  • 第16-18行:等待所有线程结束
  • 第20行:请求查看p3
  • 第22行:验证其现在是否拥有N个子节点
  • 第24行:删除p3

线程 [ThreadMajEnfants] 如下:


package tests;

...
public class ThreadMajEnfants extends Thread {
    // 线程名称
    private String name;

    // 关于 [service] 层的参考
    private IService service;

    // 待处理人员的ID
    private int idPersonne;

    // 构造函数
    public ThreadMajEnfants(String name, IService service, int idPersonne) {
        this.name = name;
        this.service = service;
        this.idPersonne = idPersonne;
    }

    // 线程核心
    public void run() {
        // 跟踪
        suivi("lancé");
        // 在未能成功递增1之前持续循环
        // 该人的子女数量 idPersonne
        boolean fini = false;
        int nbEnfants = 0;
        while (!fini) {
            // 从idPersonne获取该人的副本
            Personne personne = service.getOne(idPersonne);
            nbEnfants = personne.getNbenfants();
            // 后续操作
            suivi("" + nbEnfants + " -> " + (nbEnfants + 1) + " pour la version " + personne.getVersion());
            // 将该人员的子女数量增加1
            personne.setNbenfants(nbEnfants + 1);
            // 等待10毫秒后释放处理器
            try {
                // 跟踪
                suivi("début attente");
                // 中断以释放处理器
                Thread.sleep(10);
                // 后续
                suivi("fin attente");
            } catch (Exception ex) {
                throw new RuntimeException(ex.toString());
            }
            // 等待结束 - 尝试验证副本
            // 在此期间,其他线程可能已修改了原始文件
            try {
                // 尝试修改原始文件
                service.updateOne(personne);
                // 操作成功 - 原始文件已被修改
                fini = true;
            } catch (javax.persistence.OptimisticLockException e) {
                // 对象版本不正确:忽略异常并重试
            } catch (org.springframework.transaction.UnexpectedRollbackException e2) {
                // 不时出现的 Spring 异常
            } catch (RuntimeException e3) {
                // 其他类型的异常——已上报
                throw e3;
            }
        }
        // 跟踪
        suivi("a terminé et passé le nombre d'enfants à " + (nbEnfants + 1));
    }

    // 跟进
    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行:暂停结束后,线程请求[service]层将修改持久化。由于知道偶尔会出现异常,因此用try/catch语句包裹了该操作。
  • 第55行:测试表明会抛出类型为[javax.persistence.OptimisticLockException]的异常。 这是正常的:当线程试图修改人员 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 的值来验证这一点:所有线程的 em 值都相同。

因此,我们不禁要问:由EntityManager该值对所有线程均相同)操作的不同线程的持久化对象,是否会相互混淆并引发冲突。[ThreadMajEnfants]中就存在一个可能发生这种情况的示例:


        while (!fini) {
            // 从 idPersonne 处获取该用户的副本
            Personne personne = service.getOne(idPersonne);
            nbEnfants = personne.getNbenfants();
            // 后续
            suivi("" + nbEnfants + " -> " + (nbEnfants + 1) + " pour la version " + personne.getVersion());
            // 将该人的子节点数量增加1
            personne.setNbenfants(nbEnfants + 1);
            // 等待 10 毫秒后释放处理器
            try {
                // 后续
                suivi("début attente");
                // 中断以释放处理器
                Thread.sleep(10);
                // 后续
                suivi("fin attente");
            } catch (Exception ex) {
                throw new RuntimeException(ex.toString());
}
  • 第 3 行:一个 T1 线程获取了人员 p
  • 第 8 行:该线程将 p 的子节点数量加 1
  • 第 14 行:线程 T1 暂停

一个名为 T2 的线程接管了控制权,并同样执行了第 3 行代码:它请求的 p 与 T1 请求的 p 是同一个人。 如果线程的持久化上下文相同,那么由于 T1 已将人员 p 存入上下文,该人员应被返回给 T2。 事实上,方法 [getOne] 调用了方法 [EntityManager]。该方法通过 API 查找 JPA,并且该方法仅在请求的对象不属于持久化上下文时才会访问数据库,否则它将返回持久化上下文中的对象。 如果确实如此,T1 和 T2 将持有同一个 p。此时 T2 会再次将 p 的子节点数量增加 1(第 8 行)。 如果某个线程在暂停后成功完成了更新,那么 p 的子节点数量将增加 2 个,而不是预期的 1 个。因此,我们可能会预期这 N 个线程将子节点数量增加到 N 以上。但事实并非如此。 因此可以得出结论:T1 和 T2 引用的 p 并非同一对象。通过让各线程打印 p 的地址来验证这一点:它们各自显示的地址不同。

因此,似乎这些线程:

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

这仅是推测,此处需要专家的意见。

3.1.8. 更改为 SGBD

要更改 SGBD, 只需将文件 [src/spring-config.xml] [2] 替换为文件 [spring-config.xml],该文件来自相关文件夹 [conf] [1]文件夹中,即可解决问题。

例如,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>

    <!-- 数据源 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 行:与 SGBD 的 JDBC 连接属性

建议读者使用其他 SGBD 文件重复针对 MySQL5 描述的测试。

3.1.9. 更改 JPA 的实现

让我们回顾一下前面的测试架构:

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

  • 在 [1] 中:即 Eclipse 项目。该项目与之前的完全相同。 仅配置文件 [spring-config.xml] 和 [2] 以及库 [jpa-toplink](它取代了库 [jpa-hibernate])发生了变化。
  • [3]:本教程的示例文件夹。[4]:待导入的Eclipse项目。

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


<?xml version="1.0" encoding="UTF-8"?>

<!-- JVM 必须使用参数 -javaagent:C:\data\2006-2007\eclipse\dvp-jpa\lib\spring\spring-agent.jar 
    (à 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">

    <!-- 应用层 -->
    <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>

    <!-- 数据源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>

    <!-- 事务管理器 -->
    <tx:annotation-driven transaction-manager="txManager" />
    <bean id="txManager" class="org.springframework.orm.jpa.JpaTransactionManager">
        <property name="entityManagerFactory" ref="entityManagerFactory" />
    </bean>

    <!-- 异常处理 -->
    <bean class="org.springframework.dao.annotation.PersistenceExceptionTranslationPostProcessor" />

    <!-- 持久化 -->
    <bean class="org.springframework.orm.jpa.support.PersistenceAnnotationBeanPostProcessor" />

</beans>

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

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

就这样。值得注意的是,使用 Spring 时,我们可以非常轻松地切换 SGBD 或 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] 位于 <exemples>/lib 目录下,其前身是 [1]。该文件随 Spring 发行版 2.x 一起提供(参见第 5.11 节)。
  • 在 [3] 中,创建一个运行配置 [Run/Run...]
  • 在 [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] 中,创建一个运行配置 [Run/Run...]
  • 在 [2] 中,创建一个运行配置 TestNG
  • 在 [3] 中,选择 [Test] 选项卡
  • 在 [4] 中,为配置命名
  • 在 [5] 中,为该配置对应的 Eclipse 项目命名(使用“浏览”按钮)
  • 在 [6] 中,为测试类命名(使用“浏览”按钮)
  • 在 [7] 中,切换至 [Arguments] 选项卡。
  • 在 [8] 中:设置 JVM 的 -javaagent 参数。
  • 在 [9] 中:确认配置
  • 在 [10] 中:配置已创建
  • 在 [11] 中:执行该配置

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

3.2. 示例 2: JBoss EJB3 / JPA 与 Personne 实体

我们沿用前面的示例,但将其在 EJB3 容器中执行,该容器属于 JBoss:

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

JBoss EJB3容器的安装方法详见第5.12节。

3.2.1. Eclipse / JBoss EJB3 / Hibernate 项目

Eclipse 项目如下:

  • 在 [1] 中:Eclipse 项目。该项目可在 [6] 中找到,位于教程示例 [5] 中。我们将导入该项目。
  • 在 [2] 中:分层结构的 Java 代码以包的形式呈现:
    • [entites]:实体包 JPA
    • [dao]:数据访问层——基于 JPA 层
    • [service]:更侧重服务而非业务逻辑的层。该层将使用 EJB3 容器的事务服务。
    • [tests]:包含测试程序。
  • 在 [3] 中:库 [jpa-jbossejb3] 汇集了 Jboss Ejb3 所需的 JAR 文件(另见 [7] 和 [8])。
  • 在 [4] 中:文件夹 [conf] 汇集了本教程中使用的每个 SGBD 的配置文件。 每次都有两个:[persistence.xml] 用于配置 JPA 层,[jboss-config.xml] 用于配置 Ejb3 容器。

3.2.2. JPA 实体

此处仅管理一个实体,即第 3.1.2 节中已探讨过的实体 Personne

3.2.3. [dao] 层

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

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


package dao;

...
@Stateless
public class Dao implements IDao {

    @PersistenceContext
    private EntityManager em;

    // 通过 ID 删除人员
    @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);
    }

    // 获取所有人员
    @TransactionAttribute(TransactionAttributeType.REQUIRED)
    public List<Personne> getAll() {
        return em.createQuery("select p from Personne p").getResultList();
    }

    // 获取姓名符合特定模式的人员
    @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();
    }

    // 通过 ID 获取某人
    @TransactionAttribute(TransactionAttributeType.REQUIRED)
    public Personne getOne(Integer id) {
        return em.find(Personne.class, id);
    }

    // 保存人员
    @TransactionAttribute(TransactionAttributeType.REQUIRED)
    public Personne saveOne(Personne personne) {
        em.persist(personne);
        return personne;
    }

    // 更新人员
    @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 池,并可供其他用户的请求执行线程使用。
  • 对于开发人员而言,EJB3 @Stateless 的概念与 Spring 的单例模式非常接近。在相同场景下,开发人员会使用它。
  • 第 7 行:注解 @PersistenceContext 与 [dao] 层的 Spring 版本中出现的注解相同。 它指定了将接收 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. [metier / service]层

[service] 层实现了第 3.1.4 节中之前讨论过的 [IService] 接口。 接口 [IService] 的实现 [Service] 与第 3.1.4 节中讨论的实现完全相同,仅有三处细节不同:



@Stateless
@TransactionAttribute(TransactionAttributeType.REQUIRED)
public class Service implements IService {

    // [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">

        <!-- 提供者 JPA 是 Hibernate -->
        <provider>org.hibernate.ejb.HibernatePersistence</provider>

        <!-- 由 Java 环境管理的 DataSource JTA -->
        <jta-data-source>java:/datasource</jta-data-source>

        <properties>
            <!-- 查询 JBA 层的实体 -->
            <property name="hibernate.archive.autodetection" value="class, hbm" />

            <!-- Hibernate日志 SQL
                <property name="hibernate.show_sql" value="true"/>
                <property name="hibernate.format_sql" value="true"/>
                <property name="use_sql_comments" value="true"/>
            -->

            <!-- SGBD 管理的类型 -->
            <property name="hibernate.dialect" value="org.hibernate.dialect.MySQLInnoDBDialect" />

            <!-- 在部署持久化单元时重建所有表(drop+create) -->
            <property name="hibernate.hbm2ddl.auto" value="create" />

        </properties>
    </persistence-unit>

</persistence>

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

  • 第 5 行:持久化单元 jpa 缺少此前一直存在的 transaction-type 属性:

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

若未指定值,属性 transaction-type 的默认值为 "JTA"(用于 Java Transaction API),这表示事务管理器由 EJB 3 容器提供。 “JTA”事务管理器比“RESOURCE_LOCAL”功能更强大:它能够管理跨多个连接的事务。 使用 JTA,可以在 SGBD 1 上的 c1 连接上打开一个 t1 事务, 在 SGBD 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">

    <!-- DataSource 的工厂 -->
    <bean name="datasourceFactory" class="org.jboss.resource.adapter.jdbc.local.LocalTxDataSource">
        <!-- DataSource 的名称 JNDI -->
        <property name="jndiName">java:/datasource</property>

        <!-- 管理的数据库 -->
        <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>

        <!-- 连接池属性 -->
        <property name="minSize">0</property>
        <property name="maxSize">10</property>
        <property name="blockingTimeout">1000</property>
        <property name="idleTimeout">100000</property>

        <!-- 事务管理器,此处为 JTA -->
        <property name="transactionManager">
            <inject bean="TransactionManager" />
        </property>
        <!-- Hibernate 缓存管理器 -->
        <property name="cachedConnectionManager">
            <inject bean="CachedConnectionManager" />
        </property>
        <!-- 实例化属性 JNDI ? -->
        <property name="initialContextProperties">
            <inject bean="InitialContextProperties" />
        </property>
    </bean>

    <!-- 向工厂请求 DataSource -->
    <bean name="datasource" class="java.lang.Object">
        <constructor factoryMethod="getDatasource">
            <factory bean="datasourceFactory" />
        </constructor>
    </bean>

</deployment>
  • 第 3 行:文件的根标签为 <deployment>。该部署文件主要用于配置在 persistence.xml 中声明的 java:/datasource 数据源。
  • 数据源由第38行的Beandatasource”定义。可以看到,数据源(第40行)是从第7行Bean“datasourceFactory”定义的“工厂”中获取的。 要获取应用程序的数据源,客户端需调用 factory 中的 [getDatasource] 方法(第 39 行)。
  • 第7行:提供数据源的factory是一个JBoss类。
  • 第 9 行:数据源的名称为 JNDI。该名称必须与 persistence.xml 文件中 <jta-data-source> 标签内声明的名称一致。 实际上,JPA 层将使用该名称 JNDI 来请求数据源。
  • 第 12-15 行:较为常规的内容:连接 SGBD 的 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]。 不过,我们能够将其适配到另一个 SGBD(jboss-config.xml 的第 12-15 行,persistence.xml 的第 24 行)。由于缺乏示例,无法实现向 Toplink 的迁移。

3.2.6. 客户端程序 [InitDB]

我们将开始编写基于前述架构的第一个客户端:

[InitDB] 的代码如下:


package tests;

...
public class InitDB {

    // 服务层
    private static IService service;

    // 构造函数
    public static void main(String[] args) throws ParseException, NamingException {
        // 启动容器 EJB3 JBoss
        // 配置文件 ejb3-interceptors-aop.xml 和 embedded-jboss-beans.xml 被使用
        EJB3StandaloneBootstrap.boot(null);

        // 创建应用程序专用的 Bean
        EJB3StandaloneBootstrap.deployXmlResource("META-INF/jboss-config.xml");

        // 部署类路径中找到的所有 EJBs(速度较慢,会扫描所有内容)
        // EJB3StandaloneBootstrap.scanClasspath();

        // 部署应用程序类路径中找到的所有 EJB
        EJB3StandaloneBootstrap.scanClasspath("bin".replace("/", File.separator));

        // 初始化上下文 JNDI。使用文件 jndi.properties
        InitialContext initialContext = new InitialContext();

        // 实例化服务层
        service = (IService) initialContext.lookup("Service/local");
        // 清空数据库
        clean();
        // 正在填充数据库
        fill();
        // 进行目视检查
        dumpPersonnes();
        // 停止 EJB 容器
        EJB3StandaloneBootstrap.shutdown();

    }

    // 显示表内容
    private static void dumpPersonnes() {
        System.out.format("[personnes]-------------------------------------------------------------------%n");
        for (Personne p : service.getAll()) {
            System.out.println(p);
        }
    }

    // 填充表
    public static void fill() throws ParseException {
        // 创建人员
        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);
        // 并保存
        service.saveArray(new Personne[] { p1, p2 });
    }

    // 删除表中的数据
    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 可以通过本地(local)或网络(remote)方式访问。此处,所查找 EJB 的“Service/local”名称指代 [service] 层中的 [Service] 类,用于本地访问。
  • 现在,应用程序已部署,且持有对 [service] 层的引用。这与 Spring 版本中 [InitDB] 代码第 11 行之后的情况相同。因此,两个版本中的代码是相同的。

public class InitDB {

    // 服务层
    private static IService service;

    // 构造函数
    public static void main(String[] args) throws ParseException {
        // 应用程序配置
        ApplicationContext ctx = new ClassPathXmlApplicationContext("spring-config.xml");
        // 服务层
        service = (IService) ctx.getBean("service");
        // 清空数据库
        clean();
        // 填充数据库
        fill();
        // 目视检查
        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 {

    // 服务层
    private IService service = null;

    @BeforeClass
    public void init() throws NamingException, ParseException {
        // 日志
        log("init");
        // 启动容器 EJB3 JBoss
        // 配置文件 ejb3-interceptors-aop.xml 和 embedded-jboss-beans.xml 被利用
        EJB3StandaloneBootstrap.boot(null);

        // 创建应用程序专用的 Bean
        EJB3StandaloneBootstrap.deployXmlResource("META-INF/jboss-config.xml");

        // 部署类路径中找到的所有 EJBs(速度较慢,会扫描所有内容)
        // EJB3StandaloneBootstrap.scanClasspath();

        // 部署应用程序类路径中找到的所有 EJB
        EJB3StandaloneBootstrap.scanClasspath("bin".replace("/", File.separator));

        // 初始化上下文 JNDI。使用文件 jndi.properties
        InitialContext initialContext = new InitialContext();

        // 实例化服务层
        service = (IService) initialContext.lookup("Service/local");
        // 清空数据库
        clean();
        // 正在填充数据库
        fill();
        // 进行目视检查
        dumpPersonnes();
    }

    @AfterClass
    public void terminate() {
        // 日志
        log("terminate");
        // 关闭 EJB 容器
        EJB3StandaloneBootstrap.shutdown();
    }

    @BeforeMethod
    public void setUp() throws ParseException {
...
    }

...
}
  • init 方法(第 10-37 行)用于搭建测试所需的运行环境,其代码与前文 [InitDB] 中解释的内容相同。
  • terminate 方法(第 40-45 行)在测试结束时执行(带有 @AfterClass 注解),用于停止 EJB3 容器(第 44 行)。
  • 其余部分与 Spring 版本完全一致。

测试通过:

Image

3.2.8. 更改为 SGBD

要修改 SGBD, 只需将文件夹 [META-INF] [2] 中的内容替换为文件夹 SGBD 中的内容,该文件夹位于 [conf] [1] 文件夹内。 以 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">

        <!-- 提供者 JPA 是 Hibernate -->
        <provider>org.hibernate.ejb.HibernatePersistence</provider>

        <!-- 由 Java 环境管理的 DataSource JTA -->
        <jta-data-source>java:/datasource</jta-data-source>

        <properties>
            <!-- 查询 JBA 层的实体 -->
            <property name="hibernate.archive.autodetection" value="class, hbm" />

            <!-- Hibernate 日志 SQL
                <property name="hibernate.show_sql" value="true"/>
                <property name="hibernate.format_sql" value="true"/>
                <property name="use_sql_comments" value="true"/>
            -->

            <!-- SGBD 管理的类型 -->
            <property name="hibernate.dialect" value="org.hibernate.dialect.SQLServerDialect" />

            <!-- 在部署持久化单元时重建所有表(drop+create) -->
            <property name="hibernate.hbm2ddl.auto" value="create" />

        </properties>
    </persistence-unit>

</persistence>

仅有一行发生了变化:

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

来自 SQL 服务器的 [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">

    <!-- DataSource 的工厂 -->
    <bean name="datasourceFactory" class="org.jboss.resource.adapter.jdbc.local.LocalTxDataSource">
        <!-- DataSource 的名称 JNDI -->
        <property name="jndiName">java:/datasource</property>

        <!-- 管理的数据库 -->
        <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>

        <!-- 连接池属性 -->
    ...
    </bean>

</deployment>

仅第12-15行发生了变化:这些行提供了新JDBC连接的配置信息。

建议读者使用其他 SGBD 文件重复针对 MySQL5 描述的测试。

3.2.9. 更改 JPA 的实现

如前所述,我们尚未找到将 Jboss Ejb3 容器与 Toplink 结合使用的示例。截至目前(2007年6月),我仍不清楚这种配置是否可行。

3.3. 其他示例

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

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 版本

这些示例在架构上没有新内容。它们仅仅处于一个需要管理多个实体,且实体之间存在一对多或多对多关系的场景中,而包含实体 Personne 的示例中则没有这种情况。

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

3.4.1. 概述

本文将重新介绍以下文档中展示的一个应用程序:

[ref4]:Java 语言的 MVC 网页开发基础 [http://tahe.developpez.com/java/baseswebmvc/]。

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

该应用程序最初是基于 Ibatis / SqlMap [http://ibatis.apache.org/] 工具构建的 [dao] 层开发的,该工具负责关系型数据库与对象之间的映射。 我们只需将 Ibatis 替换为 JPA。应用程序的架构如下:

我们将编写的Web应用程序将用于管理一个人员组,并支持以下四项操作:

  • 列出组内人员
  • 向组中添加人员
  • 修改组内成员
  • 从组中删除成员

这四项基本操作在数据库表中也很常见。下方的 张截图展示了应用程序与用户交互的页面。

 

3.4.2. Eclipse 项目

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

  • 在 [1] 中:Web 项目。这是一个 [Dynamic Web Project] [2] 类型的 Eclipse 项目。 可在教程示例文件夹 [3] 中找到 [4]。将其导入。
  • 在 [5] 中:[service, dao, jpa] 层的源代码和配置。我们保留第 3.1.1 节中研究的 Eclipse 项目 [hibernate-spring-personnes-metier-dao] 的成果 [dao, entites, service]。 我们仅开发[web]层,该层在此由[web]包表示。 此外,我们保留了该项目的 [persistence.xml, spring-config.xml] 配置文件,唯一不同之处在于我们将使用 SGBD 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>

    <!-- 数据源 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] 中列出
  • 该应用程序可与多种 SGBD 版本兼容。只需修改 [spring-config.xml] 文件即可。 文件夹 [conf] [7] 包含适用于各种 SGBD 的 [spring-config.xml] 文件。

3.4.3. [web]层

我们的应用程序具有以下多层架构:

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

  • 组内人员列表
  • 向组中添加人员
  • 修改组内成员
  • 从组中删除人员

为此,该层将依托 [service] 层,而该层又将调用 [dao] 层。 我们已介绍了由 [web] 层管理的界面(第 3.4.1 节)。为描述该 Web 层,我们将依次介绍:

  • 其配置
  • 其视图
  • 其控制器
  • 若干测试

3.4.3.1. Web 应用程序的配置

让我们回顾一下 Eclipse 项目的架构:

 
  • 在 [web] 包中,我们可以找到 Web 应用程序的控制器:类 [Application]。
  • 应用程序的页面 JSP / JSTL 位于 [WEB-INF/vues] 中。
  • 文件夹 [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>
    <!-- 映射 ServletPersonne-->
    <servlet-mapping>
        <servlet-name>personnes</servlet-name>
        <url-pattern>/do/*</url-pattern>
    </servlet-mapping>
    <!--  欢迎页面 -->
    <welcome-file-list>
        <welcome-file>index.jsp</welcome-file>
    </welcome-file-list>
    <!--  意外错误页面 -->
    <error-page>
        <exception-type>java.lang.Exception</exception-type>
        <location>/WEB-INF/vues/exception.jsp</location>
    </error-page>
</web-app>
  • 第 23-26 行:URL [/do/*] 将由 Servlet [personnes] 处理
  • 第 7-8 行:Servlet [personnes] 是类 [Application] 的一个实例,我们将构建该类。
  • 第 9-20 行:定义了三个参数 [urlList, urlEdit, urlErreurs],用于标识视图 [list, edit, erreurs] 所属页面 JSP 的 URL。
  • 第 28-30 行:应用程序有一个默认登录页面 [index.jsp],位于 Web 应用程序文件夹的根目录下。
  • 第 32-35 行:该应用程序有一个默认错误页面,当 Web 服务器捕获到应用程序未处理的异常时,该页面将被显示。
    • 第 37 行:<exception-type> 标签指定了由 <error-page> 指令处理的异常类型,此处为 [java.lang.Exception] 及其派生类型,即所有异常。
    • 第 38 行:<location> 标签指定当发生 <exception-type> 定义的类型异常时,应显示页面 JSP。如果该页面包含以下指令:

<%@ page isErrorPage="true" %>
  • (续)
    • 如果 <exception-type> 指定了类型 T1,且一个未从 T1 派生的 T2 类型的异常上报至 Web 服务器,则服务器会向客户端发送一个通常不太友好的专有异常页面。 因此,[web.xml] 文件中的 <error-page> 标签就显得尤为重要。

[index.jsp]


当用户直接请求应用程序上下文而未指定 URL 时,将显示此页面,c.a.d。此处为 [/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>
  • 该视图在其模型中接收两个元素:
    • 元素 [personnes],其关联对象为类型为 [List] 的对象,该对象又包含类型为 [Personne] 的对象:即人员列表。
    • 可选元素 [erreurs],关联一个类型为 [List] 的对象,该对象包含类型为 [String] 的对象:一个错误消息列表。
  • 第 31-43 行:遍历 ${personnes} 列表,以显示一个包含该组人员的 HTML 数组。
  • 第 40 行:通过当前人员的 [id] 字段设置 [Modifier] 链接指向的 URL,以便与 URL [/do/edit] 关联的控制器知道应修改哪位人员。
  • 第41行:链接[Supprimer]也采用同样的设置。
  • 第 37 行:为了以 JJ/MM/AAAA 的形式显示该人员的出生日期, 需使用 Apache 项目 [Jakarta Taglibs] 中标签库 [DateTime] 的 <dt> 标签:

Image

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

  • 第46行:添加新人员的链接[Ajout]的目标URL是[/do/edit],与第40行的链接[Modifier]相同。 参数 [id] 的值为 -1,这表明当前操作是添加而非修改。
  • 第 10-18 行:如果模型中包含 ${erreurs} 元素,则显示其中包含的错误消息。

视图 [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>

该视图展示了一个用于添加新人员或更新现有人员的表单。为简化后续表述,我们将统一使用 [mise à jour] 这一术语。 按钮 [Valider](第 73 行)会触发表单中的 POST,并跳转至 URL [/do/validate](第 16 行)。 如果 POST 操作失败,则重新显示视图 [edit.jsp] 并显示发生的错误;否则,显示视图 [list.jsp]。

  • 无论是在 GET 还是在失败的 POST 上显示的 [edit.jsp] 视图,其模板中都会包含以下元素:
属性
GET
POST
id
被更新人员的
的标识符
同上
version
其版本
同上
prenom
他的名字
已输入的名字
nom
姓氏
输入的姓
datenaissance
出生日期
输入的出生日期
marie
婚姻状况
已输入的婚姻状况
nbenfants
子女数量
输入的子女数
erreurEdit
一条错误消息,提示添加
或修改失败
。若无错误,则为空。
erreurPrenom
表示名字有误——否则为空
erreurNom
为空
表示姓氏错误——否则为空
erreurDateNaissance
表示出生日期错误——否则为空
erreurNbEnfants
表示子女数量错误 – 否则为空
  • 第11-15行:如果表单的POST出现问题,将返回[erreurEdit!='']并显示错误信息。
  • 第 16 行:表单将提交至 URL [/do/validate]
  • 第20行:显示模板中的[id]元素
  • 第 24 行:显示模板中的 [version] 元素
  • 第26-32行:输入人员的名字:
    • 在表单(GET)初次显示时, ${prenom} 显示更新后对象 [Personne] 中字段 [prenom] 的当前值,而 ${erreurPrenom} 为空。
    • 如果在 POST 之后发生错误,则重新显示已输入的值 ${prenom} 以及可能的错误消息 ${erreurPrenom}
  • 第33-39行:输入人员姓名
  • 第40-46行:输入该人的出生日期
  • 第 47-61 行:通过单选按钮输入该人的婚姻状况。使用对象 [Personne] 中的字段 [marie] 的值来确定应勾选哪个单选按钮。
  • 第 62-68 行:输入该人的子女数量
  • 第 71 行:一个名为 [id] 的隐藏字段 HTML,其值为当前正在更新的该人的 [id] 字段;若为新增则为 -1,若为修改则为其他值。
  • 第 72 行:一个名为 [version] 的隐藏字段 HTML,其值为当前正在更新的该人的字段 [id]。
  • 第 73 行:表单中类型为 [Submit] 的按钮 [Valider]
  • 第 74 行:一个用于返回人员列表的链接。该链接命名为 [Annuler],因为它允许用户在不提交表单的情况下退出表单。

视图 [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 状态码进行报告。 不同浏览器对状态码 500 的响应各不相同:Firefox 会显示可能随该响应附带的文档,而其他浏览器则会忽略该文档并显示其自身的页面。 正因如此,我们将 500 状态码替换为 200 状态码。
  • 第 16 行:显示异常文本
  • 第 18 行:向用户提供返回人员列表的链接

视图 [erreurs.jsp]


该视图用于显示一个页面,用于报告应用程序的初始化错误(c.a.d)以及在执行控制器 Servlet 的 [init] 方法时检测到的错误。 例如,如下例所示,文件 [web.xml] 中缺少某个参数:

Image

页面 [erreurs.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>

该页面在其模板中接收了一个 [erreurs] 元素,该元素是一个 [ArrayList] 类型的对象,包含 [String] 对象,后者是错误消息。这些错误消息通过第 13-15 行的循环显示出来。

3.4.3.3. 应用程序控制器

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

Image


的结构与初始化


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


package web;

...


@SuppressWarnings("serial")
public class Application extends HttpServlet {
    // 实例参数
    private String urlErreurs = null;
    private ArrayList erreursInitialisation = new ArrayList<String>();
    private String[] paramètres = { "urlList", "urlEdit", "urlErreurs" };
    private Map params = new HashMap<String, String>();

    // 服务
    private IService service = null;

    // 初始化
    @SuppressWarnings("unchecked")
    public void init() throws ServletException {
        // 获取 Servlet 的初始化参数
        ServletConfig config = getServletConfig();
        // 处理其他初始化参数
        String valeur = null;
        for (int i = 0; i < paramètres.length; i++) {
            // 参数值
            valeur = config.getInitParameter(paramètres[i]);
            // 参数是否存在?
            if (valeur == null) {
                // 记录错误
                erreursInitialisation.add("Le paramètre [" + paramètres[i] + "] n'a pas été initialisé");
            } else {
                // 保存参数值
                params.put(paramètres[i], valeur);
            }
        }
        // 视图 [erreurs] 的 URL 具有特殊处理
        urlErreurs = config.getInitParameter("urlErreurs");
        if (urlErreurs == null)
            throw new ServletException("Le paramètre [urlErreurs] n'a pas été initialisé");
        // 应用程序配置
        ApplicationContext ctx = new ClassPathXmlApplicationContext("spring-config.xml");
        // 服务层
        service = (IService) ctx.getBean("service");
        // 清空数据库
        clean();
        // 填充数据库
        try {
            fill();
        } catch (ParseException e) {
            throw new ServletException(e);
        }
    }

    // 填充表
    public void fill() throws ParseException {
        // 创建人员
        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);
        // 并保存
        service.saveArray(new Personne[] { p1, p2 });
    }

    // 删除表中的数据
    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 {
...
    }

    // 显示人员列表
    private void doListPersonnes(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
...
    }

    // 修改/添加人员
    private void doEditPersonne(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
...
    }

    // 删除人员
    private void doDeletePersonne(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
...
    }

    // 确认修改/添加人员
    public void doValidatePersonne(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
...
    }

    // 显示预填表单
    private void showFormulaire(HttpServletRequest request, HttpServletResponse response, String erreurEdit) throws ServletException, IOException {
    ...
    }

    // 提交
    public void doPost(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
        // 将控制权移交至 GET
        doGet(request, response);
    }

}
  • 第 21-34 行:从文件 [web.xml] 中获取预期参数。
  • 第 37-39 行:参数 [urlErreurs] 必须存在,因为它指定了视图 [erreurs] 的 URL,该视图用于显示可能出现的初始化错误。 如果该视图不存在,则通过调用 [ServletException](第 39 行)来中断应用程序。此异常将上报至 Web 服务器,并由文件 [web.xml] 中的 <error-page> 标签进行处理。 因此将显示视图 [exception.jsp]:

Image

上方的链接 [Retour à la liste] 目前无法使用。只要应用程序未被修改并重新加载,使用该链接将返回相同的响应。正如我们之前所见,它对于其他类型的异常非常有用。

  • 第 40-43 行:利用 Spring 配置文件获取 [service] 层的引用。 控制器初始化完成后,其方法将获得对 [service] 层中 [service] 的引用(第 15 行),并利用该引用执行用户请求的操作。 这些操作将被方法 [doGet] 拦截,并由控制器中的特定方法进行处理:
Url
方法 HTTP
控制器方法
/do/list
GET
doListPersonnes
/do/edit
GET
doEditPersonne
/do/validate
POST
doValidatePersonne
/do/delete
GET
doDeletePersonne

方法 [doGet]


该方法旨在将用户请求的操作引导至正确的处理方法。其代码如下:


// GET
    @SuppressWarnings("unchecked")
    public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {

        
// 检查Servlet初始化情况
        if (erreursInitialisation.size() != 0) {
            // 转至错误页面
            request.setAttribute("erreurs", erreursInitialisation);
            getServletContext().getRequestDispatcher(urlErreurs).forward(request, response);
            // 结束
            return;
        }
        // 获取发送请求的方法
        String méthode = request.getMethod().toLowerCase();
        // 获取待执行的操作
        String action = request.getPathInfo();
        // 操作?
        if (action == null) {
            action = "/list";
        }
        // 执行操作
        if (méthode.equals("get") && action.equals("/list")) {
            // 人员列表
            doListPersonnes(request, response);
            return;
        }
        if (méthode.equals("get") && action.equals("/delete")) {
            // 删除人员
            doDeletePersonne(request, response);
            return;
        }
        if (méthode.equals("get") && action.equals("/edit")) {
            // 显示添加/修改人员的表单
            doEditPersonne(request, response);
            return;
        }
        if (méthode.equals("post") && action.equals("/validate")) {
            // 人员添加/修改表单验证
            doValidatePersonne(request, response);
            return;
        }
        // 其他情况
        doListPersonnes(request, response);
    }
  • 第7-13行:检查初始化错误列表是否为空。若非如此,则显示视图[erreurs(erreurs)]以报告相关错误。
  • 第 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

其代码如下:


    // 人员列表显示
    private void doListPersonnes(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        // 视图模板 [list]
        request.setAttribute("personnes", service.getAll());
        // 显示视图 [list]
        getServletContext().getRequestDispatcher((String) params.get("urlList")).forward(request, response);
}
  • 第 4 行:向 [service] 层请求该组的人员列表,并将该列表放入模型中,键名为“personnes”。
  • 第6行:显示第3.4.3.2节中描述的视图[list.jsp]。

方法 [doDeletePersonne]


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

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 行显示了链接 [Supprimer] 的 URL [/do/delete?id=XX]。 负责处理该 URL 的方法 [doDeletePersonne] 应删除 id=XX 的用户,然后显示该组的新成员列表。其代码如下:


// 删除人员
    private void doDeletePersonne(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        // 获取人员ID
        int id = Integer.parseInt(request.getParameter("id"));
        // 删除联系人
        service.deleteOne(id);
        // 跳转至人员列表
        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]。 由于刚刚处理的是 [/do/delete],因此重定向 URL 将是 [/do/list]。浏览器将被引导访问 [GET /do/list],从而显示人员列表。

方法 [doEditPersonne]


该方法处理请求 [GET /do/edit?id=XX],该请求用于获取 id=XX 该人员的更新表单。 URL [/do/edit?id=XX] 是视图 [list.jsp] 中链接 [Modifier] 和 [Ajout] 的目标地址:

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 行显示了链接 [Modifier] 的 URL [/do/edit?id=XX],第 17 行显示了链接 [Ajout] 的 URL [/do/edit?id=-1]。 方法 [doEditPersonne] 应显示 ID 为 XX 的用户编辑表单;若为新增操作,则应显示空白表单。

  • 上文中的 [1] 对应添加表单,而 [2] 对应修改表单。

方法 [doEditPersonne] 的代码如下:


// 修改/添加人员
    private void doEditPersonne(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        // 获取人员ID
        int id = Integer.parseInt(request.getParameter("id"));
        // 添加还是修改?
        Personne personne = null;
        if (id != -1) {
            // 修改 - 获取待修改的人员
            personne = service.getOne(id);
            request.setAttribute("id", personne.getId());
            request.setAttribute("version", personne.getVersion());
        } else {
            // 添加 - 创建一个空记录
            personne = new Personne();
            request.setAttribute("id", -1);
            request.setAttribute("version", -1);
        }
        // 将对象 [Personne] 放入用户会话中
        request.getSession().setAttribute("personne", personne);
        // 并将对象 [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());
        // 显示视图 [edit]
        getServletContext().getRequestDispatcher((String) params.get("urlEdit")).forward(request, response);
    }
  • GET的目标是一个类似[/do/edit?id=XX]格式的URL。第4行,我们获取[id]的值。随后有两种情况:
    1. 如果 id 不等于 -1,则表示需要修改,需显示一个预先填入待修改人员信息的表单。第 9 行,该人员信息从 [service] 层获取。
    2. 如果 id 等于 -1,则表示添加操作,需显示一个空表单。为此,在第 14 行创建了一个空人员记录。
    3. 在两种情况下,第3.4.3.2节所述的页面模板[edit.jsp]中的[id, version]元素均被初始化。
  • 生成的对象 [Personne] 被放置在页面模板 [edit.jsp] 中。该模板包含以下元素 [erreurEdit, id, version, prenom, erreurPrenom, nom, erreurNom, datenaissance, erreurDateNaissance, marie, nbenfants, erreurNbEnfants]。 这些元素在第 19-31 行被初始化,但值为空字符串 [erreurPrenom, erreurNom, erreurDateNaissance, erreurNbEnfants] 的元素除外。已知如果它们在模板中缺失,JSTL 库将显示一个空字符串作为其值。 尽管元素 [erreurEdit] 的值也是空字符串,但它仍会被初始化,因为在页面 [edit.jsp] 中对其值进行了检测。
  • 模型准备就绪后,控制权将移交至 [edit.jsp] 页面的第 33 行,该行将生成视图 [edit]。

方法 [doValidatePersonne]


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

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 包含参数 [prenom, nom, datenaissance, marie, nbenfants, id],并发送到 URL [/do/validate](第 1 行)。该请求由以下方法 [doValidatePersonne] 处理:


// 验证人员修改/添加
    public void doValidatePersonne(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        // 获取提交的数据
        boolean formulaireErroné = false;
        boolean erreur;
        // 名字
        String prenom = request.getParameter("prenom").trim();
        // 名字有效吗?
        if (prenom.length() == 0) {
            // 记录错误
            request.setAttribute("erreurPrenom", "Le prénom est obligatoire");
            formulaireErroné = true;
        }
        // 姓氏
        String nom = request.getParameter("nom").trim();
        // 名字有效吗?
        if (nom.length() == 0) {
            // 记录错误
            request.setAttribute("erreurNom", "Le nom est obligatoire");
            formulaireErroné = true;
        }
        // 出生日期
        Date datenaissance = null;
        try {
            datenaissance = new SimpleDateFormat("dd/MM/yyyy").parse(request.getParameter("datenaissance").trim());
        } catch (ParseException e) {
            // 记录错误
            request.setAttribute("erreurDateNaissance", "Date incorrecte");
            formulaireErroné = true;
        }
        // 婚姻状况
        boolean marie = Boolean.parseBoolean(request.getParameter("marie").trim());
        // 子女数量
        int nbenfants = 0;
        erreur = false;
        try {
            nbenfants = Integer.parseInt(request.getParameter("nbenfants").trim());
            if (nbenfants < 0) {
                erreur = true;
            }
        } catch (NumberFormatException ex) {
            // 记录错误
            erreur = true;
        }
        // 子女数量有误?
        if (erreur) {
            // 报告错误
            request.setAttribute("erreurNbEnfants", "Nombre d'enfants incorrect");
            formulaireErroné = true;
        }
        // 人员ID
        int id = Integer.parseInt(request.getParameter("id"));
        // 表单有误吗?
        if (formulaireErroné) {
            // 重新显示包含错误提示的表单
            showFormulaire(request, response, "");
            // 完成
            return;
        }
        // 表单正确 - 更新已存入会话中的用户信息
        // 使用客户端发送的信息
        Personne personne = (Personne)request.getSession().getAttribute("personne");
        personne.setDatenaissance(datenaissance);
        personne.setMarie(marie);
        personne.setNbenfants(nbenfants);
        personne.setNom(nom);
        personne.setPrenom(prenom);
        // 持久化
        try {
            if (id == -1) {
                // 创建
                service.saveOne(personne);
            } else {
                // 更新
                service.updateOne(personne);
            }
        } catch (DaoException ex) {
            // 重新显示表单并显示发生的错误信息
            showFormulaire(request, response, ex.getMessage());
            // 完成
            return;
        }
        // 重定向至人员列表
        response.sendRedirect("list");
    }

    // 显示预填充表单
    private void showFormulaire(HttpServletRequest request, HttpServletResponse response, String erreurEdit) throws ServletException, IOException {
        // 准备视图模板 [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());
        // 显示视图 [edit]
        getServletContext().getRequestDispatcher((String) params.get("urlEdit")).forward(request, response);
    }
  • 第 7-13 行:获取请求 POST 中的参数 [prenom] 并验证其有效性。 如果参数不正确,则将 [erreurPrenom] 元素初始化为一条错误消息,并将其放入请求的属性中。
  • 第 15-21 行:对参数 [nom] 进行类似操作
  • 第 23-30 行:对参数 [datenaissance] 进行类似操作
  • 第 32 行:获取参数 [marie]。 我们不对其有效性进行验证,因为该参数原则上来自单选按钮的值。话虽如此,没有任何东西能阻止程序生成一个 [POST /.../do/validate] 并附带一个虚构的 [marie] 参数。 因此,我们应当验证该参数的有效性。在此,我们依赖于异常处理机制:若控制器自身未处理异常,则会触发显示页面 [exception.jsp]。 因此,如果第 32 行将参数 [marie] 转换为布尔值失败,将抛出一个异常,导致向客户端发送页面 [exception.jsp]。这种工作方式符合我们的预期。
  • 第34-50行:获取参数[nbenfants]并验证其值。
  • 第 52 行:获取参数 [id],但不验证其值
  • 第 54-59 行:如果表单有误,则将其重新显示,并附上之前生成的错误信息
  • 第62-67行:如果有效,则使用表单中的元素构建一个新的[Personne]对象
  • 第69-82行:保存该人员信息。保存操作可能失败。在多用户环境中,待修改的人员可能已被删除,或已被他人修改。在此情况下,[dao]层将抛出异常,该异常在此处进行处理。
  • 第84行:若未发生异常,则将客户端重定向至URL [/do/list],以展示该组的新状态。
  • 第 79 行:如果保存时发生异常,则重新请求显示初始表单,并将其异常错误消息(第 3 个参数)传递给表单。

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

3.4.4. Web 应用程序的测试

第 3.4.1 节中已介绍了一些测试。我们建议读者重新运行这些测试。此处展示的其他屏幕截图说明了多用户环境中数据访问冲突的情况:

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

Image

[IE7] 是用户 U2 的浏览器。该用户请求相同的 URL:

Image

用户 U1 进入对用户 [p2] 的编辑界面:

Image

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

Image

用户 U1 进行修改并提交:

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

用户 U2 通过表单中的链接 [Retour à la liste] 返回人员列表:

Image

他找到了 [Lemarchand] 这一条目,该条目已被 U1 修改过(已婚,有两个孩子)。 p2的版本号已更改。现在U2将删除[p2]:

U1 仍保留其独立列表,并计划再次修改 [p2]:

U1 使用链接 [Retour à la liste] 查看具体情况:

Image

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

3.4.5. 版本 2

我们对前一版本稍作修改,改用 [service, dao, jpa] 的图层存档,而非其源代码:

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

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

 

Hibernate 找不到实体 [Personne]。为解决此问题,必须在 [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 行:声明了实体 Personne

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

 

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"?>

<!-- 必须使用参数 -javaagent:C:\data\2006-2007\eclipse\dvp-jpa\lib\spring\spring-agent.jar 
    (à 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 时,更改 SGBD 或 JPA 的实现非常简单。不过,我们还没完全搞定。当运行应用程序时,会出现一个异常:

 

这正是第 3.1.9 节中提到并描述过的问题。通过使用 Spring 代理启动 JVM 即可解决。为此,我们需要修改 Tomcat 的启动配置:

  • 将 [1] 改为 [1]:我们采用了 [Run / Run...] 选项来修改 Tomcat 的配置
  • 为 [2]:我们选择了 [Arguments] 选项卡
  • 在 [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 {
            // 获取 Servlet 的初始化参数
            ServletConfig config = getServletConfig();
            // 处理其他初始化参数
            String valeur = null;
            for (int i = 0; i < paramètres.length; i++) {
                // 参数值
                valeur = config.getInitParameter(paramètres[i]);
                // 参数是否存在?
                if (valeur == null) {
                    // 记录错误
                    erreursInitialisation.add("Le paramètre [" + paramètres[i] + "] n'a pas été initialisé");
                } else {
                    // 存储参数值
                    params.put(paramètres[i], valeur);
                }
            }
            // 视图 [erreurs] 的 URL 具有特殊处理
            urlErreurs = config.getInitParameter("urlErreurs");
            if (urlErreurs == null)
                throw new ServletException("Le paramètre [urlErreurs] n'a pas été initialisé");
            // 应用程序配置
            // 启动容器 EJB3 JBoss
            // 配置文件 ejb3-interceptors-aop.xml 和 embedded-jboss-beans.xml 被调用
            EJB3StandaloneBootstrap.boot(null);

            // 创建应用程序专用的 Bean
            EJB3StandaloneBootstrap.deployXmlResource("META-INF/jboss-config.xml");

            // 部署应用程序类路径中找到的所有 EJB
            //EJB3StandaloneBootstrap.scanClasspath("WEB-INF/classes".replace("/", File.separator));
            EJB3StandaloneBootstrap.scanClasspath();

            // 初始化上下文 JNDI。调用文件 jndi.properties
            InitialContext initialContext = new InitialContext();

            // 实例化服务层
            service = (IService) initialContext.lookup("Service/local");
            // 清空数据库
            clean();
            // 正在填充数据库
            fill();
        } catch (Exception e) {
            throw new ServletException(e);
        }
    }
  • 第 28-38 行:启动 Ejb3 容器。该容器将取代 Spring 容器。
  • 第 41 行:请求应用程序 [service] 层的引用。

原则上,只需进行上述修改。但在运行时,出现以下错误:

 

我无法确定问题究竟出在哪里。 Tomcat报告的异常似乎表明,名为“TransactionManager”的对象被请求自服务JNDI,而该服务并不认识该对象。我将留给读者自行解决此问题。若找到解决方案,将补充到本文档中。