WEB开发网
开发学院软件开发Java 使用 GWT 和 RESTful Web 服务构建动态的组织树 阅读

使用 GWT 和 RESTful Web 服务构建动态的组织树

 2010-04-22 00:00:00 来源:WEB开发网   
核心提示:简介最近几年,Web 应用程序开发的潮流是创建富 Internet 应用程序,使用 GWT 和 RESTful Web 服务构建动态的组织树,其中大多数是使用 Asynchronous JavaScript + XML (Ajax) 实现的,但是,本文基于由 National Science Foundation 支持

简介

最近几年,Web 应用程序开发的潮流是创建富 Internet 应用程序,其中大多数是使用 Asynchronous JavaScript + XML (Ajax) 实现的。但是,由于编写 JavaScript 代码比较复杂,这种方法并不容易,尤其是很难构建大型 Web 应用程序。因此出现了 GWT:它让我们能够使用 Java 编程而不是 Ajax 构建功能丰富的响应性的 Web 界面。GWT 还提供 Java 开发的所有优势,比如出色的 IDE 支持和高级调试功能。GWT 可以显著提高生产力并丰富用户的体验。在本文中,我要解释如何在 Eclipse 中构建 GWT 应用程序,以及如何使用 GWT Tree 和 TreeItem 部件为 University Corporation of Atmosphere Research (UCAR) 创建一个示例组织结构。我要解释如何实现惰性装载、如何与 RESTful Web 服务集成以及如何实现 GWT 回调和定制的异常。使用 JSON 作为 RESTful Web 服务的数据格式。

软件需求

首先,需要下载以下软件包,按照相应网站上的安装指南安装它们。

Eclipse IDE for Java EE Developers Galileo (Eclipse 3.5)

GWT 2.0

GWT plug-in for Eclipse

MySQL 5.1 或 DB2® Express-C

Tomcat 6.x

RESTful Web 服务

RESTful Web 服务为 GWT 客户机提供组织数据。在本文中,我不讨论实现 RESTful Web 服务的步骤;您只需设置数据库并把 WAR 文件部署到 Tomcat 服务器上。可能还需要在配置文件中修改几个数据库属性,比如数据库主机、登录名和密码。

设置数据库

我使用 MySQL Community Server 5.1 作为本文的数据库。但是,也可以使用 DB2 Express-C 等其他数据库在您选择的主机上下载并安装它(如果还没有这么做的话)。然后创建数据库 gwtresttutorial 和用户 gwtresttutorial,密码为 gwtresttutorial。连接 gwtresttutorial 数据库并作为 gwtresttutorial 登录。从 下载 部分下载 sql 脚本,运行脚本以创建表并在表中插入数据。

对于让 RESTful Web 服务服务器连接 DB2 Express-C 或其他 DB2 系列数据库,所用的配置与清单 1 中的 MySQL 配置非常相似,只需做以下修改:

使用 com.ibm.db2.jcc.DB2Driver 作为 driverClassName。

使用 jdbc:db2://<host>:<port>/<database_name> 作为 URL,其中的 host 是安装 DB2 Express-C 的主机的名称,port 是用于访问数据库的端口号,database_name 是数据库实例的名称。

把 DB2 Express-C 安装目录中的 db2jcc.jar 和 db2jcc_license_cu.jar 文件复制到 WEB-INF/lib 目录。

可能需要修改下载的 setup.sql 脚本,改用 DB2 语法。

把 WAR 文件部署到 Tomcat 服务器

从 下载 部分下载 WAR 文件并把它保存到 Tomcat 文件夹 <TOMCAT_HOME>/webapps 中,其中的 TOMCAT_HOME 是安装 Tomcat 服务器的位置。

把 WAR 文件部署到 <TOMCAT_HOME>/webapps 目录之后,如果 Tomcat 服务器正在运行,它会把 WAR 文件解压为 gwtRESTTutorial 文件夹。检查 <TOMCAT_HOME>/webapps/gwtRESTTutorial/WEB-INF/classes /applicationContext.xml(清单 1),确保 dataSource bean 的配置值与 MySQL 数据库所用的值匹配。注意,如果做了任何修改,可能需要重新启动 Tomcat 服务器。

清单 1. 在 applicationContext.xml 中配置 dataSource bean

1. <bean id="dataSource" 
class="org.springframework.jdbc.datasource.DriverManagerDataSource"> 
2.   <property name="driverClassName" value="com.mysql.jdbc.Driver"/> 
3.   <property name="url" value="jdbc:mysql://localhost:3306/gwtresttutorial"/> 
4.   <property name="username" value="gwtresttutorial"/> 
5.   <property name="password" value="gwtresttutorial"/> 
6. </bean>        

访问 RESTful Web 服务

我为本文实现了两个 RESTful Web 服务。第一个提供特定职员的信息。访问这个 Web 服务的 URI 是 http://localhost:8080/gwtRESTTutorial/rrh/employees/<EMP_ID>,其中的 EMP_ID 是职员的 ID。它返回一个 JSON 字符串,其中包含详细的职员数据。清单 2 给出返回的 JSON 字符串的示例。

清单 2. 职员 RESTful Web 服务返回的示例 JSON 数据

1. { 
2.   "id":20, 
3.   "firstName":"Robert", 
4.   "nickName":"Bob", 
5.   "lastName":"Sunny", 
6.   "title":"SE", 
7.   "phone":"303-123-1234", 
8.   "email":bobs@ucar.edu 
9. }        

第二个 Web 服务提供特定组织单元的信息。它的 URI 是 http://localhost:8080/gwtRESTTutorial/rrh/organizations/<ORG_ID>,其中的 ORG_ID 是组织单元的 ID。与职员服务一样,它返回一个 JSON 字符串,其中包含详细的组织数据(清单 3)。详细数据包含 ID、首字母缩写、名称、负责人姓名、负责人头衔、组织及其各级子组织中职员的总数。它还包含在这个组织单元工作的职员的数据数组和直接子组织单元的数据数组。职员数据和子组织数据只包含 ID 和显示名。

清单 3. 组织 RESTful Web 服务返回的示例 JSON 数据

1. { 
2.   "id":1, 
3.   "acronym":"NCAR", 
4.   "name":"National Center for Atmospheric Research", 
5.   "leadName":"Dan Bush -Director", 
6.   "leadTitle":"Director", 
7.   "totalEmployees":15, 
8.   "employees": 
9.   [{ 
10.     "id":2, 
11.     "displayName":"Dan Bush - Director" 
12.   }, 
13.   { 
14.     "id":3, 
15.     "displayName":"Lori Stanley - Deputy Director" 
16.   }], 
17.   "subOrgs": 
18.   [{ 
19.     "id":3, 
20.     "displayName":"CISL" 
21.   }, 
22.   { 
23.     "id":5, 
24.     "displayName":"EOL" 
25.   }, 
26.   { 
27.     "id":6, 
28.     " displayName ":"RAL" 
29.   }, 
30.   { 
31.     "id":4, 
32.     "displayName":"ESSL" 
33.   }] 
34. }        

在 Eclipse 中创建 GWT 应用程序

本文使用具有 GWT 支持的 Eclipse 作为开发 GWT 应用程序的环境。在 Eclipse 中:

选择 File > New > Web Application Project。

在 New Web Application Project 窗口中的 Project Name 框中输入 gwtRESTTutorialView,在 Package 框中输入 edu.ucar.cisl.gwtRESTTutorialView(见图 1)。

选择 Use Default SDK 并在 Google SDKs 中选择 GWT-2.0.0 或更新的版本。

图 1. 创建新的 Web 应用程序项目
使用 GWT 和 RESTful Web 服务构建动态的组织树

Eclipse 中的 GWT 插件自动地创建一个示例远程服务。可以通过删除 edu.ucar.cisl.gwtRESTTutorialView.client 包中的 GreetingService.java 和 GreetingServiceAsync.java 文件以及 edu.ucar.cisl.gwtRESTTutorialView.server 包中的 GeetingServiceImpl.java 来删除它。还需要删除 web.xml 文件中这个远程服务的 servlet 配置,删除 WAR 文件夹中的 GwtRESTTutorialView.html 文件中的 <body> 和 </body> 之间的所有内容。

下面几节详细讨论几个主题,比如创建数据 bean、通过实现 RPC 代理访问 RESTful Web 服务和回调以及构建 GWT Web 界面。这些组件位于下面四个包中。(如果在 Eclipse 中没有的话,就创建它们。)可以从 下载 部分下载源代码。

edu.ucar.cisl.gwtRESTTutorialView.client.bean — 包含客户机的应用程序 Java bean。

edu.ucar.cisl.gwtRESTTutorialView.client.callback — 包含回调类的实现。

edu.ucar.cisl.gwtRESTTutorialView.client — 包含模块入口类 GwtRESTTutorialView。它还包含用于创建 GWT Web 界面的其他几个接口、类和图像文件。RPC 代理的客户端类也在这个包中。

edu.ucar.cisl.gwtRESTTutorialView.server — 包含 RPC 代理的服务器端实现的类。

实现应用程序数据 bean

在本文中,我使用 Tree 部件显示组织结构。在 GWT 中,Tree 部件包含通常用作树节点的 TreeItem 部件。这里使用 TreeItem 部件作为树节点或树叶,分别代表组织单元和职员。我实现了一个抽象基类 ItemData(清单 4),它有三个属性:id、displayName 和 dataReady。id 是数据条目的 ID,用于构建 RESTful Web 服务请求。它标识 RESTful Web 服务服务器中的资源。displayName 属性是要显示的名称。dataReady 属性是一个标志,表示是否已经从 RESTful Web 服务服务器获得了详细数据,它用于帮助实现惰性装载。在创建 TreeItem 部件时,把一个 ItemData bean 与这个部件关联起来。它只有资源 ID 和显示名。在子类中声明的详细数据只在用户选择这个树叶或打开树节点时才会装载。使用抽象方法 buildURI 构建 RESTful Web 服务请求的 URI,这个方法由子类 EmployeeItemData(清单 5)和 OrganizationItemData(清单 6)实现。EmployeeItemData 包含职员的详细信息,OrganizationItemData 包含组织单元的详细信息。

清单 4. edu.ucar.cisl.gwtRESTTutorialView.client.bean.ItemData

1. package edu.ucar.cisl.gwtRESTTutorialView.client.bean; 
  
2. public abstract class ItemData { 
3.   protected int id = -1;   
4.   protected String displayName; 
5.   protected boolean dataReady = false; 
  
6.    ...//setters and getters 
 
7.   abstract public String buildUri(); 
8. }        

清单 5. edu.ucar.cisl.gwtRESTTutorialView.client.bean.EmployeeItemData

1. package edu.ucar.cisl.gwtRESTTutorialView.client.bean; 
  
2. public class EmployeeItemData extends ItemData { 
3.   protected String firstName; 
4.   protected String lastName; 
5.   protected String nickName; 
6.   protected String phone; 
7.   protected String email; 
8.   protected String title; 
  
9.    ...//setters and getters 
 
10.   public String buildUri(){ 
11.     return "http://localhost:8080/gwtRESTTutorial/rrh/employees/" + id; 
12.   } 
13. }

清单 6. edu.ucar.cisl.gwtRESTTutorialView.client.bean.OrganizationItemData

1. package edu.ucar.cisl.gwtRESTTutorialView.client.bean; 
 
2. public class OrganizationItemData extends ItemData { 
3.   protected String name; 
4.   protected String leadName; 
5.   protected String leadTitle; 
6.   protected int totalEmployees; 
  
7.   ...//getters and setters 
  
8.   public String buildUri() { 
9.     return "http://localhost:8080/gwtRESTTutorial/rrh/organizations/" + id; 
10.  } 
11. }

实现 RPC 代理以请求 RESTful Web 服务

有几种集成 GWT 和 RESTful Web 服务的策略。如果 RESTful Web 服务在相同的域和端口上运行,明显的方法是使用 GWT RequestBuilder 类。但是,RequestBuilder 类无法克服 Same Original Policy (SOP) 限制,即禁止对不同域中的 Web 服务服务器发出请求。为了避免 SOP 限制,我使用 RPC 代理策略。按照这种策略,GWT 客户机把 RESTful Web 服务发送给 RPC 远程服务,RPC 远程服务把请求传递给 RESTful Web 服务服务器。

创建定制的异常类

需要一个特殊的定制异常,让服务器可以把异常传递给客户机。GWT 提供一种非常简便的实现方法。只需让定制的异常类扩展 Exception 类并实现 IsSerializable 接口。定制的异常见清单 7。

清单 7. edu.ucar.cisl.gwtRESTTutorialView.client.RESTfulWebServiceException

1. package edu.ucar.cisl.gwtRESTTutorialView.client; 
 
2. import com.google.gwt.user.client.rpc.IsSerializable; 
 
3. public class RESTfulWebServiceException extends Exception implements IsSerializable { 
4.   private static final long serialVersionUID = 1L; 
5.   private String message; 
  
6.   public RESTfulWebServiceException() { 
7.   } 
  
8.   public RESTfulWebServiceException(String message) { 
9.     super(message); 
10.     this.message = message; 
11.   } 
 
12.   public RESTfulWebServiceException(Throwable cause) { 
13.     super(cause); 
14.   } 
 
15.   public RESTfulWebServiceException(String message, Throwable cause) { 
16.     super(message, cause); 
17.     this.message = message; 
18.   } 
 
19.   public String getMessage() { 
20.     return message; 
21.   } 
22. }        

创建远程服务接口

对于每个远程服务,GWT 在客户端需要两个接口:一个远程服务接口和一个远程服务异步接口。远程服务接口必须扩展 GWT RemoteService 接口并定义将向客户机公开的服务方法的签名。方法参数和返回类型必须是可序列化的。

本文使用的远程服务接口非常简单(清单 8)。它只声明一个方法,invokeGetRESTfulWebService。这个方法有两个参数,uri 和 contentType。前者是标识 RESTful Web 服务服务器上要请求的资源的 URI。后者表示应该返回的结果的内容类型。内容类型是标准 HTTP 内容类型之一,比如 application/json、application/xml、application/text 等。这个方法返回 HTTP 响应中的内容字符串,在失败时抛出定制的异常。

需要添加一个 RemoteServiceRelativePath 注解以指定服务的 URL 路径(清单 8 中的第 5 行)。通过创建简单的实用程序类很容易获得异步远程接口的实例(清单 8 中的第 7-13 行)。

清单 8. edu.ucar.cisl.gwtRESTTutorialView.client.RESTfulWebServiceProxy

1. package edu.ucar.cisl.gwtRESTTutorialView.client; 
2. import com.google.gwt.core.client.GWT; 
3. import com.google.gwt.user.client.rpc.RemoteService; 
4. import com.google.gwt.user.client.rpc.RemoteServiceRelativePath; 
  
  
5. @RemoteServiceRelativePath("RESTfulWebServiceProxy") 
6. public interface RESTfulWebServiceProxy extends RemoteService { 
7.   public static class Util { 
8.     public static RESTfulWebServiceProxyAsync getInstance() { 
9.       RESTfulWebServiceProxyAsync 
10. rs=(RESTfulWebServiceProxyAsync)GWT.create(RESTfulWebServiceProxy.class); 
11.       return rs; 
12.     } 
13.   } 
14. 
15.   public String invokeGetRESTfulWebService(String uri, String contentType) 
16.     throws RESTfulWebServiceException; 
17. }        

创建远程服务异步接口

远程服务异步接口基于远程服务接口。服务的异步接口必须在相同的包中,名称相同,但是带 “Async” 后缀。每个远程服务方法都有对应的异步方法。但是,异步方法不能有返回类型,它们必须总是返回 void。异步方法不但必须以相同的次序声明相同的参数,而且必须声明另一个泛型 AsyncCallback<T> 参数,其中的 T 是远程服务方法的返回类型。异步方法不抛出异常。清单 9 是示例应用程序的远程服务异步接口。

清单 9. edu.ucar.cisl.gwtRESTTutorialView.client.RESTfulWebServiceProxyAsync

 
1. package edu.ucar.cisl.gwtRESTTutorialView.client; 
 
2. import com.google.gwt.user.client.rpc.AsyncCallback; 
 
3. public interface RESTfulWebServiceProxyAsync { 
4.   public void invokeGetRESTfulWebService 
(String uri, String contentType, AsyncCallback<String> callback); 
5.   }

在服务器上实现代理服务

在一个扩展 GWT RemoteServiceServlet 类的服务器端类中实现远程服务。在 RESTful Web 服务代理(清单 10)中,这个类实现远程服务 invokeGetRESTfulWebService。这个方法根据 URI 和内容类型构建一个 HTTP 请求并把它发送给 RESTful Web 服务服务器。如果响应码是 200,就缓冲 HTTP 响应中的内容并使用它作为方法的返回值。否则,抛出一个定制的异常。方法捕捉其他异常(比如 MalformedURLException 和 IOException)并抛出定制的异常,让 GWT 客户机可以捕捉到它。

清单 10. edu.ucar.cisl.gwtRESTTutorialView.server.RESTfulWebServiceProxyImpl

1. package edu.ucar.cisl.gwtRESTTutorialView.server; 
 
2. import java.io.BufferedReader; 
3. import java.io.IOException; 
4. import java.io.InputStream; 
5. import java.io.InputStreamReader; 
6. import java.net.HttpURLConnection; 
7. import java.net.MalformedURLException; 
8. import java.net.URL; 
9. import com.google.gwt.user.server.rpc.RemoteServiceServlet; 
10. import edu.ucar.cisl.gwtRESTTutorialView.client.RESTfulWebServiceProxy; 
11. import edu.ucar.cisl.gwtRESTTutorialView.client.RESTfulWebServiceException; 
 
12. public class RESTfulWebServiceProxyImpl extends RemoteServiceServlet 
13.   implements RESTfulWebServiceProxy { 
14.   private static final long serialVersionUID = 1L; 
 
15.   public RESTfulWebServiceProxyImpl() { // must have 
16.   } 
 
17.   public String invokeGetRESTfulWebService(String uri, String contentType) 
18.     throws RESTfulWebServiceException { 
19.     try { 
20.       URL u = new URL(uri); 
21.       HttpURLConnection uc = (HttpURLConnection) u.openConnection(); 
22.       uc.setRequestProperty("Content-Type", contentType); 
23.       uc.setRequestMethod("GET"); 
24.       uc.setDoOutput(false); 
25.       int status = uc.getResponseCode(); 
26.       if (status != 200) 
27.         throw (new RESTfulWebServiceException("Invalid HTTP response status 
28.           code " + status + " from web service server.")); 
29.       InputStream in = uc.getInputStream(); 
30.       BufferedReader d = new BufferedReader(new InputStreamReader(in)); 
31.       String buffer = d.readLine(); 
32.       return buffer; 
33.       } 
34.       catch (MalformedURLException e) { 
35.         throw new RESTfulWebServiceException(e.getMessage(), e); 
36.       } 
37.       catch (IOException e) { 
38.         throw new RESTfulWebServiceException(e.getMessage(), e); 
39.       } 
40.   } 
41. }        

实现回调

大多数 GWT 书籍和在线教程中的回调示例是用匿名内部类实现的。在本文中,我用真正的类实现回调。这种方法有几个优点。它让代码更加清晰。它允许在运行时把客户机数据与回调类关联起来。回调类的灵活性、可扩展性和代码可重用性更强。例如,可以在回调基类中实现错误处理方法,让所有回调都可以使用它,从而确保以一致的方式处理所有远程服务异常。可以轻松地调试回调类中的代码,而并非所有 IDE 都支持跟踪内部类。

在本文中,我创建一个抽象基类 RestServiceRpcCallback(清单 11)和两个子类 EmployeeRpcCallback(清单 12)和 OrganizationRpcCallback(清单 13)。在清单 11 中,抽象类实现 AsyncCallback 接口。如果服务器请求成功,就调用 onSuccess 方法。否则,调用 onFailure 方法。onFailure 方法显示从服务器传递来的错误消息。onSuccess 方法调用 processResponse 方法处理 RESTful Web 服务服务器返回的字符串。抽象方法 processResponse 由子类实现。抽象基类有一个成员 treeItem,它是 GWT 中 TreeItem 部件的实例,包含客户机数据,在使用回调类时回调与这些数据相关联。根据 TreeItem 部件代表的内容不同,这个类成员将包含存储职员数据或组织数据的应用程序对象。TreeItem 部件用于帮助创建子树和确定弹出窗口的位置。

我创建了一个枚举类型 EventType 和类成员 eventType。这个类成员用于跟踪哪个事件触发了对 RESTful Web 服务服务器的请求,在 RESTful Web 服务服务器返回结果之后,回调需要根据它决定如何处理结果。

清单 11. edu.ucar.cisl.gwtRESTTutorialView.client.callback.RestServiceRpcCallback

1. package edu.ucar.cisl.gwtRESTTutorialView.client.callback; 
 
2. import com.google.gwt.user.client.rpc.AsyncCallback; 
3. import com.google.gwt.user.client.ui.TreeItem; 
4. import com.google.gwt.user.client.Window; 
 
5. public abstract class RestServiceRpcCallback implements AsyncCallback <String> { 
6.   TreeItem treeItem; 
7.   public enum EventType {SELECT_EVENT, STATE_CHANGE_EVENT}; 
8.   protected EventType eventType; 
 
9.   public EventType getEventType() { 
10.     return eventType; 
11.   } 
 
12.   public void setEventType(EventType eventType) { 
13.     this.eventType = eventType; 
14.   } 
 
15.   public TreeItem getTreeItem() { 
16.     return treeItem;} 
 
17.   public void setTreeItem(TreeItem treeItem) { 
18.     this.treeItem = treeItem; 
19.   } 
 
20.   public void onSuccess(String result) { 
21.     if (result == null) 
22.       return; 
23.     processResponse(result); 
24.   } 
 
25.   public void onFailure(Throwable caught) { 
26.     String msg=caught.getMessage(); 
27.     if (msg != null) 
28.       Window.alert(msg); 
29.   } 
 
30.   protected abstract void processResponse(String response); 
 
31. }

EmployeeRpcCallback 中的 processResponse 方法(清单 12 中的第 8 - 25 行)处理 RESTful Web 服务服务器返回的字符串。这个字符串包含 JSON 格式的职员数据。这个方法使用 GWT JSON 实用程序类解析 JSON 字符串,把详细的职员数据存储在应用程序对象 EmployeeItemData 中(这个对象包含在类成员 treeItem 中)。然后,它把 dataReady 标志设置为 true,表示在用户下一次单击这个节点时不需要向 RESTful Web 服务请求职员数据。最后,方法打开一个弹出窗口以显示职员的详细信息。

清单 12. edu.ucar.cisl.gwtRESTTutorialView.client.callback.EmployeeRpcCallback

1. package edu.ucar.cisl.gwtRESTTutorialView.client.callback; 
 
2. import com.google.gwt.json.client.JSONObject; 
3. import com.google.gwt.json.client.JSONParser; 
4. import com.google.gwt.json.client.JSONValue; 
 
5. import edu.ucar.cisl.gwtRESTTutorialView.client.EmployeePopup; 
6. import edu.ucar.cisl.gwtRESTTutorialView.client.bean.EmployeeItemData; 
7. import edu.ucar.cisl.gwtRESTTutorialView.client.bean.ItemData; 
 
8. public class EmployeeRpcCallback extends RestServiceRpcCallback { 
9.   protected void processResponse(String response) { 
10.     JSONValue jsonValue = JSONParser.parse(response); 
11.     ItemData iData = (ItemData) treeItem.getUserObject(); 
12.     JSONObject jobj = jsonValue.isObject(); 
13.     EmployeeItemData eItemData = (EmployeeItemData) iData; 
14.     eItemData.setId((int) jobj.get("id").isNumber().doubleValue()); 
15.     eItemData.setFirstName(jobj.get("firstName").isString().stringValue()); 
16.     eItemData.setNickName(jobj.get("nickName").isString().stringValue()); 
17.     eItemData.setLastName(jobj.get("lastName").isString().stringValue()); 
18.     eItemData.setPhone(jobj.get("phone").isString().stringValue()); 
19.     eItemData.setEmail(jobj.get("email").isString().stringValue()); 
20.     eItemData.setTitle(jobj.get("title").isString().stringValue()); 
21.     iData.setDataReady(true); 
22.     int left = treeItem.getAbsoluteLeft() + 50; 
23.     int top = treeItem.getAbsoluteTop() + 30; 
24.     EmployeePopup.show(left, top, (EmployeeItemData) eItemData); 
25.   } 
26.   }

OrganizationRpcCallback 中的 processResponse 方法(清单 13 中的第 10 – 31 行)处理 RESTful Web 服务服务器返回的组织数据。与职员数据一样,返回的组织数据也是 JSON 字符串。组织数据包含组织单元的详细信息,以及这个组织单元中的职员和直接子组织的部分信息。这个方法使用 GWT JSON 实用程序类解析 JSON 字符串,把详细的组织数据存储在应用程序对象 OrganizationItemData 中(这个对象包含在类成员 treeItem 中)。然后,它把 dataReady 标志设置为 true,表示详细的组织数据已经放在内存中了。该方法调用 processEmployees 方法处理组织单元中职员的数据,调用 processSubOrgs 处理子组织的数据。最后,如果事件是 Select,它会打开一个弹出窗口以显示详细的组织信息,比如完整名称、负责人姓名和头衔以及职员总数(包括在所有子组织中工作的职员)。

processEmployees 方法(第 44 – 54 行)处理包含职员数据的 JSON 数组。它提取每个职员的 id 和 displayName,创建应用程序对象 EmployeeItemData,创建 TreeItem 部件并把应用程序对象与部件绑定起来。

processSubOrgs 方法(第 32 – 43 行)处理 JSON 数组中的每个子组织。它提取 id 和 displayName,把它们存储在应用程序对象 OrganizationItemData 中。然后,创建 TreeItem 部件并把应用程序对象与部件绑定起来。在桌面文件管理器应用程序中,可以有空的文件夹。但是,在 GWT 中不支持这种做法。按照惰性装载策略,在创建组织 TreeItem 部件时,还没有创建所有子部件所需的数据。但是,需要让这个部件看起来像一个组织(树节点),而不是像职员(树叶)。为了解决这个问题,我创建了一个假的子 TreeItem 部件并把它设置为不可见(第 39、40 行)。

清单 13. edu.ucar.cisl.gwtRESTTutorialView.client.callback.OrganizationRpcCallback

1. package edu.ucar.cisl.gwtRESTTutorialView.client.callback; 
 
2. import com.google.gwt.json.client.JSONArray; 
3. import com.google.gwt.json.client.JSONObject; 
4. import com.google.gwt.json.client.JSONParser; 
5. import com.google.gwt.json.client.JSONValue; 
6. import com.google.gwt.user.client.ui.TreeItem; 
 
7. import edu.ucar.cisl.gwtRESTTutorialView.client.bean.EmployeeItemData; 
8. import edu.ucar.cisl.gwtRESTTutorialView.client.bean.OrganizationItemData; 
9. import edu.ucar.cisl.gwtRESTTutorialView.client.OrganizationPopup; 
 
10. public class OrganizationRpcCallback extends RestServiceRpcCallback { 
11.   protected void processResponse(String response) { 
12.     JSONValue jsonValue = JSONParser.parse(response); 
13.     OrganizationItemData 
 oItemData = (OrganizationItemData) treeItem.getUserObject(); 
14.     JSONObject jobj = jsonValue.isObject(); 
15.     oItemData.setId((int) jobj.get("id").isNumber().doubleValue()); 
16.     oItemData.setDisplayName(jobj.get("acronym").isString().stringValue()); 
17.     oItemData.setName(jobj.get("name").isString().stringValue()); 
18.     oItemData.setLeadName(jobj.get("leadName").isString().stringValue()); 
19.     oItemData.setLeadTitle(jobj.get("leadTitle").isString().stringValue()); 
20.     oItemData.setTotalEmployees((int) 
21.      obj.get("totalEmployees").isNumber().doubleValue()); 
22.     oItemData.setDataReady(true); 
23.     treeItem.setText(oItemData.getDisplayName()); 
24.     processEmployees(jobj.get("employees").isArray()); 
25.     processSubOrgs(jobj.get("subOrgs").isArray()); 
26.     if (getEventType() == EventType.SELECT_EVENT) { 
27.       int left = treeItem.getAbsoluteLeft() + 50; 
28.       int top = treeItem.getAbsoluteTop() + 30; 
29.       OrganizationPopup.show(left, top, (OrganizationItemData) oItemData); 
30.     } 
31.   } 
 
32.   protected void processSubOrgs(JSONArray jsonArray) { 
33.     for (int i = 0; i < jsonArray.size(); ++i) { 
34.       JSONObject jo = jsonArray.get(i).isObject(); 
35.       OrganizationItemData iData = new OrganizationItemData(); 
36.       iData.setId((int) jo.get("id").isNumber().doubleValue()); 
37.       iData.setDisplayName(jo.get("acronym").isString().stringValue()); 
38.       TreeItem child = treeItem.addItem(iData.getDisplayName()); 
39.       TreeItem dummy = child.addItem(""); 
40.       dummy.setVisible(false); 
41.       child.setUserObject(iData); 
42.     } 
43.   } 
 
44.   protected void processEmployees(JSONArray jsonArray) { 
45.     for (int i = 0; i < jsonArray.size(); ++i) { 
46.       JSONObject jo = jsonArray.get(i).isObject(); 
47.       EmployeeItemData eData = new EmployeeItemData(); 
48.       eData.setId((int) jo.get("id").isNumber().doubleValue()); 
49.       eData.setDisplayName(jo.get("name").isString().stringValue()); 
50.       eData.setDataReady(false); 
51.       TreeItem child = treeItem.addItem(eData.getDisplayName()); 
52.       child.setUserObject(eData); 
53.     } 
54.   } 
55.   }

因为要使用 GWT JSON 库解析 JSON 字符串,需要在 GWT 模块配置文件中包含它(清单 14)。这个文件还声明模块的入口点类(第 6 行)。这个文件在 edu.ucar.cisl.gwtRESTTutorialView 包中。

清单 14. GwtRESTTutorialView.gwt.xml

1. <?xml version="1.0" encoding="UTF-8"?> 
2. <module rename-to='gwtresttutorialview'> 
3.   <inherits name='com.google.gwt.user.User'/> 
4.   <inherits name="com.google.gwt.json.JSON"/> 
5.   <inherits name='com.google.gwt.user.theme.standard.Standard'/>  
6.   <entry-point 
 class='edu.ucar.cisl.gwtRESTTutorialView.client.GwtRESTTutorialView'/> 
7.   <source path='client'/> 
8.   </module>

在 web.xml 文件中声明 RESTful Web 服务代理

从技术上说,RPC 远程服务是一个 servlet。必须像其他 servlet 一样在 web.xml 文件中配置这个 servlet(清单 15)。

清单 15. 声明 RESTful Web 服务代理远程服务的 web.xml 片段

1. <servlet> 
2.   <servlet-name>RESTfulWebServiceServlet</servlet-name> 
3.   <servlet-class> 
4.     edu.ucar.cisl.gwtRESTTutorialView.server.RESTfulWebServiceProxyImpl 
5.   </servlet-class> 
6. </servlet> 
7. <servlet-mapping> 
8.   <servlet-name>RESTfulWebServiceServlet</servlet-name> 
9.   <url-pattern>/gwtresttutorialview/RESTfulWebServiceProxy</url-pattern> 
10. </servlet-mapping>        

实现 GWT 客户机界面

创建主窗口

清单 16 列出模块的入口点类。这个类必须实现 EntryPoint 接口。onModuleLoad 方法是装载模块之后执行的第一个方法。这个类还实现 SelectionHandler<TreeItem> 和 OpenHandler<TreeItem> 接口,从而处理树节点选择和打开事件。在以前的版本中,GWT 提供许多事件监听器接口。但是,从 1.6 版开始,它们已经被事件处理器替代了。

onModuleLoad 方法实例化一个 Tree 部件和一个 TreeItem 部件,后者作为树部件的根,表示组织的最高层。创建应用程序对象 OrganizationItemData 并与根 TreeItem 相关联。这个对象的 id 设置为 1,可以设置为作为起点的任何组织层。因为根节点代表组织而不是职员,所以它的表现和外观应该像树节点一样,是可以打开的。当前,GWT TreeItem 部件没有提供这种功能。为了解决这个问题,我创建了一个假的 TreeItem 作为根的子节点并把它设置为不可见。现在,当根 TreeItem 的状态被设置为 open 时(第 35 行),启动 Open 事件并调用 onOpen 方法,这会创建组织结构的第一层,包括职员和子组织的列表。把 Tree 部件添加到 RootPanel(GWT 应用程序中所有部件的最高层容器)中。

当用户选择职员 TreeItem 或组织 TreeItem 部件时,调用 Tree 部件事件处理器方法 onSelection(第 38-51 行)。它从部件获取应用程序条目数据,如果数据已经装载了,它会打开一个弹出窗口以显示详细数据。否则,它调用 invokeRESTfulWebService 向代理服务器发送请求。下一节讨论后一个方法。

当用户打开组织 TreeItem 部件时,调用另一个 Tree 部件事件处理器方法 onOpen(第 53-60 行)。如果组织的详细数据(包括职员数据和直接子组织数据)不可用,那么这个方法与 onSelection 一样调用 invokeRESTfulWebService 向代理服务器发送请求。

清单 16. edu.ucar.cisl.gwtRESTTutorialView.client. GwtRESTTutorialView

1. package edu.ucar.cisl.gwtRESTTutorialView.client; 
 
2. import com.google.gwt.core.client.EntryPoint; 
3. import com.google.gwt.core.client.GWT; 
4. import com.google.gwt.event.logical.shared.OpenEvent; 
5. import com.google.gwt.event.logical.shared.OpenHandler; 
6. import com.google.gwt.event.logical.shared.SelectionEvent; 
7. import com.google.gwt.event.logical.shared.SelectionHandler; 
8. import com.google.gwt.user.client.ui.RootPanel; 
9. import com.google.gwt.user.client.ui.Tree; 
10. import com.google.gwt.user.client.ui.TreeItem; 
11. import com.google.gwt.user.client.ui.Tree.Resources; 
 
12. import edu.ucar.cisl.gwtRESTTutorialView.client.bean.EmployeeItemData; 
13. import edu.ucar.cisl.gwtRESTTutorialView.client.bean.ItemData; 
14. import edu.ucar.cisl.gwtRESTTutorialView.client.bean.OrganizationItemData; 
15. import edu.ucar.cisl.gwtRESTTutorialView.client.callback.EmployeeRpcCallback; 
16. import edu.ucar.cisl.gwtRESTTutorialView.client.callback.OrganizationRpcCallback; 
17. import edu.ucar.cisl.gwtRESTTutorialView.client.callback.RestServiceRpcCallback; 
 
18. /**Entry point classes define <code>onModuleLoad()</code>. 
19. */ 
20. public class GwtRESTTutorialView implements EntryPoint, 
21.   SelectionHandler<TreeItem>, OpenHandler<TreeItem> { 
22.   final static String contentType="application/json"; 
 
23.   public void onModuleLoad() { 
24.     TreeItem root = new TreeItem("Root"); 
25.     ItemData iData = new OrganizationItemData(); 
26.     iData.setId(1); 
27.     root.setUserObject(iData); 
28.     TreeItem dummyItem = root.addItem(""); 
29.     dummyItem.setVisible(false); 
30.     Tree tree = new Tree((Resources) GWT.create(OrgTreeResource.class), true); 
31.     tree.addItem(root); 
32.     tree.addSelectionHandler(this); 
33.     tree.addOpenHandler(this); 
34.     RootPanel.get().add(tree); 
35.     root.setState(true, true); 
36.   } 
 
37.   @Override 
38.   public void onSelection(SelectionEvent<TreeItem> event) { 
39.     TreeItem item=event.getSelectedItem(); 
40.     ItemData iData = (ItemData) item.getUserObject(); 
41.     if (iData.isDataReady()) { 
42.       int left = item.getAbsoluteLeft() + 50; 
43.       int top = item.getAbsoluteTop() + 30; 
44.       if (iData instanceof EmployeeItemData) 
45.         EmployeePopup.show(left, top, (EmployeeItemData) iData); 
46.       else 
47.         OrganizationPopup.show(left, top, (OrganizationItemData) iData); 
48.     } else 
49.       invokeRESTfulWebService(item, 
50.         RestServiceRpcCallback.EventType.SELECT_EVENT);  
51.   } 
 
52.   @Override 
53.   public void onOpen(OpenEvent<TreeItem> event) { 
54.     TreeItem item = event.getTarget(); 
55.     ItemData iData = (ItemData) item.getUserObject(); 
56.     if (!iData.isDataReady()) { 
57.       invokeRESTfulWebService(item, 
58.         RestServiceRpcCallback.EventType.STATE_CHANGE_EVENT); 
59.     } 
60.   } 
 
61.   protected void invokeRESTfulWebService(TreeItem item, 
62.       RestServiceRpcCallback.EventType eventType) { 
63.     ItemData iData = (ItemData) item.getUserObject(); 
64.     RestServiceRpcCallback callback = null; 
65.     if (iData instanceof EmployeeItemData) 
66.       callback = new EmployeeRpcCallback(); 
67.     if (iData instanceof OrganizationItemData) 
68.       callback = new OrganizationRpcCallback(); 
69.     callback.setEventType(eventType); 
70.     callback.setTreeItem(item); 
71.     RESTfulWebServiceProxyAsync ls = RESTfulWebServiceProxy.Util.getInstance(); 
72.     ls.invokeGetRESTfulWebService(iData.buildUri(), contentType, callback); 
73.   } 
74.   }

向 RPC 代理服务器发送 RESTful Web 服务请求

invokeRESTfulWebService 方法(第 61–73 行)使用 RPC 服务向代理服务器发送 RESTful Web 服务请求。它先从 TreeItem 部件获取应用程序条目数据,然后根据应用程序条目数据的性质,实例化 EmployeeRpcCallback 或 OrganizationItemData 的回调实例。然后,把 TreeItem 部件和事件类型与这个回调实例关联起来,让它知道在 RESTful Web 服务返回数据之后如何处理数据。

按照 GWT 的要求,在调用远程服务之前,必须创建异步远程接口的实例并使用它调用远程服务,要使用远程服务中声明的所有参数和回调类的实例。因为远程服务调用是异步的、非阻塞的,所以 GWT 客户机并不等待服务的响应。它继续执行,直到从远程服务器收到异步回调。回调告诉 GWT 应用程序远程服务调用是否成功地执行了。如果远程服务调用成功,就调用 onSuccess 方法。否则,用 Throwable 的实例调用 onFailure 方法,Throwable 的实例包含从服务器传递来的定制异常。回调类处理服务器返回的数据。

创建定制的树图像

定制 GWT Tree 部件的树图像非常容易。只需创建一个扩展 Tree.Resource 接口的定制接口,重新声明 treeOpen、treeClosed 和 treeLeaf 方法(清单 17)。然后,在创建 Tree 部件时,使用 GWT.create 实例化这个新接口的实例并把它传递给 Tree 部件构造器(清单 16 的第 30 行)。三个图像文件的名称分别以 treeOpen、treeClosed 和 treeLeaf 开头,需要把它们放在相同的文件夹中。

清单 17. edu.ucar.cisl.gwtRESTTutorialView.client.OrgTreeResource

1. package edu.ucar.cisl.gwtRESTTutorialView.client; 
 
2. import com.google.gwt.resources.client.ImageResource; 
3. import com.google.gwt.user.client.ui.Tree.Resources; 
 
4. public interface OrgTreeResource extends Resources { 
5.   ImageResource treeOpen(); 
6.   ImageResource treeClosed(); 
7.   ImageResource treeLeaf(); 
8.   }

实现弹出窗口

为了显示职员和组织单元的详细信息,我创建了两个弹出窗口。清单 18 给出职员弹出窗口的实现。这个类扩展 GWT PopupPanel 部件。它是一个单实例类。它使用六对 Label 部件显示名、昵称、姓、头衔、电话号码和电子邮件地址的标签和值。使用一个 Grid 部件处理 Label 部件的布局。要想显示详细的职员数据,只需调用静态方法 show 并传递位置(距离它所参照的部件左边和顶边的偏移量)。在这里,参照的部件是用户选择的 TreeItem 部件。组织弹出窗口的实现是相似的(清单 19)。

清单 18. edu.ucar.cisl.gwtRESTTutorialView.client.EmployeePopup

1. package edu.ucar.cisl.gwtRESTTutorialView.client; 
 
2. import com.google.gwt.user.client.ui.Grid; 
3. import com.google.gwt.user.client.ui.Label; 
4. import com.google.gwt.user.client.ui.PopupPanel; 
 
5. import edu.ucar.cisl.gwtRESTTutorialView.client.bean.EmployeeItemData; 
 
6. public class EmployeePopup extends PopupPanel { 
7.   static protected EmployeePopup instance=null; 
8.   protected Grid grid = new Grid(6, 2); 
9.   protected Label firstNameLabel = new Label("First Name"); 
10.   protected Label firstNameValueLabel = new Label("First Name"); 
11.   protected Label nickNameLabel = new Label("Nickname"); 
12.   protected Label nickNameValueLabel = new Label("Nick Name"); 
13.   protected Label lastNameLabel = new Label("Last Name"); 
14.   protected Label lastNameValueLabel = new Label("Last Name"); 
15.   protected Label titleLabel = new Label("Title"); 
16.   protected Label titleValueLabel = new Label("Title"); 
17.   protected Label phoneLabel = new Label("Phone Number"); 
18.   protected Label phoneValueLabel = new Label("Phone Number"); 
19.   protected Label emailNameLabel = new Label("Email"); 
20.   protected Label emailValueLabel = new Label("Email"); 
 
21.   protected EmployeePopup() { 
22.     super(true); 
 
23.     grid.setWidget(0, 0, firstNameLabel); 
24.     grid.setWidget(0, 1, firstNameValueLabel); 
 
25.     grid.setWidget(1, 0, nickNameLabel); 
26.     grid.setWidget(1, 1, nickNameValueLabel); 
 
27.     grid.setWidget(2, 0, lastNameLabel); 
28.     grid.setWidget(2, 1, lastNameValueLabel); 
 
29.     grid.setWidget(3, 0, titleLabel); 
30.     grid.setWidget(3, 1, titleValueLabel); 
 
31.     grid.setWidget(4, 0, phoneLabel); 
32.     grid.setWidget(4, 1, phoneValueLabel); 
 
33.     grid.setWidget(5, 0, emailNameLabel); 
34.     grid.setWidget(5, 1, emailValueLabel); 
 
35.     grid.setWidth("300px"); 
36.      // grid.setHeight("400px"); 
37.     setWidget(grid); 
38.   } 
 
39.   public void setEmployeeData(EmployeeItemData iData) { 
40.     String firstName = iData.getFirstName(); 
41.     String lastName = iData.getLastName(); 
42.     String nickName = iData.getNickName(); 
43.     String phone = iData.getPhone(); 
44.     String email = iData.getEmail(); 
45.     String title = iData.getTitle(); 
 
46.     firstNameValueLabel.setText(firstName); 
47.     if (nickName != null && nickName.length() > 0) { 
48.       nickNameValueLabel.setVisible(true); 
49.       nickNameLabel.setVisible(true); 
50.       nickNameValueLabel.setText(nickName); 
51.     } 
52.     else { 
53.       nickNameValueLabel.setVisible(false); 
54.       nickNameLabel.setVisible(false); 
55.     } 
56.     lastNameValueLabel.setText(lastName); 
57.     phoneValueLabel.setText(phone); 
58.     emailValueLabel.setText(email); 
59.     titleValueLabel.setText(title); 
60.   } 
 
61.   protected static EmployeePopup getInstance() { 
62.     if (instance == null) 
63.       instance = new EmployeePopup(); 
64.     return instance; 
65.    } 
 
66.   public static void show(int leftOffset, int topOffset, EmployeeItemData eData) { 
67.     EmployeePopup popup = getInstance(); 
68.     popup.setEmployeeData(eData); 
69.     popup.setPopupPosition(leftOffset, topOffset); 
70.     popup.show(); 
71.   } 
 
72. }

清单 19. edu.ucar.cisl.gwtRESTTutorialView.client.OrganizationPopup

1. package edu.ucar.cisl.gwtRESTTutorialView.client; 
 
2. import com.google.gwt.user.client.ui.Grid; 
3. import com.google.gwt.user.client.ui.Label; 
4. import com.google.gwt.user.client.ui.PopupPanel; 
 
5. import edu.ucar.cisl.gwtRESTTutorialView.client.bean.OrganizationItemData; 
 
6. public class OrganizationPopup extends PopupPanel { 
7.   static protected OrganizationPopup instance=null; 
8.   protected Grid grid = new Grid(3, 2); 
9.   protected Label nameLabel = new Label("Full Name"); 
10.   protected Label nameValueLabel = new Label("Full Name"); 
11.   protected Label leadNameLabel = new Label("Lead"); 
12.   protected Label leadNameValueLabel = new Label("Lead Name"); 
13.   protected Label totalEmployeesLabel = new Label("Total Employees"); 
14.   protected Label totalEmployeesValueLabel = new Label("Total Employees"); 
 
15.   public OrganizationPopup() { 
16.     super(true); 
 
17.     grid.setWidget(0, 0, nameLabel); 
18.     grid.setWidget(0, 1, nameValueLabel); 
 
19.     grid.setWidget(1, 0, leadNameLabel); 
20.     grid.setWidget(1, 1, leadNameValueLabel); 
 
21.     grid.setWidget(2, 0, totalEmployeesLabel); 
22.     grid.setWidget(2, 1, totalEmployeesValueLabel); 
 
23.     grid.setWidth("700px"); 
24.     setWidget(grid); 
25.   } 
 
26.   public void setOrganizationData(OrganizationItemData iData) { 
27.     nameValueLabel.setText(iData.getName()); 
28.     leadNameValueLabel.setText(iData.getLeadName()); 
29.     totalEmployeesValueLabel.setText(new 
30.      Integer(iData.getTotalEmployees()).toString()); 
31.   } 
 
32.   protected static OrganizationPopup getInstance() { 
33.     if (instance == null) 
34.       instance = new OrganizationPopup(); 
35.     return instance; 
36.   } 
 
37.   public static void show(int leftOffset, 
int topOffset,OrganizationItemData oData) { 
38.     OrganizationPopup popup = getInstance(); 
39.     popup.setOrganizationData(oData); 
40.     popup.setPopupPosition(leftOffset, topOffset); 
41.     popup.show(); 
42.   } 
43.   }

把所有部分组合起来

实现了所有的类之后,在 Eclipse 中的项目的 src 文件夹中应该有以下文件夹和文件(图 2)。

图 2. Eclipse 中的项目文件夹
使用 GWT 和 RESTful Web 服务构建动态的组织树

查看原图(大图)

在 Project Explore 中右键单击项目名,选择 Run As > Web Application or Debug As > Web Application 运行它。复制 Developer Mode 窗口中的 URL 并把它粘贴到您喜欢的浏览器中。这个应用程序看起来应该像图 3 这样。

图 3. 浏览器中显示的组织树应用程序
使用 GWT 和 RESTful Web 服务构建动态的组织树

查看原图(大图)

结束语

GWT 可以帮助 Java 开发人员构建功能丰富、响应性的桌面风格应用程序,尤其是大型 Web 应用程序。在本文中,我演示了如何使用 GWT Tree 部件显示公司的组织结构。我使用一个 RPC 代理与 RESTful Web 服务集成。RESTful Web 服务使用 JSON 作为数据格式。只在需要时装载组织数据和职员数据,动态地创建树节点(组织)和树叶(职员)。将回调实现为真正的类,从而促进代码重用并允许在运行时与客户机数据关联。使用定制的树图像显示组织和职员,使用弹出窗口显示组织和职员的详细信息。

本文基于由 National Science Foundation 支持的部分研究工作,这些工作基于它与 University Corporation for Atmospheric Research 之间的合作协议。National Center for Atmospheric Research 是由 National Science Foundation 发起成立的。

下载

描述名字大小下载方法
RESTful Web 服务gwtRESTTutorialView.zip1365KBHTTP
源代码gwtRESTTutorial.war9233KBHTTP
数据库设置setup.sql8KBHTTP

Tags:使用 GWT RESTful

编辑录入:爽爽 [复制链接] [打 印]
赞助商链接