Skip to content

6. 日程安排服务 JEE 的 Flex 客户端

现在我们介绍两个基于 JEE 预约 Web 服务的 Flex 客户端。所使用的 IDE 是 Flex Builder 3。该产品的演示版可从 [https://www.adobe.com/cfusion/tdrc/index.cfm?loc=fr_fr&product=flex] 网址下载。 Flex Builder 3是基于Eclipse的开发工具。此外,为了运行Flex客户端,我们使用了Wamp工具中的Apache Web服务器。任何Apache服务器均可满足需求。显示Flex客户端的浏览器必须安装至少Flash Player 9版本的插件。

Flex 应用程序的特点是在浏览器的 Flash Player 插件中运行。在这方面,它们类似于 Ajax 应用程序——后者将脚本嵌入发送到浏览器的页面中,随后在浏览器内执行。 Flex 应用程序并非通常意义上的 Web 应用程序:它是一个由 Web 服务器提供服务的客户端应用程序。从这一点来看,它类似于作为这些服务客户端的桌面应用程序。但有一点不同:它最初是从 Web 服务器下载到安装了能够运行它的 Flash Player 插件的浏览器中的。

与桌面应用程序一样,Flex 应用程序主要由两个部分组成:

  • 呈现部分:即在浏览器中显示的视图。这些视图具备桌面应用程序窗口的丰富功能。视图通过一种名为MXML的标记语言进行描述。
  • 代码部分:主要处理用户在视图上操作所触发的事件。该代码既可以使用 MXML 编写,也可以使用一种名为 ActionScript 的面向对象语言编写。需要区分两种类型的事件:
  • 需要与Web服务器进行交互的事件:例如使用Web应用程序提供的数据填充列表、将表单数据发送至服务器等。Flex提供了一系列方法,可在开发者无需干预的情况下与服务器进行通信。这些方法默认是异步的:在向服务器发送请求期间,用户仍可继续与视图进行交互。
  • 不与服务器交换数据而直接修改显示视图的事件,例如从树中拖拽一个元素并将其放入列表中。此类事件完全在浏览器本地处理。

Flex 应用程序通常按以下方式运行:

  • 在 [1] 中,请求一个 HTML 页面
  • 在 [2] 中,该页面被发送出去。它随附了一个二进制文件 SWFShockWave Flash),其中包含完整的 Flex 应用程序:所有视图及其事件处理代码。 该文件将由浏览器的 FlashPlayer 插件执行。
  • Flex客户端通常在浏览器本地运行,除非需要外部数据。此时,它会向[3]服务器请求数据,并以[4]格式接收数据,格式包括XML或二进制格式。 被调用的Web服务器端应用程序可以使用任何编程语言编写,关键在于响应的格式。

我们描述了 Flex 应用程序的运行架构,以便读者清楚地理解它与不使用 Ajax 的传统 Web 应用程序(如前文所述的 Asp.Net 应用程序)之间的区别。在后者中,浏览器是被动的:它只是显示由 Web 服务器构建并发送给它的 HTML 页面。

接下来,我们将提供两个 Flex 客户端的示例,仅为展示 Web 服务客户端的多样性。由于笔者本人也是 Flex 初学者,某些要点可能无法像应有的那样详细阐述。

6.1. 第一个 Flex 客户端

现在,我们将编写第一个 Flex 客户端来显示客户列表。构建的客户端/服务器架构如下:

在此架构中,包含两台Web服务器:

  • 运行远程Web服务的Glassfish服务器
  • 运行远程Web服务Flex客户端的Apache服务器

我们将使用 Flex Builder 3 构建 Flex 客户端:

  • 在 Flex Builder 3 中,创建一个新项目 [1]
  • 为其命名,并指定生成文件夹
  • 在 [4] 中,为主要应用程序(即将被执行的应用程序)命名
  • 在 [5] 中,生成后的项目
  • 在 [6] 中,应用程序的主文件 MXML
  • 文件 MXML 包含一个视图及其事件处理代码。 [Source] [7] 选项卡可访问 MXML 文件。其中包含描述视图的 <mx> 标签以及 ActionScript 代码。
  • 可通过 [Design] 和 [8] 选项卡以图形化方式构建视图。此时,描述该视图的 MXML 标签将自动生成在 [Source] 选项卡中。 反之亦然:直接在 [Source] 选项卡中添加的 MXML 标签,会在 [Design] 选项卡中以图形形式体现。

与之前的 C# 客户端和 Asp.Net 一样,我们将为远程 Web 服务 S [A] 生成本地 C 代理 [B]:

要生成 C 代理,Web 服务 JEE 必须处于活动状态。

  • 在 [1] 中,选择“数据 / 导入 Web 服务”选项
  • 在 [2] 中,选择 C 代理类和接口的生成文件夹。
  • 在 [3] 中,输入远程 Web 服务 S 的 WSDL 文件的 Uri(参见第 4.10.2 节),然后进入下一步
  • 在 [4] 和 [5] 中,将 [3] 中指定的 WSDL 文件所描述的 Web 服务
  • 在 [6] 中:即将为代理 C 生成的方法列表。请注意,这些并非服务 S 的实际方法。它们的签名不正确。 在此,无论Web服务实际方法的参数数量如何,所展示的每个方法都仅有一个参数。该唯一参数是一个类实例,其字段中封装了远程方法所期望的参数。
  • 在 [7] 中:C 语言代理的类和接口将被生成在此包中
  • 在 [8] 中:将作为远程 Web 服务代理的本地类名称
  • 在 [9] 中:完成向导。
  • 在 [10] 中:生成的 C 语言代理类和接口的列表。
  • en [11]:实现 C 代理方法的类 [WsDaoJpaService]。

生成的类 [WsDaoJpaService] 实现了以下接口 [IWsDaoJpaService]:


/**
 * Service.as
 * This file was auto-generated from WSDL by the Apache Axis2 generator modified by Adobe
 * Any change made to this file will be overwritten when the code is re-generated.
 */
package generated.webservices{
    import mx.rpc.AsyncToken;
    import flash.utils.ByteArray;
    import mx.rpc.soap.types.*;
               
    public interface IWsDaoJpaService
    {
        //用于 getAllClients 操作的存根函数
        /**
         * Call the operation on the server passing in the arguments defined in the WSDL file
         * @param getAllClients
         * @return An AsyncToken
         */
        function getAllClients(getAllClients:GetAllClients):AsyncToken;
....
        function getAllClients_send():AsyncToken;
...
        function get getAllClients_lastResult():GetAllClientsResponse;
...
        function set getAllClients_lastResult(lastResult:GetAllClientsResponse):void;
...
       function addgetAllClientsEventListener(listener:Function):void;
...
        function get getAllClients_request_var():GetAllClients_request;
...
        function set getAllClients_request_var(request:GetAllClients_request):void;
...
    }
}
  • 第 11 行:由类 [WsDaoJpaService] 实现的接口 [IWsDaoJpaService]
  • 第 19-31 行:为远程 Web 服务的 getAllClients() 方法生成的各种方法。 唯一与 Web 服务实际暴露的方法相近的是第 19 行。它名称正确但签名不正确:远程 Web 服务的 getAllClients() 方法没有参数。

生成的 C 语言代理中 getAllClients 方法的唯一参数类型为 GetAllClients,具体如下:


/**
 * GetAllClients.as
 * This file was auto-generated from WSDL by the Apache Axis2 generator modified by Adobe
 * Any change made to this file will be overwritten when the code is re-generated.
 */

package generated.webservices
{
    import mx.utils.ObjectProxy;
    import flash.utils.ByteArray;
    import mx.rpc.soap.types.*;
    /**
     * Wrapper class for a operation required type
     */
    
    public class GetAllClients
    {
        /**
         * Constructor, initializes the type class
         */
        public function GetAllClients() {}
            
    }
}

这是一个空类。这可能与目标方法 getAllClients 不接受参数这一事实相符。

现在,让我们来看看为实体 MedecinClientRv Creneau 生成的类。 以类 Client 为例:


package generated.webservices
{
    import mx.utils.ObjectProxy;
    import flash.utils.ByteArray;
    import mx.rpc.soap.types.*;
    
    public class Client extends generated.webservices.Personne
    {
        public function Client() {}
            
    }
}

Client 也是空的。它(第 7 行)继承自以下类 Personne


package generated.webservices
{
    import mx.utils.ObjectProxy;
    import flash.utils.ByteArray;
    import mx.rpc.soap.types.*;
    
    public class Personne
    {
        public function Personne() {}
            
        public var id:Number;
        public var nom:String;
        public var prenom:String;
        public var titre:String;
        public var version:Number;
    }
}
  • 第 11-15 行:这里包含在 Web 服务 JEE 中定义的类 Personne 的属性。

至此,代理 C 的主要组件已准备就绪,我们可以开始使用它了。

客户端的主文件 [rdvmedecins01.xml] 如下:


<?xml version="1.0" encoding="utf-8"?>
<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml" layout="vertical" creationComplete="init();">
    <mx:Script>
        <![CDATA[
            import generated.webservices.Client;
...
            
            // 数据
            private var ws:WsDaoJpaService;
            [Bindable]
            private var clients:ArrayCollection;
            
            private function init():void{
...
            }
            
            private function loadClients():void{
...
            }
            
...            
            private function displayClient(client:Client):String{
...
            } 
        ]]>
    </mx:Script>
    <mx:Label text="Liste des clients" fontSize="14"/>
    <mx:List dataProvider="{clients}" labelFunction="displayClient"></mx:List>
    <mx:Button label="Afficher les clients" click="loadClients()"/>
    <mx:Text id="txtMsgErreur" width="454" height="75"/>
    
</mx:Application>

在此代码中,需区分以下内容:

  • 应用程序的定义(第 2 行)
  • 应用程序视图的描述(第27-30行)
  • 位于 <mx:Script> 标签内的 ActionScript 语言事件处理程序(第 3-26 行)。

首先,让我们来分析应用程序本身的定义及其视图描述:

  • 第 2 行:定义
    • 视图容器中组件的布局模式。layout="vertical" 属性表示组件将上下排列。
    • 视图实例化后需执行的方法,即 c.a.d。所有组件均已实例化之时。属性 creationComplete="init();" 表示应执行第13行中的方法init。creationComplete是类Application可触发的事件之一。
  • 第27-30行定义了视图的组件
  • 第27行:定义一段文本
  • 第28行:定义一个列表,用于放置客户列表。标签 dataProvider="{clients}" 指定了填充该列表的数据源。在此,该列表将通过第11行定义的对象 clients 进行填充。 为了能够写入 dataProvider="{clients}",字段 clients 必须具有属性 [Bindable](第 10 行)。 该属性允许在 <mx:Script> 标签外部引用变量 ActionScript。 字段 clients 的类型为 ArrayCollection,这是一种 ActionScript 类型,用于存储对象列表,此处为 Client 类型的对象列表。
  • 第 29 行:一个按钮。其 click 事件已得到处理。 click="loadClients()" 属性表示,当点击该按钮时,应执行第 17 行中的 loadClients 方法。正是该按钮将触发向 Web 服务请求客户列表的操作。
  • 第 30 行:一个文本框,用于显示服务器针对前一次请求可能返回的错误消息。

第27-30行将在[Design]选项卡中生成以下视图:

  • [1]:由第27行的组件Label生成
  • [2]:由第 28 行中的组件 List 生成
  • [3]:由第 29 行中的组件 Button 生成
  • [4]:由第 30 行中的组件 Text 生成
  • [5]:一个执行示例

现在让我们查看页面中的代码 ActionScript。该代码负责处理视图事件。


<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml" layout="vertical" creationComplete="init();">

    <mx:Script>
        <![CDATA[
            import generated.webservices.Client;
...
            
            // 数据
            private var ws:WsDaoJpaService;
            [Bindable]
            private var clients:ArrayCollection;
            
            private function init():void{
                // 实例化 Web 服务代理
                ws=new WsDaoJpaService();
                // 配置事件处理程序
                ws.addgetAllClientsEventListener(loadClientsCompleted);
                ws.addEventListener(FaultEvent.FAULT,loadClientsFault);
            }
            
            private function loadClients():void{
                // 请求客户端列表
                ws.getAllClients(new GetAllClients());
            }
            
            private function loadClientsCompleted(event:GetAllClientsResultEvent):void{
                // 从返回的结果中获取客户端
                clients=event.result as ArrayCollection;
            }
            
            private function loadClientsFault(event:FaultEvent):void{
                // 显示错误消息
                txtMsgErreur.text=event.fault.message;
            }
            
            private function displayClient(client:Client):String{
                // 显示一个客户
                return client.nom + " " + client.prenom;
            } 
        ]]>
    </mx:Script>
    <mx:Label text="Liste des clients" fontSize="14"/>
    <mx:List dataProvider="{clients}" labelFunction="displayClient"></mx:List>
    <mx:Button label="Afficher les clients" click="loadClients()"/>
<mx:Text id="txtMsgErreur" width="454" height="75"/>

  • 第 9 行:ws 将指定类型为 WsDaoJpaService 的 C 代理,即之前生成的、实现远程 Web 服务访问方法的类。
  • 第 13 行:当视图被实例化时(第 1 行),将执行方法 init
  • 第15行:创建了一个C代理实例
  • 第 17 行:为事件“异步方法 GetAllClients 已成功完成”关联了一个事件处理程序。 对于远程 Web 服务中的任何 m 方法,C 代理都会实现一个 addmEventListener 方法,该方法允许将处理程序关联到“异步方法 m 已成功完成”事件。 此处,第 17 行表示当 Flex 客户端收到客户端列表时,应执行第 26 行中的 loadClientsCompleted 方法。
  • 第 18 行:为“C 代理的异步方法以失败结束”事件关联了一个事件处理程序。 此处第18行表示,每当C代理向Web服务S发出的异步请求失败时,就应执行第31行的方法loadClientsFault。此处唯一会发出的请求是获取客户端列表的请求。
  • 最终,第13行的方法init实例化了C代理,并为后续将发出的异步请求定义了事件处理程序。
  • 第21行:点击按钮[Afficher les clients](第44行)时执行的方法
  • 第 23 行:执行代理 C 的异步方法 getAllClients。向其传递一个 GetAllClients 实例,该实例负责封装被调用的远程方法的参数。此处没有参数。 创建了一个空实例。方法 getAllClients 是异步的。执行过程会继续,而不会等待服务器返回的数据。用户可以继续与视图进行交互。用户触发的事件将继续被处理。通过方法 init,我们知道:
    • 当 Flex 客户端收到客户列表时,将执行方法 loadClientsCompleted(第 26 行)
    • 如果请求以错误结束,则将执行方法 loadClientsFault(第 31 行)。
  • 第 28 行:在事件中获取客户列表。我们知道远程 Web 服务的 getAllClients 方法会返回一个列表。我们将该列表放入第 11 行的 clients 字段中。此处需要进行类型转换。 由于第43行的列表与“客户”字段建立了绑定(Bindable),该字段会收到数据变更的通知。随后,它将显示客户列表。它将使用方法displayClient(第43行)显示列表clients中的每个元素。
  • 第36行:方法displayClient接收类型Client。它必须返回列表应为该客户显示的字符串。此处为姓名(第38行)。
  • 第 31 行:当 Web 服务请求失败时执行的方法。它接收一个类型为 FaultEvent 的参数。 该类有一个 fault 字段,用于封装服务器返回的错误。fault.message 是随错误返回的错误信息。
  • 第 33 行:错误消息显示在专门用于此目的的文本框中。

应用程序构建完成后,其可执行代码位于 Flex 项目的 [bin-debug] 文件夹中:

上文所述,

  • 文件 [rdvmedecins01.html] 代表浏览器将向 Web 服务器请求的 HTML 文件,以获取 Flex 客户端
  • 文件 [rdvmedecins01.swf] 是 Flex 客户端的二进制文件,它将被封装在发送给浏览器的 HTML 页面中,随后由浏览器的 Flash Player 插件执行。

现在我们可以准备运行 Flex 客户端了。但在那之前,我们需要先搭建其所需的运行环境。让我们回到之前测试过的客户端/服务器架构:

服务器端

  • 启动 SGBD MySQL
  • 启动 Glassfish 服务器
  • 如果尚未部署,请部署日程安排 Web 服务 JEE
  • 如有必要,测试其中一个先前客户端,以验证服务器端是否一切就绪。

客户端

启动将处理 Flex 应用程序请求的 Apache 服务器。此处我们使用工具 Wamp。借助该工具,我们可以为 Flex 项目的 [bin-debug] 文件夹分配一个别名。

  • Wamp图标位于屏幕底部 [1]
  • 左键单击图标 Wamp,选择选项 Apache [2] / 别名目录 [3, 4]
  • 选择选项 [5]:添加别名
  • 在 [6] 中为即将运行的 Web 应用程序指定一个别名(任意名称)
  • 在 [7] 中指定将使用该别名的 Web 应用程序的根目录:即我们刚刚构建的 Flex 项目的 [bin-debug] 文件夹。

回顾一下 Flex 项目中 [bin-debug] 文件夹的结构:

文件 [rdvmedecins01.html] 是 Flex 应用程序的 HTML 文件。借助我们刚刚在文件夹 [bin-debug] 上创建的别名,可以通过 URL [http://localhost/rdvmedecins/rdvmedecins01.html] 访问该文件。 我们将在安装了 Flash Player 9 或更高版本插件的浏览器中访问该网址:

  • 在 [1](即 Flex 应用程序的 URL)
  • 在 [2] 中,我们请求客户列表
  • 在 [3] 中,显示一切正常时的结果
  • 在 [4] 中,当 Web 服务已停止运行时请求显示客户所获得的结果。

您可能想查看收到的 HTML 页面的源代码

<!-- 保存自 url=(0014)about:internet -->
<html lang="en">

<!-- 
Smart developers always View Source. 

This application was built using Adobe Flex, an open source framework
for building rich Internet applications that get delivered via the
Flash Player or to desktops via Adobe AIR. 

Learn more about Flex at http://flex.org 
// -->

<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />

<!--   BEGIN 浏览器历史记录必填部分 -->
<link rel="stylesheet" type="text/css" href="history/history.css" />
<!--   END 需要浏览器历史记录的部分 -->

<title></title>
<script src="AC_OETags.js" language="javascript"></script>
...
<script language="JavaScript" type="text/javascript">
<!--
// -----------------------------------------------------------------------------
// 全局变量
// 需要 Flash 的主要版本
var requiredMajorVersion = 9;
// 需要 Flash 的次要版本
var requiredMinorVersion = 0;
// 所需的 Flash 次要版本
var requiredRevision = 124;
// -----------------------------------------------------------------------------
// -->
</script>
</head>

<body scroll="no">
<script language="JavaScript" type="text/javascript">
<!--
// 具有启动播放器产品安装(6.0r65)功能的 Flash Player 版本检查
....
// -->
</script>
<noscript>
        <object classid="clsid:D27CDB6E-AE6D-11cf-96B8-444553540000"
                        id="rdvmedecins01" width="100%" height="100%"
                        codebase="http://fpdownload.macromedia.com/get/flashplayer/current/swflash.cab">
                        <param name="movie" value="rdvmedecins01.swf" />
                        <param name="quality" value="high" />
                        <param name="bgcolor" value="#869ca7" />
                        <param name="allowScriptAccess" value="sameDomain" />
                        <embed src="rdvmedecins01.swf" quality="high" bgcolor="#869ca7"
                                width="100%" height="100%" name="rdvmedecins01" align="middle"
                                play="true"
                                loop="false"
                                quality="high"
                                allowScriptAccess="sameDomain"
                                type="application/x-shockwave-flash"
                                pluginspage="http://www.adobe.com/go/getflashplayer">
                        </embed>
        </object>
</noscript>
</body>
</html>

页面主体从第 39 行开始。它不包含经典的 HTML 代码,而是一个类型为 "application/x-shockwave-flash"(第 60 行)的对象(第 47 行)。 这就是文件 [rdvmedecins01.swf](第 54 行),可在 Flex 项目的 [bin-debug] 文件夹中看到。这是一个体积较大的文件:仅此简单示例就约有 600 KB。

6.2. 第二个 Flex 客户端

第二个 Flex 客户端将不使用为第一个客户端生成的 C 语言代理。我们想说明,尽管该步骤相对于接下来将介绍的方法具有优势,但它并非必不可少。

项目演变如下:

  • 演变为 [1](新的 Flex 应用程序)
  • 变为 [2] 及其关联的可执行文件
  • [3] 新的视图:我们将显示医生列表。

应用程序 [rdvmedecins02.mxml] 的代码 MXML 如下:


<?xml version="1.0" encoding="utf-8"?>
<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml" layout="vertical">
    <mx:Script>
        <![CDATA[
            import mx.rpc.events.ResultEvent;
            import mx.rpc.events.FaultEvent;
            import mx.collections.ArrayCollection;
            
            // 数据
            [Bindable]
            private var medecins:ArrayCollection;
            
            private function loadMedecins():void{
                // 请求医生列表
                wsrdvmedecins.getAllMedecins.send();
            }
            
            private function loadMedecinsCompleted(event:ResultEvent):void{
                // 获取医生
                medecins=event.result as ArrayCollection;
            }
            
            private function loadMedecinsFault(event:FaultEvent):void{
                // 显示错误信息
                txtMsgErreur.text=event.fault.message;
            } 
            
            // 显示某位医生
            private function displayMedecin(medecin:Object):String{
                return medecin.nom + " " + medecin.prenom;
            } 

        ]]>
    </mx:Script>
    <mx:WebService id="wsrdvmedecins" 
        wsdl="http://localhost:8080/serveur-webservice-ejb-dao-jpa-hibernate/WsDaoJpaService?wsdl">
        <mx:operation name="getAllMedecins" 
            result="loadMedecinsCompleted(event)" fault="loadMedecinsFault(event);">
            <mx:request/>
        </mx:operation>
    </mx:WebService>
    <mx:Label text="Liste des médecins" fontSize="14"/>
    <mx:List dataProvider="{medecins}" labelFunction="displayMedecin"></mx:List>
    <mx:Button label="Afficher les médecins" click="loadMedecins()"/>
    <mx:Text id="txtMsgErreur" width="300" height="113"/>
    
</mx:Application>

我们仅对新增内容进行说明:

  • 第 42 至 45 行:新视图。它与之前的视图完全相同,只是经过调整以显示医生而非客户。
  • 第35-41行:此处的Web服务由<mx:WebService>标签(第35行)描述。此处不再使用上一版本中使用的C代理。
  • 第 35 行:id 属性为 Web 服务命名。
  • 第 36 行:wsdl 属性指定了 Web 服务中 WSDL 文件的 URI。该 URI 与前一个客户端使用的 URI 相同,并在第 4.10.2 节中进行了定义。
  • 第 37-40 行:通过 <mx:operation> 标签定义远程 Web 服务的某个方法
  • 第 37 行:引用的方法由 name 属性定义。此处我们引用了远程方法 getAllMedecins
  • 第 38 行:定义操作成功时(result 属性)和失败时(fault 属性)要执行的方法。
  • 第 39 行:<mx:request> 标签用于定义操作的参数。 此处远程方法 getAllMedecins 没有参数,因此我们不填写任何内容。对于接受参数的方法 param1 和 param2,应写为:
<mx:Request>
    <param1>{param1}</param1>
    <param1>{param1}</param1>
</mx:Request>

其中 param1 param2 是声明并初始化在 <mx:Script> 标签中的变量

[Bindable]
private var param1:Type1;
[Bindable]
private var param2:Type2;

在 <mx:Script> 标签中,我们可以看到与前一个客户端中分析过的代码类似的 ActionScript 代码。 唯一不同的是第13至16行中的方法 loadMedecins。区别在于调用远程方法 [getAllMedecins] 的方式:

  • 第15行:使用第35行定义的Web服务[wsrdvmedecins]及其第37行定义的操作[getAllMedecins]。为执行该操作,使用了send方法。 正是该方法启动了对第 35 行定义的 Web 服务中 getAllMedecins 方法的异步调用。 方法 send 将使用第 39 行 <mx:request> 标签定义的参数进行调用。此处没有参数。 如果该方法包含参数 param1 param2,则脚本 loadMedecins 会在调用方法 send 之前为这些参数赋值。

现在我们只需测试这个新应用程序: