启用了 Asynchronous JavaScript + XML (Ajax) 的 Web 应用程序与传统 Web 应用程序之间的主要差别在于 Ajax 中的 A:异步。Ajax 应用程序允许浏览器更新页面的特定部分而无需完全刷新整个页面。这种简单的技巧将提供更具有交互性的用户体验,因为简单的 Web 页面现在运行得更像是桌面应用程序。
从开发人员的角度来看,这种异步行为有两个关键组件:
XMLHttpRequest
对象是由浏览器定义的 JavaScript 对象,它将使 Web 页面可以在后台线程中发送 HTTP 请求和接收响应。与传统的页面请求不同的是,调用不会中断用户体验,而浏览器在等待响应时也不会暂停。- 某类回调是在响应完成后执行的。该回调通常使用 JavaScript 文档对象模型(Document Object Model,DOM)来根据新数据操作页面元素。屏幕上发生的变化很酷,用户也很开心。
|
因此基本流程如下:向服务器发出调用,返回响应,并根据响应对页面采取操作。此外,需要记住的重要一点是所有操作都是在后台进行的,而无需忙时等待及根据浏览器中页面的典型更改进行刷新。
整个 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)帮助您工作。
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 List getAllToppings();
}
在这里完成的所有操作是获得用于实际的具体类中方法的同一个签名。这里的主要限制是接口必须扩展 com.google.gwt.user.client.rpc.RemoteService;
。此外,参数和返回值必须属于 GWT 可以序列化的类型。第 2 部分 中提供了那些类型的列表。但是,只有一个版本的服务接口是不够的。您还必须定义一个异步版本的接口,如清单 3 所示。
public void getAllToppings(AsyncCallback callback);
}
异步版本的服务接口是从上面描述的主版本中派生出来的。两个版本必须位于同一个包中,并且该包必须对 GWT 客户机代码可见。(我使用的是 com.ibm.examples.client
)。异步版本中的类名必须是初始接口的名称且末尾附加字符串 Async
。对于初始接口中的每个方法,异步版本必须有一个返回类型更改为 void
的匹配方法和一个 AsyncCallback
类型的附加参数。客户端代码将使用 AsyncCallback
作用于服务器响应上。
在单个服务接口中可以完全自由地拥有所需的任意个方法,只要它们在异步版本中全都有相对应的方法并且都是在同一个远程服务类中实现。虽然我假定让服务方法能够共享服务器端的公共数据可能会有实际价值,但是如何组织服务在很大程度上是审美决定。
此外,随后将注册服务器代码。将下面的一行添加到 Slicr.gwt.xml 文件中:
上面一行把具体远程服务类的全限定类名与实质上为此服务 URL 的路径名组成一对。您可以随意使用任何名称,只要您记住该名称并且以后都保持一致。从技术上说,如果要以托管模式运行应用程序,则只需要将这一行包含在 .xml 文件中。正如您将在下一次看到的那样,在 Web 部署过程中,您需要类似 web.xml 文件中的内容。
保持单次调用的一致需要做很多工作 —— 接口、异步接口、实际实现和模块文件。缺少其中之一或者匹配不正确都将导致在尝试实际调用服务时出错。截至本文完稿时,至少有一个 IDE (IntelliJ IDEA) 在编辑器中提供支持,以使所有这些项目保持一致状态。如果使用的是 Eclipse,则 Googlipse 插件也可以提供类似的支持。
完成服务器端操作以后,现在应该让客户机执行过程调用了。在这里基本的想法是慎重地告诉 GWT 系统正在调用的是哪个远程服务。然后将 AsyncCallback
对象发送出去;最后,GWT 将其送回,您可对结果进行操作。清单 2 显示了设置和调用的代码。这个方法位于 第 1 部分 中的 Slicr
类中。明确地说,对这种方法的调用将替换 Slicr.onModuleLoad()
中添加浇头面板的行。
清单 4. 设置和调用
ToppingServiceAsync toppingService =
(ToppingServiceAsync) GWT.create(ToppingService.class);
ServiceDefTarget target = (ServiceDefTarget) toppingService;
String relativeUrl = GWT.getModuleBaseURL() + "toppings";
target.setServiceEntryPoint(relativeUrl);
toppingService.getAllToppings(new ToppingCallback());
}
每行都是 GWT 调用中至关重要的步骤。以下是操作清单:
- 创建异步接口的实例。当然,通常您不能创建传入
GWT.create()
调用的接口的实例。GWT
类是若干个实用程序的统称。在这种情况下,它允许您创建用于实现给定接口的代理对象。注意,在部署过程中,对于这种方法,参数必须是可顾名思义的,而变量则无需如此。变量是以托管模式运行,但还是要谨慎。 GWT.create()
返回的代理对象还将实现另一个接口ServiceDefTarget
。在一两行内,您将会用到该接口中的方法。- 考虑消息将会发往的 URL。该 URL 有两个组件:
- 系统的基本 URL,可通过调用
GWT.getModuleBaseURL()
获得 - 向 .xml 文件中添加 servlet 时返回用作路径的同一个字符串
- 系统的基本 URL,可通过调用
- 配备完整的 URL,您可以通过调用
setServiceEntryPoint()
方法通知 GWT 此特定的 URL 用于查找此特定的服务。接口还包含相关的获取方法。此刻,服务已经过完全初始化可供此客户机页面使用。 - 并且在最后 —— 请击鼓 —— 您可以对服务进行实际调用,就像服务定义它一样(除了
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
类。建议避免使用该类。如果该类附带一个名称并且不是放在完全不同的代码块的中间,代码将更易于阅读和测试。
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 所示。
即使是在这个小示例中,移植到异步结构对代码结构的影响也是十分严重的。首先,将子面板添加到主面板中的顺序对于决定每个子面板在父面板内的最终布局非常重要。随着将更多的初值移植到 RPC 调用中,您将不能再依赖对子面板进行有序地创建。必须将面板的初始创建操作与将小部件插入到面板的操作分隔开来。(这可在添加新数据时展现出减少闪烁的附加优点)。总体目标是尽可能简单地将代码保存在异步回调中并且尽可能使彼此断开连接。拥有相互依赖的线程将极大地增大代码的复杂度。
在本文中,您了解了使用 GWT 创建单个 RPC 的过程。此时此刻,您有了一个传统的 GWT 应用程序。但是,自这时起,该应用程序只能在开发计算机上以托管模式运行。要更广泛地展示应用程序,必须迁移到 Web 模式并将 Web 应用程序部署到 servlet 环境中。请继续关注本系列文章的最后一部分,该部分将介绍如何部署。
学习
- 您可以参阅本文在 developerWorks 全球站点上的 英文原文 。
- 访问 Google Groups 的 GWT 论坛。
- 查看 GWT 官方博客。
- 了解关于 Eclipse Foundation 及其各个项目的更多信息。
- 获得关于 Googlipse 插件 的更多信息。
- 查阅 developerWorks Apache Derby 项目资源中心 获得文章、教程和其他资源,帮助您立即开始使用 Derby。
- 了解关于 IBM® Cloudscape™ 数据库 的更多内容,它是使用 Apache Derby 代码基础构建的。
- 访问 developerWorks 开源软件技术专区 获得广泛的 how-to 信息、工具和项目更新,帮助您使用开放源码技术进行开发,并将其与 IBM 产品一起使用。
- 浏览 developerWorks Open Source 专区中所有可获取的 Apache 文章 和 免费 Apache 教程。
- 在 Safari 书店 浏览关于上述主题和其他技术主题的图书。
- 访问 Apache Derby Project Web 站点。
- 随时关注 developerWorks 技术事件和网络广播。
- 获得 本系列文章的 RSS 提要。(查找关于 RSS 的更多信息。