Skip to content

4. J2EE 预约 Web 服务

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

本节我们将重点探讨在 Sun / Glassfish 服务器上运行的 J2EE [1] Web 服务的构建。

4.1. 数据库

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

Image

4.1.1. [MEDECINS] 表

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

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

4.1.2. 表 [CLIENTS]

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

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

4.1.3. 表 [CRENEAUX]

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

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

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

4.1.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女士。

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层,通过对象与BD进行交互
  1. 一个 EJB,负责管理与 JPA 层的交互
  2. 一个Web服务,负责以Web服务的形式向远程客户端暴露EJB接口。

组件 (b) 和 (c) 实现了前图所示的 [dao] 层。众所周知,应用程序可通过 RMI 和 JNDI 协议访问远程 EJB。 实际上,这将客户端限制为Java客户端。Web服务采用标准化通信协议,多种语言均可实现该协议:.NET、PHP、C++等。本文将通过使用.NET客户端来演示这一点。

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

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

  • 通过在Web容器中运行的、带有@WebService注解的类
  • 通过一个带有 @WebService 注解的 EJB,该 EJB 在 EJB 容器中运行

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

在课程[ref1]的第14段(第109页)中,有一个使用第二种方案的示例。

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 库位于教程附带的压缩包中。

4.5. NetBeans的自动生成工具

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

借助 NetBeans,可以自动生成 [JPA] 层以及 [Ejb] 层,后者负责控制对生成的 JPA 实体的访问。 了解这些自动生成方法很有意义,因为生成的代码为编写 JPA 实体或使用这些实体的 EJB 代码提供了宝贵的参考。

接下来我们将介绍其中一些自动生成工具。要理解生成的代码,必须对 JPA、[ref1] 以及 EJB、[ref2] 实体有充分的了解。

创建 NetBeans 与数据库的连接

  • 运行 SGBD 和 MySQL 5,以便 BD 可用
  • 在 [dbrdvmedecins] 数据库上创建 NetBeans 连接
  • 在 [Files] 选项卡中,位于 [Databases] [1] 分支下,选择 Jdbc 驱动程序 MySQL [2]
  • 然后选择 [3] “Connect Using” 选项,用于建立与 MySQL
  • 在 [4] 中,输入所需的信息
  • 然后在 [5] 中确认
  • 在 [6] 中,连接已建立。其中显示了所连接数据库的四个表。

创建一个 EJB 项目

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

将资源 JDBC 添加到 Glassfish 服务器

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

  • 在 [Services] 选项卡中,启动 Glassfish 服务器 [2, 3]
  • 在 [Projects] 选项卡中,右键单击 EJB 项目,并在 [5] 中选择 [New / Other] 选项,以便向项目添加元素。

Image

  • 在 [6] 中, 选择类别 [Glassfish],并在 [7] 中通过选择类型 [JDBC Resource] 来指定要创建资源 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 将使用该文件,要求 GlassFish 服务器创建第 4 行定义的“jdbc/dbrdvmedecins”资源。

创建持久化单元

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

  • 在 [1] 中,右键单击 EJB 项目并选择 [New / Other]在 [2]
  • 在 [3] 中,选择类别 [Persistence],然后在 [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 指定要管理的 SGBD 类型。这将在后续进行。

创建 JPA 实体

 
  • 在 [1] 中,右键单击该项目,并在 [2] 中选择 [New / Other] 选项
  • 在 [3] 中,选择类别 [Persistence],然后在 [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] 中,选择选项 [New / Other]
  • 在 [3] 中,选择类别 [Persistence],然后在 [4] 中选择类型 [Session Beans for Entity Classes]
  • 在 [5] 中,将显示先前创建的 JPA 实体
  • 在 [6] 中,全选
  • 在 [7] 中,它们已被选中
  • 在 [8] 中,继续向导
  • 在 [9] 中,为即将生成的 EJB 包命名
  • 在 [10] 步骤中,指定 EJB 需同时实现本地和远程接口
  • 结束向导
  • 在 [11] 中,生成的 EJB

以下是管理对实体 [Rv] 访问的 EJB 代码示例,即管理数据库 [dbrdvmedecins] 中表 [rv] 的访问:

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.entites] 包含 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>
       <!-- 方言 -->
      <property name="hibernate.dialect" value="org.hibernate.dialect.MySQL5InnoDBDialect"/>
    </properties>
  </persistence-unit>
</persistence>
  • 第 3 行:事务类型为 JTA:事务将由 Glassfish 的 EJB 3 容器管理
  • 第 4 行:使用 JPA/Hibernate 实现。为此,已将 Hibernate 库添加到 Glassfish 服务器中(参见第 4.4 节)。
  • 第 5 行:JPA 层使用的数据源 JTA 的名称为 JNDI “jdbc/dbrdvmedecins”。
  • 第 8 行:此行并非自动生成,需手动添加。它告知 Hibernate,所使用的 SGBD 实为 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.entites] 实现了 [Jpa] 层。

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

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

package rdvmedecins.entites;
...
@MappedSuperclass
public class Personne implements Serializable {
   // 人物特征

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

   // 默认构造函数
  public Personne() {
  }

   // 带参数的构造函数
  public Personne(String titre, String nom, String prenom) {
     // 通过设置器进行初始化
...
  }

   // 复制构造函数
  public Personne(Personne personne) {
     // 通过设置器传递
 ...
  }

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

// 获取器和设置器
....
}
  • 第 3 行:请注意,类 [Personne] 本身并非实体 (@Entity)。它将作为实体的父类。注解 @MappedSuperClass 指明了这种情况。

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

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

   // 默认构造函数
  public Client() {
  }

   // 带参数的构造函数
  public Client(String titre, String nom, String prenom) {
     // 父类
    super(titre, nom, prenom);
  }

   // 复制构造函数
  public Client(Client client) {
     // 父类
    super(client);
  }
}
  • 第 3 行:类 [Client] 是一个 JPA 实体
  • 第 4 行:它与表 [clients] 相关联
  • 第 5 行:它继承自类 [Personne]

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

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

   // 默认构造函数
  public Medecin() {
  }

   // 带参数的构造函数
  public Medecin(String titre, String nom, String prenom) {
     // 父类
    super(titre, nom, prenom);
  }

   // 拷贝构造函数
  public Medecin(Medecin medecin) {
     // 父类
    super(medecin);
  }
}

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

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

   // RV 时间段的特征
  @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;

   // 默认构造函数
  public Creneau() {

  }

   // 带参数的构造函数
  public Creneau(Medecin medecin, Integer hDebut,Integer mDebut, Integer hFin, Integer mFin) {
     // 通过设置器进行操作
...
  }

   // 通过复制初始化
  public Creneau(Creneau creneau) {
     // 通过设置器传递
...
  }

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

   // 设置器 - 获取器
...
}
  • 第 15-17 行建模了数据库中表 [creneaux] 与表 [medecins] 之间存在的“一对多”关系。

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

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

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

   // 默认构造函数
  public Rv() {
  }

   // 带参数的构造函数
  public Rv(Date jour, Client client, Creneau creneau) {
     // 通过设置器进行赋值
...
  }

   // 复制构造函数
  public Rv(Rv rv) {
     // 通过设置器传递
...
  }

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

// 获取器和设置器
...
}
  • 第 15-17 行建模了数据库中表 [rv] 与表 [clients] 之间的“一对多”关系,而第 18-20 行建模了 “一对多”关系

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 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);
  }

   // getter - setter
...
}
  • 第 6 行:该类继承自 [RuntimeException] 类。因此编译器不会强制要求使用 try/catch 进行处理。
  • 第 5 行:注解 @ApplicationException 确保该异常不会被 [EjbException] 类型的异常“吞噬”。

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

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

4.6.4. [dao] 层的 EJB

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

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

   // 客户列表
  public List<Client> getAllClients();
   // 医生列表
  public List<Medecin> getAllMedecins();
   // 医生时段列表
  public List<Creneau> getAllCreneaux(Medecin medecin);
   // 某医生在特定日期的预约列表
  public List<Rv> getRvMedecinJour(Medecin medecin, String 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(String jour, Creneau creneau, Client client);
   // 删除一个 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 的所有方法都在由 EJB 3 容器管理的事务中执行。
  • 第 5 行显示该 EJB 实现了本地远程接口。

EJB的完整代码如下:

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

  @PersistenceContext
  private EntityManager em;

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

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

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

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

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

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

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

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

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

   // 检索指定时段
  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行:查询JPQL,对表[creneaux]和[medecins]进行连接。该查询通过医生ID进行参数化。
  • 第43行:查询JPQL,对表[rv]、[creneaux]和[medecins]进行连接,并包含两个参数:医生ID和预约日期。
  • 第55-57行:创建预约并将其保存至数据库。
  • 第67行:从数据库中删除一个预约。
  • 第76行:对数据库执行SELECT操作以查找指定客户
  • 第85行:对医生执行相同操作
  • 第 94 行:对预约执行相同操作
  • 第103行:对时间段执行相同操作
  • 第9行中涉及持久化上下文(em)的所有操作都可能与数据库发生冲突。因此,这些操作均被try/catch语句包围。可能出现的异常被封装在自定义异常RdvMedecinsException中

EJB模块编译后会生成一个 的.jar文件:

4.7. 使用 NetBeans 部署 [dao] 层的 EJB

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

  • 在 EJB 项目属性中,检查运行选项 [1]。
  • 在 [2] 中,输入将部署 EJB 的服务器名称
  • 在 [Services] 选项卡中,输入 [3],然后启动 [4]。
  • 在 [5] 中,Glassfish 服务器已启动。它目前还没有 EJB 模块。
  • 启动服务器 MySQL,并确保数据库 [dbrdvmedecins] 已上线。为此,您可以使用第 4.5 节中创建的 NetBeans 连接。
  • 在 [Projects] [6] 选项卡中,部署 EJB 模块 [7]: 必须先启动 SGBD MySQL5,才能访问 EJB 所使用的 JDBC 资源“jdbc/dbrdvmedecins”。
  • 在 [8] 中,已部署的 EJB 会出现在 GlassFish 服务器的树形结构中
  • 在 [9] 中,移除已部署的 EJB
  • 在 [10] 中,该 EJB 不再出现在 Glassfish 服务器的树形结构中。

4.8. 使用 Glassfish 部署 [dao] 层的 EJB

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

  • 启动 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>
       <!-- 方言 -->
      <property name="hibernate.dialect" value="org.hibernate.dialect.MySQL5InnoDBDialect"/>
    </properties>
  </persistence-unit>
</persistence>

第 5 行表明,JPA 层使用由 EJB3 容器管理的名为“jdbc/dbrdvmedecins”的数据源 JTA、c.a.d。

我们在第 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 的 [services] [1] 选项卡中,启动 GlassFish 服务器 [2],然后访问 [3] 其管理控制台
  • 在 [4] 中,以管理员身份登录(密码:adminadmin,如果您在安装时或之后未更改该密码)。
  • 在 [5] 中,选择 Glassfish 资源分支 [Connection Pools]
  • 在 [6] 中,创建一个新的连接池。需要提醒的是,连接池是一种用于限制与 SGBD 建立/关闭连接次数的技术。 服务器启动时,将根据配置定义的数量 N 与 SGBD 建立连接。这些已建立的连接随后将提供给请求它们的 EJB,以便与 SGBD 进行操作。 一旦操作完成,EJB 便将连接归还给连接池。该连接从未被关闭,而是由访问 SGBD
  • 在 [7] 中,为连接池命名
  • 为 [8], 数据源的模型类是 [javax.sql.DataSource]
  • 在 [9] 中,持有数据源的 SGBD 在此处为 MySQl。
  • 在 [10] 中,进入下一步
  • 在 [11] 中,“Connection Validation Required” 属性确保连接池在分配连接前会验证其是否可用。若不可用,则创建新连接。这使得应用程序在与 SGBD 发生短暂中断后仍能继续运行。 中断期间,所有连接均不可用,并将向客户端抛出异常。 当中断结束时,继续请求连接的客户端将重新获得连接:得益于“Connection Validation Required”属性,连接池中的所有连接都将被重新创建。如果没有此属性,连接池会发现初始连接已被中断,但不会尝试创建新的连接。
  • 在 [12] 中,要求事务采用“Read Committed”隔离级别。该级别确保事务 T2 无法读取事务 T1 修改的数据,除非后者已完全结束。
  • 在 [13] 中,要求所有事务都使用 [12] 中指定的隔离级别
  • 在 [14] 和 [15] 中,请指定 BD 的 URL,该事务的连接由其连接池管理
  • 在 [16] 中,用户将变为 root
  • 在 [17] 中,添加一个属性
  • 在 [18] 中,添加名为“Password”的属性,其值为 ()在 [19] 中。 尽管截图 [19] 未显示,但请注意:表示空密码时,不应填写空字符串,而应填写 ()(左括号、右括号)。 如果您的 SGBD MySQL 中的用户 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] 中,选择分支 [Enterprise Applications]
  • 到 [2],使用按钮 [Deploy],表明您要部署一个新应用程序
  • 在 [3] 中,指定该应用程序为 EJB 模块
  • 在 [4] 中,选择为 TP 提供的 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] [3] 层的 EJB JAR 文件(参见第 4.6.4 节)。
  • EJB远程客户端所需的Glassfish库[4]。

测试类如下:

package dao;
...
public class MainTestsDaoRemote {

   // 已测试的 [dao] 层
  private static IDaoRemote dao;

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

  @Test
  public void test1() {
     // 测试数据
    String jour = "2006:08:23";
     // 显示客户
    List<Client> clients = null;
    try {
      clients = dao.getAllClients();
      display("Liste des clients :", clients);
    } catch (Exception ex) {
      System.out.println(ex);
    }
     // 医生显示
    List<Medecin> medecins = null;
    try {
      medecins = dao.getAllMedecins();
      display("Liste des médecins :", medecins);
    } catch (Exception ex) {
      System.out.println(ex);
    }
     // 医生预约时段显示
    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);
    }
     // 某医生在特定日期的预约列表
    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);
    }
     // 添加 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));
    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);
    }
     // 在同一天的同一时段添加一个 RV
     // 应引发异常
    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);
    }
     // 删除 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);
    }
  }

   // 实用方法 - 显示集合中的元素
  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]第111页的第14节。

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

上文我们关注的是 [dao] 层的 Web 服务。该服务的唯一作用是向能够与 Web 服务交互的多平台客户端提供 [dao] 层 EJB 的接口。

需要提醒的是,实现 Web 服务有两种方式:

  • 通过在Web容器中运行的、带有@WebService注解的类
  • 通过带有 @WebService 注解的 EJB,在 EJB 容器中运行

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

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

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

4.10.1. NetBeans 项目 - 版本 1

首先,我们创建一个“Web Application”类型的 NetBeans 项目:

  • 在 [1] 中,我们于“Java Web”类别 [2] 下创建一个“Web Application”类型的新项目 [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;

   // 客户列表
  @WebMethod
  public List<Client> getAllClients() {
    return dao.getAllClients();
  }

   // 医生列表
  @WebMethod
  public List<Medecin> getAllMedecins() {
    return dao.getAllMedecins();
  }

   // 特定医生的预约时段列表
   // 医生:该医生
  @WebMethod
  public List<Creneau> getAllCreneaux(Medecin medecin) {
    return dao.getAllCreneaux(medecin);
  }

   // 特定医生在特定日期的预约列表
   // 医生:该医生
   // 日期:该日期
  @WebMethod
  public List<Rv> getRvMedecinJour(Medecin medecin, String jour) {
    return dao.getRvMedecinJour(medecin, jour);
  }

   // 添加预约
   // 日期:预约日期
   // 时段:预约的时间段
   // 客户:预约对象
  @WebMethod
  public Rv ajouterRv(String jour, Creneau creneau, Client client) {
    return dao.ajouterRv(jour, creneau, client);
  }

   // 删除预约
   // rv:被删除的预约
  @WebMethod
  public void supprimerRv(Rv rv) {
    dao.supprimerRv(rv);
  }

   // 查询指定客户
  @WebMethod
  public Client getClientById(Long id) {
    return dao.getClientById(id);
  }

   // 检索指定医生
  @WebMethod
  public Medecin getMedecinById(Long id) {
    return dao.getMedecinById(id);
  }

   // 查询指定预约
  @WebMethod
  public Rv getRvById(Long id) {
    return dao.getRvById(id);
  }

   // 检索指定时段
  @WebMethod
  public Creneau getCreneauById(Long id) {
    return dao.getCreneauById(id);
  }
}
  • 第 4 行,类 [WsdaoJpa] 实现了接口 [IDao]。需要提醒的是,该接口在 [dao] 层的 EJB 归档中以如下形式定义:
package rdvmedecins.dao;
...
public interface IDao {

   // 客户列表
  public List<Client> getAllClients();
   // 医生列表
  public List<Medecin> getAllMedecins();
   // 某位医生的时段列表
  public List<Creneau> getAllCreneaux(Medecin medecin);
   // 某医生在指定日期的预约列表
  public List<Rv> getRvMedecinJour(Medecin medecin, String 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(String jour, Creneau creneau, Client client);
   // 删除一个 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] 中可以看到 Web 服务 WsDaoJpa,而在 [2] 中则可以看到它向远程客户端公开的方法。

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

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

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

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

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

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

接下来,我们需要将该企业应用程序部署到 Glassfish 服务器上。 接下来,需启动 SGBD 和 MySQL,以便访问 EJB 模块所使用的数据源 JDBC "jdbc/dbrdvmedecins"。

  • 在 [1] 中,启动 Glassfish 服务器
  • 如果已部署 EJB 模块 [serveur-ejb-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 模块 [serveur-webservice-ejb-dao-jpa-hibernate]
  • 在 [3] 中,即最终结果。

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

  • [ea-rdvmedecins.ear] 归档文件位于 NetBeans 的 [Files] 选项卡下的 [dist] [2] 文件夹中。
  • 在该 [3] 归档文件中,包含企业应用程序的两个组件:
  • EJB 归档文件 [serveur-ejb-dao-jpa-hibernate]。该归档文件存在是因为它是 Web 服务引用的库之一。
  • Web 服务归档文件 [serveur-webservice- ejb-dao-jpa-hibernate]。
  • [ea-rdvmedecins.ear] 归档文件由企业应用程序中的 Build 和 [4] 简单组合而成。
  • 在 [5] 中,部署操作失败。

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

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

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

一个 Web 服务完全由一个名为 XML 的文件描述,该文件名为 WSDL:

  • 在 Glassfish 服务器管理工具中,选择 Web 服务 [1]
  • 为 [2],点击链接 [View 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=Client] 层通过 [1] 层和 [S=Serveur] 层进行通信。 为了与 [S] 层进行通信,客户端 [C] 需要与 [S] 层建立网络连接,并按照特定协议与其进行通信。 网络连接为 TCP,传输协议为 HTTP。代表 Web 服务的 [S] 层由 Glassfish 服务器执行的 Java Servlet 实现。 该 Servlet 并非由我们编写。Glassfish 会根据我们编写的 [WsDaoJpa] 类中的 @Webservice@WebMethod 注解自动生成该 Servlet。同样,我们也将自动生成客户端的 [C] 层。 [C]层有时被称为远程Web服务的代理层,而proxy一词则指软件链中的中间组件。在此,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 Service References] [6]的分支。该分支展示了远程Web服务所暴露的方法。
  • 在 [Files] [7] 选项卡中,已添加了 Java 源代码 [8]。这些代码对应生成的 C 语言代理。
  • 在 [9] 中可见其中一个类的代码。从中可以看出,这些类已被放置在一个包中。我们不再对这些类的代码进行详细说明,因为它们再次相当复杂。

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

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

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

package dao;
...
public class MainTestsDaoRemote {

   // 已测试的 [dao] 层
  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 代理包中。删除相关的 import 语句,并通过“修复导入”操作重新生成它们。

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

package dao;
...

public class MainTestsDaoRemote {

   // 已测试 [dao] 层
  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 { // 调用 Web 服务操作
      rdvmedecins.ws.WsDaoJpaService service = new rdvmedecins.ws.WsDaoJpaService();
      rdvmedecins.ws.WsDaoJpa port = service.getWsDaoJpaPort();
       // 在此处理 TODO 的结果
      java.util.List<rdvmedecins.ws.Client> result = port.getAllClients();
      System.out.println("Result = "+result);
    } catch (Exception ex) {
       // TODO 在此处理自定义异常
}
  • 第 [5] 行表明,方法 [getAllClients] 是第 3 行定义的 [WsDaoJpa] 类型对象的方法。 类型 [WsDaoJpa] 是一个接口,其方法与远程 Web 服务所暴露的方法相同。
  • 在第 [3] 行,[WsDaoJpa port] 对象是从第 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 {

   // 已测试的 [dao] 层
  private static WsDaoJpa dao;

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

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

   // 辅助方法 - 显示集合中的元素
  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 方法。因此,方法 toString 并未在 C 代理的实体 [Client] 中生成。这解释了之前的显示结果。 但这并不影响 JUnit 测试的结果:该测试已通过。现在我们可以认为该 Web 服务已正常运行。