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 的集成开发环境(IDE)。此外,为运行 Flex 客户端,我们使用了 Wamp 工具 [http://www.wampserver.com/] 提供的 Apache Web 服务器。任何 Apache 服务器均可满足需求。显示 Flex 客户端的浏览器必须安装 Flash Player 9 或更高版本。

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

与桌面应用程序类似,Flex 应用程序主要由两个要素组成:

  • 展示层:即在浏览器中显示的视图。这些视图提供的丰富功能与桌面应用程序窗口相当。视图使用一种名为 MXML 的标记语言进行描述。
  • 代码组件:主要处理用户在视图上操作所触发的事件。该代码既可以用 MXML 编写,也可以使用一种名为 ActionScript 的面向对象语言编写。必须区分两种类型的事件:
  • 需要与 Web 服务器通信的事件:例如使用 Web 应用程序提供的数据填充列表、将表单数据提交至服务器等。Flex 提供了多种与服务器通信的方法,这些方法对开发者而言是透明的。这些方法默认是异步的:在服务器请求进行期间,用户可以继续与视图进行交互。
  • 无需与服务器交换数据即可修改显示视图的事件,例如将树中的项目拖拽并放入列表中。此类事件完全在浏览器本地处理。

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

  • 在 [1] 中,会请求一个 HTML 页面
  • 在 [2] 处,该页面被发送出去。其中包含一个 SWF(ShockWave Flash)二进制文件,该文件包含整个 Flex 应用程序:所有视图及其事件处理代码。该文件将由浏览器的 Flash Player 插件执行。
  • 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 集成开发环境 (IDE) 构建 Flex 客户端:

  • 在 Flex Builder 3 中,在 [1] 处创建一个新项目
  • 在 [2] 中为其命名,并在 [3] 中指定生成项目的文件夹
  • 在 [4] 中,为主要应用程序(即将被执行的程序)命名
  • 在 [5] 中,生成后的项目
  • 在 [6] 中,应用程序的主 MXML 文件
  • MXML 文件包含一个视图及其对应的事件处理代码。[源代码] 选项卡 [7] 提供了对 MXML 文件的访问。在那里,您将看到描述视图的 <mx> 标签以及 ActionScript 代码。
  • 可以通过 [设计] 选项卡 [8] 以图形化方式构建视图。随后,描述该视图的 MXML 标签会自动生成在 [源代码] 选项卡中。反之亦然:直接在 [源代码] 选项卡中添加的 MXML 标签也会在 [设计] 选项卡中以图形形式显示。

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

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

  • 在 [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 代理类和接口的列表。
  • 在 [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
    {
        //Stub functions for the getAllClients operation
        /**
         * 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 不接受任何参数。

现在,让我们检查一下为 DoctorClientAppointment Time Slot 实体生成的类。例如,我们来看一下 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 行)继承自以下 Person 类:


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 行:这些是 JEE Web 服务中定义的 Person 类的属性。

现在我们已经有了 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;
...
 
            // données
            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" 属性表示组件将上下排列。
    • 在视图实例化完成时(即所有组件均已实例化)将执行的方法。属性 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="loadClients()" 属性表示当点击该按钮时,必须执行第 17 行中的 loadClients 方法。该按钮将触发向 Web 服务请求客户列表的操作。
  • 第 30 行:一个文本框,用于显示服务器针对前一请求可能返回的任何错误消息。

第 27–30 行在 [设计] 选项卡中生成以下视图:

  • [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;
...
 
            // données
            private var ws:WsDaoJpaService;
            [Bindable]
            private var clients:ArrayCollection;
 
            private function init():void{
                // on instancie le proxy du service web
                ws=new WsDaoJpaService();
                // on configure les gestionnaires d'évts
                ws.addgetAllClientsEventListener(loadClientsCompleted);
                ws.addEventListener(FaultEvent.FAULT,loadClientsFault);
            }
 
            private function loadClients():void{
                // on demande la liste des clients
                ws.getAllClients(new GetAllClients());
            }
 
            private function loadClientsCompleted(event:GetAllClientsResultEvent):void{
                // on récupère les clients dans le résultat envoyé
                clients=event.result as ArrayCollection;
            }
 
            private function loadClientsFault(event:FaultEvent):void{
                // on affiche le msg d'erreur
                txtMsgErreur.text=event.fault.message;
            }
 
            private function displayClient(client:Client):String{
                // on affiche un client
                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 行:点击 [显示客户端] 按钮(第 44 行)时执行的方法
  • 第 23 行:执行代理 C 的异步 getAllClients 方法。该方法接收一个 GetAllClients 实例,该实例负责封装被调用远程方法的参数。此处没有参数,因此创建了一个空实例。getAllClients 方法是异步的,执行过程不会等待服务器返回的数据。特别是,用户可以继续与视图进行交互。 用户触发的事件将继续被处理。得益于 init 方法,我们知道:
    • 当 Flex 客户端收到客户端列表时,将执行 loadClientsCompleted 方法(第 26 行)
    • 如果请求出现错误,将执行 loadClientsFault 方法(第 31 行)
  • 第 28 行:从事件中获取客户列表。 我们知道远程 Web 服务的 getAllClients 方法返回一个列表。我们将该列表赋值给第 11 行的 clients 字段。此处需要进行类型转换。由于第 43 行的列表与 clients 字段绑定,因此会收到数据已更改的通知。随后它将显示客户列表。它将使用 displayClient 方法(第 43 行)显示 clients 列表中的每个项目。
  • 第 36 行:displayClient 方法接受 Client 类型的参数。它必须返回列表应为此客户显示的字符串。此处为姓名和姓氏(第 38 行)。
  • 第 31 行:当 Web 服务请求失败时执行的方法。它接收一个 FaultEvent 类型的参数。该类有一个 `fault` 字段,用于封装服务器返回的错误。`fault.message` 是伴随该错误的错误信息。
  • 第 33 行:错误消息将显示在为此目的提供的文本框中。

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

上文中的

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

现在我们可以运行 Flex 客户端了。首先,我们需要设置其所需的运行时环境。让我们回到之前测试过的客户端/服务器架构:

服务器端

  • 启动 MySQL 数据库管理系统
  • 启动 GlassFish 服务器
  • 如果尚未部署,请部署用于预约的 JEE Web 服务
  • 可选:测试其中一个先前客户端,以验证服务器端一切就绪。

客户端

启动将托管 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 或更高版本插件的浏览器中访问此 URL:

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

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

<!-- saved from 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 Browser History required section -->
<link rel="stylesheet" type="text/css" href="history/history.css" />
<!--  END Browser History required section -->

<title></title>
<script src="AC_OETags.js" language="javascript"></script>
...
<script language="JavaScript" type="text/javascript">
<!--
// -----------------------------------------------------------------------------
// Globals
// Major version of Flash required
var requiredMajorVersion = 9;
// Minor version of Flash required
var requiredMinorVersion = 0;
// Minor version of Flash required
var requiredRevision = 124;
// -----------------------------------------------------------------------------
// -->
</script>
</head>

<body scroll="no">
<script language="JavaScript" type="text/javascript">
<!--
// Version check for the Flash Player that has the ability to start Player Product Install (6.0r65)
....
// -->
</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] 新的视图中:我们将显示医生列表。

该应用程序的 MXML 代码 [rdvmedecins02.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;
 
            // données
            [Bindable]
            private var medecins:ArrayCollection;
 
            private function loadMedecins():void{
                // on demande la liste des médecins
                wsrdvmedecins.getAllMedecins.send();
            }
 
            private function loadMedecinsCompleted(event:ResultEvent):void{
                // on récupère les médecins
                medecins=event.result as ArrayCollection;
            }
 
            private function loadMedecinsFault(event:FaultEvent):void{
                // on affiche le msg d'erreur
                txtMsgErreur.text=event.fault.message;
            } 
 
            // affichage d'un médecin
            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 行:此处通过 <mx:WebService> 标签(第 35 行)描述了 Web 服务。此处不再使用上一版本中使用的 C 语言代理。
  • 第 35 行:id 属性为 Web 服务指定了一个名称。
  • 第 36 行:wsdl 属性指定 Web 服务 WSDL 文件的 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 方法之前为这些参数赋值。

接下来只需测试这个新应用程序: