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] 类型中,该类型包含多个子类:

这种转换使客户端程序能够以泛型方式处理异常,无论目标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] 后,结果如下:
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类似:它检查rollback在updateArray上的运行情况,后者操作一个包含两个人的数组,其中第二个元素未被正确初始化。 从 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] 时,将得到以下结果:

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行的Bean“datasource”定义。可以看到,数据源(第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] 后得到以下结果:
建议读者阅读这些日志。其中包含关于 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 版本完全一致。
测试通过:

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]
该视图用于显示人员列表:

其代码如下:
<%@ 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énom</th>
<th>Nom</th>
<th>Date de naissance</th>
<th>Marié</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> 标签:

该标签库的描述文件在第 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é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é</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 |
被更新人员的 的标识符 | 同上 | |
其版本 | 同上 | |
他的名字 | 已输入的名字 | |
姓氏 | 输入的姓 | |
出生日期 | 输入的出生日期 | |
婚姻状况 | 已输入的婚姻状况 | |
子女数量 | 输入的子女数 | |
空 | 一条错误消息,提示添加 或修改失败 。若无错误,则为空。 | |
空 | 表示名字有误——否则为空 | |
为空 | 表示姓氏错误——否则为空 | |
空 | 表示出生日期错误——否则为空 | |
空 | 表示子女数量错误 – 否则为空 |
- 第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 à 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] 中缺少某个参数:

页面 [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] 中:

的结构与初始化
控制器[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]:

上方的链接 [Retour à la liste] 目前无法使用。只要应用程序未被修改并重新加载,使用该链接将返回相同的响应。正如我们之前所见,它对于其他类型的异常非常有用。
- 第 40-43 行:利用 Spring 配置文件获取 [service] 层的引用。 控制器初始化完成后,其方法将获得对 [service] 层中 [service] 的引用(第 15 行),并利用该引用执行用户请求的操作。 这些操作将被方法 [doGet] 拦截,并由控制器中的特定方法进行处理:
Url | 方法 HTTP | 控制器方法 |
GET | doListPersonnes | |
GET | doEditPersonne | |
POST | doValidatePersonne | |
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] 请求:

其代码如下:
// 人员列表显示
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] 的地址:

其代码如下:
...
<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]:

- 第 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] 的目标地址:

其代码如下:
...
<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]的值。随后有两种情况:
- 如果 id 不等于 -1,则表示需要修改,需显示一个预先填入待修改人员信息的表单。第 9 行,该人员信息从 [service] 层获取。
- 如果 id 等于 -1,则表示添加操作,需显示一个空表单。为此,在第 14 行创建了一个空人员记录。
- 在两种情况下,第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] 触发:

回顾上图中表单 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]:

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

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

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

用户 U1 进行修改并提交:
![]() |
用户 U2 也进行了同样的操作:
![]() |
用户 U2 通过表单中的链接 [Retour à la liste] 返回人员列表:

他找到了 [Lemarchand] 这一条目,该条目已被 U1 修改过(已婚,有两个孩子)。 p2的版本号已更改。现在U2将删除[p2]:
![]() |
U1 仍保留其独立列表,并计划再次修改 [p2]:
![]() |
U1 使用链接 [Retour à la liste] 查看具体情况:

他发现 [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 参数。
完成上述操作后,即可查询人员列表:

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,而该服务并不认识该对象。我将留给读者自行解决此问题。若找到解决方案,将补充到本文档中。





























































