4. J2EE 预约 Web 服务
让我们回到待构建应用程序的架构:
本节我们将重点探讨在 Sun / Glassfish 服务器上运行的 J2EE [1] Web 服务的构建。
4.1. 数据库
我们将该数据库命名为 [dbrdvmedecins] ,这是一个包含四个表的 MySQL5 数据库:

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. 服务器端架构的组成部分
让我们回到待构建的应用程序架构:
在服务器端,该应用程序将由以下部分组成:
- 一个JPA层,通过对象与BD进行交互
- 一个 EJB,负责管理与 JPA 层的交互
- 一个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] 选项,以便向项目添加元素。

- 在 [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] 文件
其内容如下:
| <?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]:
| package rdvmedecins.dao;
import javax.ejb.Local;
@Local
public interface IDaoLocal extends IDao{
}
|
远程接口 [IDaoRemote] 也是如此:
| package rdvmedecins.dao;
import javax.ejb.Remote;
@Remote
public interface IDaoRemote extends IDao {
}
|
EJB [DaoJpa] 实现了这两个接口,即本地接口和远程接口:
| package rdvmedecins.dao;
...
@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] 中,完成创建向导
现在 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 模块、客户端应用程序。此处创建的项目不包含任何组件。这些组件将在后续添加。
- 在 [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 语言代理:
| 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 服务已正常运行。