Skip to content

4. 用于预约的 J2EE Web 服务

让我们回到即将构建的应用程序的架构:

在本节中,我们将重点介绍如何构建在 Sun/Glassfish 服务器上运行的 J2EE Web 服务 [1]。

4.1. 数据库

我们将该数据库命名为 [ dbrdvmedecins],这是一个包含四个表的 MySQL5 数据库:

Image

4.1.1. [MEDECINS] 表

该表包含由 [RdvMedecins] 应用程序管理的医生信息。

  • ID:医生的ID号——该表的主键
  • VERSION:用于标识表中该行版本的数字。每次对该行进行修改时,该数字都会增加1。
  • LAST_NAME:医生的姓
  • FIRST_NAME:医生的名字
  • TITLE:称谓(Ms.、Mrs.、Mr.)

4.1.2. [CLIENTS] 表

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

  • ID:用于标识客户的ID号——该表的主键
  • VERSION:标识该表中该行版本的编号。每次对该行进行修改时,该编号会递增1。
  • LAST NAME:客户的姓
  • 名字:客户的名字
  • 称谓:称谓(Ms.、Mrs.、Mr.)

4.1.3. [SLOTS] 表

该表格列出了可预约的时间段:

  • ID:时间段的ID号——该表的主键(第8行)
  • VERSION:标识表中该行版本的编号。每次对该行进行修改时,该编号会递增1。
  • DOC_ID:标识该时段所属医生的ID号——作为DOCTORS表中ID列的外键。
  • START_TIME:时间段的开始时间
  • MSTART:时间段的起始分钟
  • HFIN:时段结束时间
  • MFIN:该时段的结束分钟

例如,[SLOTS] 表(参见上文 [1])的第二行表明,第 2 号时段于上午 8:20 开始,上午 8:40 结束,属于第 1 号医生(Marie PELISSIER 女士)。

4.1.4. [RV] 表

该表列出了每位医生已预约的就诊时间:

  • ID:预约的唯一标识符——主键
  • DAY:预约日期
  • SLOT_ID:预约时段——作为外键关联至[SLOTS]表的[ID]字段——同时确定时段及负责医生。
  • CLIENT_ID:预约对象的客户ID——作为[CLIENTS]表中[ID]字段的外键

该表对关联列(DAY、SLOT_ID)的值设置了唯一性约束:

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

如果 [RV] 表中某行 (DAY, SLOT_ID) 列的值为 (DAY1, SLOT_ID1),则该值不能出现在其他任何地方。否则,这意味着同一医生在同一时间被预约了两次。从 Java 编程的角度来看,当这种情况发生时,数据库的 JDBC 驱动程序会抛出一个 SQLException

ID 为 3 行(参见上文 [1])表示,2006 年 8 月 23 日为第 20 个时段和第 4 号客户预订了一次预约。[SLOTS] 表告诉我们,第 20 个时段对应于下午 4:20 至 4:40,并属于第 1 号医生(Marie PELISSIER 女士)。 [CLIENTS] 表显示,客户编号 4 是 Brigitte BISTROU 女士。

4.2. 数据库创建

请使用您选择的工具创建MySQL数据库[dbrdvmedecins]。要创建表并向其中插入数据,您可以使用提供的[createbd.sql]脚本。其内容如下:

create table CLIENTS (
        ID bigint not null auto_increment,
        VERSION integer not null,
        TITRE varchar(5) not null,
        NOM varchar(30) not null,
        PRENOM varchar(30) not null,
        primary key (ID)
    ) ENGINE=InnoDB;

    create table CRENEAUX (
        ID bigint not null auto_increment,
        VERSION integer not null,
        HDEBUT integer not null,
        MDEBUT integer not null,
        HFIN integer not null,
        MFIN integer not null,
        ID_MEDECIN bigint not null,
        primary key (ID)
    ) ENGINE=InnoDB;

    create table MEDECINS (
        ID bigint not null auto_increment,
        VERSION integer not null,
        TITRE varchar(5) not null,
        NOM varchar(30) not null,
        PRENOM varchar(30) not null,
        primary key (ID)
    ) ENGINE=InnoDB;

    create table RV (
        ID bigint not null auto_increment,
        JOUR date not null,
        ID_CLIENT bigint not null,
        ID_CRENEAU bigint not null,
        primary key (ID)
    ) ENGINE=InnoDB;

    alter table CRENEAUX 
        add index FK9BD7A197FE16862 (ID_MEDECIN), 
        add constraint FK9BD7A197FE16862 
        foreign key (ID_MEDECIN) 
        references MEDECINS (ID);

    alter table RV 
        add index FKA4494D97AD2 (ID_CLIENT), 
        add constraint FKA4494D97AD2 
        foreign key (ID_CLIENT) 
        references CLIENTS (ID);

    alter table RV 
        add index FKA441A673246 (ID_CRENEAU), 
        add constraint FKA441A673246 
        foreign key (ID_CRENEAU) 
        references CRENEAUX (ID);

INSERT INTO CLIENTS ( VERSION, NOM, PRENOM, TITRE) VALUES (1, 'MARTIN', 'Jules', 'Mr');
...

INSERT INTO MEDECINS ( VERSION, NOM, PRENOM, TITRE) VALUES (1, 'PELISSIER', 'Marie', 'Mme');
...

INSERT INTO CRENEAUX ( VERSION, ID_MEDECIN, HDEBUT, MDEBUT, HFIN, MFIN) VALUES (1, 1, 8, 0, 8, 20);
...

INSERT INTO RV ( JOUR, ID_CRENEAU, ID_CLIENT) VALUES ('2006-08-22', 1, 2);
...

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

COMMIT WORK;

4.3. 服务器端架构组件

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

在服务器端,该应用程序将包含:

  1. 一个 JPA 层,用于通过对象与数据库进行交互
  1. 一个负责管理与 JPA 层交互操作的 EJB
  2. 一个 Web 服务,负责以 Web 服务的形式向远程客户端暴露 EJB 接口。

元素 (b) 和 (c) 实现了前一图中所示的 [DAO] 层。我们知道,应用程序可以通过 RMI 和 JNDI 协议访问远程 EJB。实际上,这将客户端限制为 Java 客户端。Web 服务使用一种标准化的通信协议,多种语言均可实现该协议:.NET、PHP、C++ 等。这就是我们在此想要通过 .NET 客户端演示的内容。

关于 Web 服务的简要介绍,请参阅课程 [ref1] 第 109 页第 14 段。

Web 服务可通过以下两种方式实现:

  • 通过在 Web 容器中运行的、带有 @WebService 注解的类
  • 由一个带有 @WebService 注解并在 EJB 容器中运行的 EJB 实现

这里我们将采用第一种解决方案:

在课程[ref1]第109页第14段中,你会看到一个使用第二种解法的例子。

4.4. ,以及 GlassFish 服务器的 Hibernate 配置

根据版本不同,NetBeans 随附的 GlassFish V2 服务器可能不包含 JPA/Hibernate 层所需的 Hibernate 库。 如果您在继续学习本教程时发现 GlassFish 未提供 JPA/Hibernate 实现,或者在服务部署过程中发生异常,提示无法找到 Hibernate 库,则必须将这些库添加到 [<glassfish>/domains/domain1/lib/ext] 文件夹中,然后重启 GlassFish 服务器:

  • 在 [1] 中,<glassfish>/.../lib/ext 文件夹
  • 在 [2] 中,Hibernate 库以及若干 JDBC 驱动程序
  • 在 [3] 中,MySQL JDBC 驱动程序

Hibernate 库已包含在随教程提供的 ZIP 文件中。

4.5. NetBeans 的自动生成工具

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

借助 NetBeans,可以自动生成 [JPA] 层以及控制对生成的 JPA 实体访问权限的 [EJB] 层。熟悉这些自动生成方法非常有用,因为生成的代码能为如何编写 JPA 实体或使用这些实体的 EJB 代码提供宝贵的参考。

接下来我们将介绍其中一些自动生成工具。要理解生成的代码,您需要对 JPA 实体 [ref1] 和 EJB [ref2] 有扎实的掌握。

创建 NetBeans 与数据库的连接

  • 启动 MySQL 5 数据库管理系统,确保数据库可用
  • 创建一个连接到 [dbrdvmedecins] 数据库的 NetBeans 连接
  • 在 [文件] 选项卡的 [数据库] 部分 [1] 中,选择 MySQL JDBC 驱动程序 [2]
  • 然后选择 [3] “使用” 选项,以建立与 MySQL 数据库的连接
  • 在 [4] 中,输入所需信息
  • 然后在 [5] 处确认
  • 在 [6] 中,连接已建立。您可以查看已连接数据库中的四个表。

创建 EJB 项目

  • 在 [1] 中,创建一个新应用程序,即 EJB 模块
  • 在 [2] 中,选择 [Java EE] 类别,并在 [3] 中选择 [EJB 模块] 类型
  • 在 [4] 中,为项目选择一个文件夹,并在 [5] 中为其命名——然后完成向导
  • 在 [6] 中,生成的项目

向 GlassFish 服务器添加 JDBC 资源

我们将向 GlassFish 服务器添加一个 JDBC 资源。

  • 在 [服务] 选项卡中,启动 GlassFish 服务器 [2, 3]
  • 在 [项目] 选项卡中,右键单击 EJB 项目,然后在 [5] 中选择 [新建 / 其他] 选项,向项目中添加一个元素。

Image

  • 在 [6] 中,选择 [GlassFish] 类别;在 [7] 中,通过选择 [JDBC 资源] 类型来指定要创建 JDBC 资源
  • 在 [8] 中,指定此 JDBC 资源将使用其自身的连接池
  • 在 [9] 中,为该 JDBC 资源命名
  • 在[10]中,继续进行下一步
  • 在 [11] 中,定义 JDBC 资源连接池的特性
  • 在 [12] 中,为连接池命名
  • 在 [13] 中,选择之前创建的 NetBeans 连接 [dbrdvmedecins]
  • 在 [14] 中,继续进行下一步
  • 在 [15] 中,此页面通常无需更改。MySQL 数据库 [dbrdvmedecins] 的连接属性已从先前创建的 NetBeans 连接 [dbrdvmedecins] 中获取
  • 在 [16] 中,继续进行下一步
  • 在 [17] 中,保留提供的默认值
  • [18],完成向导。这将生成 [sun-resources.xml] 文件 [19],内容如下:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE resources PUBLIC "-//Sun Microsystems, Inc.//DTD Application Server 9.0 Resource Definitions //EN" "http://www.sun.com/software/appserver/dtds/sun-resources_1_3.dtd">
<resources>
  <jdbc-resource enabled="true" jndi-name="jdbc/dbrdvmedecins" object-type="user" pool-name="dbrdvmedecinsPool">
    <description/>
  </jdbc-resource>
  <jdbc-connection-pool ...">
    <property name="URL" value="jdbc:mysql://localhost:3306/dbrdvmedecins"/>
    <property name="User" value="root"/>
    <property name="Password" value="()"/>
  </jdbc-connection-pool>
</resources>

上述文件包含向向导中输入的所有信息的 XML 格式。NetBeans IDE 将使用该文件,指示 GlassFish 服务器创建第 4 行中定义的“jdbc/dbrdvmedecins”资源。

创建持久化单元

持久化单元 [persistence.xml] 用于配置 JPA 层:它指定了所使用的 JPA 实现(TopLink、Hibernate 等)并对其进行配置。

  • 在 [1] 中,右键单击 EJB 项目,并在 [2] 中选择 [新建 / 其他]
  • 在 [3] 中,选择 [持久化] 类别,然后在 [4] 中,指定要创建一个 JPA 持久化单元
  • 在 [5] 中,为创建的持久化单元命名
  • 在 [6] 中,选择 [Hibernate] 作为 JPA 实现
  • 在 [7] 中,选择刚刚创建的 GlassFish 资源“jdbc/dbrdvmedecins”
  • 在 [8] 中,指定在实例化 JPA 层时不执行任何数据库操作
  • 完成向导
  • 在 [9] 中,由向导生成的 [persistence.xml] 文件

其内容如下:

1
2
3
4
5
6
7
8
9
<?xml version="1.0" encoding="UTF-8"?>
<persistence version="1.0" xmlns="http://java.sun.com/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_1_0.xsd">
  <persistence-unit name="serveur-ejb-dao-jpa-hibernate-generePU" transaction-type="JTA">
    <provider>org.hibernate.ejb.HibernatePersistence</provider>
    <jta-data-source>jdbc/dbrdvmedecins</jta-data-source>
    <exclude-unlisted-classes>false</exclude-unlisted-classes>
    <properties/>
  </persistence-unit>
</persistence>

同样,它将向导中提供的信息转换为 XML 格式。仅凭此文件还不足以操作 MySQL5 数据库“dbrdvmedecins”。我们需要向 Hibernate 指定要管理的数据库管理系统 (DBMS) 类型。这将在后续步骤中完成。

创建 JPA 实体

 
  • 在 [1] 中,右键单击该项目,然后在 [2] 中选择 [新建 / 其他] 选项
  • 在 [3] 中,选择 [持久化] 类别,然后在 [4] 中,指定要从现有数据库创建 JPA 实体。
  • 在 [5] 中,选择我们创建的 JDBC 源“jdbc/dbrdvmedecins”
  • 在 [6] 中,从关联的数据库中选择这四个表
  • 在 [7,8] 中,将它们全部纳入 JPA 实体的生成
  • 在 [9] 中,继续进行向导操作
  • 在 [10] 中,即将生成的 JPA 实体
  • 在 [11] 中,为 JPA 实体包命名
  • 在 [12] 中,选择将封装 JPA 层返回的对象列表的 Java 类型
  • 完成向导
  • 在 [13] 中,生成的四个 JPA 实体,每个数据库表对应一个。

以下是 [Rv] 实体的代码示例,该实体表示 [dbrdvmedecins] 数据库中 [rv] 表的一行。

package jpa;
...
@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)
  @Column(name = "JOUR")
  @Temporal(TemporalType.DATE)
  private Date jour;
  @JoinColumn(name = "ID_CRENEAU", referencedColumnName = "ID")
  @ManyToOne(optional = false)
  private Creneaux idCreneau;
  @JoinColumn(name = "ID_CLIENT", referencedColumnName = "ID")
  @ManyToOne(optional = false)
  private Clients idClient;

  public Rv() {
  }

...
}

创建用于访问 JPA 实体的 EJB 层

  • 在 [1] 中,右键单击项目,然后在 [2] 中选择 [新建 / 其他] 选项
  • 在 [3] 中,选择 [持久化] 类别,然后在 [4] 中选择 [用于实体类的会话 Bean] 类型
  • 在 [5] 中,将显示之前创建的 JPA 实体
  • 在 [6] 中,全选它们
  • 在 [7] 中,它们已被选中
  • 在 [8] 中,继续进行向导操作
  • 在 [9] 中,为将要生成的 EJB 包命名
  • 在 [10] 中,指定 EJB 必须同时实现本地接口和远程接口
  • 完成向导
  • 在 [11] 中,生成的 EJB

例如,以下是管理对 [Rv] 实体的访问(从而管理对 [dbrdvmedecins] 数据库中 [rv] 表的访问)的 EJB 代码:

package ejb;
...
@Stateless
public class RvFacade implements RvFacadeLocal, RvFacadeRemote {
  @PersistenceContext
  private EntityManager em;

  public void create(Rv rv) {
    em.persist(rv);
  }

  public void edit(Rv rv) {
    em.merge(rv);
  }

  public void remove(Rv rv) {
    em.remove(em.merge(rv));
  }

  public Rv find(Object id) {
    return em.find(Rv.class, id);
  }

  public List<Rv> findAll() {
    return em.createQuery("select object(o) from Rv as o").getResultList();
  }

}

如前所述,自动代码生成对于启动项目以及学习 JPA 实体和 EJB 非常有用。在接下来的章节中,我们将使用自己的代码重写 JPA 和 EJB 层,但读者会发现其中包含我们在自动生成这些层时刚刚介绍过的内容。

4.6. EJB 模块的 NetBeans 项目

我们创建一个新的空 EJB 模块(参见第 4.5 节):

 
  • [rdvmedecins.entities] 包包含 JPA 层的实体
  • [rdvmedecins.dao] 包实现了 [dao] 层的 EJB
  • [rdvmedecins.exceptions] 包实现了应用程序专用的异常类

下文假设读者已按照第 4.5 节中的所有步骤操作。读者需要重复其中的一些步骤。

4.6.1. 配置 JPA 层

让我们回顾一下我们的客户端/服务器应用程序的架构:

NetBeans 项目:

 

[JPA] 层由上文提到的 [persistence.xml] 和 [sun-resources.xml] 文件进行配置。这两个文件是由我们之前已经用过的向导生成的:

  • 第 4.5 节中描述了 [sun-resources.xml] 文件的生成过程。
  • 第 4.5 节中描述了 [persistence.xml] 文件的生成过程。

生成的 [persistence.xml] 文件必须按以下方式进行修改:

<?xml version="1.0" encoding="UTF-8"?>
<persistence version="1.0" xmlns="http://java.sun.com/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_1_0.xsd">
  <persistence-unit name="dbrdvmedecins" transaction-type="JTA">
    <provider>org.hibernate.ejb.HibernatePersistence</provider>
    <jta-data-source>jdbc/dbrdvmedecins</jta-data-source>
    <properties>
       <!-- Dialect -->
      <property name="hibernate.dialect" value="org.hibernate.dialect.MySQL5InnoDBDialect"/>
    </properties>
  </persistence-unit>
</persistence>
  • 第 3 行:事务类型为 JTA:事务将由 GlassFish EJB3 容器管理
  • 第 4 行:使用 JPA/Hibernate 实现。为此,已将 Hibernate 库添加到 GlassFish 服务器中(参见第 4.4 节)。
  • 第 5 行:JPA 层使用的 JTA 数据源的 JNDI 名称为“jdbc/dbrdvmedecins”。
  • 第 8 行:此行不会自动生成,必须手动添加。它告知 Hibernate 所使用的数据库管理系统是 MySQL5。

“jdbc/dbrdvmedecins”数据源在以下 [sun-resources.xml] 文件中进行配置:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE resources PUBLIC "-//Sun Microsystems, Inc.//DTD Application Server 9.0 Resource Definitions //EN" "http://www.sun.com/software/appserver/dtds/sun-resources_1_3.dtd">
<resources>
  <jdbc-resource enabled="true" jndi-name="jdbc/dbrdvmedecins" object-type="user" pool-name="dbrdvmedecinsPool">
    <description/>
  </jdbc-resource>
  <jdbc-connection-pool ...>
    <property name="URL" value="jdbc:mysql://localhost/dbrdvmedecins"/>
    <property name="User" value="root"/>
    <property name="Password" value="()"/>
  </jdbc-connection-pool>
</resources>
  • 第 8–10 行:数据源的 JDBC 属性(数据库 URL、用户名和密码)。MySQL 数据库 `dbrdvmedecins` 即第 4.1 节中所述的数据库。
  • 第 7 行:与该数据源关联的连接池的特性

4.6.2. JPA 层的实体

让我们回顾一下我们的客户端/服务器应用程序的架构:

NetBeans 项目:

[rdvmedecins.entities] 包实现了 [JPA] 层。

第 4.5 节中,我们了解了如何为应用程序自动生成 JPA 实体。这里我们将不使用该技术,而是自行定义实体。不过,这些实体将包含第 4.5 节中生成的许多代码。在此,我们希望 [Medecin] 和 [Client] 实体成为 [Personne] 类的子类。

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

package rdvmedecins.entites;
...
@MappedSuperclass
public class Personne implements Serializable {
   // characteristics of a person

  @Id
  @GeneratedValue(strategy = GenerationType.AUTO)
  @Column(name = "ID")
  private Long id;
  @Version
  @Column(name = "VERSION", nullable = false)
  private Integer version;

  @Column(name = "TITRE", length = 5, nullable = false)
  private String titre;
  @Column(name = "NOM", length = 30, nullable = false)
  private String nom;
  @Column(name = "PRENOM", length = 30, nullable = false)
  private String prenom;

   // default builder
  public Personne() {
  }

   // builder with parameters
  public Personne(String titre, String nom, String prenom) {
     // we use setters
...
  }

   // copy builder
  public Personne(Personne personne) {
     // we use setters
 ...
  }

   // toString
  @Override
  public String toString() {
    return "[" + titre + "," + prenom + "," + nom + "]";
  }

// getters and setters
....
}
  • 第 3 行:请注意,[Person] 类本身并非实体(@Entity)。它将作为实体的父类。@MappedSuperClass 注解表明了这种情况。

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

package rdvmedecins.entites;
....
@Entity
@Table(name = "CLIENTS")
public class Client extends Personne implements Serializable {

   // default builder
  public Client() {
  }

   // builder with parameters
  public Client(String titre, String nom, String prenom) {
     // parent
    super(titre, nom, prenom);
  }

   // copy builder
  public Client(Client client) {
     // parent
    super(client);
  }
}
  • 第 3 行:[Client] 类是一个 JPA 实体
  • 第 4 行:它与 [clients] 表相关联
  • 第 5 行:它继承自 [Person] 类

封装 [doctors] 表中各行的 [Doctor] 实体遵循相同的模式:

package rdvmedecins.entites;
...
@Entity
@Table(name = "MEDECINS")
public class Medecin extends Personne implements Serializable {

   // default builder
  public Medecin() {
  }

   // builder with parameters
  public Medecin(String titre, String nom, String prenom) {
     // parent
    super(titre, nom, prenom);
  }

   // copy builder
  public Medecin(Medecin medecin) {
     // parent
    super(medecin);
  }
}

[Creneau] 实体封装了 [creneaux] 表中的行:

package rdvmedecins.entites;
....
@Entity
@Table(name = "CRENEAUX")
public class Creneau implements Serializable {

   // characteristics of a RV slot
  @Id
  @GeneratedValue(strategy = GenerationType.AUTO)
  @Column(name = "ID")
  private Long id;
  @Version
  @Column(name = "VERSION", nullable = false)
  private Integer version;
  @ManyToOne
  @JoinColumn(name = "ID_MEDECIN", nullable = false)
  private Medecin medecin;
  @Column(name = "HDEBUT", nullable = false)
  private Integer hdebut;
  @Column(name = "MDEBUT", nullable = false)
  private Integer mdebut;
  @Column(name = "HFIN", nullable = false)
  private Integer hfin;
  @Column(name = "MFIN", nullable = false)
  private Integer mfin;

   // default builder
  public Creneau() {

  }

   // builder with parameters
  public Creneau(Medecin medecin, Integer hDebut,Integer mDebut, Integer hFin, Integer mFin) {
     // we use setters
...
  }

   // copy builder
  public Creneau(Creneau creneau) {
     // we use setters
...
  }

   // toString
  @Override
  public String toString() {
    return "[" + getId() + "," + getVersion() + "," + getMedecin() + "," + getHdebut() + ":" + getMdebut() + "," + getHfin() + ":" + getMfin() + "]";
  }

   // setters - getters
...
}
  • 第 15–17 行描述了数据库中 [slots] 表与 [doctors] 表之间的“一对多”关系。

[Rv] 实体封装了 [rv] 表中的行:

package rdvmedecins.entites;
...
@Entity
@Table(name = "RV")
public class Rv implements Serializable {
   // features

  @Id
  @GeneratedValue(strategy = GenerationType.AUTO)
  @Column(name = "ID")
  private Long id;
  @Column(name = "JOUR", nullable = false)
  @Temporal(TemporalType.DATE)
  private Date jour;
  @ManyToOne
  @JoinColumn(name = "ID_CLIENT", nullable = false)
  private Client client;
  @ManyToOne
  @JoinColumn(name = "ID_CRENEAU", nullable = false)
  private Creneau creneau;

   // default builder
  public Rv() {
  }

   // builder with parameters
  public Rv(Date jour, Client client, Creneau creneau) {
     // we use setters
...
  }

   // copy builder
  public Rv(Rv rv) {
     // we use setters
...
  }

   // toString
  @Override
  public String toString() {
    return "[" + getId() + "," + new SimpleDateFormat("dd/MM/yyyy").format(getJour()) + "," + getClient() + "," + getCreneau() + "]";
  }

// getters and setters
...
}
  • 第 15–17 行描述了数据库中 [rv] 表与 [clients] 表之间的“一对多”关系,第 18–20 行描述了 [rv] 表与 [slots] 表之间的“一对多”关系

4.6.3. 异常类

该应用程序的异常类 [ RdvMedecinsException] 如下所示:

package rdvmedecins.exceptions;

import javax.ejb.ApplicationException;

@ApplicationException(rollback=true)
public class RdvMedecinsException extends RuntimeException {

  private static final long serialVersionUID = 1L;

   // private fields
  private int code = 0;

   // manufacturers
  public RdvMedecinsException() {
    super();
  }

  public RdvMedecinsException(String message) {
    super(message);
  }

  public RdvMedecinsException(String message, Throwable cause) {
    super(message, cause);
  }

  public RdvMedecinsException(Throwable cause) {
    super(cause);
  }

  public RdvMedecinsException(String message, int code) {
    super(message);
    setCode(code);
  }

  public RdvMedecinsException(Throwable cause, int code) {
    super(cause);
    setCode(code);
  }

  public RdvMedecinsException(String message, Throwable cause, int code) {
    super(message, cause);
    setCode(code);
  }

   // getters - setters
...
}
  • 第 6 行:该类继承了 [RuntimeException] 类。因此,编译器不要求使用 try/catch 代码块来处理它。
  • 第 5 行:@ApplicationException 注解确保该异常不会被 [EjbException] “吞噬”。

为了理解 @ApplicationException 注解,让我们重新审视一下服务器端架构:

[RdvMedecinsException] 异常将由 EJB3 容器内的 [dao] 层中的 EJB 方法抛出,并被该容器拦截。如果没有 @ApplicationException 注解,EJB3 容器会将发生的异常封装在 [EjbException] 中并重新抛出。 您可能不希望进行这种封装,而希望让 [RdvMedecinsException] 类型的异常从 EJB3 容器中逸出。这正是 @ApplicationException 注解所允许的。 此外,该注解的 (rollback=true) 属性指示 EJB3 容器:若在作为与 DBMS 事务一部分执行的方法中发生 [RdvMedecinsException] 类型的异常,则必须回滚该事务。在技术术语中,这被称为回滚事务。

4.6.4. [dao] 层的 EJB

[dao] 层的 Java 接口 [ IDao] 如下:

package rdvmedecins.dao;
...
public interface IDao {

   // customer list
  public List<Client> getAllClients();
   // list of doctors
  public List<Medecin> getAllMedecins();
   // list of physician slots
  public List<Creneau> getAllCreneaux(Medecin medecin);
   // list of doctor's appointments on a given day
  public List<Rv> getRvMedecinJour(Medecin medecin, String jour);
   // find a customer identified by its id
  public Client getClientById(Long id);
   // find a customer identified by its id
  public Medecin getMedecinById(Long id);
   // find an Rv identified by its id
  public Rv getRvById(Long id);
   // find a time slot identified by its id
  public Creneau getCreneauById(Long id);
   // add a RV to the list
  public Rv ajouterRv(String jour, Creneau creneau, Client client);
   // delete a RV
  public void supprimerRv(Rv rv);
}

该 EJB 的本地接口 [IDaoLocal] 只是继承了之前的 [IDao] 接口:

1
2
3
4
5
6
7
package rdvmedecins.dao;

import javax.ejb.Local;

@Local
public interface IDaoLocal extends IDao{
}

远程接口 [IDaoRemote] 也是如此:

1
2
3
4
5
6
7
package rdvmedecins.dao;

import javax.ejb.Remote;

@Remote
public interface IDaoRemote extends IDao {
}

该 EJB [DaoJpa] 同时实现了本地和远程接口:

1
2
3
4
5
6
7
package rdvmedecins.dao;
...
@Stateless(mappedName="rdvmedecins.dao")
@TransactionAttribute(TransactionAttributeType.REQUIRED)
public class DaoJpa implements IDaoLocal,IDaoRemote {
...
}
  • 第 3 行表明远程 EJB 的名称为 "rdvmedecins.dao"
  • 第 4 行表示 EJB 的所有方法都在由 EJB3 容器管理的事务中执行。
  • 第 5 行显示该 EJB 实现了本地远程接口。

完整的 EJB 代码如下:

package rdvmedecins.dao;
...
@Stateless(mappedName="rdvmedecins.dao")
@TransactionAttribute(TransactionAttributeType.REQUIRED)
public class DaoJpa implements IDaoLocal,IDaoRemote {

  @PersistenceContext
  private EntityManager em;

   // customer list
  public List<Client> getAllClients() {
    try {
      return em.createQuery("select c from Client c").getResultList();
    } catch (Throwable th) {
      throw new RdvMedecinsException(th, 1);
    }
  }

   // list of doctors
  public List<Medecin> getAllMedecins() {
    try {
      return em.createQuery("select m from Medecin m").getResultList();
    } catch (Throwable th) {
      throw new RdvMedecinsException(th, 2);
    }
  }

   // list of time slots for a given doctor
   // doctor: the doctor
  public List<Creneau> getAllCreneaux(Medecin medecin) {
    try {
      return em.createQuery("select c from Creneau c join c.medecin m where m.id=:idMedecin").setParameter("idMedecin", medecin.getId()).getResultList();
    } catch (Throwable th) {
      throw new RdvMedecinsException(th, 3);
    }
  }

   // list of appointments for a given doctor on a given day
   // doctor: the doctor
   // day: the day
  public List<Rv> getRvMedecinJour(Medecin medecin, String jour) {
    try {
      return em.createQuery("select rv from Rv rv join rv.creneau c join c.medecin m where m.id=:idMedecin and rv.jour=:jour").setParameter("idMedecin", medecin.getId()).setParameter("jour", new SimpleDateFormat("yyyy:MM:dd").parse(jour)).getResultList();
    } catch (Throwable th) {
      throw new RdvMedecinsException(th, 4);
    }
  }

   // add Rv
   // day : day of appointment
   // creneau: Rv time slot
   // customer: customer for whom the appointment is taken
  public Rv ajouterRv(String jour, Creneau creneau, Client client) {
    try {
      Rv rv = new Rv(new SimpleDateFormat("yyyy:MM:dd").parse(jour), client, creneau);
      em.persist(rv);
      return rv;
    } catch (Throwable th) {
      throw new RdvMedecinsException(th, 5);
    }
  }

   // deleting an appointment
   // rv: Rv deleted
  public void supprimerRv(Rv rv) {
    try {
      em.remove(em.merge(rv));
    } catch (Throwable th) {
      throw new RdvMedecinsException(th, 6);
    }
  }

   // retrieve a specific customer
  public Client getClientById(Long id) {
    try {
      return (Client) em.find(Client.class, id);
    } catch (Throwable th) {
      throw new RdvMedecinsException(th, 7);
    }
  }

   // retrieve a specific doctor
  public Medecin getMedecinById(Long id) {
    try {
      return (Medecin) em.find(Medecin.class, id);
    } catch (Throwable th) {
      throw new RdvMedecinsException(th, 8);
    }
  }

   // retrieve a given Rv
  public Rv getRvById(Long id) {
    try {
      return (Rv) em.find(Rv.class, id);
    } catch (Throwable th) {
      throw new RdvMedecinsException(th, 9);
    }
  }

   // retrieve a given slot
  public Creneau getCreneauById(Long id) {
    try {
      return (Creneau) em.find(Creneau.class, id);
    } catch (Throwable th) {
      throw new RdvMedecinsException(th, 10);
    }
  }
}
  • 第 8 行:管理对持久化上下文访问的 EntityManager 对象。当类被实例化时,EJB 容器会根据第 7 行中的 @PersistenceContext 注解初始化此字段。
  • 第 15 行:JPQL 查询,用于将 [clients] 表中的所有行作为 [Client] 对象列表返回。
  • 第 22 行:针对医生表的类似查询
  • 第 32 行:一个对 [slots] 和 [doctors] 表执行连接操作的 JPQL 查询。该查询通过医生的 ID 进行参数化。
  • 第 43 行:一个对 [appointments]、[slots] 和 [doctors] 表执行连接的 JPQL 查询,该查询有两个参数:医生的 ID 和预约日期。
  • 第 55–57 行:创建预约并将其持久化到数据库中。
  • 第 67 行:从数据库中删除一个预约。
  • 第 76 行:对数据库执行 SELECT 查询以查找特定客户
  • 第 85 行:针对医生的操作同上
  • 第 94 行:对预约执行相同操作
  • 第 103 行:对时间段执行相同操作
  • 第 9 行起涉及持久化上下文的所有操作都可能遇到数据库问题。因此,这些操作均被封装在 try/catch 代码块中。任何异常都会被封装在自定义异常 RdvMedecinsException 中。

编译完成后,EJB 模块会生成一个名为“ ”的 .jar 文件:

4.7. 使用 NetBeans 部署 [DAO] 层 EJB

NetBeans 允许您轻松地将之前创建的 EJB 部署到 GlassFish 服务器上。

  • 在 EJB 项目属性中,勾选运行时选项 [1]。
  • 在 [2] 中,输入将部署 EJB 的服务器名称
  • 在 [服务] 选项卡 [3] 中,启动它 [4]。
  • 在 [5] 中,GlassFish 服务器已启动。目前尚未部署 EJB 模块。
  • 启动 MySQL 服务器,并确保 [dbrdvmedecins] 数据库处于联机状态。为此,您可以使用第 4.5 节中创建的 NetBeans 连接。
  • 在 [Projects] 选项卡 [6] 中,部署 EJB 模块 [7]:MySQL5 数据库管理系统必须正在运行,EJB 使用的 JDBC 资源 "jdbc/dbrdvmedecins" 才能被访问。
  • 在 [8] 中,已部署的 EJB 会显示在 GlassFish 服务器树中
  • 在 [9] 中,移除已部署的 EJB
  • 在 [10] 中,EJB 不再出现在 GlassFish 服务器树中。

4.8. 使用 GlassFish 部署来自 [DAO] 层的 EJB

本文将演示如何从 .jar 归档文件将 EJB 部署到 GlassFish 服务器。

  • 启动 MySQL 服务器,并确保 [dbrdvmedecins] 数据库处于联机状态。为此,您可以使用第 4.5 节中创建的 NetBeans 连接。

让我们回顾一下待部署的 EJB 模块的 JPA 配置。该配置定义在 [persistence.xml] 文件中:

<?xml version="1.0" encoding="UTF-8"?>
<persistence version="1.0" xmlns="http://java.sun.com/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_1_0.xsd">
  <persistence-unit name="dbrdvmedecins" transaction-type="JTA">
    <provider>org.hibernate.ejb.HibernatePersistence</provider>
    <jta-data-source>jdbc/dbrdvmedecins</jta-data-source>
    <properties>
       <!-- Dialect -->
      <property name="hibernate.dialect" value="org.hibernate.dialect.MySQL5InnoDBDialect"/>
    </properties>
  </persistence-unit>
</persistence>

第 5 行表明 JPA 层使用了一个 JTA 数据源,即由 EJB3 容器管理的、名为“jdbc/dbrdvmedecins”的数据源。

我们在第 4.5 节中已经了解了如何使用 NetBeans 创建此 JDBC 资源。在此,我们将展示如何直接在 GlassFish 中进行操作。我们将遵循 [ref1] 第 79 页第 13.1.2 节中描述的步骤。

首先,我们将删除该资源以便重新创建。此操作在 NetBeans 中进行:

  • 在 [1] 中,GlassFish 服务器的 JDBC 资源
  • 在 [2] 中,我们的 EJB 的 "jdbc/dbrdvmedecins" 资源
  • 在 [3] 中,此 JDBC 资源的连接池
  • 在 [4] 中,我们删除连接池。这将导致所有使用该连接池的 JDBC 资源被删除,包括“jdbc/dbrdvmedecins”资源。
  • 在 [5] 和 [6] 中,JDBC 资源和连接池已被销毁。

现在,我们使用 GlassFish 服务器管理控制台创建 JDBC 资源并部署 EJB。

  • 在 NetBeans 的 [服务] 选项卡 [1] 中,启动 GlassFish 服务器 [2],然后访问 [3] 其管理控制台
  • 在 [4] 中,请以管理员身份登录(密码:adminadmin,如果您在安装过程中或之后未更改过)。
  • 在 [5] 中,选择 GlassFish 资源的 [连接池] 分支
  • 在 [6] 中,创建一个新的连接池。请注意,连接池是一种用于限制与数据库管理系统(DBMS)之间建立和关闭的连接数量的技术。当服务器启动时,会向 DBMS 建立 N 个连接(N 由配置定义)。这些已建立的连接随后可供请求它们的 EJB 使用,以便与 DBMS 执行操作。一旦操作完成,EJB 就会将连接归还给连接池。 连接永远不会被关闭;它会在访问 DBMS 的各个线程之间共享
  • 在 [7] 中,为连接池命名
  • 在[8]中,表示数据源的类是[javax.sql.DataSource]类
  • 在 [9] 中,包含该数据源的 DBMS 这里是 MySQL。
  • 在 [10] 中,继续执行下一步
  • 在 [11] 中,“Connection Validation Required” 属性确保在授予连接之前,连接池会验证该连接是否正常工作。如果不是,则创建一个新的连接。这使得应用程序在 DBMS 发生临时中断后仍能继续运行。中断期间,所有连接均不可用,并将向客户端抛出异常。 当中断结束时,继续请求连接的客户端将再次获得连接:得益于“Connection Validation Required”属性,连接池中的所有连接都将被重新创建。如果没有此属性,连接池虽然会检测到初始连接已丢失,但不会尝试创建新的连接。
  • 在[12]中,事务被指定为“已提交读”隔离级别。该级别确保事务T2无法读取事务T1修改的数据,直到后者完全完成。
  • 在[13]中,我们规定所有事务必须使用[12]中指定的隔离级别
  • 在[14]和[15]中,指定由连接池管理的数据库的URL
  • 在 [16] 中,用户为 root
  • 在 [17] 中,添加一个属性
  • 在 [18] 中,添加“Password”属性,并在 [19] 中将其值设为 ()。尽管截图 [19] 未显示此内容,但请勿输入空字符串;应输入 ()(左括号、右括号)以表示空密码。如果您的 MySQL 数据库管理系统 (DBMS) 的 root 用户具有非空密码,请输入该密码。
  • 在 [20] 中,完成 MySQL 数据库 [dbrdvmedecins] 的连接池创建向导。
  • 在 [21] 中,连接池已创建完成。点击其链接。
  • 在[22]中,[Ping]按钮可用于与[dbrdvmedecins]数据库建立连接
  • 在[23]处,如果一切顺利,将显示一条消息,表明连接成功

创建连接池后,您可以创建一个 JDBC 资源:

  • 在 [1] 中,从服务器对象树中选择 [JDBC Resources] 分支
  • 在 [2] 中,创建一个新的 JDBC 资源
  • 在 [3] 中,为 JDBC 资源命名。该名称必须与 [persistence.xml] 文件中使用的名称一致:
    <jta-data-source>jdbc/dbrdvmedecins</jta-data-source>
  • 在 [4] 中,指定新 JDBC 资源应使用的连接池:即您刚刚创建的那个
  • 在 [5] 中,我们完成创建向导
  • 在 [6] 中,新的 JDBC 资源

现在 JDBC 资源已创建完成,您可以部署 EJB 的 JAR 文件:

  • 在 [1] 中,选择 [企业应用程序] 分支
  • 在 [2] 中,点击 [部署] 按钮,表示您要部署一个新应用程序
  • 在 [3] 中,指定该应用程序为 EJB 模块
  • 在 [4] 中,选择实验中提供的 EJB JAR 文件 [serveur-ejb-dao-jpa-hibernate.jar]。
  • 在 [5] 中,您可以根据需要更改 EJB 模块的名称
  • 在 [6] 中,完成 EJB 模块部署向导
  • 在 [7] 中,EJB 模块已部署完成。现在可以使用它了。

4.9. 测试 [DAO] 层的 EJB

既然我们应用程序[DAO]层的EJB已部署完毕,现在可以对其进行测试。我们将使用以下Java客户端进行测试:

[MainTestsDaoRemote] 类 [1] 是一个 JUnit 4 测试类。[2] 中的库包括:

  • [DAO] 层的 EJB JAR [3](参见第 4.6.4 节)。
  • 远程 EJB 客户端所需的 GlassFish 库 [4]。

测试类如下:

package dao;
...
public class MainTestsDaoRemote {

   // layer [dao] tested
  private static IDaoRemote dao;

  @BeforeClass
  public static void init() throws NamingException {
     // environment initialization JNDI
    InitialContext initialContext = new InitialContext();
     // dao layer instantiation
    dao = (IDaoRemote) initialContext.lookup("rdvmedecins.dao");
  }

  @Test
  public void test1() {
     // tEST DATA
    String jour = "2006:08:23";
     // customer display
    List<Client> clients = null;
    try {
      clients = dao.getAllClients();
      display("Liste des clients :", clients);
    } catch (Exception ex) {
      System.out.println(ex);
    }
     // physician display
    List<Medecin> medecins = null;
    try {
      medecins = dao.getAllMedecins();
      display("Liste des médecins :", medecins);
    } catch (Exception ex) {
      System.out.println(ex);
    }
     // display doctor's slots
    Medecin medecin = medecins.get(0);
    List<Creneau> creneaux = null;
    try {
      creneaux = dao.getAllCreneaux(medecin);
      display(String.format("Liste des créneaux du médecin %s", medecin), creneaux);
    } catch (Exception ex) {
      System.out.println(ex);
    }
     // list of doctor's appointments on a given day
    try {
      display(String.format("Liste des créneaux du médecin %s, le [%s]", medecin, jour), dao.getRvMedecinJour(medecin, jour));
    } catch (Exception ex) {
      System.out.println(ex);
    }
     // add a RV to the list
    Rv rv = null;
    Creneau creneau = creneaux.get(2);
    Client client = clients.get(0);
    System.out.println(String.format("Ajout d'un Rv le [%s] dans le créneau %s pour le client %s", jour, creneau, client));
    try {
      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, "2006:08:23"));
    } catch (Exception ex) {
      System.out.println(ex);
    }
     // add a RV in the same slot on the same day
     // must trigger an exception
    System.out.println(String.format("Ajout d'un Rv le [%s] dans le créneau %s pour le client %s", jour, creneau, client));
    try {
      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, "2006:08:23"));
    } catch (Exception ex) {
      System.out.println(ex);
    }
     // delete a RV
    System.out.println("Suppression du Rv ajouté");
    try {
      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, "2006:08:23"));
    } catch (Exception ex) {
      System.out.println(ex);
    }
  }

   // utility method - displays items in a collection
  private static void display(String message, List elements) {
    System.out.println(message);
    for (Object element : elements) {
      System.out.println(element);
    }
  }
}
  • 第 13 行:请注意远程 EJB 代理的实例化。我们使用其 JNDI 名称“rdvmedecins.dao”。
  • 测试方法使用了 EJB 暴露的方法(参见第 4.6.4 节)。

如果一切顺利,测试应该通过:

 

既然 [dao] 层的 EJB 已可正常运行,我们可以继续通过 Web 服务将其对外公开。

4.10. [DAO]层的Web服务

关于Web服务概念的简要介绍,请参阅[ref1]第14节,第111页。

让我们回到客户端/服务器应用程序的服务器架构:

这里我们重点关注[DAO]层的Web服务。该服务的唯一目的是向能够与Web服务通信的跨平台客户端提供[DAO]层的EJB接口。

请注意,实现 Web 服务有两种方式:

  • 使用带有 @WebService 注解的类,该类在 Web 容器中运行
  • 使用一个带有 @WebService 注解并在 EJB 容器中运行的 EJB

这里,我们采用第一种解决方案。在 NetBeans IDE 中,我们需要创建一个包含两个模块的企业项目:

  • 将在 EJB 容器中运行的 EJB 模块:即 [DAO] 层的 EJB。
  • 将在 Web 容器中运行的 Web 模块:即我们当前正在构建的 Web 服务。

我们将通过两种方式构建此企业项目。

4.10.1. NetBeans 项目 - 版本 1

首先,我们创建一个“Web 应用程序”类型的 NetBeans 项目:

  • 在 [1] 中,在“Java Web”类别 [2] 下创建一个“Web 应用程序”类型 [3] 的新项目。
  • 在 [4] 中,为项目命名;在 [5] 中,指定生成项目的文件夹
  • 在 [6] 中,我们指定将运行该 Web 应用程序的应用服务器
  • 在 [7] 中,设置应用程序上下文
  • 在 [8] 中,验证项目配置。
  • 在 [9] 中,生成的项目。我们正在构建的 Web 服务将使用前一个项目 [10] 中的 EJB。因此,它需要引用 EJB 模块 [10] 的 .jar 文件。
  • 在[11]中,将一个 NetBeans 项目添加到 Web 项目的库中 [12]
  • 在 [13] 中,选择文件系统中的 EJB 模块文件夹并确认。
  • 在 [14] 中,EJB 模块已添加到 Web 项目的库中。

在[15]中,我们使用以下[WsDaoJpa]类实现了Web服务:

package rdvmedecins.ws;
...
@WebService()
public class WsDaoJpa implements IDao {

  @EJB
  private IDaoLocal dao;

   // customer list
  @WebMethod
  public List<Client> getAllClients() {
    return dao.getAllClients();
  }

   // list of doctors
  @WebMethod
  public List<Medecin> getAllMedecins() {
    return dao.getAllMedecins();
  }

   // list of time slots for a given doctor
   // doctor: the doctor
  @WebMethod
  public List<Creneau> getAllCreneaux(Medecin medecin) {
    return dao.getAllCreneaux(medecin);
  }

   // list of appointments for a given doctor on a given day
   // doctor: the doctor
   // day: the day
  @WebMethod
  public List<Rv> getRvMedecinJour(Medecin medecin, String jour) {
    return dao.getRvMedecinJour(medecin, jour);
  }

   // add Rv
   // day : day of appointment
   // creneau: Rv time slot
   // customer: customer for whom the appointment is taken
  @WebMethod
  public Rv ajouterRv(String jour, Creneau creneau, Client client) {
    return dao.ajouterRv(jour, creneau, client);
  }

   // deleting an appointment
   // rv: Rv deleted
  @WebMethod
  public void supprimerRv(Rv rv) {
    dao.supprimerRv(rv);
  }

   // retrieve a specific customer
  @WebMethod
  public Client getClientById(Long id) {
    return dao.getClientById(id);
  }

   // retrieve a specific doctor
  @WebMethod
  public Medecin getMedecinById(Long id) {
    return dao.getMedecinById(id);
  }

   // retrieve a given Rv
  @WebMethod
  public Rv getRvById(Long id) {
    return dao.getRvById(id);
  }

   // retrieve a given slot
  @WebMethod
  public Creneau getCreneauById(Long id) {
    return dao.getCreneauById(id);
  }
}
  • 第 4 行:[WsdaoJpa] 类实现了 [IDao] 接口。请注意,该接口在 [dao] 层的 EJB 归档中定义如下:
package rdvmedecins.dao;
...
public interface IDao {

   // customer list
  public List<Client> getAllClients();
   // list of doctors
  public List<Medecin> getAllMedecins();
   // list of physician slots
  public List<Creneau> getAllCreneaux(Medecin medecin);
   // list of doctor's appointments on a given day
  public List<Rv> getRvMedecinJour(Medecin medecin, String jour);
   // find a customer identified by its id
  public Client getClientById(Long id);
   // find a customer identified by its id
  public Medecin getMedecinById(Long id);
   // find an Rv identified by its id
  public Rv getRvById(Long id);
   // find a time slot identified by its id
  public Creneau getCreneauById(Long id);
   // add a RV to the list
  public Rv ajouterRv(String jour, Creneau creneau, Client client);
   // delete a RV
  public void supprimerRv(Rv rv);
}
  • 第 3 行:@WebService 注解将 [WsDaoJpa] 类定义为 Web 服务。
  • 第 6–7 行:[DAO] 层中对 EJB 的引用将由应用服务器注入到第 7 行的字段中。请注意,注入的始终是本地实现(此处为 IDaoLocal)。之所以能够进行这种注入,是因为 Web 服务与 EJB 在同一 JVM 中运行。
  • Web 服务的所有方法都标有 @WebMethod 注解,以便远程客户端可见。未标有 @WebMethod 注解的方法将仅在 Web 服务内部可见,远程客户端无法访问。Web 服务的每个方法 M 仅调用第 7 行注入的 EJB 的对应方法 M。

该 Web 服务的创建在 NetBeans 项目中体现为一个新分支:

我们在 [1] 中看到了 WsDaoJpa Web 服务,在 [2] 中则看到了它向远程客户端公开的方法。

让我们回顾一下正在构建的Web服务的架构:

我们将要部署的 Web 服务的组件包括:

  • [1]:我们刚刚构建的 Web 模块
  • [2]:我们在前一步构建的 EJB 模块,Web 服务依赖于该模块

要将它们一起部署,我们需要将这两个模块合并到一个 NetBeans “企业” 项目中:

在 [1] 中,创建一个新的企业项目 [2, 3]。

  • 在 [4,5] 中,我们为项目命名并指定其创建目录
  • 在[6]中,选择将部署企业应用程序的应用服务器
  • 在 [7] 中,一个企业项目可以包含三个组件:Web 应用程序、EJB 模块和客户端应用程序。此处创建的项目不包含任何组件,这些组件将在后续添加。
  • 在 [8] 中,显示了新创建的企业应用程序。
  • 在 [9] 中,右键单击 [Java EE 模块] 并添加一个新模块
  • 在 [10] 中,仅显示当前在 IDE 中打开的 NetBeans 模块。在此,我们选择已构建的 Web 模块 [web-service-server-1-ejb-dao-jpa-hibernate] 和 EJB 模块 [ejb-server-dao-jpa-hibernate]。
  • 在 [11] 中,这两个模块已添加到企业项目中。

现在我们需要将此企业应用程序部署到 GlassFish 服务器上。接下来,必须启动 MySQL 数据库管理系统,以便 EJB 模块使用的 JDBC 数据源“jdbc/dbrdvmedecins”能够被访问。

  • 在 [1] 中,我们启动 GlassFish 服务器
  • 如果已部署 EJB 模块 [ejb-server-dao-jpa-hibernate],则将其卸载 [2]
  • 在 [3] 中,部署企业应用程序
  • 在 [4] 中,它已部署完成。我们可以看到它包含两个模块:Web 和 EJB。

4.10.2. NetBeans 项目 - 版本 2

接下来我们将演示,当您没有 EJB 模块的源代码,而只有其 .jar 归档文件时,如何部署该 Web 服务。

Web 服务的新 NetBeans 项目将如下所示:

该项目的显著特点如下:

  • [1]:Web 服务由一个类型为 [Web Application] 的 NetBeans 项目实现。
  • [2]:Web 服务由 [WsDaoJpa] 类实现,我们之前已经学习过该类
  • [3]:用于 [DAO] 层的 EJB 归档,它允许 [WsDaoJpa] 类访问 [DAO] 和 [JPA] 层中各类、接口及实体的定义。

随后,我们构建部署 Web 服务所需的企业项目:

  • [1] 我们创建一个企业应用程序 [ea-rdvmedecins],初始时不包含任何模块。
  • 在 [2] 中,我们将前面的 Web 模块 [webservice-ejb-dao-jpa-hibernate]
  • 在 [3] 中,即最终结果。

目前,企业应用程序 [ea-rdvmedecins] 无法从 NetBeans 部署到 GlassFish 服务器。会出现错误。因此,您必须手动部署 [ea-rdvmedecins] 应用程序的 EAR 归档文件:

  • [ea-rdvmedecins.ear] 归档文件位于 NetBeans 中 [Files] 选项卡的 [dist] 文件夹 [2] 内。
  • 在此归档文件 [3] 中,您将找到该企业应用程序的两个组件:
  • EJB 归档文件 [ejb-server-dao-jpa-hibernate]。该归档文件的存在是因为它是 Web 服务所引用的库的一部分。
  • Web 服务归档文件 [webservice-server-ejb-dao-jpa-hibernate]。
  • 该归档文件 [ea-rdvmedecins.ear] 是通过企业应用程序的简单构建 [4] 生成的。
  • 在 [5] 中,部署操作失败。

要部署企业应用程序归档文件 [ea-rdvmedecins.ear],我们按照第 4.2 节中部署 EJB 归档文件 [ejb-server-dao-jpa-hibernate.jar] 时的步骤进行。我们再次使用 GlassFish 服务器的 Web 管理客户端。此处不再赘述已描述过的步骤。

首先,我们将从“卸载”第 4.10.1 节中部署的企业应用程序开始:

  • [1]:选择 GlassFish 服务器的 [企业应用程序] 分支
  • 在 [2] 中选择要卸载的企业应用程序,然后在 [3] 中将其卸载
  • 在 [4] 中,企业应用程序已卸载
  • 在 [1] 中,选择 GlassFish 服务器的 [企业应用程序] 分支
  • 在 [2] 中,部署一个新的企业应用程序
  • 在 [3] 中,选择 [企业应用程序] 类型
  • 在 [4] 中,指定 NetBeans 项目 [ea-rdvmedecins] 的 .ear 文件
  • 在 [5] 中,部署此归档文件
  • 在 [6] 中,应用程序已部署
  • 在 [7] 中,Web 服务 [WsDaoJpa] 会出现在 GlassFish 服务器的 [Web Services] 分支中。请选择它。
  • 在 [8] 中,您可以查看 Web 服务的各项详细信息。对客户端而言,最相关的是 [9]:Web 服务的 URI。
  • 在 [10] 中,您可以测试该 Web 服务
  • 在 [11] 中,Web 服务 URI 已添加了参数 ?test。该 URI 将显示一个测试页面。Web 服务公开的所有方法 (@WebMethod) 均在此显示并可供测试。在此,我们将测试方法 [13],该方法用于检索客户端列表。
  • 在[14]中,我们仅展示了响应页面的部分视图。但可以看出,getAllClients方法确实返回了客户端列表。截图显示,该方法以XML格式发送响应。

Web 服务通过一个名为 WSDL 文件的 XML 文件进行完整描述:

  • 在 [1] 中,于 GlassFish 服务器管理 Web 工具中,选择 [WsDaoJpa] Web 服务
  • 在 [2] 处,点击 [查看 WSDL] 链接
  • 在 [3] 中:WSDL 文件的 URI。这是必须了解的重要信息。配置此 Web 服务的客户端时需要用到它。
  • 在[4]中,是该 Web 服务的 XML 描述。对于这一复杂内容,我们不再赘述。

4.10.3. Web 服务的 JUnit 测试

我们创建一个 NetBeans 项目,用于“运行”之前使用 EJB 客户端执行的测试,这次将使用针对新部署的 Web 服务的客户端。此处的操作流程与 [ref1] 第 115 页第 14.2.1 节中描述的流程类似。

  • 在[1]中,一个经典的Java项目
  • 在[2]中,测试类
  • 在[3]中,客户端使用EJB归档文件来访问[DAO]层接口和JPA实体的定义。请注意,该归档文件位于EJB模块文件夹的[dist]子文件夹中。

要访问远程 Web 服务,必须生成代理类:

在上图中,第 [2] 层 [C=客户端] 与第 [1] 层 [S=服务器] 进行通信。为了与第 [S] 层交互,客户端 [C] 必须与第 [S] 层建立网络连接,并使用特定协议与其通信。 这些网络连接是 TCP 连接,传输协议为 HTTP。代表 Web 服务的 [S] 层由运行在 GlassFish 服务器上的 Java Servlet 实现。该 Servlet 并非由我们编写,而是由 GlassFish 根据我们编写的 [WsDaoJpa] 类中的 @WebService@WebMethod 注解自动生成的。 同样,我们将自动生成客户端层 [C]。[C] 层有时被称为远程 Web 服务的代理层,其中“代理”一词指代软件链中的中间环节。在此,C 代理充当我们即将编写的客户端与已部署的 Web 服务之间的中间环节。

在 NetBeans 6.5 中,C 代理可通过以下方式生成(后续步骤中,Web 服务必须已在 GlassFish 服务器上运行):

  • 在 [1] 中,向 Java 项目添加一个新元素
  • 在 [2] 中,选择 [Web Services] 分支
  • 在 [3] 中,选择 [Web Service Client]
  • 在 [4] 中,提供 Web 服务 WSDL 文件的 URI。该 URI 已在第 4.10.2 节中给出。
  • 在 [5] 中,保留默认值 [JAX-WS]。另一个可选值为 [JAX-RPC]
  • 确认 Web 服务代理创建向导后,NetBeans 项目被展开,包含了一个 [Web 服务引用] 分支 [6]。该分支显示了远程 Web 服务公开的方法。
  • 在 [文件] 选项卡 [7] 中,已添加了 Java 源代码 [8]。它对应于生成的 C 代理。
  • [9] 处是其中一个类的代码。我们可以看到 [10] 这些类已被放置在 [rdvmedecins.ws] 包中。对于这些类的代码,我们将不予评论,因为它们同样相当复杂。

对于我们正在构建的 Java 客户端,生成的 C 代理充当中介。要访问远程 Web 服务的 M 方法,Java 客户端会调用 C 代理的 M 方法。因此,Java 客户端调用的是本地方法(在同一 JVM 中执行),而对其而言,这些本地调用会被透明地转换为远程调用。

我们仍需了解如何调用 C 代理的 M 方法。让我们回到我们的 JUnit 测试类:

在[1]中,测试类[MainTestsDaoRemote]就是之前在测试[dao]层的EJB时所使用的那个:

package dao;
...
public class MainTestsDaoRemote {

   // layer [dao] tested
  private static IDaoRemote dao;

  @BeforeClass
  public static void init() throws NamingException {
  }

  @Test
  public void test1() {
...
  }
}
  • 第 [13] 行,test1 测试保持不变。
  • 第 [9] 行,[init] 方法的内容已被删除。

此时,项目中会出现错误,因为 [test1] 测试方法使用了 [Client]、[Medecin]、[Creneau] 和 [Rv] 实体,而这些实体已不再位于之前的包中。它们现在位于生成的 C 代理包中。我们删除相关的导入语句,并使用“修复导入”操作重新生成它们。

让我们回到测试类 [MainTestsDaoRemote] 的代码:

package dao;
...

public class MainTestsDaoRemote {

   // layer [dao] tested
  private static IDaoRemote dao;

  @BeforeClass
  public static void init() throws NamingException {
}

第 10 行中的 [init] 方法必须初始化第 7 行中对 [dao] 层的引用。我们需要了解如何在代码中使用生成的 C 代理。NetBeans 会协助我们完成这一过程。

  • 用鼠标选中 [1] 中 Web 服务的 [getAllClients] 方法,然后将该方法拖拽并放入测试类的 [init] 方法中。

我们得到结果 [2]。这个代码骨架向我们展示了如何使用生成的 C 代理:

1
2
3
4
5
6
7
8
9
    try { // Call Web Service Operation
      rdvmedecins.ws.WsDaoJpaService service = new rdvmedecins.ws.WsDaoJpaService();
      rdvmedecins.ws.WsDaoJpa port = service.getWsDaoJpaPort();
       // TODO process result here
      java.util.List<rdvmedecins.ws.Client> result = port.getAllClients();
      System.out.println("Result = "+result);
    } catch (Exception ex) {
       // TODO handle custom exceptions here
}
  • 第 [5] 行表明 [getAllClients] 方法是第 3 行定义的 [WsDaoJpa] 对象的一个方法。[WsDaoJpa] 类型是一个接口,它暴露了与远程 Web 服务所暴露的方法相同的方法。
  • 在第 [3] 行中,[WsDaoJpaPort] 对象是从第 2 行定义的另一个 [WsDaoJpaService] 类型的对象中获取的。[WsDaoJpaService] 类型代表本地生成的 C 代理。
  • 访问远程 Web 服务可能会失败,因此整个代码块被包含在 try/catch 块中。
  • C 代理对象位于 [rdvmedecins.ws] 包中

理解了这段代码后,我们可以看到,可以通过以下代码获取对远程 Web 服务的本地引用:

WsDaoJpa dao=new WsDaoJpaService().getWsDaoJpaPort();

JUnit 测试类的代码随后变为如下所示:

package dao;

import rdvmedecins.ws.Client;
import rdvmedecins.ws.Creneau;
import rdvmedecins.ws.Medecin;
import rdvmedecins.ws.Rv;
import rdvmedecins.ws.WsDaoJpa;
import rdvmedecins.ws.WsDaoJpaService;
...

public class MainTestsDaoRemote {

   // layer [dao] tested
  private static WsDaoJpa dao;

  @BeforeClass
  public static void init(){
    dao=new WsDaoJpaService().getWsDaoJpaPort();
  }

  @Test
  public void test1() {
...
  }

   // utility method - displays items in a collection
  private static void display(String message, List elements) {
 ...
  }
}

现在我们可以进行测试了:

在 [1] 处,JUnit 测试被执行。在 [2] 处,测试通过。如果查看 NetBeans 控制台的输出,我们会看到类似以下的行:

Liste des clients :
rdvmedecins.ws.Client@1982fc1
rdvmedecins.ws.Client@676437
rdvmedecins.ws.Client@1e4853f
rdvmedecins.ws.Client@1e808ca

在服务器端,[Client] 实体有一个 toString 方法,用于显示 [Client] 对象的各个字段。 在自动生成 C 代理的过程中,实体会在 C 代理中创建,但仅包含私有字段及其对应的 get/set 方法。因此,C 代理中的 [Client] 实体未生成 toString 方法。这解释了之前的显示情况。这并不影响 JUnit 测试:测试已通过。现在,我们将认为我们拥有了一个可运行的 Web 服务。