使用 Google Web Toolkit、Apache Derby 和 Eclipse 构建 Ajax 应用程序,第 3 部分: 通信

远程过程调用(Remote Procedure Call,RPC)和 Google Web Toolkit

在本系列文章的前两篇文章里,您已经了解了如何使用 Google Web Toolkit (GWT) 和 Apache Derby 关系数据库创建简单的 Web 应用程序。第 1 部分 主要介绍了如何使用 GWT 构建名为 Slicr 的比萨交付系统的前端。在 第 2 部分 中,向您展示了如何使用 Derby 创建关系数据库,并简要演示了将数据库行转换为 Java™ 对象的机制。现在将开始变得有趣起来!在第三篇文章中,了解如何使客户机与服务器相互进行对话。您将使用 GWT 内的远程过程调用(Remote Procedure Call,RPC)框架从服务器获取数据,这几乎就像执行一次 Java 方法调用一样简单。

Noel Rappin (noelrappin@gmail.com), 高级软件工程师, Motorola, Inc.

Noel Rappin 是佐治亚理工学院 Graphics, Visualization and Usability Center 的哲学博士,也是摩托罗拉公司的一名高级软件工程师。他还是 wxPython in Action and Jython Essentials 的合著者之一。



2007 年 4 月 04 日

Ajax 中的 A

启用了 Asynchronous JavaScript + XML (Ajax) 的 Web 应用程序与传统 Web 应用程序之间的主要差别在于 Ajax 中的 A:异步。Ajax 应用程序允许浏览器更新页面的特定部分而无需完全刷新整个页面。这种简单的技巧将提供更具有交互性的用户体验,因为简单的 Web 页面现在运行得更像是桌面应用程序。

从开发人员的角度来看,这种异步行为有两个关键组件:

  • XMLHttpRequest 对象是由浏览器定义的 JavaScript 对象,它将使 Web 页面可以在后台线程中发送 HTTP 请求和接收响应。与传统的页面请求不同的是,调用不会中断用户体验,而浏览器在等待响应时也不会暂停。
  • 某类回调是在响应完成后执行的。该回调通常使用 JavaScript 文档对象模型(Document Object Model,DOM)来根据新数据操作页面元素。屏幕上发生的变化很酷,用户也很开心。

查看 Ajax 资源中心,这是您的一站式 Ajax 编程模型信息中心,包括文章和教程、论坛、blog、wiki、事件和新闻。所发生的任何事情都会在这里介绍。

因此基本流程如下:向服务器发出调用,返回响应,并根据响应对页面采取操作。此外,需要记住的重要一点是所有操作都是在后台进行的,而无需忙时等待及根据浏览器中页面的典型更改进行刷新。

整个 XMLHttpRequest/DOM 方法只有一个问题:它很令人头痛。一方面,每个浏览器都各自实现相应的 JavaScript 对象。另一方面,使数据到达服务器可能会十分棘手,因此将响应转换为有用的数据也可能会十分棘手。结果,每个名副其实的 Ajax 框架都有某种简单的包装器用于创建、调用和管理 RPC。GWT 也不例外。

GWT 使用让人回想起 Enterprise JavaBeans (EJB) 技术的多接口基础设施管理 RPC,但是 —— 谢天谢地 —— 简单了很多。问题是要定义系统执行的远程调用的列表。GWT 将实现后台通道,用于转换返回服务器的数据、进行服务器调用,并将返回的数据转换回客户机数据。然后定义远程调用完全返回后要执行哪些操作。它不像执行一次普通的 Java 方法调用那样简单,但是它也不难。

将对 Slicr 应用程序执行的更改是为了从服务器数据库中检索浇头的初始列表,而不是将浇头硬编码到客户机代码中。无可否认,这个简单示例将引导您完成执行 RPC 调用所需的所有步骤。要轻松地运行此示例,请以托管模式运行。在托管模式下,GWT 将自动模拟远程调用。如果使用诸如 Eclipse 或 IntelliJ IDEA 之类的集成开发环境 (IDE),您将获得一些关于在托管模式下运行的支持。

注:在本系列文章的最后一篇文章里,我将向您展示如何在普通 servlet 环境中运行服务器端。

定义服务

GWT 过程调用中的大部分工作都是在两个类中进行的。在服务器端,定义 RemoteServiceServlet 的子类。在这个类中,将操作服务器数据并将值返回给客户机(或者抛出异常,但是此刻先让我们乐观一些认为不会出现这种情况)。在客户端,定义一个实现 AsyncCallback 接口的类。在这个类中,定义服务器操作完成时客户机页面如何处理数据(或异常)。除了这两个类之外,必须编写一些绑定代码使 GWT 可以将客户端类和服务器端类绑定在一起。没错,那真是一大堆 绑定代码。绑定代码包含两个不同的接口外加一些客户端代码以及一两个设置。但是不必担心;大部分代码都是样例,并且如果您按照它逐步执行,将不会遇到任何问题。

从服务器端开始。在这里的目标只是要生成在 上一篇文章 末尾放入数据库中的所有浇头的列表。服务器将使用该文章中的简单 ObjectFactory(参见清单 1)帮助您工作。

清单 1. 浇头服务实现
public class ToppingServiceImpl extends RemoteServiceServlet 
		implements ToppingService {

	public static final String DRIVER = "org.apache.derby.jdbc.EmbeddedDriver";

	public static final String PROTOCOL = "jdbc:derby:slicr;";

	public List getAllToppings() {
		try {
			Class.forName(DRIVER).newInstance();
			Connection con = DriverManager.getConnection(PROTOCOL);
			Statement s = con.createStatement();
			ResultSet rs = s.executeQuery(
			        "SELECT * FROM toppings order by name");
			return ObjectFactory.convertToObjects(rs, Topping.class);
		} catch (Exception e) {
			e.printStackTrace();
			return new ArrayList();
		} finally {
			try {
				DriverManager.getConnection("jdbc:derby:;shutdown=true");
			} catch (SQLException ignore) {}
		}
	}

}

关于这段代码需要注意的第一件事是它实在没有任何神奇之处。GWT 远程服务的要求很简单,它必须扩展 RemoteServiceServlet 并实现一个接口,您将在下面两个段落中创建该接口。

注:看似好像大部分 GWT 文档让您先创建接口。那很好,一点儿问题都没有。我只是认为先创建具体代码会显得更清晰。

除那之外,这段代码与上一篇文章中的 ToppingTestr 示例(重新包装以供在 GWT 内使用)具有几乎相同的功能。对 Derby 数据库进行一次调用,使用对象工厂创建浇头对象,并返回浇头对象。

绑定代码

要使新服务可用于客户端应用程序,必须定义两个相关接口。第一个是使用的 ToppingService 接口,但未在 清单 1 中说明。它实际上十分简单,如下面的清单 2 所示。

清单 2. 定义第一个相关接口,ToppingService
public interface ToppingService extends RemoteService {
	public List getAllToppings();
}

在这里完成的所有操作是获得用于实际的具体类中方法的同一个签名。这里的主要限制是接口必须扩展 com.google.gwt.user.client.rpc.RemoteService;。此外,参数和返回值必须属于 GWT 可以序列化的类型。第 2 部分 中提供了那些类型的列表。但是,只有一个版本的服务接口是不够的。您还必须定义一个异步版本的接口,如清单 3 所示。

清单 3. 定义异步版本的接口
public interface ToppingServiceAsync {
	public void getAllToppings(AsyncCallback callback);
}

异步版本的服务接口是从上面描述的主版本中派生出来的。两个版本必须位于同一个包中,并且该包必须对 GWT 客户机代码可见。(我使用的是 com.ibm.examples.client)。异步版本中的类名必须是初始接口的名称且末尾附加字符串 Async。对于初始接口中的每个方法,异步版本必须有一个返回类型更改为 void 的匹配方法和一个 AsyncCallback 类型的附加参数。客户端代码将使用 AsyncCallback 作用于服务器响应上。

在单个服务接口中可以完全自由地拥有所需的任意个方法,只要它们在异步版本中全都有相对应的方法并且都是在同一个远程服务类中实现。虽然我假定让服务方法能够共享服务器端的公共数据可能会有实际价值,但是如何组织服务在很大程度上是审美决定。

此外,随后将注册服务器代码。将下面的一行添加到 Slicr.gwt.xml 文件中:

<servlet path="/toppings" class="com.ibm.examples.server.ToppingServiceImpl"/>

上面一行把具体远程服务类的全限定类名与实质上为此服务 URL 的路径名组成一对。您可以随意使用任何名称,只要您记住该名称并且以后都保持一致。从技术上说,如果要以托管模式运行应用程序,则只需要将这一行包含在 .xml 文件中。正如您将在下一次看到的那样,在 Web 部署过程中,您需要类似 web.xml 文件中的内容。

保持单次调用的一致需要做很多工作 —— 接口、异步接口、实际实现和模块文件。缺少其中之一或者匹配不正确都将导致在尝试实际调用服务时出错。截至本文完稿时,至少有一个 IDE (IntelliJ IDEA) 在编辑器中提供支持,以使所有这些项目保持一致状态。如果使用的是 Eclipse,则 Googlipse 插件也可以提供类似的支持。

执行客户端调用

完成服务器端操作以后,现在应该让客户机执行过程调用了。在这里基本的想法是慎重地告诉 GWT 系统正在调用的是哪个远程服务。然后将 AsyncCallback 对象发送出去;最后,GWT 将其送回,您可对结果进行操作。清单 2 显示了设置和调用的代码。这个方法位于 第 1 部分 中的 Slicr 类中。明确地说,对这种方法的调用将替换 Slicr.onModuleLoad() 中添加浇头面板的行。

清单 4. 设置和调用
public void callForToppings() {
	ToppingServiceAsync toppingService = 
		(ToppingServiceAsync) GWT.create(ToppingService.class);
	ServiceDefTarget target = (ServiceDefTarget) toppingService;
	String relativeUrl = GWT.getModuleBaseURL() + "toppings";
	target.setServiceEntryPoint(relativeUrl);
	toppingService.getAllToppings(new ToppingCallback());
}

每行都是 GWT 调用中至关重要的步骤。以下是操作清单:

  1. 创建异步接口的实例。当然,通常您不能创建传入 GWT.create() 调用的接口的实例。GWT 类是若干个实用程序的统称。在这种情况下,它允许您创建用于实现给定接口的代理对象。注意,在部署过程中,对于这种方法,参数必须是可顾名思义的,而变量则无需如此。变量是以托管模式运行,但还是要谨慎。
  2. GWT.create() 返回的代理对象还将实现另一个接口 ServiceDefTarget。在一两行内,您将会用到该接口中的方法。
  3. 考虑消息将会发往的 URL。该 URL 有两个组件:
    • 系统的基本 URL,可通过调用 GWT.getModuleBaseURL() 获得
    • 向 .xml 文件中添加 servlet 时返回用作路径的同一个字符串
  4. 配备完整的 URL,您可以通过调用 setServiceEntryPoint() 方法通知 GWT 此特定的 URL 用于查找此特定的服务。接口还包含相关的获取方法。此刻,服务已经过完全初始化可供此客户机页面使用。
  5. 并且在最后 —— 请击鼓 —— 您可以对服务进行实际调用,就像服务定义它一样(除了 ToppingCallback 对象以外,该对象是下一部分的主题)。

只需要在客户机页面中初始化一次服务(示例的前四行)。在服务被创建并与其接入点关联起来之后,可以重用同一个服务对象执行对服务器的多次回调。还可以创建一些助手方法以简化服务器对象的样例初始化。

异步的乐趣

正如我先前所暗示的那样,GWT RPC 与常规方法调用之间的主要差别在于您不知道远程调用何时完成或者是否完成。由于 Web 用户通常都不喜欢在角落刷新选择框时冻结整个页面,因此 GWT 程序将执行远程调用并继续它的愉快旅程。最后,服务器将竭力维持响应,并且只有在那一刻 GWT 代码才执行规划的奇妙操作。

通常,在此类环境中跟踪后台线程会是一项挑战。但是,在例子中,GWT 利用上面描述的机制和已经看到的 AsyncCallback 接口执行了所需的大量工作。GWT 有效地减少了处理多线程的运行时间。

AsyncCallback 接口定义了两个方法:OnSuccess(Object obj)OnFailure(Throwable t)。必须定义一个可以实现这两个方法的类。当执行远程调用时,创建类的实例并将其传递给异步服务方法,如 清单 4 所示。最后,服务器端资源完成,然后调用代码中两个方法之一。成功方法的参数是接口和实现中的调用的返回值。在例子中,那就是浇头的列表。最好将它转换为需要的类型,然后对新数据执行一些有用的操作。GWT 有希望在近期某个时候支持 Java 1.5 Generics,将减少大量的转换需求。如果服务器端代码失败,则将调用失败的方法,并用抛出的异常作为参数。此外,可执行任意操作响应异常。

清单 5 中所示的代码显示了 清单 4 中引用的 ToppingCallback 类。我以前可能提到过,您将经常看到定义为匿名内部类AsyncCallback 类。建议避免使用该类。如果该类附带一个名称并且不是放在完全不同的代码块的中间,代码将更易于阅读和测试。

清单 5. ToppingCallback 类
public class ToppingCallback implements AsyncCallback {

	public void onFailure(Throwable caught) {
	    GWT.log("Error ", caught);
		caught.printStackTrace();
	}

	public void onSuccess(Object result) {
		List allToppings = (List) result;
		VerticalPanel toppings = new VerticalPanel();
		toppings.add(new HTML("<h2>Toppings</h2>"));
		Grid topGrid = new Grid(allToppings.size() + 1, 3);
		topGrid.setText(0, 0, "Topping");
		topGrid.setText(0, 1, "Left");
		topGrid.setText(0, 2, "Right");
		for (int i = 0; i < allToppings.size(); i++) {
			Topping t = (Topping) allToppings.get(i);
			Button button = new Button(t.getName() );
			CheckBox leftCheckBox = new CheckBox();
			CheckBox rightCheckBox = new CheckBox();
			clearables.add(leftCheckBox);
			clearables.add(rightCheckBox);
			button.addClickListener(new ToppingButtonListener(leftCheckBox,
					rightCheckBox));
			topGrid.setWidget(i + 1, 0, button);	
			topGrid.setWidget(i + 1, 1, leftCheckBox);
			topGrid.setWidget(i + 1, 2, rightCheckBox);
		}
		toppings.add(topGrid);
		panel.add(toppings, DockPanel.EAST);
	}
	
}

ToppingCallback 类的实例是通过异步接口发送的。GWT 将管理后台的一切,因此,首先将执行对服务器的适当调用,然后把服务器调用的结果提供给回调对象,最后调用相应的响应方法。

在这种情况下,失败的情形更易于描述。如果服务器端方法出于某种原因抛出异常,回调中的控制将其传递给 onFailure() 方法。此方法的参数是异常或服务器端返回的其他可抛出对象。此参数让您对失败做出得体的响应(如果可能)。我使用 GWT.log() 方法,该方法使用字符串和 Throwable 作为参数。此方法将把日志消息打印到 GWT 托管模式的 shell 窗口中。同样地,它只在托管模式下运行;在 Web 模式下,将忽略该方法。GWT.log() 方法实际上用于在开发过程中进行跟踪。您可能需要使用此方法将适当的默认值或错误消息输出到将要刷新的屏幕中,或者可能将整个浏览器重定向到错误屏幕中。

如果您已经阅读了 本系列的第一篇文章,应立即就会注意到 onSuccess() 代码几乎与初始版本中的 buildToppingTypePanel() 方法完全相同。其中有一点微不足道的差别:现在将在一张列表内而不是在数组中循环,这将导致需要在循环控制中执行一些小更改。我还在显示内容中添加了浇头的价格,因为该信息也可在数据库中获得。

更深入地看,代码的先前版本能够将 toppings 面板返回给调用代码,并且调用代码随后可以将 toppings 面板添加到父面板中。在这段代码中,不能返回值,因此必须将新创建的 Toppings 面板作为此方法的一部分添加到父面板中。运行它时,您将看到先显示 Pizza 面板;然后,经过短暂延迟等待 GWT 模拟服务器调用后,Toppings 面板将显示以下效果,如图 1 所示。

图 1. 新的浇头面板
新的浇头面板

即使是在这个小示例中,移植到异步结构对代码结构的影响也是十分严重的。首先,将子面板添加到主面板中的顺序对于决定每个子面板在父面板内的最终布局非常重要。随着将更多的初值移植到 RPC 调用中,您将不能再依赖对子面板进行有序地创建。必须将面板的初始创建操作与将小部件插入到面板的操作分隔开来。(这可在添加新数据时展现出减少闪烁的附加优点)。总体目标是尽可能简单地将代码保存在异步回调中并且尽可能使彼此断开连接。拥有相互依赖的线程将极大地增大代码的复杂度。

稍后有哪些精彩内容?

在本文中,您了解了使用 GWT 创建单个 RPC 的过程。此时此刻,您有了一个传统的 GWT 应用程序。但是,自这时起,该应用程序只能在开发计算机上以托管模式运行。要更广泛地展示应用程序,必须迁移到 Web 模式并将 Web 应用程序部署到 servlet 环境中。请继续关注本系列文章的最后一部分,该部分将介绍如何部署。

参考资料

学习

获得产品和技术

  • 下载 Google Web Toolkit
  • 下载 Eclipse:它是开放源码、可免费获取的、可扩展的 IDE。
  • 使用 IBM 试用软件 革新您的下一个开放源码开发项目,可通过下载获得,也可通过 DVD 获得。

讨论

条评论

developerWorks: 登录

标有星(*)号的字段是必填字段。


需要一个 IBM ID?
忘记 IBM ID?


忘记密码?
更改您的密码

单击提交则表示您同意developerWorks 的条款和条件。 查看条款和条件

 


在您首次登录 developerWorks 时,会为您创建一份个人概要。您的个人概要中的信息(您的姓名、国家/地区,以及公司名称)是公开显示的,而且会随着您发布的任何内容一起显示,除非您选择隐藏您的公司名称。您可以随时更新您的 IBM 帐户。

所有提交的信息确保安全。

选择您的昵称



当您初次登录到 developerWorks 时,将会为您创建一份概要信息,您需要指定一个昵称。您的昵称将和您在 developerWorks 发布的内容显示在一起。

昵称长度在 3 至 31 个字符之间。 您的昵称在 developerWorks 社区中必须是唯一的,并且出于隐私保护的原因,不能是您的电子邮件地址。

标有星(*)号的字段是必填字段。

(昵称长度在 3 至 31 个字符之间)

单击提交则表示您同意developerWorks 的条款和条件。 查看条款和条件.

 


所有提交的信息确保安全。


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=Open source, Information Management, Java technology, Web development
ArticleID=206904
ArticleTitle=使用 Google Web Toolkit、Apache Derby 和 Eclipse 构建 Ajax 应用程序,第 3 部分: 通信
publish-date=04042007