Skip to content

3. 示例应用程序 – 01:rdvmedecins-jsf2-ejb

以下内容 参考了以下文档:

  • [ref7]:Java 5 入门(2010 年 6 月)EE [http://tahe.developpez.com/java/javaee]。 本文档旨在帮助您了解 JSF 1EJB3
  • [ref8]:Java 持久化实践(2007 年 6 月)[http://tahe.developpez.com/java/jpa]。本文档介绍了如何使用 JPAJava 持久化 API)实现数据持久化。
  • [ref9]:使用 NetBeans 和 GlassFish 服务器构建 Java Web 服务(2009 年 1 月)EE。本文探讨了 Web 服务的构建。

即将探讨的示例应用程序来自 [ref9]。

3.1. 应用场景

某家IT服务公司[ISTIA-AGI]希望提供预约服务。其首个目标市场是独立执业的医生。这类医生通常没有秘书。 因此,需要预约的客户只能直接致电医生。这导致医生在一天中频繁被打扰,从而降低了其为患者提供诊疗的时间。[ISTIA-AGI]公司希望为他们提供一项基于以下原则运行的预约服务:

  • 由一个秘书处统一为众多医生处理预约事宜。该秘书处仅需一人即可运作,其薪资由所有使用该服务的医生共同分摊。
  • 秘书处及所有医生均接入互联网
  • RV预约信息存储于中央数据库中,秘书处和医生均可通过互联网访问
  • RV的录入通常由秘书处负责。医生本人也可进行录入。例如,当诊疗结束时,医生可亲自为患者设定新的RV。

RV开立服务的架构如下:

如果医生不再需要管理RV,工作效率将得到提升。如果参与人数足够多,他们对秘书处运营费用的分摊将微乎其微。

[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:医生标识号——该表的主键
  • VERSION:表中该行的版本标识号。每次对该行进行修改时,该数字都会增加1。
  • NOM:医生姓名
  • PRENOM:其名字
  • TITRE:其称谓(小姐、女士、先生)

3.3.2. 表 [CLIENTS]

各医生的患者信息存储在表 [CLIENTS] 中:

  • ID:客户标识号——该表的主键
  • VERSION:标识表中该行版本的编号。每次对该行进行修改时,该数字都会递增1。
  • NOM:客户姓名
  • PRENOM:客户名字
  • TITRE:其称谓(小姐、女士、先生)

3.3.3. 表 [CRENEAUX]

该表列出了可进行 RV 操作的时间段:

  • ID:时间段标识号——该表的主键(第8行)
  • VERSION:表中该行的版本标识号。每次对该行进行修改时,该数字会递增1。
  • ID_MEDECIN:标识该时段所属医生的编号——MEDECINS(ID)列的外键。
  • HDEBUT:时段开始时间
  • MDEBUT:时段开始分钟
  • HFIN:时段结束时间
  • MFIN:时段结束分钟

例如,表格 [CRENEAUX](参见上文 [1])的第二行显示,第 2 号时段于 8 点 20 分开始,8 点 40 分结束,归属于第 1 号医生 (玛丽女士 PELISSIER)。

3.3.4. 表 [RV]

该表列出了每位医生所分配的 RV:

  • ID:唯一标识RV的编号——主键
  • JOUR:RV的日期
  • ID_CRENEAU:RV的时间段——作为外键关联至[CRENEAUX]表中的[ID]字段——同时确定了时间段和相关医生。
  • ID_CLIENT:预订对象的客户编号——作为表[CLIENTS]中字段[ID]的外键

该表的 对关联列(JOUR、ID_CRENEAU)的值施加了唯一性约束:

ALTER TABLE RV ADD CONSTRAINT UNQ1_RV UNIQUE (JOUR, ID_CRENEAU);

如果表[RV]中某行具有值 (JOUR1, ID_CRENEAU1) 作为列 (JOUR, ID_CRENEAU) 的值,则该值在其他任何地方都不能出现。 否则,这意味着同一时间针对同一位医生生成了两个 RV。从 Java 编程的角度来看,当这种情况发生时,数据库中的 JDBC 驱动程序会触发一个 SQLException

id 行值为 3(参见上文的 [1]),表示 2006 年 8 月 23 日第 20 个时段的第 4 号客户已预约了 RV。 [CRENEAUX]表显示,第20号时段对应16:20-16:40的时间段,由第1号医生(Marie PELISSIER女士)负责。 表[CLIENTS]显示,第4号客户是布里吉特·BISTROU小姐。

3.3.5. 数据库生成

要创建并填充这些表,可以使用示例网站上的脚本 [dbrdvmedecins2.sql]。结合 [WampServer](参见第 1.3.3 节),操作步骤如下:

  • 在 [1] 中,点击 [WampServer] 图标,并选择 [PhpMyAdmin] [2] 选项,
  • 在 [3] 中,在弹出的窗口中,选择链接 [Bases de données],
  • 转为 [2],创建一个数据库,将其命名为 [4],编码设为 [5],
  • 在 [7] 中,数据库已创建。点击其链接,
  • 在 [8] 中,导入一个文件 SQL,
  • 通过按钮在文件系统中指定该文件:[9],
  • 在 [11] 中,选择脚本 SQL,并在 [12] 中执行它,
  • 在 [13] 中,数据库的四个表已创建完成。点击其中一个链接,
  • 在 [14] 中,显示了表的内容。

此后,我们将不再提及该数据库。但欢迎读者通过程序的执行过程关注其演变,尤其是在程序无法正常运行时。

3.4. [DAO] 和 [JPA] 层

让我们回到需要构建的架构:

我们将构建四个 Maven 项目:

  • 一个用于 [DAO] 和 [JPA] 层的项目,
  • 一个用于 [métier] 层的项目,
  • 一个用于 [web] 层的项目,
  • 一个企业项目,用于整合前三个项目。

现在我们开始构建 [DAO] 和 [JPA] 层级的 Maven 项目。

:理解 [métier]、[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] 层以及 [EJB] 层,后者负责控制对生成的 JPA 实体的访问。 了解这些自动生成方法很有意义,因为生成的代码为编写 JPA 实体或使用这些实体的 EJB 代码提供了宝贵的参考。

接下来我们将介绍其中一些自动生成工具。 要理解生成的代码,必须对 JPA、[ref8] 以及 EJB、[ref7] 实体有充分的了解(参见第 3 段)。

3.4.2.1. 创建 NetBeans 与数据库的连接

  • 运行 SGBD 和 MySQL 5,以便 BD 可用,
  • 在 [dbrdvmedecins2] 数据库上创建 NetBeans 连接,
  • 在 [Services] [1] 选项卡中, 在分支 [Databases] [2] 中,选择驱动程序 JDBC MySQL [3],
  • 然后选择选项 [4] “Connect Using”,用于建立与数据库 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] 中,选择实现 JPA EclipseLink (JPA 2.0),
  • 在 [4] 中,指定数据库事务将由 Glassfish 服务器的 EJB 容器管理,
  • 在 [5] 中,指定 BD 中的表已创建,因此不再创建,
  • 在 [6] 中,为 Glassfish 服务器创建一个新的数据源,
  • 在 [7] 中,指定名称 JNDI(Java Naming Directory Interface),
  • 在 [8] 中,将该名称与上一步创建的连接 MySQL 关联,
  • 为 [9],完成向导,
  • 在 [10] 中,新项目,
  • 在 [11] 处,文件 [persistence.xml] 已生成在文件夹 [META-INF] 中,
  • 在 [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 Transaction API)事务,
  • 第 4 行:数据源的名称 JNDI。

通常,该文件中会包含所使用的实现类型 JPA。 在向导中,我们指定了 EclipseLink。由于这是 Glassfish 服务器默认使用的 JPA 实现,因此该文件中未提及 [persistence.xml]。

在 [Design] 选项卡中,可以全面查看 [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 行:指定使用 JPA 作为 EclipseLink 的实现;
  • 第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 的属性 JDBC [dbrdvmedecins2],
  • 第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 属性。 需要说明的是,这意味着它们仅在编译时必需,而运行时则不需要。实际上,在运行时,这些工件(provided)由 Glassfish 服务器提供,
  • 第 41-48 行:定义了一个新的 Maven 工件仓库,EclipseLink 工件可在该仓库中找到。

3.4.2.3. 生成 JPA 实体

JPA 实体可通过 NetBeans 向导生成:

  • 在 [1] 中,从数据库创建 JPA 实体,
  • 在 [2] 中,选择之前创建的数据源 [jdbc / dbrdvmedecins2],
  • 在 [3] 中,列出该数据源的表列表,
  • 在 [4] 中,选中所有表,
  • 在 [5] 中,选定这些表,
  • 在 [6] 中,为与这四个表关联的 Java 类命名,
  • 以及包名 [7],
  • 在 [8] 中,JPA 将 BD 中的表行聚合到集合中。我们选择列表作为集合,
  • 在 [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;

// 构造函数
....

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

  @Override
  public int hashCode() {
  ...
  }

  @Override
  public boolean equals(Object object) {
  ...
  }

  @Override
  public String toString() {
    ...
  }
  
}
  • 第 4 行,@Entity 注解将类 [Medecin] 定义为实体 JPA、c.a.d。 一个通过 API 和 JPA 与 BD 表关联的类,
  • 第 5 行,BD 表的名称,该表与实体 JPA 相关联。表中的每个字段都对应 Java 类中的一个字段,
  • 第 6 行,该类实现了 Serializable 接口。这在客户端/服务器应用程序中是必要的,因为实体会在客户端和服务器之间进行序列化。
  • 第10-11行:类[Medecin]的id字段对应于表[medecins]中的字段[ID](第10行),
  • 第 13-14 行:类 [Medecin] 的 titre 字段对应于表 [medecins] 中的 [TITRE] 字段(第 13 行),
  • 第 16-17 行:类 [Medecin] 的“名称”字段对应于表 [medecins] 中的字段 [NOM](第 16 行),
  • 第 19-20 行:类 [Medecin] 的 version 字段对应于表 [medecins] 中的 [VERSION] 字段(第 19 行)。 在此,向导未能识别该列实际上是一个版本列,该列应在所属行每次修改时递增。为赋予其此功能,需添加 @Version 注释。我们将在后续步骤中进行此操作,
  • 第22-23行:类[Medecin]中的prenom字段对应于表[medecins]中的[PRENOM]字段,
  • 第 10-11 行:id 字段对应于该表的主键 [ID]。第 8-9 行的注解对此进行了说明,
  • 第 8 行:注解 @Id 表示被注解的字段与表的主相关联,
  • 第 9 行:[JPA] 层将为插入到 [Medecins] 表中的行生成主键。 有多种策略可供选择。此处策略 GenerationType.IDENTITY 表示,层 JPA 将使用表 MySQL 的模式 auto_increment,
  • 第25-26行:表[creneaux]对表[medecins]设置了外键。一个时段属于一位医生。反之,一位医生则关联有多个时段。 因此,这是一种“一对多”关系(一名医生对应多个时段),该关系由注释 @OneToMany 通过 JPA 进行限定(第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;

// 构造函数
...
// getter 和 setter
...

  @Override
  public int hashCode() {
    ...
  }

  @Override
  public boolean equals(Object object) {
    ...
  }

  @Override
  public String toString() {
    ...
  }
  
}

我们仅对新注释进行说明:

  • 我们曾提到,表 [creneaux] 拥有指向表 [medecins] 的外键:一个时段与一位医生相关联。多位医生可以关联多个时段。 表 [creneaux] 与表 [medecins] 之间存在一种关系,该关系被定义为多对一(多个时段对应一名医生)。 第32行的注释@ManyToOne用于定义外键,
  • 第31行带有注释@JoinColumn,用于明确外键关系: 表 [creneaux] 的 [ID_MEDECIN] 列是表 [medecins] 的 [ID] 列的外键,
  • 第 33 行:指向该时段所属医生的引用。此处同样无需编程即可获取。

因此,实体 [Creneau] 与实体 [Medecin] 之间的外键关系通过以下两条注释体现:

  • 在实体 [Creneau] 中:

@JoinColumn(name = "ID_MEDECIN", referencedColumnName = "ID")
  @ManyToOne(optional = false)
private Medecin idMedecin;
  • 在实体 [Medecin] 中:

@OneToMany(cascade = CascadeType.ALL, mappedBy = "idMedecin")
private List<Creneau> creneauList;

这两个注释反映了相同的关系:即表 [creneaux] 到表 [medecins] 的外键关系。我们说它们是彼此的逆关系。 只有关系 @ManyToOne 是必不可少的。它明确地定义了外键关系。关系 @OneToMany 是可选的。如果存在,它仅引用与其关联的关系 @ManyToOne。 这就是实体 [Medecin] 第 1 行中 mappedBy 属性的含义。 该属性的值是实体 [Creneau] 中带有注释 @ManyToOne 的字段名称,该注释指定了外键。同样在实体 [Medecin] 的第 1 行中, 属性 cascade=CascadeType.ALL 确定了实体 [Medecin] 相对于实体 [Creneau] 的行为:

  • 如果向数据库中插入一个新的实体 [Medecin],则第 2 行字段中的实体 [Creneau] 也必须被插入,
  • 如果修改数据库中的实体 [Medecin],则第 2 行字段中的实体 [Creneau] 也必须被修改,
  • 如果从数据库中删除实体 [Medecin],则第 2 行字段中的实体 [Creneau] 也必须被删除。

由于另外两个实体的代码未引入新的标记,因此在此不作特别说明。

实体 [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;

// 构造函数
...
// getter 和 setter
...

  @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;

   // 构造函数
...

   // getter 和 setter
...

  @Override
  public int hashCode() {
    ...
  }

  @Override
  public boolean equals(Object object) {
    ...
  }

  @Override
  public String toString() {
    ...
  }
  
}
  • 第13行定义了Java类型的“日”字段Date。说明在表[rv]中,列[JOUR](第12行)的类型为日期(不含时间),
  • 第16-18行:定义了表[rv]与表[creneaux]之间的外键关系,
  • 第20-22行:定义了表[rv]与表[clients]之间的外键关系。

通过自动生成实体 JPA,我们可以获得一个工作基础。有时这已足够,有时则不然。本例即属后者:

  • 需要为各实体的版本字段添加 @Version 注解,
  • 需要编写比生成的方法更明确的 toString 方法,
  • 实体 [Medecin] 和 [Client] 性质相似。我们将让它们继承自类 [Personne],
  • 并将删除 @OneToMany 关系(作为 @ManyToOne 关系的反向关系)。这些关系并非必需,且会增加编程复杂度,
  • 我们将移除 @NotNull 对主键的验证。当将实体 JPA 与 MySQL 进行持久化时,原始实体的主键为 null。 只有在数据写入数据库后,被持久化的元素的主键才会获得具体值。

根据这些规范,各类定义如下:

Personne 类用于表示医生和客户:


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;
// 构造函数
...

// getter 和 setter
  ...

  @Override
  public String toString() {
    return String.format("[%s,%s,%s,%s,%s]", id, version, titre, prenom, nom);
  }
  
}
  • 第 8 行:请注意,类 [Personne] 本身并非实体(@Entity)。它将作为实体的父类。注解 @MappedSuperClass 指明了这种情况。

实体 [Client] 封装了表 [clients] 的行。它继承自前面的类 [Personne]:


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;

// 构造函数
...

  @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 行:它继承自类 [Personne]。

封装表 [medecins] 行数据的实体 [Medecin] 遵循相同的模式:


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;

  // 构造函数
...

  @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;

  // 构造函数
  ...

  // getter 和 setter
  ...
 
  @Override
  public int hashCode() {
    ...
  }

  @Override
  public boolean equals(Object object) {
    // TODO:警告——如果 id 字段未设置,此方法将无法工作
    ...
  }

  @Override
  public String toString() {
    return String.format("Creneau [%s, %s, %s:%s, %s:%s,%s]", id, version, hdebut, mdebut, hfin, mfin, medecin);
  }
}
  • 第 45-47 行建模了数据库中表 [creneaux] 与表 [medecins] 之间存在的“多对一”关系:一名医生拥有多个时段,而一个时段仅属于一名医生。

实体 [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;

   // 构造函数
...

   // getter 和 setter
...

  @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] 表与 [creneaux] 表之间存在的“多对一”关系(一个时段可能出现在多个 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 int code = 0;

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

  // 获取器和设置器
  public int getCode() {
    return code;
  }

  public void setCode(int code) {
    this.code = code;
  }
}
  • 第 7 行:该类继承自 [RuntimeException] 类。因此编译器不会强制要求使用 try/catch 进行处理。
  • 第 6 行:注解 @ApplicationException 确保该异常不会被 [EjbException] 类型的异常“吞噬”。

要理解 @ApplicationException 注解,让我们回顾一下服务器端使用的架构:

类型为 [RdvMedecinsException] 的异常将由 [DAO] 层中的 EJB 方法在 EJB3 容器内部抛出,并由该容器进行拦截。 如果没有 @ApplicationException 注解,容器 EJB3 会将发生的异常封装为类型为 [EjbException] 的异常,并重新抛出该异常。 若不希望进行此封装,可让 EJB3 容器抛出类型为 [RdvMedecinsException] 的异常。这正是 @ApplicationException 注解所实现的功能。 此外,该注解的 (rollback=true) 属性告知容器 EJB3:若在包含 SGBD 的事务中执行的方法内部发生了类型为 [RdvMedecinsException] 的异常, 则该事务必须被回滚。从技术角度讲,这被称为对事务执行rollback操作。

3.4.4. [DAO] 层的 EJB

[IDao] 层( )的 Java 接口 [DAO] 如下:


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 {

  // 客户列表
  public List<Client> getAllClients();
  // 医生列表
  public List<Medecin> getAllMedecins();
  // 医生时段列表
  public List<Creneau> getAllCreneaux(Medecin medecin);
  // 某医生在特定日期的预约列表
  public List<Rv> getRvMedecinJour(Medecin medecin, Date jour);
  // 按ID查找客户
  public Client getClientById(Long id);
  // 按ID查找客户
  public Medecin getMedecinById(Long id);
  // 按ID查找预约
  public Rv getRvById(Long id);
  // 根据ID查找时间段
  public Creneau getCreneauById(Long id);
  // 添加一个 RV
  public Rv ajouterRv(Date jour, Creneau creneau, Client client);
  // 删除一个 RV
  public void supprimerRv(Rv rv);
}

该接口是在确定 [web] 层的需求后构建的:

  • 第 14 行:客户列表。我们需要它来填充客户下拉列表,
  • 第16行:医生列表。我们需要它来填充医生的下拉列表,
  • 第18行:医生的预约时段列表。我们将需要它来显示医生在特定日期的日程安排,
  • 第20行:某位医生在特定日期内的预约列表。结合前面的方法,我们可以显示该医生在特定日期内的日程表,其中包含已预订的时间段,
  • 第22行:支持通过客户编号查询客户。该方法允许我们通过客户下拉列表中的选择来查找客户,
  • 第24行:医生操作同上,
  • 第26行:通过编号查找预约。在删除预约前,可用于验证该预约是否确实存在,
  • 第28行:根据时段编号查找特定时段。用于识别用户想要添加或删除的时段,
  • 第30行:用于添加预约,
  • 第32行:用于删除预约。

[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 实现了本地远程这两个接口:


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;

  // 客户列表
  public List<Client> getAllClients() {
    try {
      return em.createQuery("select rc from Client rc").getResultList();
    } catch (Throwable th) {
      throw new RdvMedecinsException(th, 1);
    }
  }

  // 医生列表
  public List<Medecin> getAllMedecins() {
    try {
      return em.createQuery("select rm from Medecin rm").getResultList();
    } catch (Throwable th) {
      throw new RdvMedecinsException(th, 2);
    }
  }

  // 特定医生的预约时段列表
  // 医生:该医生
  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);
    }
  }

  // 特定医生在特定日期的预约列表
  // 医生:该医生
  // 日期:该日期
  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);
    }
  }

  // 添加预约
  // 日期:预约日期
  // 时段:预约的时间段
  // 客户:预约对象
  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);
    }
  }

  // 删除预约
  // rv:被删除的预约
  public void supprimerRv(Rv rv) {
    try {
      em.remove(em.merge(rv));
    } catch (Throwable th) {
      throw new RdvMedecinsException(th, 5);
    }
  }

  // 检索指定客户
  public Client getClientById(Long id) {
    try {
      return (Client) em.find(Client.class, id);
    } catch (Throwable th) {
      throw new RdvMedecinsException(th, 6);
    }
  }

  // 检索指定医生
  public Medecin getMedecinById(Long id) {
    try {
      return (Medecin) em.find(Medecin.class, id);
    } catch (Throwable th) {
      throw new RdvMedecinsException(th, 6);
    }
  }

  // 查询指定预约
  public Rv getRvById(Long id) {
    try {
      return (Rv) em.find(Rv.class, id);
    } catch (Throwable th) {
      throw new RdvMedecinsException(th, 6);
    }
  }

  // 检索指定时段
  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行:一个名为JPQL的查询,用于对表[creneaux]和[medecins]进行连接。该查询通过医生的ID进行参数化,
  • 第57行:查询JPQL,对表[rv]、[creneaux]和[medecins]进行连接,并包含两个参数:医生ID和预约日期,
  • 第69-73行:创建预约并将其保存至数据库,
  • 第83行:从数据库中删除一个预约,
  • 第92行:在数据库中执行select查询以查找指定客户,
  • 第101行:对医生执行相同操作,
  • 第110行:对预约执行相同操作,
  • 第119行:对时间段执行相同操作,
  • 所有使用第22行持久化上下文em的操作都可能遇到数据库问题。因此,这些操作均被try/catch语句包围。可能出现的异常被封装在自定义异常RdvMedecinsException中

3.4.5. 从 MySQL 部署 JDBC 驱动程序

在以下架构中:

EclipseLink 需要 MySQL 的驱动程序 JDBC。 需将该驱动程序安装到 Glassfish 服务器的库目录中,路径为 <glassfish> /domains /domain1 /lib /ext,其中 <glassfish> 是 Glassfish 服务器的安装目录。可通过以下方式获取该驱动程序:

JDBC(源自MySQL)驱动程序的安装路径为:<Domains folder>[1]/domain1/lib/ext [2]。 该驱动程序可在 URL [http://www.mysql.fr/downloads/connector/j/] 中获取。安装完成后,需重启 Glassfish 服务器,以便其加载该新库。

3.4.6. 部署 EJB 层(基于 [DAO])

让我们回顾一下目前构建的架构:

[web, métier, DAO, JPA] 组件需部署到 Glassfish 服务器上。操作步骤如下:

  • 生成 [1],构建 Maven 项目,
  • 在 [2] 中,我们运行它,
  • 在 [3] 中,该项目已部署到 Glassfish 服务器([Services] 选项卡)

我们可能会好奇地查看 Glassfish 的日志:

在 [1] 中,Glassfish 日志可在 [Output / Glassfish Server 3+] 选项卡中查看。具体内容如下:

Config: The access type for the persistent class [class rdvmedecins.jpa.Personne] is set to [FIELD].
Config: The access type for the persistent class [class rdvmedecins.jpa.Client] is set to [FIELD].
Config: The access type for the persistent class [class rdvmedecins.jpa.Rv] is set to [FIELD].
Config: The target entity (reference) class for the many to one mapping element [field client] is being defaulted to: class rdvmedecins.jpa.Client.
Config: The target entity (reference) class for the many to one mapping element [field creneau] is being defaulted to: class rdvmedecins.jpa.Creneau.
Config: The access type for the persistent class [class rdvmedecins.jpa.Medecin] is set to [FIELD].
Config: The access type for the persistent class [class rdvmedecins.jpa.Creneau] is set to [FIELD].
Config: The target entity (reference) class for the many to one mapping element [field medecin] is being defaulted to: class rdvmedecins.jpa.Medecin.
Config: The alias name for the entity class [class rdvmedecins.jpa.Client] is being defaulted to: Client.
Config: The alias name for the entity class [class rdvmedecins.jpa.Rv] is being defaulted to: Rv.
Config: The alias name for the entity class [class rdvmedecins.jpa.Medecin] is being defaulted to: Medecin.
Config: The alias name for the entity class [class rdvmedecins.jpa.Creneau] is being defaulted to: Creneau.
Infos: rdvmedecins.jpa.Creneau actually got transformed
Infos: rdvmedecins.jpa.Medecin actually got transformed
Infos: rdvmedecins.jpa.Personne actually got transformed
Infos: rdvmedecins.jpa.Client actually got transformed
Infos: rdvmedecins.jpa.Rv actually got transformed
Infos: EclipseLink, version: Eclipse Persistence Services - 2.3.2.v20111125-r10461
Précis: Detected database platform: org.eclipse.persistence.platform.database.MySQLPlatform
Config: connecting(DatabaseLogin(
    platform=>DatabasePlatform
    user name=> ""
    connector=>JNDIConnector datasource name=>null
))
Config: Connected: jdbc:mysql://localhost:3306/dbrdvmedecins2
    User: root@localhost
    Database: MySQL  Version: 5.5.8-log
    Driver: MySQL-AB JDBC Driver  Version: mysql-connector-java-5.1.6 ( Revision: ${svn.Revision} )
Config: connecting(DatabaseLogin(
    platform=>MySQLPlatform
    user name=> ""
    connector=>JNDIConnector datasource name=>null
))
Config: Connected: jdbc:mysql://localhost:3306/dbrdvmedecins2
    User: root@localhost
    Database: MySQL  Version: 5.5.8-log
    Driver: MySQL-AB JDBC Driver  Version: mysql-connector-java-5.1.6 ( Revision: ${svn.Revision} )
Infos: file:/D:/data/istia-1112/netbeans/dvp/jsf2-pf-pfm/maven/netbeans/rdvmedecins-jsf2-ejb/mv-rdvmedecins-ejb-dao-jpa/target/classes/_dbrdvmedecins2-PU login successful
Infos: EJB5181:Portable JNDI names for EJB DaoJpa: [java:global/istia.st_mv-rdvmedecins-ejb-dao-jpa_ejb_1.0-SNAPSHOT/DaoJpa!rdvmedecins.dao.IDaoLocal, java:global/istia.st_mv-rdvmedecins-ejb-dao-jpa_ejb_1.0-SNAPSHOT/DaoJpa!rdvmedecins.dao.IDaoRemote]
Infos: EJB5182:Glassfish-specific (Non-portable) JNDI names for EJB DaoJpa: [rdvmedecins.dao#rdvmedecins.dao.IDaoRemote, rdvmedecins.dao]
Infos: istia.st_mv-rdvmedecins-ejb-dao-jpa_ejb_1.0-SNAPSHOT a été déployé en 270 ms.

标识为 [Config]、[Précis] 的行是 EclipseLink 的日志,标识为 [Infos] 的行来自 Glassfish。

  • 第 1-12 行:EclipseLink 处理其发现的 JPA 实体,
  • 第13-17行:信息表明对JPA实体的处理已正常完成,
  • 第18行:EclipseLink 发出信号,
  • 第19行:EclipseLink确认其正在与SGBD和MySQL通信,
  • 第20-24行:EclipseLink尝试连接BD,
  • 第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 服务器上的 EJB [DAO] 的远程接口。

首先,我们创建一个新的 Maven 项目:

  • 在 [1] 中,我们创建了一个新项目,
  • 在 [2,3] 中,我们创建一个 [Java Application] 类型的 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] 中,选择 [Open Projects] 选项卡,
  • 在 [3] 中,选择 EJB 的 Maven 项目 [DAO],
  • 在 [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 {

  // 已测试的 [dao] 层
  private static IDaoRemote dao;
  // 今日日期
  Date jour = new Date();

  @BeforeClass
  public static void init() throws NamingException {
    // JNDI 环境初始化
    InitialContext initialContext = new InitialContext();
    // DAO层实例化
    dao = (IDaoRemote) initialContext.lookup("rdvmedecins.dao");
  }

  @Test
  public void test1() {
    // 显示客户
    List<Client> clients =dao.getAllClients();
    display("Liste des clients :", clients);
    // 显示医生
    List<Medecin> medecins =dao.getAllMedecins();
    display("Liste des médecins :", medecins);
    // 显示医生的预约时段
    Medecin medecin = medecins.get(0);
    List<Creneau> creneaux = dao.getAllCreneaux(medecin);
    display(String.format("Liste des créneaux du médecin %s", medecin), creneaux);
    // 某医生在特定日期的预约列表
    display(String.format("Liste des créneaux du médecin %s, le [%s]", medecin, jour), dao.getRvMedecinJour(medecin, jour));
    // 添加 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));
    // 在同一天的同一时段添加一个 RV
    // 应引发异常
    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();
      }
      // 记录错误
      erreur=true;
    }
    // 验证是否发生错误
    Assert.assertTrue(erreur);
    // 列出 RV
    display(String.format("Liste des Rv du médecin %s, le [%s]", medecin, jour), dao.getRvMedecinJour(medecin, jour));
    // 删除一个 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));
  }

  // 实用方法 - 显示集合中的元素
  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] 时,测试已通过。

让我们更仔细地看看测试的显示结果:

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 15:34:15 CEST 2012]
Ajout d'un Rv le [Wed May 23 15:34:15 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 15:34:15 CEST 2012]
Rv[242, Creneau [3, 1, 8:40, 9:0,Médecin[1,Mme,Marie,PELISSIER]], Client[1,Mr,Jules,MARTIN]]
Ajout d'un Rv le [Wed May 23 15:34:15 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]
java.rmi.MarshalException: CORBA MARSHAL 1330446347 Maybe; nested exception is: 
    org.omg.CORBA.MARSHAL: Avertissement: IOP00810011: Exception from readValue on ValueHandler in CDRInputStream  vmcid: OMG  minor code: 11 completed: Maybe
java.rmi.MarshalException: CORBA MARSHAL 1330446347 Maybe; nested exception is: 
    org.omg.CORBA.MARSHAL: Avertissement: IOP00810011: Exception from readValue on ValueHandler in CDRInputStream  vmcid: OMG  minor code: 11 completed: Maybe
java.rmi.MarshalException: CORBA MARSHAL 1330446347 Maybe; nested exception is: 
    org.omg.CORBA.MARSHAL: Avertissement: IOP00810011: Exception from readValue on ValueHandler in CDRInputStream  vmcid: OMG  minor code: 11 completed: Maybe
java.rmi.MarshalException: CORBA MARSHAL 1330446347 Maybe; nested exception is: 
    org.omg.CORBA.MARSHAL: Avertissement: IOP00810011: Exception from readValue on ValueHandler in CDRInputStream  vmcid: OMG  minor code: 11 completed: Maybe
Liste des Rv du médecin Médecin[1,Mme,Marie,PELISSIER], le [Wed May 23 15:34:15 CEST 2012]
Rv[242, Creneau [3, 1, 8:40, 9:0,Médecin[1,Mme,Marie,PELISSIER]], Client[1,Mr,Jules,MARTIN]]
Suppression du Rv ajouté
Rv supprimé
Liste des Rv du médecin Médecin[1,Mme,Marie,PELISSIER], le [Wed May 23 15:34:15 CEST 2012]

建议读者在阅读这些日志时,同时参考生成这些日志的代码。 我们将重点关注在添加已存在的预约时发生的异常(第41-49行)。异常堆栈在第42-48行中重现。这是意料之外的。让我们回到添加预约方法的代码:


  // 添加一个 Rv
  // 日期:预约日期
  // 时段:预约的时间段
  // 客户:预约所针对的客户
  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 的日志:

...
Infos: avant persist : Rv[null, Creneau [3, 1, 8:40, 9:0,Médecin[1,Mme,Marie,PELISSIER]], Client[1,Mr,Jules,MARTIN]]
Infos: après persist : Rv[null, Creneau [3, 1, 8:40, 9:0,Médecin[1,Mme,Marie,PELISSIER]], Client[1,Mr,Jules,MARTIN]]
Précis: INSERT INTO rv (JOUR, ID_CLIENT, ID_CRENEAU) VALUES (?, ?, ?)
    bind => [3 parameters bound]
Précis: SELECT LAST_INSERT_ID()
Précis: SELECT t1.ID, t1.JOUR, t1.ID_CLIENT, t1.ID_CRENEAU FROM creneaux t0, rv t1 WHERE (((t0.ID_MEDECIN = ?) AND (t1.JOUR = ?)) AND (t0.ID = t1.ID_CRENEAU))
    bind => [2 parameters bound]
Infos: avant persist : Rv[null, Creneau [3, 1, 8:40, 9:0,Médecin[1,Mme,Marie,PELISSIER]], Client[1,Mr,Jules,MARTIN]]
Infos: après persist : Rv[null, Creneau [3, 1, 8:40, 9:0,Médecin[1,Mme,Marie,PELISSIER]], Client[1,Mr,Jules,MARTIN]]
Précis: INSERT INTO rv (JOUR, ID_CLIENT, ID_CRENEAU) VALUES (?, ?, ?)
    bind => [3 parameters bound]
Précis: SELECT 1
Avertissement: Local Exception Stack: 
Exception [EclipseLink-4002] (Eclipse Persistence Services - 2.3.2.v20111125-r10461): org.eclipse.persistence.exceptions.DatabaseException
Internal Exception: com.mysql.jdbc.exceptions.jdbc4.MySQLIntegrityConstraintViolationException: Duplicate entry '2012-05-23-3' for key 'UNQ1_RV'
Error Code: 1062
...
  • 第 2 行:第一个 persist 之前,
  • 第3:第一个persist之后,
  • 第4行:即将执行的INSERT命令。 需要注意的是,它并非与操作 persist 同时发生。如果是这样,该日志本应出现在第 2 行之前。因此,操作 INSERT 通常发生在执行该方法的交易结束时,
  • 第6行:EclipseLink向MySQL查询最近使用的主键。它将获取新增预约的主键。该值将填充持久化实体[Rv]的id字段,
  • 第7-8行:查询SELECT将显示医生的预约,
  • 第 9-10 行:第二个 persist 的屏幕显示,
  • 第11-12行:即将执行的INSERT命令。该命令应引发异常。该异常出现在第15-16行,且性质明确。 该异常最初由 MySQL 的驱动程序 JDBC 因违反预约唯一性约束而触发。由此推断,我们应该在 JUnit 的测试日志中看到这些异常。但实际情况并非如此:
1
2
3
4
5
6
7
8
java.rmi.MarshalException: CORBA MARSHAL 1330446347 Maybe; nested exception is: 
    org.omg.CORBA.MARSHAL: Avertissement: IOP00810011: Exception from readValue on ValueHandler in CDRInputStream  vmcid: OMG  minor code: 11 completed: Maybe
java.rmi.MarshalException: CORBA MARSHAL 1330446347 Maybe; nested exception is: 
    org.omg.CORBA.MARSHAL: Avertissement: IOP00810011: Exception from readValue on ValueHandler in CDRInputStream  vmcid: OMG  minor code: 11 completed: Maybe
java.rmi.MarshalException: CORBA MARSHAL 1330446347 Maybe; nested exception is: 
    org.omg.CORBA.MARSHAL: Avertissement: IOP00810011: Exception from readValue on ValueHandler in CDRInputStream  vmcid: OMG  minor code: 11 completed: Maybe
java.rmi.MarshalException: CORBA MARSHAL 1330446347 Maybe; nested exception is: 
    org.omg.CORBA.MARSHAL: Avertissement: IOP00810011: Exception from readValue on ValueHandler in CDRInputStream  vmcid: OMG  minor code: 11 completed: Maybe

回顾一下该测试的客户端/服务器架构:

当 EJB 或 [DAO] 抛出异常时,该异常必须经过序列化才能传送到客户端。很可能正是这一操作因某种我尚未弄清的原因而失败。由于我们的完整应用程序不会采用客户端/服务器模式运行,因此我们可以忽略此问题。

既然 [DAO] 层的 EJB 已正常运行,我们可以转到 [métier] 层的 EJB。

3.5. [métier]层

让我们回到正在构建的应用程序架构:

我们将为 EJB 和 [métier] 构建一个新的 Maven 项目。 如上所示,该项目将依赖于为 [DAO] 和 [JPA] 层构建的 Maven 项目。

3.5.1. NetBeans 项目

我们正在构建一个新的 EJB 类型的 Maven 项目。为此,只需按照第 174 页所述的已用流程操作即可。

  • 在 [1] 中,即 [métier] 层的 Maven 项目,
  • 在 [2] 中,添加一个依赖项,
  • 在 [3] 中,选择 [DAO] 和 [JPA] 层的 Maven 项目,
  • 在 [4] 中,选择 [provided] 的作用域。需要说明的是,这意味着该项目在编译时需要该作用域,但在运行时则不需要。实际上,EJB(来自 [métier] 层)将与 [DAO] 和 [JPA] 层的 EJB 一起部署到 Glassfish 服务器上。 因此,当其运行时,来自 [DAO] 和 [JPA] 层的 EJB 已经存在,
  • 在 [6] 中,即包含其依赖项的新项目。

现在介绍 [métier] 层的源代码:

EJB [Metier] 将具有以下 [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层
    // 客户列表
    public List<Client> getAllClients();

    // 医生列表
    public List<Medecin> getAllMedecins();

    // 医生时段列表
    public List<Creneau> getAllCreneaux(Medecin medecin);

    // 某医生在特定日期的预约列表
    public List<Rv> getRvMedecinJour(Medecin medecin, Date jour);

    // 按ID查找客户
    public Client getClientById(Long id);

    // 按ID查找客户
    public Medecin getMedecinById(Long id);

    // 根据ID查找预约
    public Rv getRvById(Long id);

    // 根据ID查找时间段
    public Creneau getCreneauById(Long id);

    // 添加一个 RV
    public Rv ajouterRv(Date jour, Creneau creneau, Client client);

    // 删除一个 RV
    public void supprimerRv(Rv rv);
    
    // 业务类型
  public AgendaMedecinJour getAgendaMedecinJour(Medecin medecin, Date jour);

}

要理解该接口,需回顾项目架构:

我们已定义了 [DAO] 层的接口(第 3.4.4 节),并指出该接口满足 [web] 层的需求,即用户需求。 [web]层仅通过[métier]层与[DAO]层进行通信。 这解释了为何在 [métier] 层中包含 [DAO] 层的所有方法。这些方法仅会将 [web] 层的请求委托给 [DAO] 层。 仅此而已。

在研究该应用程序时,出现了一项需求:能够在网页上显示某位医生在特定日期的日程表,以便了解该日已预约和空闲的时间段。这通常发生在秘书通过电话回应请求时。有人向她询问某天与某位医生的预约情况。 为满足这一需求,[métier]层提供了第46行的方法。


    // 职业
  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;
    // 字段
    private Medecin medecin;
    private Date jour;
    private CreneauMedecinJour[] creneauxMedecinJour;

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

    // 获取器和设置器
...
  
}
  • 第 12 行:该日程表所属的医生,
  • 第13行:日程表对应的日期,
  • 第14行:该医生在该日的时间段。
  • 该类包含构造函数(第17、21行)以及一个适配的方法 toString(第27行)。

类 [CreneauMedecinJour](第 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;
    // 字段
    private Creneau creneau;
    private Rv rv;

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

    // getter 和 setter

  ...
}
  • 第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{

}

EJB 和 [Metier] 通过以下方式实现这些接口:


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 层
  @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<Creneau> creneauxHoraires = dao.getAllCreneaux(medecin);
    // 该医生同一天的预约列表
    List<Rv> reservations = dao.getRvMedecinJour(medecin, jour);
    // 根据已预约的就诊时间创建字典
    Map<Long, Rv> hReservations = new Hashtable<Long, Rv>();
    for (Rv resa : reservations) {
      hReservations.put(resa.getCreneau().getId(), resa);
    }
    // 生成指定日期的日程表
    AgendaMedecinJour agenda = new AgendaMedecinJour();
    // 医生
    agenda.setMedecin(medecin);
    // 日期
    agenda.setJour(jour);
    // 预约时段
    CreneauMedecinJour[] creneauxMedecinJour = new CreneauMedecinJour[creneauxHoraires.size()];
    agenda.setCreneauxMedecinJour(creneauxMedecinJour);
    // 填入预约时段
    for (int i = 0; i < creneauxHoraires.size(); i++) {
      // 日程表第 i 行
      creneauxMedecinJour[i] = new CreneauMedecinJour();
      // 时段ID
      creneauxMedecinJour[i].setCreneau(creneauxHoraires.get(i));
      // 时段是空闲还是已预订?
      if (hReservations.containsKey(creneauxHoraires.get(i).getId())) {
        // 时段已被占用 - 记录预订
        Rv resa = hReservations.get(creneauxHoraires.get(i).getId());
        creneauxMedecinJour[i].setRv(resa);
      }
    }
    // 返回结果
    return agenda;
  }
}
  • 第 22 行,类 [Metier] 是 EJB 的单例,
  • 第 23 行,EJB 的每个方法都在事务中执行。 这意味着事务在方法开始时启动,位于 [métier] 层。该层将调用 [DAO] 层的方法。这些方法将在同一事务中执行,
  • 第24行,EJB 实现了其本地和远程接口,并且是可序列化的,
  • 第27行:[DAO]层对EJB的引用,
  • 第29行:该引用将通过Glassfish服务器的EJB容器注入,借助@EJB注解。 因此,当 [Metier] 类的方法执行时,对 [DAO] 层中 EJB 的引用已被初始化,
  • 第 33-81 行:该引用用于将对 [métier] 层的调用委托给 [DAO] 层,
  • 第84行:方法getAgendaMedecinJour用于获取某位医生在特定日期的日程安排。请读者自行查阅注释。

3.5.2. [métier] 层的部署

[métier] 层依赖于 [DAO] 层。每个层均通过 EJB 进行实现。 要测试 EJB 和 [métier],我们需要部署这两个 EJB。为此,我们需要一个企业项目。

  • [1],创建一个新项目,
  • 类型为Maven [2]和企业应用程序 [3],
  • 并为其命名 [4]。后缀 ear 将自动添加,
  • 变为 [5],选择 Glassfish 服务器和 Java 6 EE,
  • 变为 [6],企业应用程序包含模块,通常是 EJB 模块和 Web 模块。 在此,企业应用程序将包含我们构建的两个 EJB 模块。由于这些模块已存在,因此不勾选复选框,
  • 在 [7,8] 中,已创建了两个项目。[8] 是我们将要使用的企业项目。[7] 是一个我不清楚其作用的项目。 我从未使用过它,且未深入研究Maven,因此不清楚它的用途。所以我们将忽略它。

现在企业项目已创建,我们可以为其定义模块。

  • 在 [1] 中,创建一个新的依赖项,
  • 在 [2] 中,选择 EJB [DAO] 项目,
  • 在 [3] 中,声明其类型为 EJB。请勿留空类型字段,否则系统将默认使用 jar 类型,而该类型在此处并不适用,
  • 在 [4] 中,使用了 [compile] 的作用域,
  • 在 [5] 中,该项目及其新依赖项,
  • 在 [6, 7, 8] 中,重新开始以添加来自 [métier] 层级的 EJB,
  • 并将其转换为 [9],将这两个依赖项
  • 生成 [10],构建项目,
  • 在 [11] 中,执行该项目,
  • 在 [12] 中,通过 [Services] 选项卡可见项目已部署到 Glassfish 服务器。这意味着这两个 EJB 现已存在于服务器上。

在 Glassfish 服务器的日志中,可以找到关于这两个 EJB 部署的信息:

  • 在 [1] 的 Glassfish 日志选项卡中。

其中包含以下日志:

Infos: rdvmedecins.jpa.Creneau actually got transformed
Infos: rdvmedecins.jpa.Medecin actually got transformed
Infos: rdvmedecins.jpa.Personne actually got transformed
Infos: rdvmedecins.jpa.Client actually got transformed
Infos: rdvmedecins.jpa.Rv actually got transformed
Infos: EclipseLink, version: Eclipse Persistence Services - 2.3.2.v20111125-r10461
Infos: file:/D:/data/istia-1112/netbeans/dvp/jsf2-pf-pfm/maven/netbeans/rdvmedecins-jsf2-ejb/mv-rdvmedecins-metier-dao/mv-rdvmedecins-metier-dao-ear/target/gfdeploy/istia.st_mv-rdvmedecins-metier-dao-ear_ear_1.0-SNAPSHOT/mv-rdvmedecins-ejb-dao-jpa-1.0-SNAPSHOT_jar/_dbrdvmedecins2-PU login successful
Infos: EJB5181:Portable JNDI names for EJB DaoJpa: [java:global/istia.st_mv-rdvmedecins-metier-dao-ear_ear_1.0-SNAPSHOT/mv-rdvmedecins-ejb-dao-jpa-1.0-SNAPSHOT/DaoJpa!rdvmedecins.dao.IDaoRemote, java:global/istia.st_mv-rdvmedecins-metier-dao-ear_ear_1.0-SNAPSHOT/mv-rdvmedecins-ejb-dao-jpa-1.0-SNAPSHOT/DaoJpa!rdvmedecins.dao.IDaoLocal]
Infos: EJB5182:Glassfish-specific (Non-portable) JNDI names for EJB DaoJpa: [rdvmedecins.dao#rdvmedecins.dao.IDaoRemote, rdvmedecins.dao]
Infos: EJB5181:Portable JNDI names for 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, java:global/istia.st_mv-rdvmedecins-metier-dao-ear_ear_1.0-SNAPSHOT/mv-rdvmedecins-ejb-metier-1.0-SNAPSHOT/Metier!rdvmedecins.metier.service.IMetierLocal]
Infos: EJB5182:Glassfish-specific (Non-portable) JNDI names for EJB Metier: [rdvmedecins.metier.service.IMetierRemote#rdvmedecins.metier.service.IMetierRemote, rdvmedecins.metier.service.IMetierRemote]
  • 第1-5行:已识别出JPA实体,
  • 第7行:表示[dbrdvmedecins2-PU]持久化单元的构建成功,且已连接到相关的数据库,
  • 第8行:EJB的远程和本地接口的便携式名称为[DaoJpa]。portable表示被所有应用服务器识别,
  • 第 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

在测试[métier]层时,我们将需要它。

3.5.3. [métier] 层测试

与对 [DAO] 层所做的一样,我们将通过客户端/服务器应用程序对 [métier] 层进行测试:

客户端将测试部署在 Glassfish 服务器上的 EJB [Metier] 的远程接口。

首先,我们创建一个新的 Maven 项目。为此,我们将遵循创建 [dao] 层测试项目时采用的步骤(参见第 3.4.7 节),但不包括创建 JUnit 测试。由此创建的项目如下

  • 在 [1] 中, 所创建的项目及其依赖关系:与EJB层中的[dao]相关,与[métier]层中的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 {

  // 远程接口名称 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";
  // 当前日期
  private static Date jour = new Date();

  public static void main(String[] args) {
    try {
      // Glassfish 服务器的上下文 JNDI
      InitialContext initialContext = new InitialContext();
      // 远程层引用 [metier]
      IMetierRemote metier = (IMetierRemote) initialContext.lookup(IDaoRemoteName);
      // 客户显示
      List<Client> clients = metier.getAllClients();
      display("Liste des clients :", clients);
      // 医生列表
      List<Medecin> medecins = metier.getAllMedecins();
      display("Liste des médecins :", medecins);
      // 医生预约时段显示
      Medecin medecin = medecins.get(0);
      List<Creneau> creneaux = metier.getAllCreneaux(medecin);
      display(String.format("Liste des créneaux du médecin %s", medecin), creneaux);
      // 某医生在特定日期的预约列表
      display(String.format("Liste des rendez-vous du médecin %s, le [%s]", medecin, jour), metier.getRvMedecinJour(medecin, jour));
      // 日程表显示
      AgendaMedecinJour agenda = metier.getAgendaMedecinJour(medecin, jour);
      System.out.println(agenda);
      // 添加 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 = 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));
      // 显示日程表
      agenda = metier.getAgendaMedecinJour(medecin, jour);
      System.out.println(agenda);
      // 删除 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));
      // 显示日历
      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();
      }
    }
  }

  // 实用方法 - 显示集合中的元素
  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行:PELISSIER女士的日程表,2012年5月23日。没有预留时段,
  • 第39行:添加了一个约会,
  • 第42行:PELISSIER女士的新日程表。现已为MARTIN先生预留了一个时段,
  • 第44行:该预约已被删除,
  • 第46行:PELISSIER女士的日程显示未预留任何时段。

我们现在认为 [DAO] 和 [métier] 层已投入运行。接下来我们需要使用 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] 中,删除 [index.jsp] 页面及 [Source Packages] 中的包后,
  • 在 [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。这些是形式为 /faces/* 的URL,
  • 第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 层基于 [métier]、[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.entete=Les M\u00e9decins Associ\u00e9s
layout.basdepage=ISTIA, universit\u00e9 d'Angers
layout.entete.langue1=Fran\u00e7ais
layout.entete.langue2=Anglais
# 异常
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
# 表格 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
# 表单 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
# 表格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.titre=Une erreur s'est produite.
erreur.message=Message d'erreur
erreur.accueil=Page d'accueil
erreur.classe=Cause

[messages_en.properties]:英语消息文件


# 布局
layout.entete=Associated Doctors
layout.basdepage=ISTIA, Angers university
layout.entete.langue1=French
layout.entete.langue2=English
# 异常
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
# 表单 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
# 表单 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
# 表单 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.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 行)中名为“内容”(第 8 行)的区域。这是视图的中央区域:

 

页面 [index.xhtml] 是该应用程序的唯一页面。 因此,页面之间不会进行导航。它显示的是四个页面之一:[form1.xhtml, form2.xhtml, form3.xhtml, erreur.xhtml]。该显示由表单 Bean 中的四个布尔值 [form1Rendered, form2Rendered, form3Rendered, erreurRendered] 控制,我们将在后面进行说明。

3.6.5. 项目的 Bean

[utils]包中的类已介绍过:

  • 类 [ChangeLocale] 负责语言切换功能。该类已在第 2.4.4 节中进行过探讨。
  • 类 [Messages] 用于简化应用程序消息的国际化。该类已在第 2.8.5.7 节中进行过探讨。

3.6.5.1. Application 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{

  // 业务层
  @EJB
  private IMetierLocal metier;
  // 缓存
  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>();
  // 错误
  private List<Erreur> erreurs = new ArrayList<Erreur>();
  private Boolean erreur = false;

  public Application() {
  }

  @PostConstruct
  public void init() {
    // 将医生和客户缓存
    try {
      medecins = metier.getAllMedecins();
      clients = metier.getAllClients();
    } catch (Throwable th) {
      // 记录错误
      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;
    }
    // 验证列表
    if (medecins.size() == 0) {
      // 记录错误
      erreur = true;
      erreurs.add(new Erreur("", "La liste des médecins est vide"));
    }
    if (clients.size() == 0) {
      // 记录错误
      erreur = true;
      erreurs.add(new Erreur("", "La liste des clients est vide"));
    }
    // 错误?
    if (erreur) {
      return;
    }

    // 字典
    for (Medecin m : medecins) {
      hMedecins.put(m.getId(), m);
    }
    for (Client c : clients) {
      hClients.put(c.getId(), c);
    }
  }

  // 获取器和设置器
  ...
}
  • 第15-16行:类[Application]是一个应用程序范围的Bean。它在应用程序JSF的生命周期开始时创建一次,并可供所有用户的所有请求访问。通常,我们会在其中放置只读数据。 在此,我们将医生列表和客户列表存放在该Bean中。因此,我们假设这些数据不会频繁变更。XHTML页面可通过application名称访问该Bean,
  • 第 20-21 行:Glassfish 的 EJB 容器将注入对 EJB 和 [Metier] 本地接口的引用。回顾一下应用程序的架构:

JSF 应用程序以及 EJB 和 [Metier] 将运行在同一个 JVM(Java 虚拟机)中。 因此,[JSF] 层将使用 EJB 的本地接口。在此处,应用程序 Bean 使用了 EJB 和 [Metier]。 即使并非如此,在 [métier] 层中找到相关引用也是正常的。因为这确实是一项可被所有用户的所有请求共享的信息,因此属于 Application 作用域的数据。

  • 第 34-35 行:在实例化 [Application] 类后(存在 @PostConstruct 注解),会立即执行 init 方法,
  • 第36-73行:该方法创建以下元素:第23行的医生列表、第24行的客户列表、第25行按ID索引的医生字典,以及第26行同类型的客户字典。可能发生错误,这些错误会被记录在第28行的列表中。

类 [Erreur] 如下所示:


package beans;

public class Erreur {
  
  public Erreur() {
  }
  
  // 字段
  private String classe;
  private String message;

  // 构造函数
  public Erreur(String classe, String message){
    this.setClasse(classe);
    this.message=message;
  }
  
  // getter 和 setter
...  
}
  • 第 9 行:若抛出异常,则为异常类的名称;
  • 第 10 行:错误消息。

3.6.5.2. Bean [Form]

其代码如下:


package beans;

...

@Named(value = "form")
@SessionScoped
public class Form implements Serializable {

  public Form() {
  }

  // 应用程序 Bean
  @Inject
  private Application application;

  // 模型
  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() {
    // 初始化是否成功?
    if (application.getErreur()) {
      // 获取错误列表
      erreurs = application.getErreurs();
      // 显示错误视图
      setForms(false, false, false, true);
    }
  }

  // 显示视图
  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且作用域为session的Bean。需注意,此时该类必须可序列化。
  • 第 13-14 行:form Bean 持有对 application Bean 的引用。该引用将由应用程序运行的 Servlet 容器注入(存在 @Inject 注解)。
  • 第 17-31 行:页面模板 [form1.xhtml, form2.xhtml, form3.xhtml, erreur.xhtml]。这些页面的显示由第 19-22 行的布尔值控制。需要注意的是,默认情况下渲染的是页面 [form1.xhtml],
  • 第33-34行:类实例化后立即执行init方法(存在@PostConstruct注解),
  • 第35-41行: init 方法用于确定应首先显示哪一页:通常是页面 [form1.xhtml](第 19 行),除非应用程序初始化失败(第 36 行),在这种情况下将显示页面 [erreur.xhtml] (第40行)。

页面 [erreur.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 行)来显示错误列表。生成的页面类似于以下内容:

Image

接下来我们将定义应用程序生命周期的各个阶段。

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 {

  // Application Bean
  @Inject
  private Application application;
  // 模型
  private Long idMedecin;
  private Date jour = new Date();
  
// 医生列表
  public List<Medecin> getMedecins() {
    return application.getMedecins();
  }
  // 日程表
  public void getAgenda() {
    ...
}
  • 第 9 行字段负责读取和写入页面第 18 行列表中的值。 页面初次显示时,它会将下拉列表中的选定值固定下来。在初次显示时,idMedecin 等于 null,因此将选定第一位医生,
  • 第13-15行的方法生成医生下拉列表的选项(页面第19行)。每个生成的选项将以(itemLabel)作为标签,显示医生的头衔姓氏和名字,并以(itemValue)作为值,即医生的ID
  • 第10行的字段通过读写方式为页面第21行的输入字段提供数据。因此,初始显示时,显示的是当天日期,
  • 第17-19行:方法getAgenda负责处理页面第26行按钮[Agenda]的点击事件。 由于没有导航(始终请求的是页面 [index.html]),因此通常会使用属性 actionListener 代替属性 action。 在这种情况下,在模板中调用的方法不会返回任何结果。

当点击按钮 [Agenda] 时,

  • 数据已提交:在医生下拉列表中选定的值已保存至表单的idMedecin字段,在“日期”字段中选定的日期,
  • 并调用表单中的方法 getAgenda

方法 getAgenda 如下:


  // 应用程序 Bean
  @Inject
  private Application application;

  // 模板
  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;

  // 日程表
  public void getAgenda() {
    try {
      // 查询医生
      medecin = application.gethMedecins().get(idMedecin);
      // 表单标题 2
      form2Titre = Messages.getMessage(null, "form2.titre", new Object[]{medecin.getTitre(), medecin.getPrenom(), medecin.getNom(), new SimpleDateFormat("dd MMM yyyy").format(jour)}).getSummary();
      // 某一天的医生日程表
      agendaMedecinJour = application.getMetier().getAgendaMedecinJour(medecin, jour);
      // 显示表单2
      setForms(false, true, false, false);
    } catch (Throwable th) {
      // 错误视图
      prepareVueErreur(th);
    }
  }

  // 准备vueErreur
  private void prepareVueErreur(Throwable th) {
    // 创建错误列表
    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()));
    }
// 显示错误视图
    setForms(false, false, false, true);
}

回顾一下方法 getAgenda 应显示的内容:

  • 第 21 行:从医生字典中检索选定的医生,该医生已存储在 application Bean 中。为此,使用其 ID(该 ID 已通过 POST 方式发送至 idMedecin),
  • 第23行:准备待显示的页面[form2.xhtml]的标题。该消息从消息文件中提取,以便进行国际化处理。此技术已在第2.8.5.7节第135页)中描述。
  • 第25行:调用[métier]层,计算所选医生在所选日期的日程安排,
  • 第27行:显示[form2.xhtml],
  • 第28行:若发生异常,则构建错误列表(第37-42行)并显示页面[erreur.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 在模型中初始化了两个字段:


// 模板
  private String form2Titre;
private AgendaMedecinJour agendaMedecinJour;

这两个字段为页面 [form2.xhtml] 提供数据:

  • 第 10 行,即页面标题,
  • 第 12 行:医生的日程表通过一个三列的 <h:dataTable> 标签显示,
  • 第13-18行:第一列显示时间段,
  • 第19-30行:第二列显示已预约该时段的客户姓名(若无预约则留空)。此处使用第7行引用的JSTL Core库中的标签,
  • 第30-35行:第三列显示链接[Réserver](若时段空闲),或链接[Supprimer](若时段已预订)。

第三列的链接关联到以下模板:


// 模板
  private Long idCreneau;

  // 对 RV 执行操作
  public void action() {
    ...
}
  • 当用户点击“预订/删除”链接(第32行)时,将调用方法 action。请注意,此处使用了属性 action。该属性指向的方法应具有 String action() 的签名,因为该方法必须返回一个导航键。 但此处的签名却是 void action()。这并未引发错误,因此可以推测在此情况下不存在导航操作。这正是预期效果。若将 actionListener 替换为 action 则会导致功能失效,
  • 第 2 行中的字段 idCreneau 将获取被点击链接的时间段 ID(页面第 33 行)。

3.6.6.3. 删除预约

让我们来分析处理删除预约的代码。这对应于以下视图序列:

与该操作相关的代码如下:


// Application bean
  @Inject
  private Application application;

  // 模板
  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;

  // 对 RV 采取操作
  public void action() {
    // 正在日历中查找空档
    int i = 0;
    Boolean trouvé = false;
    while (!trouvé && i < agendaMedecinJour.getCreneauxMedecinJour().length) {
      if (agendaMedecinJour.getCreneauxMedecinJour()[i].getCreneau().getId() == idCreneau) {
        trouvé = true;
      } else {
        i++;
      }
    }
    // 找到了吗?
    if (!trouvé) {
      // 真奇怪——重新显示 form2
      setForms(false, true, false, false);
      return;
    }
    // 找到了
    creneauChoisi = agendaMedecinJour.getCreneauxMedecinJour()[i];
    // 根据所需操作
    if (creneauChoisi.getRv() == null) {
      reserver();
    } else {
      supprimer();
    }
  }
  // 预约

  public void reserver() {
    ...
  }

  public void supprimer() {
    try {
      // 删除预约
      application.getMetier().supprimerRv(creneauChoisi.getRv());
      // 更新日程表
      agendaMedecinJour = application.getMetier().getAgendaMedecinJour(medecin, jour);
      // 显示表单2
      setForms(false, true, false, false);
    } catch (Throwable th) {
      // 错误视图
      prepareVueErreur(th);
    }
  }
  • 第16行:当方法action启动时,所选时段的ID已通过idCreneau(第11行)传递进来,
  • 第18-26行:试图根据id(第21行)中的信息检索该时间段。 在第10行的当前日程表 agendaMedecinJour 中进行查找。通常应能找到该时段。若未找到,则不执行任何操作(第28-32行),
  • 第34行:若找到目标时段,则获取其引用并存储在第12行,
  • 第36行:检查所选时段是否已有预约。如有,则将其删除(第39行);否则,则预留一个(第37行),
  • 第51行:删除所选时段内的预约。此操作由[métier]层负责,
  • 第53行:向[métier]层请求医生的最新日程表。当然,此时会发现预约减少了一个。但由于该应用是多用户系统,因此可能看到其他用户所做的修改,
  • 第55行:重新显示页面[form2.xhtml],
  • 第 58 行:由于调用了 [métier] 层,可能会引发异常。此时,将异常堆栈记录在第 13 行的错误列表中,并通过视图 [erreur.xhtml] 显示这些异常。

3.6.6.4. 预约

预约流程如下:

此操作涉及的模板如下:


// 模板
  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;

  // 对 RV 执行操作
  public void action() {
...
    // 已找到
    creneauChoisi = agendaMedecinJour.getCreneauxMedecinJour()[i];
    // 根据所需操作
    if (creneauChoisi.getRv() == null) {
      reserver();
    } else {
      supprimer();
    }
  }
  // 预订

    public void reserver() {
    try {
      // 表单标题 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();
      // 下拉列表中选定的客户
      idClient=null;
      // 显示表单 3
      setForms(false, false, true, false);
    } catch (Throwable th) {
      // 显示错误页面
      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
  @Inject
  private Application application;

  // 模板
  private Long idClient;

  // 客户列表
  public List<Client> getClients() {
    return application.getClients();
  }
  • 第 6 行:客户编号为页面第 12 行客户下拉列表的 value 属性提供数据。它确定了下拉列表中选中的项,
  • 第9-11行:方法getClients为下拉列表(第13行)提供内容。 每个选项的标签(itemLabel)是客户的[Titre Prénom Nom],关联的值(itemValue)是客户的ID。因此,将提交的就是该值。

3.6.6.5. 确认预约

预约确认遵循以下流程:

对应点击按钮 [Valider]:


        <h:commandButton value="#{msg['form3.valider']}" actionListener="#{form.validerRv}" />

因此,将由方法 [Form].validerRv 处理此事件。其代码如下:


  // 应用程序 Bean
  @Inject
  private Application application;
  
  // 模型
  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;

  // 预约验证
  public void validerRv() {
    try {
      // 获取所选时间段的实例
      Creneau creneau = application.getMetier().getCreneauById(idCreneau);
      // 添加预约
      application.getMetier().ajouterRv(jour, creneau, application.gethClients().get(idClient));
      // 更新日程表
      agendaMedecinJour = application.getMetier().getAgendaMedecinJour(medecin, jour);
      // 显示 form2
      setForms(false, true, false, false);
    } catch (Throwable th) {
      // 错误视图
      prepareVueErreur(th);
    }
}
  • 第12行:在方法validerRv执行之前,字段idClient已接收用户选定的客户ID,
  • 第19行:基于前一步骤中存储的时间段ID(该Bean为会话作用域),向[métier]层请求该时间段本身的引用,
  • 第21行:请求[métier]层为选定的日期(day)、时段(slot)和客户(idClient)添加一个预约,
  • 第23行:请求[métier]层刷新医生的日程表。此时将显示已添加的预约以及其他应用用户可能做出的所有修改,
  • 第25行:重新显示日程表[form2.xhtml],
  • 第28行:若发生错误,则显示错误页面。

3.6.6.6. 取消预约

这对应以下流程:

页面 [form3.xhtml] 中的按钮 [Annuler] 如下所示:


        <h:commandButton value="#{msg['form3.annuler']}" actionListener="#{form.annulerRv}"/>

因此将调用方法 [Form].annulerRv


  // 取消预约
  public void annulerRv() {
    // 显示表单2
    setForms(false, true, false, false);
}

3.6.6.7. 返回首页

还有一项操作需要查看,即以下序列的操作:

页面 [form2.xhtml] 中按钮 [Accueil] 的代码如下:


    <h:commandButton value="#{msg['form2.accueil']}" action="#{form.accueil}" />

[Form].accueil 方法如下:


  public void accueil() {
    // 显示主页
    setForms(true, false, false, false);
}

3.7. 结论

我们构建了以下应用程序:

我们更关注应用程序的功能,而非其用户界面。用户界面将通过使用组件库 PrimeFaces 得到改进。 我们构建了一个基础但能体现分层Java架构的应用程序,该架构利用了EE组件库。该应用程序可通过多种方式进行优化:

  • 需要身份验证。并非所有人都被授权添加/删除预约;
  • 在查找有空档的日期时,应能前后滚动浏览日历,
  • 应能查询某位医生所有有空档日期的列表。因为如果是眼科医生,其预约通常需要提前六个月预订,
  • ……

3.8. 使用Eclipse进行测试

3.8.1. [DAO]层

  • 在 [1] 中,导入来自 [DAO] 层的项目 EJB 及其客户,
  • 在 [2] 中,选择来自 [DAO] 层级的项目 EJB 并将其执行为 [3],
  • 在 [4] 中,将其在服务器上执行,
  • 在 [5] 中,仅提供 Glassfish 服务器,因为它是唯一拥有 EJB 容器的服务器,
  • 在 [6] 中,已部署了 EJB 模块,
  • 在 [7] 中,显示日志:
1
2
3
4
5
6
7
8
Infos: Hibernate Validator 4.2.0.Final

Infos: Created EjbThreadPoolExecutor with thread-core-pool-size 16 thread-max-pool-size 32 thread-keep-alive-seconds 60 thread-queue-capacity 2147483647 allow-core-thread-timeout false 
...

Infos: EJB5181:Portable JNDI names for EJB DaoJpa: [java:global/mv-rdvmedecins-ejb-dao-jpa/DaoJpa!rdvmedecins.dao.IDaoRemote, java:global/mv-rdvmedecins-ejb-dao-jpa/DaoJpa!rdvmedecins.dao.IDaoLocal]
Infos: EJB5182:Glassfish-specific (Non-portable) JNDI names for EJB DaoJpa: [rdvmedecins.dao#rdvmedecins.dao.IDaoRemote, rdvmedecins.dao]
Infos: mv-rdvmedecins-ejb-dao-jpa a été déployé en 5 523 ms.

这些就是我们在 NetBeans 中看到的日志。

  • 在 [7A] [7B] 中执行客户端测试 JUnit,
  • 在 [8] 中,测试成功,
  • 在 [9] 中,控制台日志。

在 [10] 中,卸载应用程序 EJB。

3.8.2. [métier]层

  • 在 [1] 中,导入 [métier] 层面的四个 Maven 项目,
  • 在 [2] 中,选择企业项目并将其部署到 [3],在 Glassfish 服务器 [4] [5] 上,
  • 在 [6] 中,企业项目已部署到 Glassfish 上,
  • 在 [7],查看 Glassfish 日志,
1
2
3
4
Infos: EJB5181:Portable JNDI names for EJB DaoJpa: [java:global/mv-rdvmedecins-metier-dao-ear/mv-rdvmedecins-ejb-dao-jpa-1.0-SNAPSHOT/DaoJpa!rdvmedecins.dao.IDaoLocal, java:global/mv-rdvmedecins-metier-dao-ear/mv-rdvmedecins-ejb-dao-jpa-1.0-SNAPSHOT/DaoJpa!rdvmedecins.dao.IDaoRemote]
Infos: EJB5182:Glassfish-specific (Non-portable) JNDI names for EJB DaoJpa: [rdvmedecins.dao#rdvmedecins.dao.IDaoRemote, rdvmedecins.dao]
Infos: EJB5181:Portable JNDI names for EJB Metier: [java:global/mv-rdvmedecins-metier-dao-ear/mv-rdvmedecins-ejb-metier-1.0-SNAPSHOT/Metier!rdvmedecins.metier.service.IMetierLocal, java:global/mv-rdvmedecins-metier-dao-ear/mv-rdvmedecins-ejb-metier-1.0-SNAPSHOT/Metier!rdvmedecins.metier.service.IMetierRemote]
Infos: EJB5182:Glassfish-specific (Non-portable) JNDI names for EJB Metier: [rdvmedecins.metier.service.IMetierRemote#rdvmedecins.metier.service.IMetierRemote, rdvmedecins.metier.service.IMetierRemote]

第3行,我们记下EJB [Metier]的便携名称,并将其粘贴到该EJB的控制台客户端中:


public class ClientRdvMedecinsMetier {

  // 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";
  // 今日日期
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。