8. 第 4 版 – Web 服务架构中的客户端/服务器模式
在此新版本中,[Pam] 应用程序将在 Web 服务架构中以客户端/服务器模式运行。让我们回顾一下前一个应用程序的架构:
![]() |
在上图中,通信层 [C, RMI, S] 实现了客户端 [ui] 与远程层 [metier] 之间的透明通信。 我们将采用类似的架构,其中通信层 [C, RMI, S] 将被 [C, HTTP / SOAP, S] 层所取代:
![]() |
HTTP / SOAP 协议相较于之前的 RMI / EJB 协议,具有跨平台的优势。 因此,Web 服务可以使用 Java 编写并部署在 Glassfish 服务器上,而客户端则可以是 .NET 或 PHP 客户端。
我们将根据三种不同的模式来开发该架构:
- Web 服务由 EJB [Metier] 提供
- Web 服务将由使用 EJB 和 [Metier] 的 Web 应用程序提供
- Web 服务将由使用 Spring 的 Web 应用程序提供
在 Java 服务器 EE 中,Web 服务可以通过多种方式实现:
- 通过一个带有 @WebService 注解的类,该类在 Web 容器中运行
![]() |
- 通过一个带有 @WebService 注解的 EJB,该注解在容器中运行
![]() |
我们先从后一种架构开始。
8.1. 由 EJB 实现的 Web 服务
8.1.1. 服务器端
8.1.1.1. NetBeans 项目
首先创建一个新的 Maven 项目,复制自项目 EJB [mv-pam-ejb-metier-dao-jpa-eclipselink]:
![]() |
采用以下架构:
![]() |
[metier] 层将作为 [ui] 层调用的 Web 服务。该类无需实现任何接口。正是注解将 POJO(普通 Java 对象)转换为 Web 服务。 实现上述 [metier] 层的 [Metier] 类,其转换方式如下:
package metier;
...
@WebService
@Stateless()
@TransactionAttribute(TransactionAttributeType.REQUIRED)
public class Metier implements IMetierLocal,IMetierRemote {
// 关于层的参考 [DAO]
@EJB
private ICotisationDaoLocal cotisationDao = null;
@EJB
private IEmployeDaoLocal employeDao=null;
@EJB
private IIndemniteDaoLocal indemniteDao=null;
// 获取工资单
@WebMethod
public FeuilleSalaire calculerFeuilleSalaire(String SS,
...
}
// 员工列表
@WebMethod
public List<Employe> findAllEmployes() {
...
}
// 重要 - EJB 没有 getter 和 setter
}
- 第 4 行,注解 @WebService 将类 [Metier] 转换为 Web 服务。Web 服务向其客户端公开方法。这些方法必须使用 @WebMethod 属性进行注解。
- 第 19 行和第 25 行:类 [Metier] 中的两个方法成为 Web 服务的方法。
- 第 29 行:必须删除 getter 和 setter 方法,否则它们将在 Web 服务中暴露,从而导致安全错误。
NetBeans 会检测到这些注解的添加,并据此调整项目属性:
![]() |
在 [1] 中,项目中出现了一个 [Web Services] 目录树。其中包含业务 Web 服务及其两个方法。服务器应用程序可部署在 [2] 中。 需启动服务器 MySQL,且其数据库 [dbpam_eclipselink] 必须存在并已填充数据。 为避免名称冲突,可能需要先从先前研究的客户端/服务器项目 EJB 中删除 [3] 和 EJB。 事实上,我们的新项目带来了与前一个项目相同的 EJB。
![]() |
在 [1] 中,我们可以看到我们的应用程序 serveur 已部署在 Glassfish 服务器上。Web 服务部署完成后,即可进行测试:
![]() |
- 在 [1] 中,我们当前项目中正在测试 Web 服务 [Metier]
- 可通过不同的 URL 访问该 Web 服务。URL 和 [2] 用于测试 Web 服务
- 在[3]中,该文件包含指向定义Web服务的XML文件的链接。 Web 服务的客户端需要了解该文件的 URL。Web 服务的客户端层(存根)正是基于此生成的。
- 在 [4,5] 中,提供了一个表单,用于测试 Web 服务公开的方法。这些方法会显示其参数,用户可以自行定义这些参数。
例如,我们来测试方法 [findAllEmployes],该方法无需任何参数:
![]() |
上文我们测试了该方法。随后收到了如下响应(部分视图)。其中确实显示了两名员工及其津贴信息。建议读者以同样的方式测试方法 [4],并向其传递该方法所需的三个参数。

8.1.2. 客户端部分
![]() |
8.1.2.1. 客户端的 NetBeans 项目 控制台
现在,我们将为应用程序的 client 部分创建一个 [Java Application] 类型的 Java 项目。截至 2012 年 6 月,尚无法为此客户端创建 Maven 项目。会出现一个错误,该错误在网络上似乎已有报道,但至今仍未解决。
![]() | ![]() |
项目创建完成后,我们将它设为刚刚部署在 Glassfish 服务器上的 Web 服务的客户端:
![]() |
- 在 [2] 中,我们选择新项目并点击 [New File] 按钮
- 在 [3] 中,我们指定要创建一个 Web 服务客户端
![]() |
- 通过 [4],我们将指定 Web 服务的 NetBeans 项目
- 在 [5] 窗口中,列出了所有具有分支的项目 [Web Services],此处仅显示项目 [mv-pam-ws-metier-dao-eclipselink]。
- 一个项目可以部署多个Web服务。在[6]中,我们指定要连接的Web服务。
![]() |
- 在 [7] 中,显示的是 Web 服务定义的 URL。该 URL 由生成客户端层的软件工具使用,该客户端层将与 Web 服务进行交互。
![]() |
- 即将生成的客户端层 [C] [1] 由一组 Java 类组成,这些类将被放置在同一个包中。该包的名称固定为 [8]。
- 点击按钮 [Finish] 完成 Web 服务客户端创建向导后,上述 [C] 层即被创建。
这体现在项目中的若干变化上:
- 在上述的 [10] 中,出现了一个名为 [Generated Sources] 的树形结构,其中包含 [C] 层的类,这些类使客户端 [3] 能够与 Web 服务进行通信。 该层使客户端 [3] 能够与 [metier] 和 [4] 层进行通信,就好像它们是本地而非远程的。
- 在 [11] 中,出现了一个 [Web Service References] 树结构,其中列出了已生成客户端层的 Web 服务。
需要注意的是,在生成的 [C] [10] 层中,我们可以看到已部署在服务器端的类:Indemnite、Cotisation、Employe、FeuilleSalaire、ElementsSalaire、 Metier。其中 Metier 是 Web 服务,其余类则是该服务所需的辅助类。 我们可能会出于好奇查看它们的代码。我们会发现,这些类在实例化后代表服务所操作的对象,其定义包含类字段及其访问器的定义,以及添加了允许将类序列化为流的注解 XML。 Metier类已转变为一个接口,其中包含两个被标注为@WebMethod的方法。 每个方法都会生成两个类,例如 [CalculerFeuilleSalaire.java] 和 [CalculerFeuilleSalaireResponse.java],其中一个封装方法调用,另一个封装其结果。 最后,MetierService 类是允许客户端获取远程业务 Web 服务引用(reference)的类:
第 2 行中的 getMetierPort 方法用于获取远程业务 Web 服务的引用。
8.1.2.2. 业务 Web 服务的控制台客户端
接下来只需编写业务 Web 服务的客户端。我们将项目 [mv-pam-client-metier-dao-jpa-eclipselink] 中的类 [MainRemote](该类曾是服务器 EJB 的客户端)复制到新项目中。
![]() |
- 更名为 [1],即 Web 服务客户端类。类 [MainRemote] 存在错误。 要修正这些错误,首先需删除类中所有现有的 [import] 指令,然后使用 [Fix Imports] 选项重新生成它们。 实际上,[MainRemote]类所使用的某些类现已归入生成的[client]包中。
- 在 [3] 中,[metier] 层被实例化的代码片段是 [3]。 该实例化操作通过 JNDI 代码实现,以获取对远程 EJB 的引用。
我们按以下方式修改代码:
- 删除代码 JNDI
- 由于客户端不存在类 [PamException],因此我们删除关联的 catch,仅保留父类 [Exception] 上的 catch。
![]() |
- 在 [4] 中,我们需要获取远程 Web 服务 [Metier] 的引用,以便调用其方法 [calculerFeuilleSalaire]。
- 在 [5] 中,我们使用鼠标将 Web 服务 [Metier] 中的方法 [calculerFeuilleSalaire] 拖拽(drag)到 [4] 中。 系统将生成代码 [6]。该通用代码随后可由开发人员进行调整。
![]() |
- 第 112 行可见,[calculerFeuilleSalaire] 是类 [client.Metier](第 111 行)的一个方法。既然我们已知如何获取 [metier] 层,则上述代码可重写为:
第 7 行获取了 Metier Web 服务的引用。完成此操作后,类中的代码保持不变,只是在第 10 行,处理的不再是 [Exception] 类型的异常,而是更通用的 Throwable 类型, 即类 Exception 的父类。如果发生异常,我们将显示该异常的所有嵌套原因,直至追溯到原始原因。
我们已准备好进行测试:
- 确保 SGBD 和 MySQL5 已启动,且数据库 dbpam_eclipselink 已创建并初始化
- 确保 Web 服务已部署到 Glassfish 服务器
- 构建客户端(清理并构建)
- 配置客户端运行
![]() |
- 运行客户端
控制台显示的结果如下:
配置如下:

则得到以下结果:
需要注意的是,虽然 Web 服务 [Metier] 发送的是 [PamException] 类型的异常,但客户端接收到的异常却是 [SOAPFaultException] 类型。 即使在异常链中,也未出现类型为 [PamException] 的异常。
8.1.3. 业务 Web 服务的 Swing 客户端
待办事项:将项目 [mv-pam-client-ejb-metier-dao-jpa-eclipselink] 的 Swing 客户端移植到新项目中,使其同样成为部署在 Glassfish 服务器上的 Web 服务的客户端。
8.2. 由Web应用程序实现的Web服务
现在,我们处于以下架构框架中:
![]() |
该Web服务由在Glassfish服务器Web容器中运行的Web应用程序提供。该Web服务将基于部署在EJB3容器中的EJB [Metier]。
8.2.1. 服务器端
我们创建一个Web应用程序:
![]() |
- 在 [1] 中,我们创建了一个新项目
- 在 [2] 中,该项目类型为 [Web Application]
- 在 [3] 中,我们将该项目命名为 [mv-pam-ws-ejb-metier-dao-eclipselink]
![]() |
- 在 [4] 中,我们选择 Java 版本 EE 6
- 在 [6] 中,创建的项目
在下图所示的架构中,创建的 Web 应用程序将在 Web 容器中运行。它将使用 EJB [Metier],而该组件将部署在服务器的 EJB 容器中。
![]() |
为了使创建的 Web 应用程序能够访问与 EJB [Metier] 相关的类,我们将 [mv-pam-ws-ejb-metier-dao-eclipselink] 添加到 Web 应用程序的库中, 已探讨过的服务器依赖项 EJB [mv-pam-ejb-metier-dao-eclipselink]。
![]() |
- 在 [1] 中,向 Web 项目的依赖项中添加一个项目,
- 在 [2] 中,选择项目 [mv-pam-ejb-metier-dao-eclipselink],
- 在 [3] 中,依赖关系类型为 ejb,
- 在 [4] 中,依赖关系的范围是 provided,即该依赖将由运行时环境提供,
- 在 [5] 中,该依赖项已被添加。
要创建与之前相同的 Web 服务,我们需要:
- 创建一个标记为 @Webservice 的类
- 并包含两个标记为 @WebMethod 的方法 calculerFeuilleSalaire 和 findAllEmployes
我们在 [pam.ws] 包中创建一个名为 [PamWsEjbMetier] 的类:
![]() |
![]() |
类 [PamWsEjbMetier] 如下所示:
- 第 7-10 行:该类导入了 EJB 和 [pam-serveurws-metier-dao-jpa-eclipselink] 模块中的类,其 Maven 项目已添加到该项目的依赖项中。
- 第 12 行:该类是一个 Web 服务
- 第 13 行:它实现了在模块 EJB 中定义的接口 IMetier
- 第 18-19 行:方法 calculerFeuilleSalaire 被作为 Web 服务的方法公开
- 第 23-24 行:方法 findAllEmployes 作为 Web 服务的方法对外暴露
- 第 15-16 行:EJB 的本地接口 [Metier] 被注入到第 16 行的字段中。 我们使用本地接口,因为 Web 应用程序和 EJB 模块在同一个 JVM 中运行。
- 第 20 行和第 25 行:方法 calculerFeuilleSalaire 和 findAllEmployes 将其处理委托给 EJB [Metier] 中同名的方法。 因此,该类仅用于将 EJB 和 [Metier] 中的方法作为 Web 服务的方法向远程客户端公开。
在 NetBeans 中,该 Web 应用程序被识别为提供 Web 服务:
![]() |
要在 Glassfish 服务器上部署 Web 服务,我们需要同时部署:
- 将 Web 模块部署到服务器的 Web 容器中
- 将模块 EJB 部署到服务器的 EJB 容器中
为此,我们需要创建一个名为 [Enterprise Application] 的应用程序,该应用程序将同时部署这两个模块。为此,必须将这两个项目加载到 NetBeans [2] 中。
完成上述操作后,我们创建一个新项目 [3]。
![]() |
- 在 [4] 中,我们选择 [Enterprise Application] 类型的项目。
- 在 [5] 中,我们为项目命名
![]() |
- 在 [6] 中,我们配置该项目。Java 版本 EE 将为 Java EE 6。企业项目可包含两个模块:一个 EJB 模块和一个 Web 模块。 在此,企业项目将封装已创建并加载到 NetBeans 中的 Web 模块和 EJB 模块。因此,我们无需创建新模块。
- 在 [7] 中,即为这样创建的企业项目 [mv-pam-webapp-ear]。同时还创建了另一个 Maven 项目 [mv-pam-webapp]。我们不会处理该项目。
- 在 [8] 中,我们向企业项目
![]() |
- 在 [9] 中,添加 WAR 类型的 Web 项目,
- 在 [10] 中,添加 EJB 类型的项目 EJB,
![]() |
- 在 [11] 中,添加包含这两个依赖项的企业项目。
我们通过“清理并构建”来构建企业项目。我们几乎已经准备好将其部署到 Glassfish 服务器上了。在此之前,可能需要卸载服务器上已加载的应用程序,以避免 EJB 和 [11] 之间可能出现的名称冲突:
![]() |
必须启动 MySQL 服务器,并且 [dbpam_eclipselink] 数据库必须已就绪且数据已填充。完成这些步骤后,即可部署企业应用程序 [12]。 在 [13] 中,可以看到该应用程序已成功部署到 Glassfish 服务器上。
我们可以测试刚刚部署的Web服务:
![]() |
- 在 [1] 中,我们请求测试 Web 服务 [PamWsEjbMetier]
- 转换为 [2],即测试页面。具体测试操作请读者自行完成。
8.2.2. 客户端部分
任务:按照第 8.1.2.1 节所述的方法,构建上述 Web 服务的控制台客户端。
8.3. 使用Spring和Tomcat实现的Web服务
现在,我们处于以下架构框架中:
![]() |
Web服务由在Tomcat服务器Web容器中运行的Web应用程序提供。该应用程序的架构如下:
![]() |
我们将基于第 5.11 节构建的 [mv-pam-spring-hibernate] 项目:
![]() |
8.3.1. 服务器端
我们创建一个名为 [mv-pam-ws-spring-tomcat] [1] 的 Web 类型 Maven 应用程序:
![]() |
我们修改文件 [pom.xml],以包含以下 [2] 依赖项:
<dependencies>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>mv-pam-spring-hibernate</artifactId>
<version>${project.version}</version>
</dependency>
<!-- Apache CXF 依赖项 -->
<dependency>
<groupId>org.apache.cxf</groupId>
<artifactId>cxf-rt-frontend-jaxws</artifactId>
<version>2.2.12</version>
</dependency>
<dependency>
<groupId>org.apache.cxf</groupId>
<artifactId>cxf-rt-transports-http</artifactId>
<version>2.2.12</version>
</dependency>
</dependencies>
- 第 3-7 行:对项目 [spring-pam-jpa-hibernate] 的依赖,
- 第 8-17 行:对 Apache 框架 CXF 和 [http://cxf.apache.org/] 的依赖。该框架有助于创建 Web 服务。
该文件 [pom.xml] 引入了大量依赖项 [2]。
让我们回到应用程序的架构:
![]() |
我们将构建的Web服务调用由CXF框架中的一个Servlet进行管理。这在[WEB-INF / web.xml]文件中体现如下:
<?xml version="1.0" encoding="UTF-8"?>
<web-app version="2.5" xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd">
<display-name>mv-pam-ws-spring-tomcat</display-name>
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<!-- CXF 的配置 -->
<servlet>
<servlet-name>CXFServlet</servlet-name>
<servlet-class>org.apache.cxf.transport.servlet.CXFServlet</servlet-class>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>CXFServlet</servlet-name>
<url-pattern>/ws/*</url-pattern>
</servlet-mapping>
<session-config>
<session-timeout>
30
</session-timeout>
</session-config>
<welcome-file-list>
<welcome-file>index.jsp</welcome-file>
</welcome-file-list>
</web-app>
- CXF框架依赖于Spring。第4-6行:声明了一个监听器。相应的类将与Web应用程序同时加载。它将使用Spring配置文件[WEB-INF / applicationContext.xml]:
![]() |
- 第 8-12 行:servlet CXF 将处理对我们即将创建的 Web 服务的调用,
- 第13-16行:由Servlet CXF处理的URL请求将属于/ws/*类型。其余请求将不被CXF处理。
为了定义 Web 服务,我们定义了一个接口及其实现:
![]() |
[IWsMetier] 接口如下:
package pam.ws;
import javax.jws.WebService;
import metier.IMetier;
@WebService
public interface IWsMetier extends IMetier{
}
- 第 7 行:接口 [IWsMetier] 继承自项目 [mv-pam-spring-hibernate] 中 [métier] 层的接口 [IMetier],
- 第 6 行:接口 [IWsMetier] 是 Web 服务的接口。
该接口的实现类如下:
package pam.ws;
import java.util.List;
import javax.jws.WebMethod;
import javax.jws.WebService;
import jpa.Employe;
import metier.FeuilleSalaire;
import metier.IMetier;
@WebService
public class PamWsMetier implements IWsMetier {
// 业务层
private IMetier metier;
// 构造函数
public PamWsMetier(){
}
@WebMethod
public FeuilleSalaire calculerFeuilleSalaire(String SS, double nbHeuresTravaillees, int nbJoursTravailles) {
return metier.calculerFeuilleSalaire(SS, nbHeuresTravaillees, nbJoursTravailles);
}
@WebMethod
public List<Employe> findAllEmployes() {
return metier.findAllEmployes();
}
// 获取器和设置器
public void setMetier(IMetier metier) {
this.metier = metier;
}
}
- 第 11 行:类 [PamWsMetier] 实现了上述定义的接口,
- 第 10 行:将该类定义为 Web 服务,
- 第 14 行:[métier] 层将由 Spring 注入,
- 第 21、26 行:注解 @WebMethod 将一个方法设为 Web 服务公开的方法,
- 第 23、28 行:方法通过 [métier] 层进行实现。
接下来我们需要定义 Spring 配置文件 [applicationContext.xml] 的内容:
![]() |
其内容如下:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:tx="http://www.springframework.org/schema/tx"
xmlns:jaxws="http://cxf.apache.org/jaxws"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-2.0.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx-2.0.xsd
http://cxf.apache.org/jaxws
http://cxf.apache.org/schemas/jaxws.xsd">
<!-- Apache CXF -->
<import resource="classpath:META-INF/cxf/cxf.xml" />
<import resource="classpath:META-INF/cxf/cxf-extension-soap.xml" />
<import resource="classpath:META-INF/cxf/cxf-servlet.xml" />
<!-- 底层 -->
<import resource="classpath:spring-config-metier-dao.xml" />
<!-- Web服务 -->
<bean id="wsMetier" class="pam.ws.PamWsMetier">
<property name="metier" ref="metier"/>
</bean>
<jaxws:endpoint id="wsmetier"
implementor="#wsMetier"
address="/metier">
</jaxws:endpoint>
</beans>
- 第 13-15 行:导入 Apache 配置文件 CXF。这些文件在项目的 Classpath 中查找(classpath 属性),
- 第4、9、10行:声明了Apache CXF特有的命名空间,
- 第 18 行:导入 [mv-pam-spring-hibernate] 项目的 Spring 配置文件,
- 第 21-23 行:定义 Web 服务 Bean 及其对 [métier] 层的依赖(第 22 行),
- 第 24-27 行:定义 Web 服务本身,
- 第 25 行:实现 Web 服务的 Spring Bean 即第 21 行所定义的 Bean;
- 第26行:定义Web服务将可用的URL路径,此处为/metier。 结合 Apache CXF 处理的 URL 应有的格式(参见 web.xml 文件),该 URL 路径变为 /ws/metier。
我们的项目已准备就绪。我们运行(Run)该项目,并在浏览器中请求 URL [http://localhost:8080/mv-pam-ws-spring-tomcat/ws]:

该页面列出了所有已部署的 Web 服务。此处仅有一个。我们点击链接 WSDL:
![]() |
显示的文本 [1] 来自文件 XML,该文件定义了 Web 服务的功能、调用方式以及返回的响应。 请注意该文件 WSDL 中的 URL 和 [2]。所有 Web 服务的客户端都需要了解这些信息。
8.3.2. 客户端部分
任务:按照第 8.1.2.1 节所述的方法,为上述 Web 服务构建一个控制台客户端。
注:要指定 Web 服务文件 WSDL 的 URL,操作步骤如下:
![]() |
将之前标记为 [2] 的 URL 改为 [3]。












































