3. 示例应用程序 – 01:rdvmedecins-jsf2-ejb
- [ref7]: 《Java EE 5 入门》(2010 年 6 月)[http://tahe.developpez.com/java/javaee]。本文档介绍了 JSF 1 和 EJB 3。
- [ref8]: 《Java 持久化实践》(2007 年 6 月)[http://tahe.developpez.com/java/jpa]。本文档介绍了使用 JPA(Java 持久化 API)进行数据持久化。
- [ref9]: 使用 NetBeans 和 GlassFish 服务器构建 Java EE Web 服务(2009 年 1 月)[http://tahe.developpez.com/java/webservice-jee]。本文探讨了构建 Web 服务的过程。
本文将研究的示例应用程序源自[ref9]。
3.1. 应用程序
一家IT服务公司[ISTIA-AGI]希望提供预约安排服务。其主要目标市场是个体执业医生。这些医生通常没有行政人员。因此,希望预约的客户会直接致电医生。这经常打乱医生全天的工作安排,从而减少了他们为患者提供诊疗的时间。公司[ISTIA-AGI]希望根据以下原则为他们提供预约安排服务:
- 由一名接待员负责为大量医生处理预约安排。该接待员可以仅由一人担任。其薪资由所有使用该预约服务的医生共同分摊。
- 行政办公室和所有医生均连接到互联网
- 预约信息记录在集中式数据库中,行政办公室和医生均可通过互联网访问
- 预约通常由行政办公室安排。医生也可以自行安排预约。特别是在诊疗结束时,医生为患者安排下次预约的情况。
预约安排服务的架构如下:
![]() |
如果医生不再需要管理预约,工作效率就会提高。如果医生数量足够多,他们对行政办公室运营成本的贡献将微乎其微。
该公司[ISTIA-AGI]决定开发两个版本的应用程序:
- 一个基于 JSF / EJB3 / JPA EclipseLink / Glassfish 服务器的版本:
![]() |
- 以及一个 JSF / Spring / JPA Hibernate / Tomcat 服务器版本:
![]() |
3.2. 应用程序的工作原理
我们将该应用程序命名为 [RdvMedecins]。以下是展示其工作原理的屏幕截图。
该应用程序的主页如下所示:
![]() |
用户(前台接待员、医生)将在此初始页面上执行多项操作。我们将在下文中进行说明。左侧视图显示用户发起请求的页面;右侧视图显示服务器返回的响应。
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
最后,还可能出现一个错误页面:
![]() |
3.3. 数据库
让我们回到即将构建的应用程序的架构:
![]() |
我们将该数据库命名为 [ dbrdvmedecins2],这是一个包含四个表的 MySQL5 数据库:
![]() |
3.3.1. [MEDECINS] 表
该表包含由 [RdvMedecins] 应用程序管理的医生信息。
![]() | ![]() |
- ID:医生的ID号——该表的主键
- VERSION:一个标识表中该行版本的数字。每次对该行进行修改时,该数字都会增加 1。
- LAST_NAME:医生的姓
- FIRST_NAME:医生的名字
- TITLE:称谓(Ms.、Mrs.、Mr.)
3.3.2. [CLIENTS] 表
各医生的患者信息存储在 [CLIENTS] 表中:
![]() | ![]() |
- ID:客户的ID号——该表的主键
- VERSION:一个标识该表中行版本的数字。每次对该行进行修改时,该数字会递增1。
- LAST NAME:客户的姓
- FIRST NAME:客户的名字
- TITLE:称谓(Ms.、Mrs.、Mr.)
3.3.3. [SLOTS] 表
该表列出了可预约的时间段:
![]() |
![]() |
- ID:时间段的ID号——该表的主键(第8行)
- VERSION:标识表中该行版本的编号。每次对该行进行修改时,该编号会递增1。
- DOCTOR_ID:标识该时段所属医生的ID号——作为DOCTORS表中ID列的外键。
- START_TIME:时间段的开始时间
- MSTART:时间段的开始分钟
- HFIN:时段结束时间
- MFIN:该时段的结束分钟
例如,[SLOTS] 表(参见上文 [1])的第二行表明,第 2 号时段于上午 8:20 开始,上午 8:40 结束,属于第 1 号医生(Marie PELISSIER 女士)。
3.3.4. [RV] 表
该表列出了每位医生的预约情况:
![]() |
- ID:预约的唯一标识符——主键
- DAY:预约日期
- SLOT_ID:预约时段——作为外键关联至[SLOTS]表的[ID]字段——同时确定时段及负责医生。
- CLIENT_ID:被预约客户的ID——作为[CLIENTS]表中[ID]字段的外键
该表对关联列(DAY、SLOT_ID)的值设置了唯一性约束:
如果 [RV] 表中某行在 (DAY, SLOT_ID) 列中显示的值为 (DAY1, SLOT_ID1),则该值不能出现在其他任何地方。否则,这意味着同一医生在同一时间段内被预约了两次。从 Java 编程的角度来看,当这种情况发生时,数据库的 JDBC 驱动程序会抛出一个 SQLException 异常。
ID 为 3 的行(参见上文 [1])表示,2006 年 8 月 23 日为第 20 个时段和第 4 号客户预订了一次预约。[SLOTS] 表告诉我们,第 20 个时段对应于下午 4:20 至 4:40,并属于第 1 号医生(Marie PELISSIER 女士)。 [CLIENTS] 表显示,客户编号 4 是 Brigitte BISTROU 女士。
3.3.5. 生成数据库
要创建这些表并填充数据,您可以使用示例网站上的脚本 [dbrdvmedecins2.sql]。使用 [WampServer](参见第1.3.3节),请按以下步骤操作:
![]() |
- 在 [1] 中,点击 [WampServer] 图标并选择 [PhpMyAdmin] 选项 [2],
- 在 [3] 处,于弹出的窗口中选择 [Databases] 链接,
![]() |
- 在 [2] 中,创建一个名称为 [4]、编码为 [5] 的数据库,
- 在 [7] 中,数据库已创建。点击其链接,
![]() |
- 在 [8] 中,导入一个 SQL 文件,
- 该文件可通过 [9] 按钮从文件系统中选择,
![]() |
- 在 [11] 中选择 SQL 脚本,并在 [12] 中执行它,
- 在 [13] 中,数据库中的四个表已创建完成。请点击其中一个链接,
![]() |
- 在 [14] 中,显示该表的内容。
我们不会再回到这个数据库。不过,欢迎读者在整个程序过程中关注它的演变,特别是在出现问题时。
3.4. [DAO] 和 [JPA] 层
让我们回到需要构建的架构:
![]() |
我们将构建四个 Maven 项目:
- 一个项目用于 [DAO] 和 [JPA] 层,
- 一个项目用于 [业务] 层,
- 一个用于 [Web] 层的项目,
- 以及一个将前三个项目整合在一起的企业项目。
现在我们将构建 [DAO] 和 [JPA] 层的 Maven 项目。
注意:理解 [业务]、[DAO] 和 [JPA] 层需要具备 Java EE 知识。关于此内容,可参考 [ref7](参见第 3 段)。
3.4.1. NetBeans 项目
操作步骤如下:
![]() |
- 在 [1] 中,我们创建一个类型为 [EJB Module] 的 Maven 项目 [2],
- 在 [3] 中,我们为项目命名,
![]() |
- 在 [4] 中,选择 GlassFish 服务器,
- 在 [5] 中,选择生成的项目。
3.4.2. 生成 [JPA] 层
让我们回到需要构建的架构:
![]() |
借助 NetBeans,可以自动生成 [JPA] 层以及控制对生成的 JPA 实体访问权限的 [EJB] 层。熟悉这些自动生成方法非常有用,因为生成的代码能为如何编写 JPA 实体或使用这些实体的 EJB 代码提供宝贵的参考。
接下来我们将介绍其中一些自动生成工具。要理解生成的代码,您需要对 JPA 实体 [ref8] 和 EJB [ref7] 有扎实的掌握(参见第 3 节)。
3.4.2.1. 创建 NetBeans 与数据库的连接
- 启动 MySQL 5 数据库管理系统,以便数据库可用,
- 创建一个连接到数据库 [dbrdvmedecins2] 的 NetBeans 连接,
![]() |
- 在 [服务] 选项卡 [1] 的 [数据库] 部分 [2] 下,选择 MySQL JDBC 驱动程序 [3],
- 然后选择 [4] “使用” 选项以建立与 MySQL 数据库的连接,
- 在 [5] 中,输入所需信息。在 [6] 中输入数据库名称;在 [7] 中输入数据库用户名和密码;
- 在 [8] 中,您可以测试所提供的信息,
- 在 [9] 中,若信息正确,将显示预期消息,
![]() |
- 在[10]中,连接已建立。您可以在已连接的数据库中看到这四个表。
3.4.2.2. 创建持久化单元
让我们回到正在构建的架构:
![]() |
我们目前正在构建 [JPA] 层。其配置在 [persistence.xml] 文件中完成,该文件中定义了持久化单元。每个持久化单元都需要以下信息:
- 数据库的 JDBC 连接信息(URL、用户名、密码),
- 将代表数据库表的类,
- 所使用的 JPA 实现。实际上,JPA 是一项由多种产品实现的规范。在此,我们将使用 EclipseLink,这是 GlassFish 服务器默认采用的实现。这样可以避免我们需要向 GlassFish 中添加其他实现的库。
NetBeans 可以通过向导生成此持久化文件。
![]() |
- 右键单击项目并选择“创建持久化单元”[1],
- 在 [2] 中,为正在创建的持久化单元命名,
- 在 [3] 中,选择 EclipseLink JPA 实现(JPA 2.0),
- 在 [4] 中,指定数据库事务将由 GlassFish 服务器的 EJB 容器管理,
- 在 [5] 中,指定数据库表已创建,因此不会再次创建,
![]() |
- 在 [6] 中,为 GlassFish 服务器创建一个新的数据源,
- 在 [7] 中,提供一个 JNDI(Java 命名目录接口)名称,
- 在 [8] 中,将此名称与上一步创建的 MySQL 连接关联,
![]() |
- 在 [9] 中,完成向导,
- 在 [10] 中,新建项目,
- 在 [11],[META-INF] 文件夹中生成了 [persistence.xml] 文件,
- 在 [12],已生成一个 [setup] 文件夹,
- 在 [13],Maven 项目中已添加了新的依赖项。
生成的 [META-INF/persistence.xml] 文件如下:
<?xml version="1.0" encoding="UTF-8"?>
<persistence version="2.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_2_0.xsd">
<persistence-unit name="dbrdvmedecins2-PU" transaction-type="JTA">
<jta-data-source>jdbc/dbrdvmedecins2</jta-data-source>
<exclude-unlisted-classes>false</exclude-unlisted-classes>
<properties/>
</persistence-unit>
</persistence>
它包含向导中提供的信息:
- 第 3 行:持久化单元的名称,
- 第 3 行:数据库事务类型,本例中为由 GlassFish 服务器 EJB3 容器管理的 JTA(Java 事务 API)事务,
- 第 4 行:数据源的 JNDI 名称。
通常,该文件会指定所使用的 JPA 实现类型。在向导中,我们选择了 EclipseLink。由于这是 GlassFish 服务器使用的默认 JPA 实现,因此 [persistence.xml] 文件中未提及该信息。
在 [设计] 选项卡中,您可以查看 [persistence.xml] 文件的概览:
![]() |
为了获取 EclipseLink 日志,我们将使用以下 [persistence.xml] 文件:
<?xml version="1.0" encoding="UTF-8"?>
<persistence version="2.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_2_0.xsd">
<persistence-unit name="dbrdvmedecins2-PU" transaction-type="JTA">
<provider>org.eclipse.persistence.jpa.PersistenceProvider</provider>
<jta-data-source>jdbc/dbrdvmedecins2</jta-data-source>
<exclude-unlisted-classes>false</exclude-unlisted-classes>
<properties>
<property name="eclipselink.logging.level" value="FINE"/>
</properties>
</persistence-unit>
</persistence>
- 第 4 行:指定使用 EclipseLink JPA 实现,
- 第 7–9 行:包含 JPA 提供程序的配置属性,此处为 EclipseLink,
- 第 8 行:此属性启用了对 EclipseLink 将要执行的 SQL 语句的日志记录。
生成的 [glassfish-resources.xml] 文件如下:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE resources PUBLIC "-//GlassFish.org//DTD GlassFish Application Server 3.1 Resource Definitions//EN" "http://glassfish.org/dtds/glassfish-resources_1_5.dtd">
<resources>
<jdbc-connection-pool allow-non-component-callers="false" ... steady-pool-size="8" validate-atmost-once-period-in-seconds="0" wrap-jdbc-objects="false">
<property name="serverName" value="localhost"/>
<property name="portNumber" value="3306"/>
<property name="databaseName" value="dbrdvmedecins2"/>
<property name="User" value="root"/>
<property name="Password" value=""/>
<property name="URL" value="jdbc:mysql://localhost:3306/dbrdvmedecins2"/>
<property name="driverClass" value="com.mysql.jdbc.Driver"/>
</jdbc-connection-pool>
<jdbc-resource enabled="true" jndi-name="jdbc/dbrdvmedecins2" object-type="user" pool-name="mysql_dbrdvmedecins2_rootPool"/>
</resources>
该文件包含我们在之前使用的两个向导中输入的信息:
- 第 5–11 行:MySQL5 数据库 [dbrdvmedecins2] 的 JDBC 属性,
- 第 13 行:数据源的 JNDI 名称。
该文件将用于为 GlassFish 服务器创建 JNDI 数据源 [jdbc/dbrdvmedecins2]。此方法仅适用于该服务器。若使用其他服务器,则需要采用不同的方法,通常需借助管理工具。GlassFish 同样提供了此类工具。
最后,已向项目中添加了依赖项。[pom.xml] 文件内容如下:
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>istia.st</groupId>
<artifactId>mv-rdvmedecins-ejb-dao-jpa</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>ejb</packaging>
<name>mv-rdvmedecins-ejb-dao-jpa</name>
...
<dependencies>
<dependency>
<groupId>org.eclipse.persistence</groupId>
<artifactId>eclipselink</artifactId>
<version>2.3.0</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.eclipse.persistence</groupId>
<artifactId>javax.persistence</artifactId>
<version>2.0.3</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.eclipse.persistence</groupId>
<artifactId>org.eclipse.persistence.jpa.modelgen.processor</artifactId>
<version>2.3.0</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>javax</groupId>
<artifactId>javaee-api</artifactId>
<version>6.0</version>
<scope>provided</scope>
</dependency>
</dependencies>
...
<repositories>
<repository>
<url>http://download.eclipse.org/rt/eclipselink/maven.repo/</url>
<id>eclipselink</id>
<layout>default</layout>
<name>Repository for library Library[eclipselink]</name>
</repository>
</repositories>
</project>
- 第 32–37 行:[JPA] 层需要 [javaee-api] 工件;
- 第 16、22、28 行:此处使用的 JPA/EclipseLink 实现所需的工件。
- 第 18、24、30、36 行:所有工件都具有 provided 属性。请注意,这意味着它们在编译时是必需的,但在运行时并非必需。实际上,在运行时,它们由 GlassFish 服务器提供,
- 第 41–48 行:定义一个新的 Maven 工件仓库,EclipseLink 工件可在此处找到。
3.4.2.3. 生成 JPA 实体
可以使用 NetBeans 向导生成 JPA 实体:
![]() |
- 在 [1] 中,从数据库创建 JPA 实体,
- 在 [2] 中,选择之前创建的数据源 [jdbc / dbrdvmedecins2],
- 在 [3] 中,选择该数据源的表列表,
- 在 [4] 中,全选,
![]() |
- 在 [5] 中,所选表,
- 在 [6] 中,为与这四个表关联的 Java 类命名,
- 以及一个包名 [7],
- 在 [8] 中,JPA 将数据库表中的行分组为集合。我们选择列表作为集合,
![]() |
- 在[9]中,由向导生成的Java类。
3.4.2.4. 生成的 JPA 实体
[Medecin] 实体对应 [medecins] 表。该 Java 类布满了注解,乍看之下使得代码难以阅读。如果我们只保留理解该实体作用所必需的内容,则得到以下代码:
package rdvmedecins.jpa;
...
@Entity
@Table(name = "medecins")
public class Medecin implements Serializable {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "ID")
private Long id;
@Column(name = "TITRE")
private String titre;
@Column(name = "NOM")
private String nom;
@Column(name = "VERSION")
private int version;
@Column(name = "PRENOM")
private String prenom;
@OneToMany(cascade = CascadeType.ALL, mappedBy = "idMedecin")
private List<Creneau> creneauList;
// manufacturers
....
// getters and setters
....
@Override
public int hashCode() {
...
}
@Override
public boolean equals(Object object) {
...
}
@Override
public String toString() {
...
}
}
- 第 4 行:@Entity 注解将 [Medecin] 类定义为 JPA 实体,即通过 JPA API 与数据库表关联的类。
- 第 5 行:与 JPA 实体关联的数据库表名。表中的每个字段都对应 Java 类中的一个字段,
- 第 6 行:该类实现了 Serializable 接口。在客户端/服务器应用程序中,实体需要在客户端和服务器之间进行序列化,因此此接口是必需的。
- 第 10–11 行:[Medecin] 类的 id 字段对应于 [medecins] 表中的 [ID] 字段(第 10 行),
- 第 13–14 行:[Doctor] 类的 *title* 字段对应于 [doctors] 表中的 [TITLE] 字段(第 13 行),
- 第 16–17 行:[Doctor] 类的 name 字段对应于 [doctors] 表中的 [NAME] 字段(第 16 行),
- 第 19–20 行:[Medecin] 类的 version 字段对应于 [medecins] 表中的 [VERSION] 字段(第 19 行)。在此处,向导未能识别该列实际上是一个版本列,该列必须在所属行每次被修改时递增。若要为其赋予此角色,必须添加 @Version 注解。 我们将在后续步骤中进行此操作,
- 第 22–23 行:[Doctor] 类的 first_name 字段对应于 [doctors] 表的 [FIRST_NAME] 字段,
- 第 10–11 行:id 字段对应于该表的主键 [ID]。第 8–9 行的注解阐明了这一点,
- 第 8 行:@Id 注解表明被注解的字段与表的主键相关联,
- 第 9 行:[JPA] 层将为插入到 [Doctors] 表中的行生成主键。有几种可能的策略。此处,GenerationType.IDENTITY 策略表示 JPA 层将使用 MySQL 表的 auto_increment 模式,
- 第 25–26 行:[slots] 表拥有指向 [doctors] 表的外键。一个时段属于一位医生。反之,一位医生则关联着多个时段。 因此,我们建立了一对多关系(一个医生对应多个时段),这种关系在 JPA 中通过 @OneToMany 注解进行定义(第 25 行)。第 26 行的字段将包含该医生所有的时段。这一功能无需任何编程即可实现。为了全面理解第 25 行,我们需要介绍 [Creneau] 类。
其定义如下:
package rdvmedecins.jpa;
import java.io.Serializable;
import java.util.List;
import javax.persistence.*;
import javax.validation.constraints.NotNull;
@Entity
@Table(name = "creneaux")
public class Creneau implements Serializable {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "ID")
private Long id;
@Column(name = "MDEBUT")
private int mdebut;
@Column(name = "HFIN")
private int hfin;
@Column(name = "HDEBUT")
private int hdebut;
@Column(name = "MFIN")
private int mfin;
@Column(name = "VERSION")
private int version;
@JoinColumn(name = "ID_MEDECIN", referencedColumnName = "ID")
@ManyToOne(optional = false)
private Medecin idMedecin;
@OneToMany(cascade = CascadeType.ALL, mappedBy = "idCreneau")
private List<Rv> rvList;
// manufacturers
...
// getters and setters
...
@Override
public int hashCode() {
...
}
@Override
public boolean equals(Object object) {
...
}
@Override
public String toString() {
...
}
}
我们仅对新的注释进行评论:
- 我们已指定 [slots] 表与 [doctors] 表之间存在外键关系:一个时段(slot)与一位医生相关联。多位医生可以关联多个时段。我们定义了从 [slots] 表到 [doctors] 表的多对一关系(时段到医生)。第 32 行上的 @ManyToOne 注解用于定义外键,
- 第 31 行通过 @JoinColumn 注解指定了外键关系:[slots] 表中的 [ID_MEDECIN] 列是 [doctors] 表中 [ID] 列的外键,
- 第 33 行:引用拥有该时段的医生。此处同样无需任何编码即可实现。
因此,[Creneau] 实体与 [Medecin] 实体之间的外键关系通过两个注解实现:
- 在 [Creneau] 实体中:
@JoinColumn(name = "ID_MEDECIN", referencedColumnName = "ID")
@ManyToOne(optional = false)
private Medecin idMedecin;
- 在 [Doctor] 实体中:
@OneToMany(cascade = CascadeType.ALL, mappedBy = "idMedecin")
private List<Creneau> creneauList;
这两个注解反映了相同的关系:即从 [Appointments] 表到 [Doctors] 表的外键关系。它们被称为彼此的逆向关系。只有 @ManyToOne 关系是必不可少的。它明确地定义了外键关系。 @OneToMany 关系是可选的。如果存在,它只是引用与其关联的 @ManyToOne 关系。这就是 [Doctor] 实体第 1 行中 mappedBy 属性的含义。 该属性的值是 [Slot] 实体中带有 @ManyToOne 注解(用于指定外键)的字段名称。同样在 [Medecin] 实体的第 1 行,cascade=CascadeType.ALL 属性定义了 [Medecin] 实体相对于 [Creneau] 实体的行为:
- 如果向数据库插入新的 [Doctor] 实体,则第 2 行字段中的 [TimeSlot] 实体也必须被插入,
- 如果数据库中修改了 [Doctor] 实体,则第 2 行字段中的 [Slot] 实体也必须被修改,
- 如果从数据库中删除了一个 [Doctor] 实体,则第 2 行字段中的 [Slot] 实体也必须被删除。
我们提供另外两个实体的代码时不作具体说明,因为它们没有引入任何新的符号。
[Client] 实体
package rdvmedecins.jpa;
...
@Entity
@Table(name = "clients")
public class Client implements Serializable {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "ID")
private Long id;
@Column(name = "TITRE")
private String titre;
@Column(name = "NOM")
private String nom;
@Column(name = "VERSION")
private int version;
@Column(name = "PRENOM")
private String prenom;
@OneToMany(cascade = CascadeType.ALL, mappedBy = "idClient")
private List<Rv> rvList;
// manufacturers
...
// getters and setters
...
@Override
public int hashCode() {
...
}
@Override
public boolean equals(Object object) {
...
}
@Override
public String toString() {
...
}
}
- 第 24–25 行反映了 [rv] 表与 [clients] 表之间的外键关系。
[Rv] 实体:
package rdvmedecins.jpa;
...
@Entity
@Table(name = "rv")
public class Rv implements Serializable {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "ID")
private Long id;
@Column(name = "JOUR")
@Temporal(TemporalType.DATE)
private Date jour;
@JoinColumn(name = "ID_CRENEAU", referencedColumnName = "ID")
@ManyToOne(optional = false)
private Creneau idCreneau;
@JoinColumn(name = "ID_CLIENT", referencedColumnName = "ID")
@ManyToOne(optional = false)
private Client idClient;
// manufacturers
...
// getters and setters
...
@Override
public int hashCode() {
...
}
@Override
public boolean equals(Object object) {
...
}
@Override
public String toString() {
...
}
}
- 第 13 行指定 `jour` 字段的类型为 Java Date。这表明在 [rv] 表中,[JOUR] 列(第 12 行)的类型为日期(不含时间),
- 第16–18行:定义从[rv]表到[slots]表的外键关系,
- 第 20–22 行:定义从 [rv] 表到 [clients] 表的外键关系。
JPA 实体的自动生成为我们提供了一个可用的基础。有时这已足够,有时则不然。本例即属于后者:
- 我们需要在实体的各个版本字段上添加 @Version 注解,
- 我们需要编写比生成的方法更明确的 toString 方法,
- [Medecin] 和 [Client] 实体情况类似。我们将让它们继承自 [Person] 类,
- 我们将从 @ManyToOne 关系中移除反向的 @OneToMany 关系。这些关系并非必需,且会增加编程复杂度,
- 我们将移除主键上的 @NotNull 验证。当使用 JPA 将实体持久化到 MySQL 时,该实体初始的主键为空。只有在数据被持久化到数据库后,已持久化实体的主键才会获得值。
根据这些规范,各类将变为如下形式:
Person 类用于表示医生和客户:
package rdvmedecins.jpa;
import java.io.Serializable;
import javax.persistence.*;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
@MappedSuperclass
public class Personne implements Serializable {
private static final long serialVersionUID = 1L;
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "ID")
private Long id;
@Basic(optional = false)
@Size(min = 1, max = 5)
@Column(name = "TITRE")
private String titre;
@Basic(optional = false)
@NotNull
@Size(min = 1, max = 30)
@Column(name = "NOM")
private String nom;
@Basic(optional = false)
@NotNull
@Column(name = "VERSION")
@Version
private int version;
@Basic(optional = false)
@NotNull
@Size(min = 1, max = 30)
@Column(name = "PRENOM")
private String prenom;
// manufacturers
...
// getters and setters
...
@Override
public String toString() {
return String.format("[%s,%s,%s,%s,%s]", id, version, titre, prenom, nom);
}
}
- 第 8 行:请注意,[Person] 类本身并非实体(@Entity)。它将作为实体的父类。@MappedSuperClass 注解表明了这一点。
[Client] 实体封装了 [clients] 表中的行。它继承自前面的 [Person] 类:
package rdvmedecins.jpa;
import java.io.Serializable;
import javax.persistence.*;
@Entity
@Table(name = "clients")
public class Client extends Personne implements Serializable {
private static final long serialVersionUID = 1L;
// manufacturers
...
@Override
public int hashCode() {
...
}
@Override
public boolean equals(Object object) {
...
}
@Override
public String toString() {
return String.format("Client[%s,%s,%s,%s]", getId(), getTitre(), getPrenom(), getNom());
}
}
- 第 6 行:[Client] 类是一个 JPA 实体,
- 第 7 行:它与 [clients] 表相关联,
- 第 8 行:它继承自 [Person] 类。
封装 [doctors] 表中各行的 [Doctor] 实体遵循相同的模式:
package rdvmedecins.jpa;
import java.io.Serializable;
import javax.persistence.*;
@Entity
@Table(name = "medecins")
public class Medecin extends Personne implements Serializable {
private static final long serialVersionUID = 1L;
// manufacturers
...
@Override
public int hashCode() {
...
}
@Override
public boolean equals(Object object) {
...
}
@Override
public String toString() {
return String.format("Médecin[%s,%s,%s,%s]", getId(), getTitre(), getPrenom(), getNom());
}
}
[Creneau] 实体封装了 [creneaux] 表中的行:
package rdvmedecins.jpa;
import java.io.Serializable;
import java.util.List;
import javax.persistence.*;
import javax.validation.constraints.NotNull;
@Entity
@Table(name = "creneaux")
public class Creneau implements Serializable {
private static final long serialVersionUID = 1L;
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Basic(optional = false)
@Column(name = "ID")
private Long id;
@Basic(optional = false)
@NotNull
@Column(name = "MDEBUT")
private int mdebut;
@Basic(optional = false)
@NotNull
@Column(name = "HFIN")
private int hfin;
@Basic(optional = false)
@NotNull
@Column(name = "HDEBUT")
private int hdebut;
@Basic(optional = false)
@NotNull
@Column(name = "MFIN")
private int mfin;
@Basic(optional = false)
@NotNull
@Column(name = "VERSION")
@Version
private int version;
@JoinColumn(name = "ID_MEDECIN", referencedColumnName = "ID")
@ManyToOne(optional = false)
private Medecin medecin;
// manufacturers
...
// getters and setters
...
@Override
public int hashCode() {
...
}
@Override
public boolean equals(Object object) {
// TODO: Warning - this method won't work in the case the id fields are not set
...
}
@Override
public String toString() {
return String.format("Creneau [%s, %s, %s:%s, %s:%s,%s]", id, version, hdebut, mdebut, hfin, mfin, medecin);
}
}
- 第45–47行描述了数据库中[slots]表与[doctors]表之间的“多对一”关系:一名医生拥有多个时段,而一个时段仅属于一名医生。
[Rv] 实体封装了 [rv] 表中的行:
package rdvmedecins.jpa;
import java.io.Serializable;
import java.util.Date;
import javax.persistence.*;
import javax.validation.constraints.NotNull;
@Entity
@Table(name = "rv")
public class Rv implements Serializable {
private static final long serialVersionUID = 1L;
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Basic(optional = false)
@Column(name = "ID")
private Long id;
@Basic(optional = false)
@NotNull
@Column(name = "JOUR")
@Temporal(TemporalType.DATE)
private Date jour;
@JoinColumn(name = "ID_CRENEAU", referencedColumnName = "ID")
@ManyToOne(optional = false)
private Creneau creneau;
@JoinColumn(name = "ID_CLIENT", referencedColumnName = "ID")
@ManyToOne(optional = false)
private Client client;
// manufacturers
...
// getters and setters
...
@Override
public int hashCode() {
...
}
@Override
public boolean equals(Object object) {
...
}
@Override
public String toString() {
return String.format("Rv[%s, %s, %s]", id, creneau, client);
}
}
- 第 29–31 行描述了数据库中 [rv] 表与 [clients] 表之间的“多对一”关系(一个客户可能出现在多个 Rv 条目中),而第 25–27 行描述了 [rv] 表与 [slots] 表之间的“多对一”关系(一个插槽可能出现在多个 Rv 中)。
3.4.3. 异常类
![]() |
该应用程序的异常类 [ RdvMedecinsException] 如下所示:
package rdvmedecins.exceptions;
import java.io.Serializable;
import javax.ejb.ApplicationException;
@ApplicationException(rollback=true)
public class RdvMedecinsException extends RuntimeException implements Serializable{
// private fields
private int code = 0;
// manufacturers
public RdvMedecinsException() {
super();
}
public RdvMedecinsException(String message) {
super(message);
}
public RdvMedecinsException(String message, Throwable cause) {
super(message, cause);
}
public RdvMedecinsException(Throwable cause) {
super(cause);
}
public RdvMedecinsException(String message, int code) {
super(message);
setCode(code);
}
public RdvMedecinsException(Throwable cause, int code) {
super(cause);
setCode(code);
}
public RdvMedecinsException(String message, Throwable cause, int code) {
super(message, cause);
setCode(code);
}
// getters - setters
public int getCode() {
return code;
}
public void setCode(int code) {
this.code = code;
}
}
- 第 7 行:该类继承了 [RuntimeException] 类。因此,编译器不要求使用 try/catch 代码块来处理它。
- 第 6 行:@ApplicationException 注解确保该异常不会被 [EjbException] “吞噬”。
为了理解 @ApplicationException 注解,让我们重新审视一下服务器端架构:
![]() |
[RdvMedecinsException] 异常将由 EJB3 容器内的 [DAO] 层中的 EJB 方法抛出,并被该容器拦截。如果没有 @ApplicationException 注解,EJB3 容器会将发生的异常封装在 [EjbException] 中并重新抛出。 您可能不希望进行这种封装,而更希望让 [RdvMedecinsException] 类型的异常从 EJB3 容器中逸出。这正是 @ApplicationException 注解所允许的。 此外,该注解的 (rollback=true) 属性指示 EJB3 容器:若在作为与 DBMS 事务一部分执行的方法中发生 [RdvMedecinsException],则必须回滚该事务。在技术术语中,这被称为回滚事务。
3.4.4. [DAO] 层的 EJB
![]() |
![]() |
package rdvmedecins.dao;
import java.util.Date;
import java.util.List;
import rdvmedecins.jpa.Client;
import rdvmedecins.jpa.Creneau;
import rdvmedecins.jpa.Medecin;
import rdvmedecins.jpa.Rv;
public interface IDao {
// customer list
public List<Client> getAllClients();
// list of doctors
public List<Medecin> getAllMedecins();
// list of physician slots
public List<Creneau> getAllCreneaux(Medecin medecin);
// list of doctor's appointments on a given day
public List<Rv> getRvMedecinJour(Medecin medecin, Date jour);
// find a customer identified by its id
public Client getClientById(Long id);
// find a customer identified by its id
public Medecin getMedecinById(Long id);
// find an Rv identified by its id
public Rv getRvById(Long id);
// find a time slot identified by its id
public Creneau getCreneauById(Long id);
// add a RV to the list
public Rv ajouterRv(Date jour, Creneau creneau, Client client);
// delete a RV
public void supprimerRv(Rv rv);
}
该接口是在确定[Web]层的需求后构建的:
- 第 14 行:客户列表。我们需要此数据来填充客户下拉列表,
- 第 16 行:医生列表。我们需要它来填充医生的下拉列表,
- 第 18 行:某位医生的可用时段列表。我们需要它来显示该医生在特定日期的日程安排,
- 第20行:医生在特定日期的预约列表。结合前面的方法,这将使我们能够显示医生在特定日期的日程安排,并包含其已预订的时段,
- 第 22 行:允许我们通过客户 ID 号查找客户。此方法将让我们通过从客户下拉列表中选择来查找客户,
- 第24行:与上述医生操作相同,
- 第26行:根据预约编号检索预约。在删除预约时,可用于事先验证该预约是否确实存在,
- 第 28 行:根据时段编号检索特定时段。可用于确定用户想要添加或删除的时段,
- 第 30 行:用于添加预约,
- 第 32 行:删除预约。
该 EJB 的本地接口 [IDaoLocal] 直接继承自之前的 [IDao] 接口:
package rdvmedecins.dao;
import javax.ejb.Local;
@Local
public interface IDaoLocal extends IDao{
}
远程接口 [IDaoRemote] 也是如此:
package rdvmedecins.dao;
import javax.ejb.Remote;
@Remote
public interface IDaoRemote extends IDao{
}
该 EJB [DaoJpa] 同时实现了本地接口和远程接口:
package rdvmedecins.dao;
...
@Singleton (mappedName="rdvmedecins.dao")
@TransactionAttribute(TransactionAttributeType.REQUIRED)
public class DaoJpa implements IDaoLocal, IDaoRemote, Serializable {
- 第 5 行表明远程 EJB 的名称为 "rdvmedecins.dao"。此外,@Singleton 注解(Java EE6)确保仅会创建一个 EJB 实例。@Stateless 注解(Java EE5)定义了一个可创建多个实例以填充 EJB 池的 EJB,
- 第 6 行表明所有 EJB 方法都在由 EJB3 容器管理的事务中运行,
- 第 7 行显示该 EJB 实现了本地和远程接口,并且是可序列化的。
完整的 EJB 代码如下:
package rdvmedecins.dao;
import java.io.Serializable;
import java.util.Date;
import java.util.List;
import javax.ejb.Singleton;
import javax.ejb.TransactionAttribute;
import javax.ejb.TransactionAttributeType;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import rdvmedecins.exceptions.RdvMedecinsException;
import rdvmedecins.jpa.Client;
import rdvmedecins.jpa.Creneau;
import rdvmedecins.jpa.Medecin;
import rdvmedecins.jpa.Rv;
@Singleton (mappedName="rdvmedecins.dao")
@TransactionAttribute(TransactionAttributeType.REQUIRED)
public class DaoJpa implements IDaoLocal, IDaoRemote, Serializable {
@PersistenceContext
private EntityManager em;
// customer list
public List<Client> getAllClients() {
try {
return em.createQuery("select rc from Client rc").getResultList();
} catch (Throwable th) {
throw new RdvMedecinsException(th, 1);
}
}
// list of doctors
public List<Medecin> getAllMedecins() {
try {
return em.createQuery("select rm from Medecin rm").getResultList();
} catch (Throwable th) {
throw new RdvMedecinsException(th, 2);
}
}
// list of time slots for a given doctor
// doctor: the doctor
public List<Creneau> getAllCreneaux(Medecin medecin) {
try {
return em.createQuery("select rc from Creneau rc join rc.medecin m where m.id=:idMedecin").setParameter("idMedecin", medecin.getId()).getResultList();
} catch (Throwable th) {
throw new RdvMedecinsException(th, 3);
}
}
// list of appointments for a given doctor on a given day
// doctor: the doctor
// day: the day
public List<Rv> getRvMedecinJour(Medecin medecin, Date jour) {
try {
return em.createQuery("select rv from Rv rv join rv.creneau c join c.idMedecin m where m.id=:idMedecin and rv.jour=:jour").setParameter("idMedecin", medecin.getId()).setParameter("jour", jour).getResultList();
} catch (Throwable th) {
throw new RdvMedecinsException(th, 3);
}
}
// add Rv
// day : day of appointment
// creneau: Rv time slot
// customer: customer for whom the appointment is taken
public Rv ajouterRv(Date jour, Creneau creneau, Client client) {
try {
Rv rv = new Rv(null, jour);
rv.setClient(client);
rv.setCreneau(creneau);
em.persist(rv);
return rv;
} catch (Throwable th) {
throw new RdvMedecinsException(th, 4);
}
}
// deleting an appointment
// rv: Rv deleted
public void supprimerRv(Rv rv) {
try {
em.remove(em.merge(rv));
} catch (Throwable th) {
throw new RdvMedecinsException(th, 5);
}
}
// retrieve a specific customer
public Client getClientById(Long id) {
try {
return (Client) em.find(Client.class, id);
} catch (Throwable th) {
throw new RdvMedecinsException(th, 6);
}
}
// retrieve a specific doctor
public Medecin getMedecinById(Long id) {
try {
return (Medecin) em.find(Medecin.class, id);
} catch (Throwable th) {
throw new RdvMedecinsException(th, 6);
}
}
// retrieve a given Rv
public Rv getRvById(Long id) {
try {
return (Rv) em.find(Rv.class, id);
} catch (Throwable th) {
throw new RdvMedecinsException(th, 6);
}
}
// retrieve a given slot
public Creneau getCreneauById(Long id) {
try {
return (Creneau) em.find(Creneau.class, id);
} catch (Throwable th) {
throw new RdvMedecinsException(th, 6);
}
}
}
- 第 22 行:管理对持久化上下文访问的 EntityManager 对象。当类被实例化时,EJB 容器会根据第 21 行的 @PersistenceContext 注解初始化此字段,
- 第 27 行:JPQL(Java 持久化查询语言)查询,用于将 [clients] 表中的所有行作为 [Client] 对象列表返回,
- 第 36 行:针对医生的类似查询,
- 第 46 行:一个对 [slots] 和 [doctors] 表执行连接操作的 JPQL 查询。该查询通过医生的 ID 进行参数化,
- 第 57 行:一个 JPQL 查询,用于对 [appointments]、[slots] 和 [doctors] 表进行连接,并包含两个参数:医生 ID 和预约日期,
- 第 69–73 行:创建一个预约,随后将其持久化到数据库中,
- 第 83 行:从数据库中删除一个预约,
- 第 92 行:对数据库执行 SELECT 查询以查找指定的客户,
- 第 101 行:对医生执行相同的操作,
- 第 110 行:对预约执行相同操作,
- 第 119 行:对时间段执行相同操作,
- 所有使用第22行持久化上下文的操作都可能遇到数据库问题。因此,这些操作均被封装在try/catch代码块中。任何异常都会被封装在自定义异常RdvMedecinsException中。
3.4.5. MySQL JDBC 驱动程序的实现
在以下架构中:
![]() |
EclipseLink 需要 MySQL JDBC 驱动程序。该驱动程序必须安装在 GlassFish 服务器库的 <glassfish>/domains/domain1/lib/ext 文件夹中,其中 <glassfish> 是 GlassFish 服务器的安装目录。获取方法如下:
![]() |
MySQL JDBC 驱动程序应放置在 <Domains 文件夹>[1]/domain1/lib/ext [2] 目录中。该驱动程序可通过 URL [http://www.mysql.fr/downloads/connector/j/] 获取。安装完成后,必须重启 GlassFish 服务器,以便其识别此新库。
3.4.6. [DAO] 层 EJB 的部署
让我们回到目前构建的架构:
![]() |
整个 [Web、业务逻辑、DAO、JPA] 技术栈必须部署在 GlassFish 服务器上。具体操作如下:
![]() |
- 在 [1] 中,我们构建 Maven 项目,
- 在 [2] 中,运行该项目,
- 在[3]中,该项目已部署到GlassFish服务器([Services]选项卡)
您可能想查看 GlassFish 的日志:
![]() |
在 [1] 中,GlassFish 日志位于 [输出 / GlassFish Server 3+] 选项卡下。具体如下:
标有 [Config] 和 [Details] 的行是 EclipseLink 日志;标有 [Info] 的行来自 GlassFish。
- 第 1–12 行:EclipseLink 处理其发现的 JPA 实体,
- 第 13–17 行:信息表明 JPA 实体的处理过程正常,
- 第 18 行:EclipseLink 报告其存在,
- 第 19 行:EclipseLink 识别出当前正在处理 MySQL 数据库管理系统,
- 第 20–24 行:EclipseLink 尝试连接数据库,
- 第 25–28 行:连接成功,
- 第 29–33 行:它尝试重新连接,这次明确使用 MySQL 平台(第 30 行),
- 第 34–37 行:再次成功,
- 第 38 行:确认持久化单元 [dbrdvmedecins-PU] 已成功实例化,
- 第 39 行:EJB [DaoJpa] 的远程和本地接口的可移植名称,其中“可移植”意味着被所有 Java EE 6 应用服务器所识别,
- 第 40 行:EJB [DaoJpa] 的远程和本地接口名称,专用于 GlassFish。在接下来的测试中,我们将使用名称“rdvmedecins.dao”。
第 39 行和第 40 行非常重要。在 GlassFish 上编写 EJB 客户端时,必须了解这些名称。
3.4.7. 测试 [DAO] 层 EJB
现在,我们应用程序的 [DAO] 层 EJB 已部署完毕,可以进行测试了。我们将以客户端/服务器应用程序为背景进行测试:
![]() |
客户端将测试部署在 GlassFish 服务器上的 [DAO] EJB 的远程接口。
![]() |
- 在 [1] 中,我们创建一个新项目,
- 在 [2,3] 中,我们创建了一个类型为 [Java 应用程序] 的 Maven 项目,
- 在 [4] 中,我们为其命名并将其放置在与 EJB [DAO] 相同的文件夹中,
![]() |
- 在 [5] 中,生成的项目,
- 在 [6] 中,生成了一个类 [App.java]。我们将删除它,
- 在 [7] 中,已生成一个 [Source Packages] 分支。我们尚未遇到过该分支。我们可以将 JUnit 测试放入该分支。我们将这样做。我们不会保留生成的测试类 [AppTest],
- 在 [8] 中,是 Maven 项目的依赖项。[Dependencies] 分支为空。我们需要在此添加新的依赖项。[Test Dependencies] 分支包含测试所需的依赖项。这里使用的库是 JUnit 3.8 框架。我们需要将其更改。
该项目的演变过程如下:
![]() |
- 在 [1] 中,该项目已移除了两个生成的类以及 JUnit 依赖项。
让我们回到将用于测试的客户端/服务器架构:
![]() |
客户端需要了解 EJB [DAO] 提供的远程接口。此外,它还将与 EJB 交换 JPA 实体。因此,它需要这些实体的定义。为了确保 EJB 测试项目能够访问这些信息,我们将把 EJB [DAO] 项目作为依赖项添加到该项目中:
![]() |
- 在 [1] 中,向 [Test Dependencies] 分支添加一个依赖项,
- 在 [2] 中,选择 [打开项目] 选项卡,
- 在 [3] 中,选择 EJB [DAO] Maven 项目,
![]() |
- 在 [4] 中,依赖项已添加。
让我们回到测试的客户端/服务器架构:
![]() |
运行时,客户端和服务器通过 TCP-IP 网络进行通信。我们无需编写这些通信代码。对于每台应用服务器,都有一个需要集成到客户端依赖项中的库。Glassfish 对应的库名为 [gf-client]。我们将它添加进去:
![]() |
- 在 [1] 中,我们添加一个依赖项,
- 在 [2] 中,我们指定所需构建成果的特性,
- 在 [3] 中,会添加大量依赖项。Maven 将下载这些依赖项。此过程可能需要几分钟。随后,它们将被存储在本地 Maven 仓库中。
现在我们可以创建 JUnit 测试:
![]() |
- 在 [2] 中,右键单击 [Test Packages] 以创建一个新的 JUnit 测试,
![]() |
- 在 [3] 中,为测试类命名并指定其包 [4],
- 在 [5] 中,选择 JUnit 4.x 框架,
- 在 [6] 中,生成的测试类,
- 在 [7] 中,显示新的 Maven 项目依赖项。
生成的 [pom.xml] 文件如下:
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>istia.st</groupId>
<artifactId>mv-client-rdvmedecins-ejb-dao</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>jar</packaging>
<name>mv-client-rdvmedecins-ejb-dao</name>
<url>http://maven.apache.org</url>
<repositories>
<repository>
<url>http://download.eclipse.org/rt/eclipselink/maven.repo/</url>
<id>eclipselink</id>
<layout>default</layout>
<name>Repository for library Library[eclipselink]</name>
</repository>
<repository>
<url>http://repo1.maven.org/maven2/</url>
<id>junit_4</id>
<layout>default</layout>
<name>Repository for library Library[junit_4]</name>
</repository>
</repositories>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.10</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>mv-rdvmedecins-ejb-dao-jpa</artifactId>
<version>${project.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.glassfish.appclient</groupId>
<artifactId>gf-client</artifactId>
<version>3.1.1</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>
注:
- 第 32–51 行,即项目依赖项,
- 第 13–26 行:已定义了两个 Maven 仓库,一个用于 EclipseLink(第 14–19 行),另一个用于 JUnit4(第 20–25 行)。
测试类如下所示:
package rdvmedecins.tests.dao;
import java.util.Date;
import java.util.List;
import javax.naming.InitialContext;
import javax.naming.NamingException;
import junit.framework.Assert;
import org.junit.BeforeClass;
import org.junit.Test;
import rdvmedecins.dao.IDaoRemote;
import rdvmedecins.jpa.Client;
import rdvmedecins.jpa.Creneau;
import rdvmedecins.jpa.Medecin;
import rdvmedecins.jpa.Rv;
public class JUnitTestDao {
// layer [dao] tested
private static IDaoRemote dao;
// today's date
Date jour = new Date();
@BeforeClass
public static void init() throws NamingException {
// environment initialization JNDI
InitialContext initialContext = new InitialContext();
// dao layer instantiation
dao = (IDaoRemote) initialContext.lookup("rdvmedecins.dao");
}
@Test
public void test1() {
// customer display
List<Client> clients =dao.getAllClients();
display("Liste des clients :", clients);
// physician display
List<Medecin> medecins =dao.getAllMedecins();
display("Liste des médecins :", medecins);
// display doctor's slots
Medecin medecin = medecins.get(0);
List<Creneau> creneaux = dao.getAllCreneaux(medecin);
display(String.format("Liste des créneaux du médecin %s", medecin), creneaux);
// list of doctor's appointments on a given day
display(String.format("Liste des créneaux du médecin %s, le [%s]", medecin, jour), dao.getRvMedecinJour(medecin, jour));
// add a RV
Rv rv = null;
Creneau creneau = creneaux.get(2);
Client client = clients.get(0);
System.out.println(String.format("Ajout d'un Rv le [%s] dans le créneau %s pour le client %s", jour, creneau, client));
rv = dao.ajouterRv(jour, creneau, client);
System.out.println("Rv ajouté");
display(String.format("Liste des Rv du médecin %s, le [%s]", medecin, jour), dao.getRvMedecinJour(medecin, jour));
// add a RV in the same slot on the same day
// must trigger an exception
System.out.println(String.format("Ajout d'un Rv le [%s] dans le créneau %s pour le client %s", jour, creneau, client));
Boolean erreur = false;
try {
rv = dao.ajouterRv(jour, creneau, client);
System.out.println("Rv ajouté");
} catch (Exception ex) {
Throwable th = ex;
while (th != null) {
System.out.println(ex.getMessage());
th = th.getCause();
}
// we note the error
erreur=true;
}
// check for errors
Assert.assertTrue(erreur);
// RV list
display(String.format("Liste des Rv du médecin %s, le [%s]", medecin, jour), dao.getRvMedecinJour(medecin, jour));
// delete a RV
System.out.println("Suppression du Rv ajouté");
dao.supprimerRv(rv);
System.out.println("Rv supprimé");
display(String.format("Liste des Rv du médecin %s, le [%s]", medecin, jour), dao.getRvMedecinJour(medecin, jour));
}
// utility method - displays items in a collection
private static void display(String message, List elements) {
System.out.println(message);
for (Object element : elements) {
System.out.println(element);
}
}
}
- 第 23–29 行:带有 @BeforeClass 注解的方法会在其他所有方法之前执行。在此,我们创建了对 EJB 远程接口 [DaoJpa] 的引用。请注意,我们为其指定了 JNDI 名称 "rdvmedecins.dao",
- 第 34–35 行:显示客户端列表,
- 第 37–38 行:显示医生列表,
- 第 40–42 行:显示第一位医生的时段,
- 第 44 行:显示第 21 行指定日期中第一位医生的预约,
- 第 46–51 行:为第一位医生添加一个预约,时间段为 #2,日期为第 21 行指定的日期,
- 第 52 行:为验证目的,显示第 21 行指定日期中第一位医生的预约。此时必须至少有一条——即刚刚添加的那条,
- 第55–70行:添加相同的预约。由于[RV]表具有唯一性约束,此操作必将引发异常。我们在第70行验证这一点,
- 第 72 行:显示第 21 行指定日期中第一位医生的预约记录以供验证。我们试图添加的那个预约不应出现在此处,
- 第 74–76 行:删除刚才添加的那个预约,
- 第 77 行:显示第 21 行指定日期中第一位医生的预约以供验证。我们刚刚删除的那个不应出现在此处。
这是一个简单的 JUnit 测试。它仅包含一个断言(第 70 行)。这是一种视觉测试,因此存在相应的局限性。
如果一切顺利,测试应通过:
![]() |
- 在 [1] 中,我们构建测试项目,
- 在 [2] 中,运行测试,
- 在 [3] 中,测试通过。
让我们仔细看看测试输出:
建议读者在阅读这些日志时,同时参考生成这些日志的代码。我们将重点关注在添加现有预约时发生的异常(第41–49行)。异常堆栈跟踪显示在第42–48行。这是意料之外的。让我们回到添加预约的方法的代码中:
// add Rv
// day : day of appointment
// creneau: Rv time slot
// customer: customer for whom the appointment is taken
public Rv ajouterRv(Date jour, Creneau creneau, Client client) {
try {
Rv rv = new Rv(null, jour);
rv.setClient(client);
rv.setCreneau(creneau);
System.out.println(String.format("avant persist : %s",rv));
em.persist(rv);
System.out.println(String.format("après persist : %s",rv));
return rv;
} catch (Throwable th) {
throw new RdvMedecinsException(th, 4);
}
}
让我们看看添加这两个预约时 GlassFish 的日志:
- 第 2 行:在第一次持久化之前,
- 第 3 行:第一个 persist 之后,
- 第 4 行:即将执行的 INSERT 语句。请注意,它并非与 persist 操作同时发生。如果同时发生,该日志本应出现在第 2 行之前。INSERT 操作通常发生在执行该方法的事务结束时,
- 第 6 行:EclipseLink 向 MySQL 查询上次使用的主键。它将获取新添加的预约的主键。该值将填充已持久化的 [Rv] 实体的 id 字段,
- 第 7–8 行:用于显示医生预约记录的 SELECT 查询,
- 第 9–10 行:显示第二次持久化操作的屏幕,
- 第 11–12 行:即将执行的 INSERT 语句。它应该会抛出异常。该异常出现在第 15–16 行,且原因明确。该异常最初由 MySQL JDBC 驱动程序抛出,原因是违反了预约表上的唯一约束。我们可以推断,这些异常应该会出现在 JUnit 测试日志中。然而,实际情况并非如此:
让我们回顾一下该测试的客户端/服务器架构:
![]() |
当 [DAO] EJB 抛出异常时,该异常必须经过序列化才能传递给客户端。很可能由于某种我尚未理解的原因,此操作失败了。由于我们的完整应用程序不会在客户端/服务器环境中运行,因此我们可以忽略此问题。
既然 [DAO] 层的 EJB 已可正常运行,我们可以继续处理 [业务] 层的 EJB。
3.5. [业务]层
让我们回到正在构建的应用程序架构:
![]() |
我们将为 [业务] EJB 创建一个新的 Maven 项目。如上所述,该项目将依赖于为 [DAO] 和 [JPA] 层构建的 Maven 项目。
3.5.1. NetBeans 项目
我们将构建一个新的 EJB 类型 Maven 项目。要完成此操作,只需按照第 174 页所述的已用流程进行即可。
![]() |
- 在[1]中,针对[业务]层的Maven项目,
- 在[2]中,添加一个依赖项,
- 在 [3] 中,选择 [DAO] 和 [JPA] 层的 Maven 项目,
- 在 [4] 中,选择 [provided] 作用域。请注意,这意味着该依赖项仅在编译时必需,运行项目时则非必需。实际上,[业务] 层的 EJB 将与 [DAO] 和 [JPA] 层的 EJB 一起部署在 GlassFish 服务器上。因此,运行时 [DAO] 和 [JPA] 层的 EJB 已经存在,
![]() |
- 在 [6] 中,显示了包含其依赖项的新项目。
现在让我们来看一下[业务]层的源代码:
![]() |
[Business] EJB 将具有以下 [IMetier] 接口:
package rdvmedecins.metier.service;
import java.util.Date;
import java.util.List;
import rdvmedecins.jpa.Client;
import rdvmedecins.jpa.Creneau;
import rdvmedecins.jpa.Medecin;
import rdvmedecins.jpa.Rv;
import rdvmedecins.metier.entites.AgendaMedecinJour;
public interface IMetier {
// dao layer
// customer list
public List<Client> getAllClients();
// list of doctors
public List<Medecin> getAllMedecins();
// list of physician slots
public List<Creneau> getAllCreneaux(Medecin medecin);
// list of doctor's appointments on a given day
public List<Rv> getRvMedecinJour(Medecin medecin, Date jour);
// find a customer identified by its id
public Client getClientById(Long id);
// find a customer identified by its id
public Medecin getMedecinById(Long id);
// find an Rv identified by its id
public Rv getRvById(Long id);
// find a time slot identified by its id
public Creneau getCreneauById(Long id);
// add a RV to the list
public Rv ajouterRv(Date jour, Creneau creneau, Client client);
// delete a RV
public void supprimerRv(Rv rv);
// job
public AgendaMedecinJour getAgendaMedecinJour(Medecin medecin, Date jour);
}
要理解这个接口,我们必须回顾该项目的架构:
![]() |
我们在第3.4.4节中定义了[DAO]层接口,并明确指出它旨在满足[web]层的需求,即用户需求。 [web]层仅通过[business]层与[DAO]层进行通信。这解释了为何[DAO]层的所有方法都位于[business]层中。这些方法仅负责将[web]层的请求转发给[DAO]层,仅此而已。
在应用程序分析过程中,出现了一项需求:能够在网页上显示某位医生某一天的日程安排,以展示哪些时段已被预约、哪些时段尚有空位。这通常发生在秘书通过电话接收预约请求时。来电者会要求在特定日期预约特定医生。为满足这一需求,[业务]层提供了第46行的方法。
// job
public AgendaMedecinJour getAgendaMedecinJour(Medecin medecin, Date jour);
有人可能会疑惑该将此方法放在何处:
- 它可以放在 [DAO] 层。然而,这个方法实际上并非解决数据访问需求,而是解决业务需求;
- 我们也可以将其放在[web]层。但这绝非良策。因为如果我们将[web]层改为[Swing]层,即使需求依然存在,该方法也会随之消失。
该方法将医生和需要查询预约日程的日期作为参数。它返回一个 [AgendaMedecinJour] 对象,该对象表示该医生当天的日程安排:
package rdvmedecins.metier.entites;
import java.io.Serializable;
import java.text.SimpleDateFormat;
import java.util.Date;
import rdvmedecins.jpa.Medecin;
public class AgendaMedecinJour implements Serializable {
private static final long serialVersionUID = 1L;
// fields
private Medecin medecin;
private Date jour;
private CreneauMedecinJour[] creneauxMedecinJour;
// manufacturers
public AgendaMedecinJour() {
}
public AgendaMedecinJour(Medecin medecin, Date jour, CreneauMedecinJour[] creneauxMedecinJour) {
this.medecin = medecin;
this.jour = jour;
this.creneauxMedecinJour = creneauxMedecinJour;
}
public String toString() {
StringBuffer str = new StringBuffer("");
for (CreneauMedecinJour cr : creneauxMedecinJour) {
str.append(" ");
str.append(cr.toString());
}
return String.format("Agenda[%s,%s,%s]", medecin, new SimpleDateFormat("dd/MM/yyyy").format(jour), str.toString());
}
// getters and setters
...
}
- 第12行:该日程表所属的医生,
- 第13行:日程表的日期,
- 第14行:该医生当天的时段。
- 该类包含构造函数(第17、21行)以及自定义的toString方法(第27行)。
[DoctorTimeSlotDay] 类(第 14 行)如下所示:
package rdvmedecins.metier.entites;
import java.io.Serializable;
import rdvmedecins.jpa.Creneau;
import rdvmedecins.jpa.Rv;
public class CreneauMedecinJour implements Serializable {
private static final long serialVersionUID = 1L;
// fields
private Creneau creneau;
private Rv rv;
// manufacturers
public CreneauMedecinJour() {
}
public CreneauMedecinJour(Creneau creneau, Rv rv) {
this.creneau=creneau;
this.rv=rv;
}
// toString
@Override
public String toString() {
return String.format("[%s %s]", creneau,rv);
}
// getters and setters
...
}
- 第 12 行:医生的时段,
- 第13行:关联的预约,若时段为空则为null。
我们可以看到,[AgendaMedecinJour] 类第 14 行中的 creneauxMedecinJour 字段允许我们检索该医生所有时间段,并获取每个时间段的“忙碌”或“空闲”状态。这正是 [IMetier] 类中新增的 [getAgendaMedecinJour] 方法的用途。
我们的 EJB [Metier] 将拥有一个本地接口和一个远程接口,它们仅是主接口 [IMetier] 的扩展:
package rdvmedecins.metier.service;
import javax.ejb.Local;
@Local
public interface IMetierLocal extends IMetier{
}
package rdvmedecins.metier.service;
import javax.ejb.Remote;
@Remote
public interface IMetierRemote extends IMetier{
}
[Metier] EJB 实现这些接口如下:
package rdvmedecins.metier.service;
import java.io.Serializable;
import java.util.Date;
import java.util.Hashtable;
import java.util.List;
import java.util.Map;
import javax.ejb.EJB;
import javax.ejb.Singleton;
import javax.ejb.TransactionAttribute;
import javax.ejb.TransactionAttributeType;
import rdvmedecins.dao.IDaoLocal;
import rdvmedecins.jpa.Client;
import rdvmedecins.jpa.Creneau;
import rdvmedecins.jpa.Medecin;
import rdvmedecins.jpa.Rv;
import rdvmedecins.metier.entites.AgendaMedecinJour;
import rdvmedecins.metier.entites.CreneauMedecinJour;
@Singleton
@TransactionAttribute(TransactionAttributeType.REQUIRED)
public class Metier implements IMetierLocal, IMetierRemote, Serializable {
// dao layer
@EJB
private IDaoLocal dao;
public Metier() {
}
@Override
public List<Client> getAllClients() {
return dao.getAllClients();
}
@Override
public List<Medecin> getAllMedecins() {
return dao.getAllMedecins();
}
@Override
public List<Creneau> getAllCreneaux(Medecin medecin) {
return dao.getAllCreneaux(medecin);
}
@Override
public List<Rv> getRvMedecinJour(Medecin medecin, Date jour) {
return dao.getRvMedecinJour(medecin, jour);
}
@Override
public Client getClientById(Long id) {
return dao.getClientById(id);
}
@Override
public Medecin getMedecinById(Long id) {
return dao.getMedecinById(id);
}
@Override
public Rv getRvById(Long id) {
return dao.getRvById(id);
}
@Override
public Creneau getCreneauById(Long id) {
return dao.getCreneauById(id);
}
@Override
public Rv ajouterRv(Date jour, Creneau creneau, Client client) {
return dao.ajouterRv(jour, creneau, client);
}
@Override
public void supprimerRv(Rv rv) {
dao.supprimerRv(rv);
}
@Override
public AgendaMedecinJour getAgendaMedecinJour(Medecin medecin, Date jour) {
// list of doctor's time slots
List<Creneau> creneauxHoraires = dao.getAllCreneaux(medecin);
// list of bookings for the same doctor on the same day
List<Rv> reservations = dao.getRvMedecinJour(medecin, jour);
// a dictionary is created from the Rvs taken
Map<Long, Rv> hReservations = new Hashtable<Long, Rv>();
for (Rv resa : reservations) {
hReservations.put(resa.getCreneau().getId(), resa);
}
// create the agenda for the requested day
AgendaMedecinJour agenda = new AgendaMedecinJour();
// the doctor
agenda.setMedecin(medecin);
// the day
agenda.setJour(jour);
// reservation slots
CreneauMedecinJour[] creneauxMedecinJour = new CreneauMedecinJour[creneauxHoraires.size()];
agenda.setCreneauxMedecinJour(creneauxMedecinJour);
// filling reservation slots
for (int i = 0; i < creneauxHoraires.size(); i++) {
// line i agenda
creneauxMedecinJour[i] = new CreneauMedecinJour();
// slot id
creneauxMedecinJour[i].setCreneau(creneauxHoraires.get(i));
// is the slot free or reserved?
if (hReservations.containsKey(creneauxHoraires.get(i).getId())) {
// the slot is occupied - we note the resa
Rv resa = hReservations.get(creneauxHoraires.get(i).getId());
creneauxMedecinJour[i].setRv(resa);
}
}
// we return the result
return agenda;
}
}
- 第 22 行,[Metier] 类是一个单例 EJB,
- 第 23 行:每个 EJB 方法都在事务中运行。这意味着事务在 [business] 层的方法开始时启动。该层将调用 [DAO] 层中的方法。这些方法将在同一事务中运行,
- 第 24 行:该 EJB 实现了其本地和远程接口,并且是可序列化的,
- 第 27 行:[DAO] 层中对该 EJB 的引用,
- 第 29 行:该引用将由 GlassFish 服务器的 EJB 容器注入,这得益于 @EJB 注解。因此,当 [Business] 类的方法执行时,指向 [DAO] 层 EJB 的引用已初始化,
- 第 33–81 行:此引用用于将对 [Business] 层的调用委托给 [DAO] 层,
- 第 84 行:getAgendaMedecinJour 方法,用于获取指定日期医生的日程安排。具体实现细节请参阅代码注释。
3.5.2. [business] 层的部署
[业务]层依赖于[DAO]层。每个层都通过一个EJB实现。要测试[业务]EJB,我们需要部署这两个EJB。为此,我们需要一个企业项目。
![]() |
- [1],创建一个新项目,
- 类型为 Maven [2] 和企业应用程序 [3],
- 并为其命名 [4]。后缀“ear”将自动添加,
![]() |
- 在 [5] 中,我们选择 GlassFish 服务器和 Java EE 6,
- 在 [6] 中,企业应用程序包含模块,通常是 EJB 模块和 Web 模块。此处,该企业应用程序将包含我们已构建的两个 EJB 的模块。由于这些模块已存在,我们不勾选复选框,
- 在[7,8]中,已创建了两个项目。[8]是我们将要使用的企业项目。[7]是一个我尚不确定用途的项目。我尚未用过它,而且由于我尚未深入研究Maven,因此不清楚它的具体用途。因此我们将忽略它。
现在企业项目已创建完成,我们可以定义其模块了。
![]() |
- 在 [1] 中,我们创建一个新的依赖项,
- 在 [2] 中,我们选择 EJB [DAO] 项目,
- 在 [3] 中,我们声明其为 EJB。请勿留空类型字段,否则系统将默认使用 jar 类型,而该类型在此处并不适用,
- 在 [4] 中,我们使用 [compile] 作用域,
- 在 [5] 中,该项目及其新依赖项,
![]() |
- 在 [6, 7, 8] 中,重复该过程以添加来自 [business] 层的 EJB,
- 在 [9] 中,两个依赖项,
- 在 [10] 中,我们构建项目,
![]() |
- 在 [11] 中,我们运行它,
- 在 [12] 的 [Services] 选项卡中,我们可以看到该项目已部署到 GlassFish 服务器上。这意味着这两个 EJB 现已存在于服务器上。
在 GlassFish 服务器日志中,您可以找到关于这两个 EJB 部署的信息:
![]() |
- 在 [1] 的 GlassFish 日志选项卡中。
在那里发现了以下日志:
- 第 1-5 行:已识别 JPA 实体,
- 第 7 行:表示持久化单元 [dbrdvmedecins2-PU] 已成功构建,且已建立与相关数据库的连接,
- 第 8 行:EJB [DaoJpa] 的远程和本地接口的可移植名称。“可移植”意味着被所有应用服务器识别,
- 第 9 行:同上,但使用 GlassFish 专有名称,
- 第 10–11 行:EJB [Metier] 的情况相同。
我们将使用 EJB [Metier] 远程接口的可移植名称:
java:global/istia.st_mv-rdvmedecins-metier-dao-ear_ear_1.0-SNAPSHOT/mv-rdvmedecins-ejb-metier-1.0-SNAPSHOT/Metier!rdvmedecins.metier.service.IMetierRemote
在测试 [业务] 层时,我们将需要此名称。
3.5.3. 测试[业务]层
与测试 [DAO] 层一样,我们将把 [业务] 层的测试作为客户端/服务器应用程序的一部分进行:
![]() |
客户端将测试部署在 GlassFish 服务器上的 [Business] EJB 的远程接口。
我们首先创建一个新的 Maven 项目。为此,我们将遵循与创建 [DAO] 层测试项目(参见第 3.4.7 节)相同的步骤,但不包括创建 JUnit 测试。生成的项目如下:
![]() |
- [1] 展示了已创建的项目及其依赖关系:包括 [DAO] 层的 EJB、[Business] 层的 EJB 以及 [gf-client] 库。
此时,项目的 [pom.xml] 文件如下所示:
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>istia.st</groupId>
<artifactId>mv-client-rdvmedecins-ejb-metier</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>jar</packaging>
<name>mv-client-rdvmedecins-ejb-metier</name>
<url>http://maven.apache.org</url>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>org.glassfish.appclient</groupId>
<artifactId>gf-client</artifactId>
<version>3.1.1</version>
</dependency>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>mv-rdvmedecins-ejb-dao-jpa</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>mv-rdvmedecins-ejb-metier</artifactId>
<version>${project.version}</version>
</dependency>
</dependencies>
</project>
请确保已添加第 17–33 行中描述的依赖项。该测试将是一个简单的控制台类:
![]() |
[ClientRdvMedecinsMetier] 类的代码如下:
package istia.st.client;
import java.util.Date;
import java.util.List;
import javax.naming.InitialContext;
import rdvmedecins.jpa.Client;
import rdvmedecins.jpa.Creneau;
import rdvmedecins.jpa.Medecin;
import rdvmedecins.jpa.Rv;
import rdvmedecins.metier.entites.AgendaMedecinJour;
import rdvmedecins.metier.service.IMetierRemote;
public class ClientRdvMedecinsMetier {
// the remote interface name of the EJB [Metier]
private static String IDaoRemoteName = "java:global/istia.st_mv-rdvmedecins-metier-dao-ear_ear_1.0-SNAPSHOT/mv-rdvmedecins-ejb-metier-1.0-SNAPSHOT/Metier!rdvmedecins.metier.service.IMetierRemote";
// today's date
private static Date jour = new Date();
public static void main(String[] args) {
try {
// context JNDI of Glassfish server
InitialContext initialContext = new InitialContext();
// reference on remote [metier] layer
IMetierRemote metier = (IMetierRemote) initialContext.lookup(IDaoRemoteName);
// customer display
List<Client> clients = metier.getAllClients();
display("Liste des clients :", clients);
// physician display
List<Medecin> medecins = metier.getAllMedecins();
display("Liste des médecins :", medecins);
// display doctor's slots
Medecin medecin = medecins.get(0);
List<Creneau> creneaux = metier.getAllCreneaux(medecin);
display(String.format("Liste des créneaux du médecin %s", medecin), creneaux);
// list of doctor's appointments on a given day
display(String.format("Liste des rendez-vous du médecin %s, le [%s]", medecin, jour), metier.getRvMedecinJour(medecin, jour));
// calendar display
AgendaMedecinJour agenda = metier.getAgendaMedecinJour(medecin, jour);
System.out.println(agenda);
// add a RV to the list
Rv rv = null;
Creneau creneau = creneaux.get(2);
Client client = clients.get(0);
System.out.println(String.format("Ajout d'un Rv le [%s] dans le créneau %s pour le client %s", jour, creneau, client));
rv = metier.ajouterRv(jour, creneau, client);
System.out.println("Rv ajouté");
display(String.format("Liste des Rv du médecin %s, le [%s]", medecin, jour), metier.getRvMedecinJour(medecin, jour));
// calendar display
agenda = metier.getAgendaMedecinJour(medecin, jour);
System.out.println(agenda);
// delete a RV
System.out.println("Suppression du Rv ajouté");
metier.supprimerRv(rv);
System.out.println("Rv supprimé");
display(String.format("Liste des Rv du médecin %s, le [%s]", medecin, jour), metier.getRvMedecinJour(medecin, jour));
// calendar display
agenda = metier.getAgendaMedecinJour(medecin, jour);
System.out.println(agenda);
} catch (Throwable ex) {
System.out.println("Erreur...");
while (ex != null) {
System.out.println(String.format("%s : %s", ex.getClass().getName(), ex.getMessage()));
ex = ex.getCause();
}
}
}
// utility method - displays items in a collection
private static void display(String message, List elements) {
System.out.println(message);
for (Object element : elements) {
System.out.println(element);
}
}
}
- 第 18 行:从 GlassFish 日志中获取了 EJB [Metier] 远程接口的可移植名称,
- 第 24–27 行:获取 EJB [Metier] 远程接口的引用,
- 第29–30行:显示客户,
- 第 32–33 行:显示医生,
- 第 35–37 行:显示医生的可用时段,
- 第 39 行:显示某位医生在指定日期的预约,
- 第 41–42 行:该医生当天的日程安排,
- 第44–49行:添加预约,
- 第 50 行:显示该医生的预约。此时应多显示一条,
- 第52–53行:显示医生的日程安排。新增的预约应可见,
- 第55-57行:删除刚刚添加的预约,
- 第58行:这应在医生的预约列表中体现出来,
- 第60–61行:以及在他们的历法中。
我们运行测试:
![]() | ![]() |
Liste des clients :
Client[1,Mr,Jules,MARTIN]
Client[2,Mme,Christine,GERMAN]
Client[3,Mr,Jules,JACQUARD]
Client[4,Melle,Brigitte,BISTROU]
Liste des médecins :
Médecin[1,Mme,Marie,PELISSIER]
Médecin[2,Mr,Jacques,BROMARD]
Médecin[3,Mr,Philippe,JANDOT]
Médecin[4,Melle,Justine,JACQUEMOT]
Liste des créneaux du médecin Médecin[1,Mme,Marie,PELISSIER]
Creneau [1, 1, 8:0, 8:20,Médecin[1,Mme,Marie,PELISSIER]]
Creneau [2, 1, 8:20, 8:40,Médecin[1,Mme,Marie,PELISSIER]]
Creneau [3, 1, 8:40, 9:0,Médecin[1,Mme,Marie,PELISSIER]]
Creneau [4, 1, 9:0, 9:20,Médecin[1,Mme,Marie,PELISSIER]]
Creneau [5, 1, 9:20, 9:40,Médecin[1,Mme,Marie,PELISSIER]]
Creneau [6, 1, 9:40, 10:0,Médecin[1,Mme,Marie,PELISSIER]]
Creneau [7, 1, 10:0, 10:20,Médecin[1,Mme,Marie,PELISSIER]]
Creneau [8, 1, 10:20, 10:40,Médecin[1,Mme,Marie,PELISSIER]]
Creneau [9, 1, 10:40, 11:0,Médecin[1,Mme,Marie,PELISSIER]]
Creneau [10, 1, 11:0, 11:20,Médecin[1,Mme,Marie,PELISSIER]]
Creneau [11, 1, 11:20, 11:40,Médecin[1,Mme,Marie,PELISSIER]]
Creneau [12, 1, 11:40, 12:0,Médecin[1,Mme,Marie,PELISSIER]]
Creneau [13, 1, 14:0, 14:20,Médecin[1,Mme,Marie,PELISSIER]]
Creneau [14, 1, 14:20, 14:40,Médecin[1,Mme,Marie,PELISSIER]]
Creneau [15, 1, 14:40, 15:0,Médecin[1,Mme,Marie,PELISSIER]]
Creneau [16, 1, 15:0, 15:20,Médecin[1,Mme,Marie,PELISSIER]]
Creneau [17, 1, 15:20, 15:40,Médecin[1,Mme,Marie,PELISSIER]]
Creneau [18, 1, 15:40, 16:0,Médecin[1,Mme,Marie,PELISSIER]]
Creneau [19, 1, 16:0, 16:20,Médecin[1,Mme,Marie,PELISSIER]]
Creneau [20, 1, 16:20, 16:40,Médecin[1,Mme,Marie,PELISSIER]]
Creneau [21, 1, 16:40, 17:0,Médecin[1,Mme,Marie,PELISSIER]]
Creneau [22, 1, 17:0, 17:20,Médecin[1,Mme,Marie,PELISSIER]]
Creneau [23, 1, 17:20, 17:40,Médecin[1,Mme,Marie,PELISSIER]]
Creneau [24, 1, 17:40, 18:0,Médecin[1,Mme,Marie,PELISSIER]]
Liste des créneaux du médecin Médecin[1,Mme,Marie,PELISSIER], le [Wed May 23 16:25:26 CEST 2012]
Agenda[Médecin[1,Mme,Marie,PELISSIER],23/05/2012, [Creneau [1, 1, 8:0, 8:20,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [2, 1, 8:20, 8:40,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [3, 1, 8:40, 9:0,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [4, 1, 9:0, 9:20,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [5, 1, 9:20, 9:40,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [6, 1, 9:40, 10:0,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [7, 1, 10:0, 10:20,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [8, 1, 10:20, 10:40,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [9, 1, 10:40, 11:0,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [10, 1, 11:0, 11:20,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [11, 1, 11:20, 11:40,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [12, 1, 11:40, 12:0,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [13, 1, 14:0, 14:20,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [14, 1, 14:20, 14:40,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [15, 1, 14:40, 15:0,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [16, 1, 15:0, 15:20,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [17, 1, 15:20, 15:40,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [18, 1, 15:40, 16:0,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [19, 1, 16:0, 16:20,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [20, 1, 16:20, 16:40,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [21, 1, 16:40, 17:0,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [22, 1, 17:0, 17:20,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [23, 1, 17:20, 17:40,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [24, 1, 17:40, 18:0,Médecin[1,Mme,Marie,PELISSIER]] null]]
Ajout d'un Rv le [Wed May 23 16:25:26 CEST 2012] dans le créneau Creneau [3, 1, 8:40, 9:0,Médecin[1,Mme,Marie,PELISSIER]] pour le client Client[1,Mr,Jules,MARTIN]
Rv ajouté
Liste des Rv du médecin Médecin[1,Mme,Marie,PELISSIER], le [Wed May 23 16:25:26 CEST 2012]
Rv[252, Creneau [3, 1, 8:40, 9:0,Médecin[1,Mme,Marie,PELISSIER]], Client[1,Mr,Jules,MARTIN]]
Agenda[Médecin[1,Mme,Marie,PELISSIER],23/05/2012, [Creneau [1, 1, 8:0, 8:20,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [2, 1, 8:20, 8:40,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [3, 1, 8:40, 9:0,Médecin[1,Mme,Marie,PELISSIER]] Rv[252, Creneau [3, 1, 8:40, 9:0,Médecin[1,Mme,Marie,PELISSIER]], Client[1,Mr,Jules,MARTIN]]] [Creneau [4, 1, 9:0, 9:20,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [5, 1, 9:20, 9:40,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [6, 1, 9:40, 10:0,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [7, 1, 10:0, 10:20,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [8, 1, 10:20, 10:40,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [9, 1, 10:40, 11:0,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [10, 1, 11:0, 11:20,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [11, 1, 11:20, 11:40,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [12, 1, 11:40, 12:0,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [13, 1, 14:0, 14:20,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [14, 1, 14:20, 14:40,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [15, 1, 14:40, 15:0,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [16, 1, 15:0, 15:20,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [17, 1, 15:20, 15:40,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [18, 1, 15:40, 16:0,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [19, 1, 16:0, 16:20,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [20, 1, 16:20, 16:40,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [21, 1, 16:40, 17:0,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [22, 1, 17:0, 17:20,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [23, 1, 17:20, 17:40,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [24, 1, 17:40, 18:0,Médecin[1,Mme,Marie,PELISSIER]] null]]
Suppression du Rv ajouté
Rv supprimé
Liste des Rv du médecin Médecin[1,Mme,Marie,PELISSIER], le [Wed May 23 16:25:26 CEST 2012]
Agenda[Médecin[1,Mme,Marie,PELISSIER],23/05/2012, [Creneau [1, 1, 8:0, 8:20,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [2, 1, 8:20, 8:40,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [3, 1, 8:40, 9:0,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [4, 1, 9:0, 9:20,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [5, 1, 9:20, 9:40,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [6, 1, 9:40, 10:0,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [7, 1, 10:0, 10:20,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [8, 1, 10:20, 10:40,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [9, 1, 10:40, 11:0,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [10, 1, 11:0, 11:20,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [11, 1, 11:20, 11:40,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [12, 1, 11:40, 12:0,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [13, 1, 14:0, 14:20,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [14, 1, 14:20, 14:40,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [15, 1, 14:40, 15:0,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [16, 1, 15:0, 15:20,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [17, 1, 15:20, 15:40,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [18, 1, 15:40, 16:0,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [19, 1, 16:0, 16:20,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [20, 1, 16:20, 16:40,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [21, 1, 16:40, 17:0,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [22, 1, 17:0, 17:20,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [23, 1, 17:20, 17:40,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [24, 1, 17:40, 18:0,Médecin[1,Mme,Marie,PELISSIER]] null]]
- 第37行:佩利西耶女士的日程安排,2012年5月23日。未预留时段,
- 第39行:新增一个预约,
- 第42行:佩利西耶女士的新日程。现已为马丁先生预留了一个时段,
- 第44行:该预约已被删除,
- 第46行:佩利西耶女士的日历显示未预留任何时段。
我们现在认为 [CAD] 和 [business] 层已投入运行。我们仍需使用 JSF 框架编写 [web] 层。为此,我们将运用本文开头所掌握的知识。
3.6. [Web]层
让我们回到当前正在构建的架构:
![]() |
我们将构建最后一层,即[Web]层。
3.6.1. NetBeans 项目
我们将构建一个 Maven 项目:
![]() |
- 在[1]中,我们创建了一个新项目,
- 在 [2, 3] 中,创建了一个类型为 [Web Application] 的 Maven 项目,
- 在 [4] 中,为其命名,
![]() |
- 在 [5] 中,选择 GlassFish 服务器和 Java EE 6 Web,
- 在 [6] 中,选择由此创建的项目,
- 在 [7] 中,删除 [Source Packages] 中的 [index.jsp] 页面和包后的项目,
![]() |
- 在 [8, 9] 中,在项目属性中添加一个框架,
- 在 [10] 中,选择 Java Server Faces,
![]() |
- 在 [11] 中,进行 Java Server Faces 配置。保留默认值。请注意,这里使用的是 JSF 2,
- 在 [12] 中,项目将进行两项修改:生成一个 [web.xml] 文件,以及一个 [index.html] 页面。
[web.xml] 文件内容如下:
<?xml version="1.0" encoding="UTF-8"?>
<web-app version="3.0" xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd">
<context-param>
<param-name>javax.faces.PROJECT_STAGE</param-name>
<param-value>Development</param-value>
</context-param>
<servlet>
<servlet-name>Faces Servlet</servlet-name>
<servlet-class>javax.faces.webapp.FacesServlet</servlet-class>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>Faces Servlet</servlet-name>
<url-pattern>/faces/*</url-pattern>
</servlet-mapping>
<session-config>
<session-timeout>
30
</session-timeout>
</session-config>
<welcome-file-list>
<welcome-file>faces/index.xhtml</welcome-file>
</welcome-file-list>
</web-app>
我们之前已经遇到过这个文件。
- 第 7–11 行:定义将处理所有发往该应用程序请求的 Servlet。这是 JSF Servlet,
- 第 12–15 行:定义了该 Servlet 处理的 URL。这些 URL 的格式为 /faces/*,
- 第 21–23 行:将 [index.xhtml] 页面定义为主页。
该页面内容如下:
<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="http://java.sun.com/jsf/html">
<h:head>
<title>Facelet Title</title>
</h:head>
<h:body>
Hello from Facelets
</h:body>
</html>
我们之前见过这种情况。我们可以运行这个项目:
![]() |
- 在 [1] 中,我们运行该项目,并在浏览器中得到结果 [2]。
接下来我们将展示完整的项目,然后详细介绍其各个组成部分。
![]() |
- 在[1]中,该项目的XHTML页面,
- 在 [2] 中,Java 代码,
- 在 [3] 中,消息文件,因为该应用程序已实现国际化,
![]() |
- 在 [4] 中,项目依赖项。
3.6.2. 项目依赖项
让我们回到项目架构:
![]() |
JSF 层依赖于 [业务]、[DAO] 和 [JPA] 层。这三个层被封装在我们构建的两个 Maven 项目中,这也解释了项目依赖关系 [4]。下面简单展示一下如何添加这些依赖:
![]() |
- 在 [1] 中,我们将使用 ejb 来表示该依赖项指向一个 EJB 项目,
- 在 [2] 中,我们将输入 [provided]。这是因为 Web 项目将与两个 EJB 项目一同部署。因此,它无需包含 EJB JAR 文件。
3.6.3. 项目配置
项目配置与本文开头介绍的 JSF 项目配置相同。我们仅列出配置文件,不再赘述。
![]() | ![]() |
[web.xml]:用于配置 Web 应用程序。
<?xml version="1.0" encoding="UTF-8"?>
<web-app version="3.0" xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd">
<context-param>
<param-name>javax.faces.PROJECT_STAGE</param-name>
<param-value>Production</param-value>
</context-param>
<context-param>
<param-name>javax.faces.FACELETS_SKIP_COMMENTS</param-name>
<param-value>true</param-value>
</context-param>
<servlet>
<servlet-name>Faces Servlet</servlet-name>
<servlet-class>javax.faces.webapp.FacesServlet</servlet-class>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>Faces Servlet</servlet-name>
<url-pattern>/faces/*</url-pattern>
</servlet-mapping>
<session-config>
<session-timeout>
30
</session-timeout>
</session-config>
<welcome-file-list>
<welcome-file>faces/index.xhtml</welcome-file>
</welcome-file-list>
<error-page>
<error-code>500</error-code>
<location>/faces/exception.xhtml</location>
</error-page>
<error-page>
<exception-type>Exception</exception-type>
<location>/faces/exception.xhtml</location>
</error-page>
</web-app>
请注意,第 26 行中的 [index.xhtml] 页面是应用程序的首页。
[faces-config.xml]:配置 JSF 应用程序
<?xml version='1.0' encoding='UTF-8'?>
<!-- =========== FULL CONFIGURATION FILE ================================== -->
<faces-config version="2.0"
xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-facesconfig_2_0.xsd">
<application>
<resource-bundle>
<base-name>
messages
</base-name>
<var>msg</var>
</resource-bundle>
<message-bundle>messages</message-bundle>
</application>
</faces-config>
[beans.xml]:内容为空,但 @Named 注解需要该文件
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/beans_1_0.xsd">
</beans>
[styles.css]:应用程序的样式表
.reservationsHeaders {
text-align: center;
font-style: italic;
color: Snow;
background: Teal;
}
.creneau {
height: 25px;
text-align: center;
background: MediumTurquoise;
}
.client {
text-align: left;
background: PowderBlue;
}
.action {
width: 6em;
text-align: left;
color: Black;
background: MediumTurquoise;
}
.erreursHeaders {
background: Teal;
background-color: #ff6633;
color: Snow;
font-style: italic;
text-align: center
}
.erreurClasse {
background: MediumTurquoise;
background-color: #ffcc66;
height: 25px;
text-align: center
}
.erreurMessage {
background: PowderBlue;
background-color: #ffcc99;
text-align: left
}
[messages_fr.properties]:法语消息文件
# layout
layout.entete=Les M\u00e9decins Associ\u00e9s
layout.basdepage=ISTIA, universit\u00e9 d'Angers
layout.entete.langue1=Fran\u00e7ais
layout.entete.langue2=Anglais
# exception
exception.header=L'exception suivante s'est produite
exception.httpCode=Code HTTP de l'erreur
exception.message=Message de l'exception
exception.requestUri=Url demand\u00e9e lors de l'erreur
exception.servletName=Nom de la servlet demand\u00e9e lorsque l'erreur s'est produite
# formulaire 1
form1.titre=R\u00e9servations
form1.medecin=M\u00e9decin
form1.jour=Jour (jj/mm/aaaa)
form1.button.agenda=Agenda
form1.jour.required=date requise
form1.jour.erreur=date erron\u00e9e
# formulaire 2
form2.titre=Agenda de {0} {1} {2} le {3}
form2.titre_detail=Agenda de {0} {1} {2} le {3}
form2.creneauHoraire=Cr\u00e9neau horaire
form2.client=Client
form2.accueil=Accueil
form2.supprimer=Supprimer
form2.reserver=R\u00e9server
# formulaire 3
form3.titre=Prise de rendez-vous de {0} {1} {2}, le {3} dans le cr\u00e9neau {4,number,#00}:{5,number,#00} - {6,number,#00}:{7,number,#00}
form3.titre_detail=Prise de rendez-vous de {0} {1} {2}, le {3} dans le cr\u00e9neau {4,number,#00}:{5,number,#00} - {6,number,#00}:{7,number,#00}
form3.client=Client
form3.valider=Valider
form3.annuler=Annuler
# erreur
erreur.titre=Une erreur s'est produite.
erreur.message=Message d'erreur
erreur.accueil=Page d'accueil
erreur.classe=Cause
[messages_en.properties]:英文消息文件
# layout
layout.entete=Associated Doctors
layout.basdepage=ISTIA, Angers university
layout.entete.langue1=French
layout.entete.langue2=English
# exception
exception.header=The following exceptions occurred
exception.httpCode=Error HTTP code
exception.message=Exception message
exception.requestUri=Url targeted when error occurred
exception.servletName=Servlet targeted's name when error occurred
# formulaire 1
form1.titre=Reservations
form1.medecin=Doctor
form1.jour=Date (dd/mm/yyyy)
form1.button.agenda=Diary
form1.jour.required=The date is required
form1.jour.erreur=The date is invalid
# formulaire 2
form2.titre={0} {1} {2}'' diary on {3}
form2.titre_detail={0} {1} {2}'' diary on {3}
form2.creneauHoraire=Time Period
form2.client=Client
form2.accueil=Welcome Page
form2.supprimer=Delete
form2.reserver=Reserve
# formulaire 3
form3.titre=Reservation for {0} {1} {2}, on {3} in the time period {4,number,#00}:{5,number,#00} - {6,number,#00}:{7,number,#00}
form3.titre_detail=Reservation for {0} {1} {2}, on {3} in the time period {4,number,#00}:{5,number,#00} - {6,number,#00}:{7,number,#00}
form3.client=Client
form3.valider=Submit
form3.annuler=Cancel
# erreur
erreur.titre=An error occurred
erreur.message=Error message
erreur.accueil=Welcome Page
erreur.classe=Cause
3.6.4. 项目视图
让我们回顾一下应用程序的工作原理。主页如下所示:
![]() |
从这个初始页面开始,用户(行政人员、医生)将执行一系列操作。我们将在下面进行说明。左侧视图显示了用户发起请求时的界面;右侧视图显示了服务器发送的响应。
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
最后,还可能显示一个错误页面:
![]() |
这些不同的视图由该网络项目的以下页面生成:
![]() |
- 在[1]中,页面[basdepage、entete、layout]负责处理所有视图的格式设置,
- 在 [2] 中,由 [layout.xhtml] 生成的视图。
此处采用了 Facelets 技术。相关内容已在第 2.11 节中进行过说明。我们仅提供用于布局的 XHTML 页面的代码:
[entete.xhtml]
<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:f="http://java.sun.com/jsf/core"
xmlns:ui="http://java.sun.com/jsf/facelets">
<body>
<h2><h:outputText value="#{msg['layout.entete']}"/></h2>
<div align="left">
<h:commandLink value="#{msg['layout.entete.langue1']}" actionListener="#{changeLocale.setFrenchLocale}"/>
<h:outputText value=" "/>
<h:commandLink value="#{msg['layout.entete.langue2']}" actionListener="#{changeLocale.setEnglishLocale}"/>
</div>
</body>
</html>
请注意第10至12行,那两个用于更改应用程序语言的链接。
[basdepage.xhtml]
<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="http://java.sun.com/jsf/html">
<body>
<h:outputText value="#{msg['layout.basdepage']}"/>
</body>
</html>
[layout.xhtml]
<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:f="http://java.sun.com/jsf/core"
xmlns:ui="http://java.sun.com/jsf/facelets">
<f:view locale="#{changeLocale.locale}">
<h:head>
<title>RdvMedecins</title>
<h:outputStylesheet library="css" name="styles.css"/>
</h:head>
<h:body style="background-image: url('${request.contextPath}/resources/images/standard.jpg');">
<h:form id="formulaire">
<table style="width: 1200px">
<tr>
<td colspan="2" bgcolor="#ccccff">
<ui:include src="entete.xhtml"/>
</td>
</tr>
<tr>
<td style="width: 100px; height: 200px" bgcolor="#ffcccc">
</td>
<td>
<ui:insert name="contenu" >
<h2>Contenu</h2>
</ui:insert>
</td>
</tr>
<tr bgcolor="#ffcc66">
<td colspan="2">
<ui:include src="basdepage.xhtml"/>
</td>
</tr>
</table>
</h:form>
</h:body>
</f:view>
</html>
此页面是 [index.xhtml] 页面的模板:
<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:f="http://java.sun.com/jsf/core"
xmlns:ui="http://java.sun.com/jsf/facelets">
<ui:composition template="layout.xhtml">
<ui:define name="contenu">
<h:panelGroup rendered="#{form.form1Rendered}">
<ui:include src="form1.xhtml"/>
</h:panelGroup>
<h:panelGroup rendered="#{form.form2Rendered}">
<ui:include src="form2.xhtml"/>
</h:panelGroup>
<h:panelGroup rendered="#{form.form3Rendered}">
<ui:include src="form3.xhtml"/>
</h:panelGroup>
<h:panelGroup rendered="#{form.erreurRendered}">
<ui:include src="erreur.xhtml"/>
</h:panelGroup>
</ui:define>
</ui:composition>
</html>
第 8–21 行定义了 [layout.xhtml](第 7 行)中名为“content”(第 8 行)的区域。这是视图的中心区域:
![]() |
[index.xhtml] 页面是应用程序中的唯一页面。 因此,页面之间不会有导航。它会显示以下四个页面之一:[form1.xhtml、form2.xhtml、form3.xhtml、error.xhtml]。该显示由表单 Bean 中的四个布尔变量 [form1Rendered、form2Rendered、form3Rendered、errorRendered] 控制,我们稍后将对此进行说明。
3.6.5. 该项目的 Bean
![]() |
[utils] 包中的类已经介绍过了:
- [ChangeLocale] 类是负责处理语言切换的类。该类已在第 2.4.4 节中讨论过。
- [Messages] 类是一个用于促进应用程序消息国际化的类。该类已在第 2.8.5.7 节中讨论过。
3.6.5.1. 应用程序 Bean
[Application] Bean 如下所示:
package beans;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.annotation.PostConstruct;
import javax.ejb.EJB;
import javax.enterprise.context.ApplicationScoped;
import javax.inject.Named;
import rdvmedecins.jpa.Client;
import rdvmedecins.jpa.Medecin;
import rdvmedecins.metier.service.IMetierLocal;
@Named(value = "application")
@ApplicationScoped
public class Application implements Serializable{
// business layer
@EJB
private IMetierLocal metier;
// cache
private List<Medecin> medecins;
private List<Client> clients;
private Map<Long, Medecin> hMedecins = new HashMap<Long, Medecin>();
private Map<Long, Client> hClients = new HashMap<Long, Client>();
// errors
private List<Erreur> erreurs = new ArrayList<Erreur>();
private Boolean erreur = false;
public Application() {
}
@PostConstruct
public void init() {
// caching doctors and customers
try {
medecins = metier.getAllMedecins();
clients = metier.getAllClients();
} catch (Throwable th) {
// we note the error
erreur = true;
erreurs.add(new Erreur(th.getClass().getName(), th.getMessage()));
while (th.getCause() != null) {
th = th.getCause();
erreurs.add(new Erreur(th.getClass().getName(), th.getMessage()));
}
return;
}
// list checking
if (medecins.size() == 0) {
// we note the error
erreur = true;
erreurs.add(new Erreur("", "La liste des médecins est vide"));
}
if (clients.size() == 0) {
// we note the error
erreur = true;
erreurs.add(new Erreur("", "La liste des clients est vide"));
}
// mistake?
if (erreur) {
return;
}
// dictionaries
for (Medecin m : medecins) {
hMedecins.put(m.getId(), m);
}
for (Client c : clients) {
hClients.put(c.getId(), c);
}
}
// getters and setters
...
}
- 第 15-16 行:[Application] 类是一个应用程序范围的 Bean。它在 JSF 应用程序生命周期开始时创建一次,所有用户的所有请求均可访问它。我们通常将只读数据存储在应用程序中。在此,我们将存储医生列表和客户列表。因此,我们假设这些数据不会经常更改。XHTML 页面通过应用程序名称访问它们,
- 第 20–21 行:GlassFish EJB 容器将注入对 [Business] EJB 本地接口的引用。让我们回顾一下应用程序架构:
![]() |
JSF 应用程序和 [Metier] EJB 将运行在同一台 JVM(Java 虚拟机)中。 因此,[JSF] 层将使用该 EJB 的本地接口。在此,应用程序 Bean 使用了 [Business] EJB。即使情况并非如此,在 [business] 层中找到对其的引用也是正常的。这确实是所有用户发出的所有请求均可共享的信息,因此属于应用程序作用域的数据。
- 第 34–35 行:init 方法在 [Application] 类实例化后立即执行(存在 @PostConstruct 注解)。
- 第 36–73 行:该方法创建了以下元素:第 23 行的医生列表、第 24 行的客户列表、第 25 行以医生 ID 为索引的医生字典,以及第 26 行以客户 ID 为索引的客户字典。可能发生错误,这些错误会被记录在第 28 行的列表中。
[Error] 类定义如下:
package beans;
public class Erreur {
public Erreur() {
}
// field
private String classe;
private String message;
// manufacturer
public Erreur(String classe, String message){
this.setClasse(classe);
this.message=message;
}
// getters and setters
...
}
- 第 9 行:如果抛出了异常,则显示异常类的名称,
- 第 10 行:错误消息。
3.6.5.2. [Form] Bean
其代码如下:
package beans;
...
@Named(value = "form")
@SessionScoped
public class Form implements Serializable {
public Form() {
}
// bean Application
@Inject
private Application application;
// model
private Long idMedecin;
private Date jour = new Date();
private Boolean form1Rendered = true;
private Boolean form2Rendered = false;
private Boolean form3Rendered = false;
private Boolean erreurRendered = false;
private String form2Titre;
private String form3Titre;
private AgendaMedecinJour agendaMedecinJour;
private Long idCreneau;
private Medecin medecin;
private Client client;
private Long idClient;
private CreneauMedecinJour creneauChoisi;
private List<Erreur> erreurs;
@PostConstruct
private void init() {
// was the initialization successful?
if (application.getErreur()) {
// retrieve the list of errors
erreurs = application.getErreurs();
// the error view is displayed
setForms(false, false, false, true);
}
}
// view display
private void setForms(Boolean form1Rendered, Boolean form2Rendered, Boolean form3Rendered, Boolean erreurRendered) {
this.form1Rendered = form1Rendered;
this.form2Rendered = form2Rendered;
this.form3Rendered = form3Rendered;
this.erreurRendered = erreurRendered;
}
.................................................
}
- 第 5-7 行:[Form] 类是一个名为“form”且作用域为会话的 Bean。请注意,因此该类必须是可序列化的。
- 第 13-14 行:表单 Bean 持有对应用程序 Bean 的引用。该引用将由应用程序运行的 Servlet 容器注入(存在 @Inject 注解)。
- 第 17-31 行:页面模板 [form1.xhtml, form2.xhtml, form3.xhtml, error.xhtml]。这些页面的显示由第 19-22 行的布尔变量控制。请注意,默认情况下将渲染 [form1.xhtml] 页面,
- 第 33–34 行:类实例化后立即执行 init 方法(存在 @PostConstruct 注解),
- 第 35–41 行:init 方法用于确定应首先显示哪个页面:通常是 [form1.xhtml] 页面(第 19 行),除非应用程序初始化失败(第 36 行),在这种情况下将显示 [error.xhtml] 页面(第 40 行)。
[error.xhtml] 页面内容如下:
<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:f="http://java.sun.com/jsf/core"
xmlns:ui="http://java.sun.com/jsf/facelets">
<body>
<h2><h:outputText value="#{msg['erreur.titre']}"/></h2>
<p>
<h:commandButton value="#{msg['erreur.accueil']}" actionListener="#{form.accueil()}"/>
</p>
<hr/>
<h:dataTable value="#{form.erreurs}" var="erreur" headerClass="erreursHeaders" columnClasses="erreurClasse,erreurMessage">
<h:column>
<f:facet name="header">
<h:outputText value="#{msg['erreur.classe']}"/>
</f:facet>
<h:outputText value="#{erreur.classe}"/>
</h:column>
<h:column>
<f:facet name="header">
<h:outputText value="#{msg['erreur.message']}"/>
</f:facet>
<h:outputText value="#{erreur.message}"/>
</h:column>
</h:dataTable>
</body>
</html>
它使用 <h:dataTable> 标签(第 14–27 行)来显示错误列表。生成的页面类似于以下内容:

接下来,我们将定义应用程序生命周期的各个阶段。
3.6.6. 页面与模型之间的交互
3.6.6.1. 显示主页
如果一切顺利,首先显示的页面是 [form1.xhtml]。这将呈现如下视图:
![]() |
[form1.xhtml] 页面内容如下:
<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:f="http://java.sun.com/jsf/core"
xmlns:ui="http://java.sun.com/jsf/facelets">
<body>
<h2><h:outputText value="#{msg['form1.titre']}"/></h2>
<h:panelGrid columns="3">
<h:panelGroup>
<div align="center"><h3><h:outputText value="#{msg['form1.medecin']}"/></h3></div>
</h:panelGroup>
<h:panelGroup>
<div align="center"><h3><h:outputText value="#{msg['form1.jour']}"/></h3></div>
</h:panelGroup>
<h:panelGroup/>
<h:selectOneMenu value="#{form.idMedecin}">
<f:selectItems value="#{form.medecins}" var="medecin" itemLabel="#{medecin.titre} #{medecin.prenom} #{medecin.nom}" itemValue="#{medecin.id}"/>
</h:selectOneMenu>
<h:inputText id="jour" value="#{form.jour}" required="true" requiredMessage="#{msg['form1.jour.required']}" converterMessage="#{msg['form1.jour.erreur']}">
<f:convertDateTime pattern="dd/MM/yyyy"/>
</h:inputText>
<h:message for="jour" styleClass="error"/>
</h:panelGrid>
<h:commandButton value="#{msg['form1.button.agenda']}" actionListener="#{form.getAgenda}"/>
</body>
</html>
本页面由以下模型提供支持:
@Named(value = "form")
@SessionScoped
public class Form implements Serializable {
// bean Application
@Inject
private Application application;
// model
private Long idMedecin;
private Date jour = new Date();
// list of doctors
public List<Medecin> getMedecins() {
return application.getMedecins();
}
// agenda
public void getAgenda() {
...
}
- 第 9 行中的字段会读取并写入页面第 18 行列表中的值。页面首次加载时,它会设置下拉列表中选中的值。在初始加载时,idMedecin 等于 null,因此将选中第一位医生。
- 第 13–15 行中的方法用于生成医生下拉列表中的项目(页面第 19 行)。每个生成的选项将使用医生的头衔、姓氏和名字作为标签(itemLabel),并使用医生的 ID 作为值(itemValue),
- 第10行的字段为页面第21行的输入字段提供读写访问权限。初始显示时,会显示当前日期,
- 第 17–19 行:getAgenda 方法处理页面第 26 行 [Agenda] 按钮的点击事件。由于不存在导航(请求的总是 [index.html] 页面),我们通常会使用 actionListener 属性代替 action 属性。在此情况下,模型中调用的方法不返回任何结果。
点击 [Agenda] 按钮时,
- 数据将通过 POST 方式提交:医生下拉菜单中选定的值存储在模型的 idMedecin 字段中,日期字段中选定的日期;
- 模型的 getAgenda 方法被调用。
getAgenda 方法如下:
// bean Application
@Inject
private Application application;
// model
private Long idMedecin;
private Date jour = new Date();
private Boolean form1Rendered = true;
private Boolean form2Rendered = false;
private Boolean form3Rendered = false;
private Boolean erreurRendered = false;
private String form2Titre;
private AgendaMedecinJour agendaMedecinJour;
private Medecin medecin;
private List<Erreur> erreurs;
// agenda
public void getAgenda() {
try {
// we get the doctor back
medecin = application.gethMedecins().get(idMedecin);
// title form 2
form2Titre = Messages.getMessage(null, "form2.titre", new Object[]{medecin.getTitre(), medecin.getPrenom(), medecin.getNom(), new SimpleDateFormat("dd MMM yyyy").format(jour)}).getSummary();
// the doctor's diary for a given day
agendaMedecinJour = application.getMetier().getAgendaMedecinJour(medecin, jour);
// form 2 is displayed
setForms(false, true, false, false);
} catch (Throwable th) {
// error view
prepareVueErreur(th);
}
}
// preparation vueErreur
private void prepareVueErreur(Throwable th) {
// create an error list
erreurs = new ArrayList<Erreur>();
erreurs.add(new Erreur(th.getClass().getName(), th.getMessage()));
while (th.getCause() != null) {
th = th.getCause();
erreurs.add(new Erreur(th.getClass().getName(), th.getMessage()));
}
// the error view is displayed
setForms(false, false, false, true);
}
让我们回顾一下 getAgenda 方法应该显示什么:
![]() |
- 第 21 行:我们从应用程序 Bean 中存储的 doctors 字典中检索选定的医生。为此,我们使用其 ID,该 ID 已提交至 idMedecin 字段,
- 第23行:我们准备待显示的页面[form2.xhtml]的标题。该消息从消息文件中获取,以便实现国际化。此技术已在第2.8.5.7节(第135页)中描述。
- 第 25 行:我们调用 [业务] 层,以计算所选医生在所选日期的日程安排,
- 第 27 行:显示 [form2.xhtml],
- 第 28 行:若发生异常,将构建错误列表(第 37–42 行)并显示页面 [error.xhtml](第 44 行)。
3.6.6.2. 显示医生的日程安排
页面 [form2.xhtml] 对应以下视图:
![]() |
[form2.xhtml] 页面的代码如下:
<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:f="http://java.sun.com/jsf/core"
xmlns:ui="http://java.sun.com/jsf/facelets"
xmlns:c="http://java.sun.com/jsp/jstl/core">
<body>
<h2><h:outputText value="#{form.form2Titre}"/></h2>
<h:commandButton value="#{msg['form2.accueil']}" action="#{form.accueil}" />
<h:dataTable value="#{form.agendaMedecinJour.creneauxMedecinJour}" var="creneauMedecinJour" headerClass="reservationsHeaders" columnClasses="creneau,client,action">
<h:column>
<f:facet name="header">
<h:outputText value="#{msg['form2.creneauHoraire']}"/>
</f:facet>
<h:outputText value="#{creneauMedecinJour.creneau.hdebut}:#{creneauMedecinJour.creneau.mdebut} - #{creneauMedecinJour.creneau.hfin}:#{creneauMedecinJour.creneau.mfin}" />
</h:column>
<h:column>
<f:facet name="header">
<h:outputText value="#{msg['form2.client']}"/>
</f:facet>
<c:if test="#{creneauMedecinJour.rv==null}">
<h:outputText value=""/>
<c:otherwise>
<h:outputText value="#{creneauMedecinJour.rv.client.titre} #{creneauMedecinJour.rv.client.prenom} #{creneauMedecinJour.rv.client.nom}"/>
</c:otherwise>
</c:if>
</h:column>
<h:column>
<f:facet name="header"/>
<h:commandLink action="#{form.action()}" value="#{creneauMedecinJour.rv==null ? msg['form2.reserver'] : msg['form2.supprimer']}">
<f:setPropertyActionListener value="#{creneauMedecinJour.creneau.id}" target="#{form.idCreneau}"/>
</h:commandLink>
</h:column>
</h:dataTable>
</body>
</html>
请注意,getAgenda 方法在模型中初始化了两个字段:
// modèle
private String form2Titre;
private AgendaMedecinJour agendaMedecinJour;
这两个字段用于填充 [form2.xhtml] 页面:
- 第 10 行,页面标题,
- 第 12 行:使用三列的 <h:dataTable> 标签显示医生的日程安排,
- 第 13–18 行:第一列显示时间段,
- 第 19–30 行:第二列显示可能已预订该时段的客户姓名,若未预订则显示空值。为实现此筛选功能,我们使用了第 7 行引用的 JSTL Core 库中的标签,
- 第30–35行:第三列在时间段可用时显示[预约]链接,若时间段已被预约则显示[删除]链接。
第三列中的链接指向以下模板:
// modèle
private Long idCreneau;
// action sur RV
public void action() {
...
}
- 当用户点击“预订/删除”链接时(第32行),会调用 action 方法。请注意,此处使用了 action 属性。 该属性指向的方法应具有 String action() 的签名,因为该方法必须返回一个导航键。然而,此处使用的是 void action()。这并未 引发错误,我们可以假设在此情况下不存在导航。这正是设计意图。使用 actionListener 代替 action 会导致功能失效,
- 第 2 行中的 `idCreneau` 字段将获取与被点击链接相关联的时间段的 ID(页面第 33 行)。
3.6.6.3. 删除预约
让我们来分析处理删除预约的代码。这对应于以下视图序列:
![]() |
该操作涉及的代码如下:
// bean Application
@Inject
private Application application;
// model
private Boolean form1Rendered = true;
private Boolean form2Rendered = false;
private Boolean form3Rendered = false;
private Boolean erreurRendered = false;
private AgendaMedecinJour agendaMedecinJour;
private Long idCreneau;
private CreneauMedecinJour creneauChoisi;
private List<Erreur> erreurs;
// action on RV
public void action() {
// search for the time slot in the calendar
int i = 0;
Boolean trouvé = false;
while (!trouvé && i < agendaMedecinJour.getCreneauxMedecinJour().length) {
if (agendaMedecinJour.getCreneauxMedecinJour()[i].getCreneau().getId() == idCreneau) {
trouvé = true;
} else {
i++;
}
}
// have we found?
if (!trouvé) {
// it's weird - form2 is redisplayed
setForms(false, true, false, false);
return;
}
// we found
creneauChoisi = agendaMedecinJour.getCreneauxMedecinJour()[i];
// according to desired action
if (creneauChoisi.getRv() == null) {
reserver();
} else {
supprimer();
}
}
// reservation
public void reserver() {
...
}
public void supprimer() {
try {
// deleting an appointment
application.getMetier().supprimerRv(creneauChoisi.getRv());
// updating the agenda
agendaMedecinJour = application.getMetier().getAgendaMedecinJour(medecin, jour);
// form2 is displayed
setForms(false, true, false, false);
} catch (Throwable th) {
// error view
prepareVueErreur(th);
}
}
- 第 16 行:当操作方法开始时,所选时间段的 ID 已存储在 idCreneau 中(第 11 行),
- 第18–26行:我们尝试根据ID检索该时间段(第21行)。我们在第10行定义的当前日历agendaMedecinJour中进行搜索。通常情况下,我们应该能找到它。如果找不到,则不执行任何操作(第28–32行),
- 第 34 行:若找到该时段,则获取其引用并将其存储在第 12 行,
- 第36行:检查所选时间段是否已有预约。如有,则删除该预约(第39行);否则,预留该时间段(第37行),
- 第 51 行:选定时段内的预约被删除。此操作由 [业务] 层处理,
- 第 53 行:我们向 [业务] 层请求医生的更新后的日程表。当然,我们会发现那里的预约减少了一个。但由于应用程序是多用户的,我们可能会看到其他用户所做的更改,
- 第 55 行:重新显示页面 [form2.xhtml],
- 第 58 行:由于调用了 [business] 层,可能会发生异常。在此情况下,我们将异常堆栈存储在第 13 行的错误列表中,并通过 [error.xhtml] 视图进行显示。
3.6.6.4. 预约安排
预约安排遵循以下流程:
![]() |
此操作涉及的模型如下:
// model
private Date jour = new Date();
private Boolean form1Rendered = true;
private Boolean form2Rendered = false;
private Boolean form3Rendered = false;
private Boolean erreurRendered = false;
private String form3Titre;
private AgendaMedecinJour agendaMedecinJour;
private Medecin medecin;
private CreneauMedecinJour creneauChoisi;
private List<Erreur> erreurs;
// action on RV
public void action() {
...
// we found
creneauChoisi = agendaMedecinJour.getCreneauxMedecinJour()[i];
// according to desired action
if (creneauChoisi.getRv() == null) {
reserver();
} else {
supprimer();
}
}
// reservation
public void reserver() {
try {
// title form 3
form3Titre = Messages.getMessage(null, "form3.titre", new Object[]{medecin.getTitre(), medecin.getPrenom(), medecin.getNom(), new SimpleDateFormat("dd MMM yyyy").format(jour),
creneauChoisi.getCreneau().getHdebut(), creneauChoisi.getCreneau().getMdebut(), creneauChoisi.getCreneau().getHfin(), creneauChoisi.getCreneau().getMfin()}).getSummary();
// customer selected in combo
idClient=null;
// form 3 is displayed
setForms(false, false, true, false);
} catch (Throwable th) {
// error view
prepareVueErreur(th);
}
}
- 第 14 行:如果所选时段没有预约,则视为预订,
- 第 30 行:我们使用与 [form2.xhtml] 页面标题相同的技术来准备 [form3.xhtml] 的页面标题,
- 第 34 行:在此表单中,有一个下拉列表框,其值由 idClient 提供。我们将该字段的值设为 null,以便不选中任何人,
- 第 36 行:显示页面 [form3.xhtml],
- 第 39 行:若发生异常,则显示错误页面。
页面 [form3.xhtml] 内容如下:
<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:f="http://java.sun.com/jsf/core"
xmlns:ui="http://java.sun.com/jsf/facelets">
<body>
<h2><h:outputText value="#{form.form3Titre}"/></h2>
<h:panelGrid columns="2">
<h:outputText value="#{msg['form3.client']}"/>
<h:selectOneMenu value="#{form.idClient}">
<f:selectItems value="#{form.clients}" var="client" itemLabel="#{client.titre} #{client.prenom} #{client.nom}" itemValue="#{client.id}"/>
</h:selectOneMenu>
<h:panelGroup>
<h:commandButton value="#{msg['form3.valider']}" actionListener="#{form.validerRv}" />
<h:commandButton value="#{msg['form3.annuler']}" actionListener="#{form.annulerRv}"/>
</h:panelGroup>
</h:panelGrid>
</body>
</html>
本页面由以下模型提供支持:
// bean Application
@Inject
private Application application;
// model
private Long idClient;
// customer list
public List<Client> getClients() {
return application.getClients();
}
- 第 6 行:客户端 ID 填充了页面第 12 行客户端下拉框的 value 属性。它设置了下拉框中选中的项目,
- 第 9–11 行:getClients 方法为下拉列表(第 13 行)填充数据。每个选项的标签(itemLabel)为 [标题 名字 姓氏],对应客户的客户 ID,而关联的值(itemValue)即为客户的 ID。因此,提交的正是该值。
3.6.6.5. 确认预约
确认预约的流程如下:
![]() |
并对应点击[确认]按钮:
<h:commandButton value="#{msg['form3.valider']}" actionListener="#{form.validerRv}" />
因此,[Form].validerRv 方法将处理此事件。其代码如下:
// bean Application
@Inject
private Application application;
// model
private Date jour = new Date();
private Boolean form1Rendered = true;
private Boolean form2Rendered = false;
private Boolean form3Rendered = false;
private Boolean erreurRendered = false;
private Long idCreneau;
private Long idClient;
private List<Erreur> erreurs;
// rv validation
public void validerRv() {
try {
// retrieve an instance of the selected time slot
Creneau creneau = application.getMetier().getCreneauById(idCreneau);
// we add the Rv
application.getMetier().ajouterRv(jour, creneau, application.gethClients().get(idClient));
// updating the agenda
agendaMedecinJour = application.getMetier().getAgendaMedecinJour(medecin, jour);
// form2 is displayed
setForms(false, true, false, false);
} catch (Throwable th) {
// error view
prepareVueErreur(th);
}
}
- 第 12 行:在 `validerRv` 方法执行之前,`idClient` 字段已接收用户所选客户的 ID,
- 第 19 行:利用上一步骤存储的时间段 ID(该 Bean 属于会话作用域),我们向 [业务] 层请求该时间段本身的引用,
- 第 21 行:请求 [业务] 层为选定的日期 (day)、时段 (slot) 和客户 (idClient) 添加预约,
- 第 23 行:我们请求 [业务] 层刷新医生的日历。我们将看到新增的预约,以及应用程序其他用户可能做出的任何更改,
- 第 25 行:重新显示日历 [form2.xhtml],
- 第 28 行:若发生错误,则显示错误页面。
3.6.6.6. 取消预约
这对应于以下流程:
![]() |
[form3.xhtml] 页面上的 [取消] 按钮如下所示:
<h:commandButton value="#{msg['form3.annuler']}" actionListener="#{form.annulerRv}"/>
因此调用了 [Form].cancelAppointment 方法:
// annulation prise de Rdv
public void annulerRv() {
// on affiche form2
setForms(false, true, false, false);
}
3.6.6.7. 返回首页
在接下来的序列中,还有一项操作需要查看:
![]() |
[form2.xhtml] 页面上 [Home] 按钮的代码如下:
<h:commandButton value="#{msg['form2.accueil']}" action="#{form.accueil}" />
[Form].accueil 方法如下:
public void accueil() {
// on affiche la page d'accueil
setForms(true, false, false, false);
}
3.7. 结论
我们已构建了以下应用程序:
![]() |
我们侧重于应用程序的功能而非用户界面。用户界面将利用 PrimeFaces 组件库进行改进。我们构建了一个基础应用程序,但它仍体现了基于 EJB 的分层 Java EE 架构。该应用程序可以通过多种方式进行改进:
- 需要身份验证。并非所有人都被授权添加或删除预约,
- 在查找有空档的日期时,应能向前或向后滚动日历,
- 应能查询医生有空档期的日期列表。事实上,如果该医生是眼科医生,预约通常需要提前六个月预订,
- ……
3.8. 使用 Eclipse 进行测试
3.8.1. [DAO] 层
![]() |
- 在 [1] 中,导入 [DAO] 层的 EJB 项目及其客户端,
- 在[2]中,我们选择[DAO]层的EJB项目并运行它[3],
- 在 [4] 中,在服务器上运行它,
![]() |
- 在[5]中,仅提供Glassfish服务器,因为它是唯一具备EJB容器的服务器,
- 在 [6] 中,EJB 模块已部署,
![]() |
- 在[7]中,显示了日志:
这些是我们在 NetBeans 中遇到的情况。
![]() |
- 在 [7A] [7B] 中,我们运行了客户端的 JUnit 测试,
![]() |
- 在 [8] 中,测试通过,
- 在 [9] 中,控制台输出日志。
![]() |
在[10]中,EJB应用程序被卸载。
3.8.2. [业务]层
![]() |
- 在 [1] 中,导入 [业务] 层的四个 Maven 项目,
- 在[2]中,选择企业项目并运行它在[3]中,在GlassFish服务器[4] [5]上,
![]() |
- 在 [6] 中,企业项目已部署到 GlassFish 上,
![]() |
- 在[7]中,我们查看了GlassFish日志,
在第 3 行,我们记录下 EJB [Metier] 的可移植名称,并将其粘贴到该 EJB 的控制台客户端中:
public class ClientRdvMedecinsMetier {
// the remote interface name of the EJB [Metier]
private static String IDaoRemoteName = "java:global/mv-rdvmedecins-metier-dao-ear/mv-rdvmedecins-ejb-metier-1.0-SNAPSHOT/Metier!rdvmedecins.metier.service.IMetierRemote";
// today's date
private static Date jour = new Date();
![]() |
- 在[8]中,我们运行了控制台客户端,
- 在[9]中,其日志。
![]() |
- 在 [10] 中,企业应用程序被卸载;
3.8.3. [Web] 层
![]() |
- 在[1]中,我们导入了来自[web]层的三项Maven项目。其中扩展名为.ear的项目是企业项目,需要部署在GlassFish上,
- 在[2]中,我们运行它,
![]() |
- 在 GlassFish 服务器上 [3],
- 在[4]中,该企业应用程序已成功部署,
![]() |
- 在 [5] 中,我们在 Eclipse 的内置浏览器中输入应用程序的 URL。


























































































































