Skip to content

8. 第 4 版 – Web 服务架构中的客户端/服务器

在此新版本中,[Pam] 应用程序将在 Web 服务架构内以客户端/服务器模式运行。让我们回顾一下上一版应用程序的架构:

在上图中,通信层 [C, RMI, S] 实现了客户端 [ui] 与远程 [business] 层之间的透明通信。我们将采用类似的架构,其中通信层 [C, RMI, S] 将被 [C, HTTP / SOAP, S] 层所取代:

与之前的 RMI/EJB 协议相比,HTTP/SOAP 协议具有跨平台的优势。因此,Web 服务可以使用 Java 编写并部署在 Glassfish 服务器上,而客户端可以是 .NET 或 PHP 客户端。

我们将以三种不同的模式来构建该架构:

  1. Web 服务由 EJB [Business] 提供
  2. Web 服务将由使用 [Business] EJB 的 Web 应用程序提供
  3. Web 服务将由使用 Spring 的 Web 应用程序提供

在 Java EE 服务器中,Web 服务可以通过多种方式实现:

  • 由一个标注了 @WebService 注解并在 Web 容器中运行的类
  • 由一个标注了 @WebService 并运行在 EJB 容器中的 EJB 实现

我们将从后一种架构开始。

8.1. 由 EJB 实现的 Web 服务

8.1.1. 服务器端

8.1.1.1. NetBeans 项目

首先,让我们创建一个新的 Maven 项目,该项目是 EJB 项目 [mv-pam-ejb-metier-dao-jpa-eclipselink] 的副本:

  

在以下架构中:

[业务]层将作为[UI]层调用的Web服务。该类无需实现任何接口。正是注解将POJO(普通Java对象)转换为Web服务。实现上述[业务]层的[Business]类将按如下方式进行转换:


    <dependency>
      <groupId>org.swinglabs</groupId>
      <artifactId>swing-layout</artifactId>
      <version>1.0.3</version>
</dependency>
  • 第 4 行:@WebService 注解将 [Business] 类转换为 Web 服务。Web 服务向其客户端公开方法。这些方法必须使用 @WebMethod 注解进行标注。
  • 第 19 行和第 25 行:[Metier] 类的这两个方法成为 Web 服务的方法。
  • 第 29 行:必须移除 getter 和 setter 方法;否则,它们将在 Web 服务中被暴露,从而导致安全错误。

NetBeans 会检测到这些注解的添加,并据此更改项目的性质:

在[1]中,项目中已出现一个[Web Services]树。该树包含Metier Web服务及其两个方法。服务器应用程序可以部署[2]。 MySQL 服务器必须处于运行状态,且其数据库 [dbpam_eclipselink] 必须存在并已填充数据。为避免名称冲突,可能需要预先从先前研究的客户端/服务器 EJB 项目中移除 [3] 相关 EJB。事实上,我们的新项目包含与先前项目中相同的 EJB。

在[1]中,我们可以看到服务器应用程序已部署在 GlassFish 服务器上。Web 服务部署完成后,即可进行测试:

  • 在[1]中,我们将在当前项目中测试[Metier] Web服务
  • 可以通过不同的 URL 访问该 Web 服务。URL [2] 允许您测试该 Web 服务
  • 在[3]中,提供了一个指向定义该Web服务的XML文件的链接。Web服务的客户端需要知道该文件的URL。Web服务的客户端层(存根)即由该文件生成。
  • 在[4,5]中,提供了一个用于测试Web服务所公开方法的表单。这些方法与其参数一同呈现,用户可以定义这些参数。

例如,让我们测试 [findAllEmployees] 方法,该方法无需参数:

 

在上文中,我们测试了该方法。随后我们收到了如下响应(部分视图)。我们确实可以看到这两名员工及其福利信息。欢迎读者以同样的方式测试 [4] 方法,并向其传入它所期望的三个参数。

Image

8.1.2. 客户端

8.1.2.1. 客户的 NetBeans 项目 -console

现在,我们将为应用程序的客户端创建一个类型为 [Java 应用程序] 的 Java 项目。截至 2012 年 6 月,尚无法为此客户端创建 Maven 项目。此时会出现一个错误,该错误在网上似乎已被发现,但至今仍未解决。

 

项目创建完成后,我们需要指定它将作为我们在 GlassFish 服务器上刚刚部署的 Web 服务的客户端:

  • 在 [2] 中,我们选择新项目并点击 [新建文件] 按钮
  • 在 [3] 中,我们指定要创建一个 Web 服务客户端
  • 在 [4] 中,我们选择 Web 服务的 NetBeans 项目
  • 在 [5] 窗口中,列出了所有带有 [Web Services] 分支的项目;此处仅显示 [mv-pam-ws-metier-dao-eclipselink] 项目。
  • 一个项目可以部署多个 Web 服务。在 [6] 中,我们指定要连接的 Web 服务。
  • 在 [7] 中,显示了定义 Web 服务的 URL。生成将与 Web 服务进行交互的客户端层的软件工具会使用此 URL。
  • 即将生成的客户端层 [C] [1] 由一组 Java 类组成,这些类将被放置在同一个包中。该包的名称在 [8] 中设置。
  • 点击[完成]按钮完成Web服务客户端创建向导后,上述[C]层即被创建。

这体现在项目中的若干变化上:

  • 在上述 [10] 中,会出现一个 [生成的源代码] 树,其中包含 [C] 层的类,这些类使客户端 [3] 能够与 Web 服务进行通信。该层使客户端 [3] 能够与 [业务] 层 [4] 进行通信,仿佛该层是本地的而非远程的。
  • 在 [11] 中,会出现一个 [Web Service References] 树,列出了已为其生成客户端层的 Web 服务。

请注意,在生成的 [C] 层 [10] 中,我们可以看到已部署在服务器端的类:IndemniteCotisationEmployeFeuilleSalaireElementsSalaireMetier。其中 Metier 是 Web 服务,其余类则是该服务所需的类。 您可能好奇想查看这些代码。我们将看到,这些类的定义(当实例化后,它们代表由服务操作的对象)包括定义类字段及其访问器,以及添加注解以实现将类序列化为 XML 流。 Metier 类已成为一个接口,其中包含两个已使用 @WebMethod 注解的方法。每个方法都会生成两个类,例如 [CalculatePayroll.java] 和 [CalculatePayrollResponse.java],其中一个封装方法调用,另一个封装其结果。最后,MetierService 类是允许客户端获取远程 Metier Web 服务引用(reference)的类:


package metier;
 
...
@WebService
@Stateless()
@TransactionAttribute(TransactionAttributeType.REQUIRED)
public class Metier implements IMetierLocal,IMetierRemote {
  
  // references on layers [DAO]
  @EJB
  private ICotisationDaoLocal cotisationDao = null;
  @EJB
  private IEmployeDaoLocal employeDao=null;
  @EJB
  private IIndemniteDaoLocal indemniteDao=null;
 
 
  // get your payslip
@WebMethod
  public FeuilleSalaire calculerFeuilleSalaire(String SS,
...
  }
 
  // list of employees
@WebMethod
   public List<Employe> findAllEmployes() {
...
  }
// important - no getters and setters for EJB
 }

第 2 行中的 getMetierPort 方法用于获取远程 Metier Web 服务的引用。

8.1.2.2. Metier Web 服务的控制台客户端

剩下的就是编写 Metier Web 服务的客户端了。我们将 [mv-pam-client-metier-dao-jpa-eclipselink] 项目中的 [MainRemote] 类(该类原本是 EJB 服务器的客户端)复制到新项目中。

  • 在[1]中,是Web服务客户端类。[MainRemote]类中存在错误。为修复这些错误,我们将首先删除该类中所有现有的[import]语句,然后使用[Fix Imports]选项重新生成它们。这是因为[MainRemote]类所使用的某些类现在已属于生成的[client]包。
  • 在 [3] 中,是实例化 [business] 层的代码片段 [3]。它通过 JNDI 代码获取远程 EJB 的引用来完成实例化。

我们将代码更新如下:

  • 已删除 JNDI 代码
  • 由于客户端不存在 [PamException] 类,因此我们移除了相关的 catch 代码块,仅保留父类 [Exception] 的 catch 代码块
  • 在 [4] 中,我们仍需获取远程 Web 服务 [Metier] 的引用,以便调用其方法 [calculatePayroll]。
  • 在 [5] 中,我们使用鼠标将 [Metier] Web 服务中的 [calculerFeuilleSalaire] 方法拖拽并放置到 [4] 中。系统将生成代码 [6]。开发人员随后可以对这段通用代码进行调整。
  • 在第 112 行,我们可以看到 [calculatePayroll] 是 [client.Metier] 类的成员方法(第 111 行)。既然我们已经知道如何获取 [metier] 层,那么之前的代码可以重写为如下形式:
1
2
3
4
    @WebEndpoint(name = "MetierPort")
    public Metier getMetierPort() {
        return super.getPort(new QName("http://metier/", "MetierPort"), Metier.class);
}

第 7 行获取了 Metier Web 服务的引用。完成此操作后,类代码保持不变,只是第 10 行处理的不再是 [Exception] 类型,而是更通用的 Throwable 类型(即 Exception 类的父类)。如果发生异常,我们会显示其所有嵌套原因,直至根原因。

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

  • 确保 MySQL5 数据库管理系统正在运行,并且已创建并初始化 dbpam_eclipselink 数据库
  • 确保 Web 服务已部署在 GlassFish 服务器上
  • 构建客户端(清理并构建)
  • 配置客户端以供运行
  
  • 运行客户端

控制台显示的结果如下:

...    
// it's okay - we can ask for the payslip
    FeuilleSalaire feuilleSalaire = null;
    Metier metier = null;
    try {
       // instantiation layer [metier]
      metier = new MetierService().getMetierPort();
       // wage sheet calculation
      feuilleSalaire = metier.calculerFeuilleSalaire(args[0], nbHeuresTravaillées, nbJoursTravaillés);
    } catch (Throwable th) {
       // exception chain
      System.out.println("Chaîne des exceptions --------------------------------------");
      System.out.println(th.getClass().getName() + ":" + th.getMessage());
      while (th.getCause() != null) {
        th = th.getCause();
        System.out.println(th.getClass().getName() + ":" + th.getMessage());
      }
      System.exit(1);
    }
     // quick viewing
...

配置如下:

Image

我们得到以下结果:

...
Valeurs saisies :
N° de sécurité sociale de l'employé : 254104940426058
Nombre d'heures travaillées : 150
Nombre de jours travaillés : 20

Informations Employé : 
Nom : Jouveinal
Prénom : Marie
Adresse : 5 rue des oiseaux
...

请注意,虽然 [Metier] Web 服务发送的是 [PamException] 类型的异常,但客户端收到的异常却是 [SOAPFaultException] 类型。即使在异常链中,[PamException] 类型也没有出现。

8.1.3. Metier Web 服务的 Swing 客户端


任务:将 [mv-pam-client-ejb-metier-dao-jpa-eclipselink] 项目中的 Swing 客户端移植到新项目中,使其同样成为部署在 GlassFish 服务器上的 Web 服务的客户端。


8.2. 由 Web 应用程序实现的 Web 服务

我们目前的工作基于以下架构框架:

该 Web 服务由运行在 GlassFish 服务器 Web 容器中的 Web 应用程序提供。该 Web 服务将依赖于部署在 EJB3 容器中的 [Metier] EJB。

8.2.1. 服务器端

我们创建一个 Web 应用程序:

  • 在 [1] 中,我们创建了一个新项目
  • 在 [2] 中,该项目类型为 [Web Application]
  • 在 [3] 中,我们将该项目命名为 [mv-pam-ws-ejb-metier-dao-eclipselink]
  • 在 [4] 中,我们选择 Java EE 6
  • 在 [6] 中,项目已创建

在下图中,创建的 Web 应用程序将在 Web 容器中运行。它将使用 [Metier] EJB,该 EJB 将部署在服务器的 EJB 容器中。

为了确保创建的 Web 应用程序能够访问与 [Metier] EJB 相关的类,我们将 EJB 服务器依赖项 [mv-pam-ejb-metier-dao-eclipselink](我们之前已经介绍过)添加到 Web 应用程序的库 [mv-pam-ws-ejb-metier-dao-eclipselink] 中。

  • 在 [1] 中,我们将一个项目添加到 Web 项目的依赖项中;
  • 在 [2] 中,选择项目 [mv-pam-ejb-metier-dao-eclipselink];
  • 在 [3] 中,依赖类型为 ejb
  • 在 [4] 中,指定了依赖项的范围为 provided,这意味着它将由运行时环境提供,
  • 在 [5] 中,依赖项已添加。

要创建与之前相同的 Web 服务,我们需要:

  • 创建一个带有 @WebService 注解的类
  • 该类包含两个方法:calculerFeuilleSalaire findAllEmployes,并标注了 @WebMethod 注解

我们在 [pam.ws] 包中创建一个名为 [PamWsEjbMetier] 的类:

  

[PamWsEjbMetier] 类的定义如下:

1
2
3
4
Chaîne des exceptions --------------------------------------
javax.xml.ws.soap.SOAPFaultException:L'employé de n°[xx] est introuvable
com.sun.xml.internal.ws.developer.ServerSideException:L'employé de n°[xx] est introuvable
Java Result: 1
  • 第 7–10 行:该类导入了来自 EJB 模块 [pam-serveurws-metier-dao-jpa-eclipselink] 的类,其 Maven 项目已添加到该项目的依赖项中。
  • 第 12 行:该类是一个 Web 服务
  • 第 13 行:它实现了 EJB 模块中定义的 IMetier 接口
  • 第 18–19 行:`calculerFeuilleSalaire` 方法作为 Web 服务方法对外暴露
  • 第 23–24 行:`findAllEmployees` 方法被作为 Web 服务方法对外暴露
  • 第 15–16 行:EJB [Metier] 的本地接口被注入到第 16 行的字段中。我们使用本地接口是因为 Web 应用程序和 EJB 模块在同一个 JVM 中运行。
  • 第 20 行和第 25 行:`calculerFeuilleSalaire` `findAllEmployees` 方法将其处理任务委托给 [Metier] EJB 中同名的方法。因此,该类仅用于将 [Metier] EJB 的方法作为 Web 服务方法暴露给远程客户端。

在 NetBeans 中,该 Web 应用程序被识别为暴露了一个 Web 服务:

要在 GlassFish 服务器上部署该 Web 服务,我们必须同时部署以下两项:

  • 将 Web 模块部署到服务器的 Web 容器中
  • 将 EJB 模块部署到服务器的 EJB 容器中

为此,我们需要创建一个 [企业应用程序],以便同时部署这两个模块。要实现这一点,必须将这两个项目都加载到 NetBeans 中 [2]。

完成上述操作后,我们创建一个新项目 [3]。

  • 在[4]处,我们选择[企业应用程序]项目类型。
  • 在[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]中,添加了该企业项目及其两个依赖项。

我们使用 Clean 和 Build 构建企业项目。现在,我们几乎已经准备好将其部署到 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]:

package pam.ws;

import java.util.List;
import javax.ejb.EJB;
import javax.jws.WebMethod;
import javax.jws.WebService;
import jpa.Employe;
import metier.FeuilleSalaire;
import metier.IMetier;
import metier.IMetierLocal;

@WebService
public class PamWsEjbMetier implements IMetier{

  @EJB
  private IMetierLocal metier;

  @WebMethod
  public FeuilleSalaire calculerFeuilleSalaire(String SS, double nbHeuresTravaillées, int nbJoursTravaillés) {
    return metier.calculerFeuilleSalaire(SS, nbHeuresTravaillées, nbJoursTravaillés);
  }

  @WebMethod
  public List<Employe> findAllEmployes() {
    return metier.findAllEmployes();
  }

}
  • 第 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] 文件中体现如下:


  <dependencies>
    <dependency>
      <groupId>${project.groupId}</groupId>
      <artifactId>mv-pam-spring-hibernate</artifactId>
      <version>${project.version}</version>
    </dependency>
    <!-- Apache CXF dependencies -->
    <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>
  • CXF 框架依赖于 Spring。第 4–6 行:声明了一个监听器。相应的类将在 Web 应用程序加载时一并加载。它将使用 Spring 配置文件 [WEB-INF/applicationContext.xml]:
  • 第 8–12 行:将处理对我们即将创建的 Web 服务调用的 CXF Servlet,
  • 第 13–16 行:由 CXF Servlet 处理的 URL 将采用 /ws/* 的形式。其他 URL 将不会由 CXF 处理。

要定义 Web 服务,我们需要定义一个接口及其实现:

接口 [IWsMetier] 将如下所示:


<?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 configuration -->
  <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>
  • 第 7 行:接口 [IWsMetier] 继承自 [mv-pam-spring-hibernate] 项目 [business] 层中的接口 [IMetier],
  • 第 6 行:[IWsMetier] 接口是一个 Web 服务接口。

该接口的实现类如下:


package pam.ws;
 
import javax.jws.WebService;
import metier.IMetier;
 
@WebService
public interface IWsMetier extends IMetier{
 
}
  • 第 11 行:[PamWsMetier] 类实现了之前定义的接口,
  • 第 10 行:将该类定义为 Web 服务,
  • 第 14 行:[business] 层将由 Spring 注入,
  • 第 21、26 行:@WebMethod 注解将方法转换为 Web 服务公开的方法,
  • 第 23、28 行:这些方法通过 [business] 层进行实现。

我们还需要定义 Spring 配置文件 [applicationContext.xml] 的内容:

其内容如下:


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 {
 
  // business layer
  private IMetier metier;
 
  // manufacturer
  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();
  }
 
  // getters and setters
 
  public void setMetier(IMetier metier) {
    this.metier = metier;
  }
 
}
  • 第 13–15 行:导入 Apache CXF 配置文件。这些文件会在项目的类路径(classpath 属性)中进行搜索,
  • 第 4、9、10 行:声明 Apache CXF 专用的命名空间,
  • 第 18 行:导入 [mv-pam-spring-hibernate] 项目的 Spring 配置文件,
  • 第 21–23 行:定义 Web 服务 Bean 及其对 [business] 层的依赖(第 22 行),
  • 第 24–27 行:定义 Web 服务本身,
    • 第 25 行:实现 Web 服务的 Spring Bean 即第 21 行所定义的 Bean;
    • 第 26 行:定义 Web 服务可访问的 URL,此处为 /business。结合 Apache CXF 处理 URL 所需的格式(参见 web.xml 文件),该 URL 将变为 /ws/business。

项目已准备就绪。我们运行它(Run),并在浏览器中请求 URL [http://localhost:8080/mv-pam-ws-spring-tomcat/ws]:

Image

该页面列出了所有已部署的 Web 服务。此处仅有一个。我们点击 WSDL 链接:

显示的文本 [1] 来自一个 XML 文件,该文件定义了 Web 服务的功能、调用方式以及返回的响应。请注意此 WSDL 文件的 URL [2]。所有 Web 服务的客户端都需要知道该 URL。

8.3.2. 客户端


任务:按照第 8.1.2.1 节所述的步骤,为前面的 Web 服务构建一个控制台客户端。


注意:要指定 Web 服务 WSDL 文件的 URL,请按以下步骤操作:

将之前在[2]中记录的URL输入到[3]中。