使用 GWT RPC
至此,您已创建了 StockWatcher 应用的初始实现,在客户端代码中模拟股票数据。
在本节中,您将进行一个 GWT 远程过程调用到一个服务器端方法,该方法返回股票数据。从客户端调用的服务器端代码也称为 _服务_;进行远程过程调用的行为称为 _调用服务_。您将学习
**注意:**有关 GWT 应用中 RPC 通信的更广泛指南,请参阅 与服务器通信 - 远程过程调用。
什么是 GWT RPC?
GWT RPC 框架使您的 Web 应用的客户端和服务器组件能够轻松地通过 HTTP 交换 Java 对象。从客户端调用的服务器端代码通常称为 _服务_。GWT RPC 服务的实现基于众所周知的 Java servlet 架构。在客户端代码中,您将使用自动生成的代理类来调用服务。GWT 将处理来回传递的 Java 对象的序列化 - 方法调用中的参数和返回值。
**重要:**GWT RPC 服务与基于 SOAP 或 REST 的 Web 服务不同。它们仅仅是用于在您的服务器和客户端上的 GWT 应用之间传输数据的轻量级方法。要比较集成 GWT RPC 服务到您的应用中的单层和多层部署选项,请参阅开发人员指南 架构视角。
GWT RPC 机制的 Java 组件
在设置 GWT RPC 时,您将专注于调用远程服务器上运行的过程所涉及的这三个要素。
- 运行在服务器上的服务(您正在调用的方法)
- 调用服务的客户端代码
- 在客户端和服务器之间传递的 Java 数据对象
服务器和客户端都能够序列化和反序列化数据,以便数据对象可以作为普通文本在它们之间传递。
为了定义您的 RPC 接口,您需要编写三个组件
- 为您的服务定义一个接口(StockPriceService),该接口扩展了 RemoteService 并列出了您所有的 RPC 方法。
- 创建一个类(StockPriceServiceImpl),该类扩展了 RemoteServiceServlet 并实现了您上面创建的接口。
- 为您的服务定义一个异步接口(StockPriceServiceAsync),以便从客户端代码中调用。
服务实现必须扩展 RemoteServiceServlet,并且必须实现相关的服务接口。请注意,服务实现没有实现服务的异步版本接口。每个服务实现最终都是一个 servlet,但它不是扩展 HttpServlet,而是扩展 RemoteServiceServlet。RemoteServiceServlet 自动处理在客户端和服务器之间传递的数据的序列化以及调用服务实现中预期的方法。
创建服务
在本教程中,您将采用 refreshWatchList 方法中的功能并将其从客户端移到服务器。当前,您向 refreshWatchList 方法传递一个股票代码数组,它返回与每个股票相关的股票数据。然后,它调用 updateTable 方法来用股票数据填充 FlexTable。
当前的客户端实现
/**
* Generate random stock prices.
*/
private void refreshWatchList() {
final double MAX_PRICE = 100.0; // $100.00
final double MAX_PRICE_CHANGE = 0.02; // +/- 2%
StockPrice[] prices = new StockPrice[stocks.size()];
for (int i = 0; i < stocks.size(); i++) {
double price = Random.nextDouble() * MAX_PRICE;
double change = price * MAX_PRICE_CHANGE
* (Random.nextDouble() * 2.0 - 1.0);
prices[i] = new StockPrice(stocks.get(i), price, change);
}
updateTable(prices);
}
要创建服务,您将
- 定义服务接口:StockPriceService
- 实现服务:StockPriceServiceImpl
定义服务:StockPriceService 接口
在 GWT 中,RPC 服务由一个接口定义,该接口扩展了 GWT RemoteService 接口。对于 StockPriceService 接口,您只需要定义一个方法:一个接受股票代码数组并返回与每个股票相关的数据(作为 StockPrice 对象数组)的方法。
- 在客户端子包中,创建一个接口并将其命名为 StockPriceService。
- 在 Eclipse 中,在包资源管理器窗格中,选择包
com.google.gwt.sample.stockwatcher.client
- 从 Eclipse 菜单栏中,选择
文件 > 新建 > 接口
- Eclipse 将打开一个新的 Java 接口窗口。
- 在 Eclipse 中,在包资源管理器窗格中,选择包
- 填写新的 Java 接口窗口。
- 在名称中输入
StockPriceService
- 接受其他字段的默认值。
- 按
完成
- Eclipse 将创建 StockPriceService 接口的存根代码。
- 在名称中输入
- 用以下代码替换存根。
package com.google.gwt.sample.stockwatcher.client;
import com.google.gwt.user.client.rpc.RemoteService;
import com.google.gwt.user.client.rpc.RemoteServiceRelativePath;
@RemoteServiceRelativePath("stockPrices")
public interface StockPriceService extends RemoteService {
StockPrice[] getPrices(String[] symbols);
}
- **实现说明:**请注意 @RemoteServiceRelativePath 注释。这将服务与相对于模块基本 URL 的默认路径关联起来。
实现服务:StockPriceServiceImpl 类
现在创建位于服务器上的类(StockPriceServiceImpl)。正如接口中定义的那样,StockPriceServiceImpl 将只包含一个方法,即返回股票价格数据的方法。
为此,通过创建一个类来实现服务接口,该类还扩展了 GWT RemoteServiceServlet 类。RemoteServiceServlet 负责完成从客户端反序列化传入请求以及序列化传出响应的工作。
打包服务器端代码
服务实现以 Java 字节码的形式运行在服务器上;它不会被转换为 JavaScript。因此,服务实现没有与客户端代码相同的语言约束。为了使客户端代码与服务器代码分开,您将把它放在一个单独的包中(com.google.gwt.sample.stockwatcher.server)。
创建一个新类
- 为 StockPriceService 创建服务实现。
- 在 Eclipse 中,打开新的 Java 类向导(文件 > 新建 > 类)。
- 在包中,将名称从
.client
更改为.server
- Eclipse 将为服务器端代码创建一个包。
- 在名称中,输入
StockPriceServiceImpl
- **实现说明:**按照惯例,服务实现类的名称以服务接口的名称命名,并带有 Impl 后缀,因此将新类命名为 StockPriceServiceImpl。
- 扩展 RemoteServiceServlet 类。
- 在超类中,输入
com.google.gwt.user.server.rpc.RemoteServiceServlet
- 在超类中,输入
- 实现 StockPriceService 接口。
- 在接口中,添加一个接口
com.google.gwt.sample.stockwatcher.client.StockPriceService
- 在接口中,添加一个接口
- 继承抽象方法。
按
完成
- Eclipse 将创建包
com.google.gwt.sample.stockwatcher.server
- Eclipse 将创建一个存根 StockPriceServiceImpl 类。
package com.google.gwt.sample.stockwatcher.server; import com.google.gwt.sample.stockwatcher.client.StockPrice; import com.google.gwt.sample.stockwatcher.client.StockPriceService; import com.google.gwt.user.server.rpc.RemoteServiceServlet; public class StockPriceServiceImpl extends RemoteServiceServlet implements StockPriceService { public StockPrice[] getPrices(String[] symbols) { // TODO Auto-generated method stub return null; } }
- Eclipse 将创建包
编写服务器端实现
替换返回随机股票价格的客户端实现。
创建实例变量以初始化价格和变化数据。
private static final double MAX_PRICE = 100.0; // $100.00 private static final double MAX_PRICE_CHANGE = 0.02; // +/- 2%
用以下代码替换 TODO。返回价格而不是 null。
public StockPrice[] getPrices(String[] symbols) { Random rnd = new Random(); StockPrice[] prices = new StockPrice[symbols.length]; for (int i=0; i<symbols.length; i++) { double price = rnd.nextDouble() * MAX_PRICE; double change = price * MAX_PRICE_CHANGE * (rnd.nextDouble() * 2f - 1f); prices[i] = new StockPrice(symbols[i], price, change); } return prices; }
Eclipse 将标记 Random 并建议您包含导入声明。
包含来自
java.util
的导入声明,而不是来自com.google.gwt.user.client
的导入声明。import java.util.Random;
**实现说明:**请记住,服务实现以 Java 字节码的形式运行在服务器上,因此您可以使用任何 Java 类或库,而无需担心它是否可以转换为 JavaScript。在这种情况下,您可以使用 Java 运行时库中的 Random 类(java.util.Random),而不是模拟的 GWT 版本(com.google.gwt.user.client.Random)。
此清单显示了已完成的 StockPriceServiceImpl 类。
package com.google.gwt.sample.stockwatcher.server; import com.google.gwt.sample.stockwatcher.client.StockPrice; import com.google.gwt.sample.stockwatcher.client.StockPriceService; import com.google.gwt.user.server.rpc.RemoteServiceServlet; import java.util.Random; public class StockPriceServiceImpl extends RemoteServiceServlet implements StockPriceService { private static final double MAX_PRICE = 100.0; // $100.00 private static final double MAX_PRICE_CHANGE = 0.02; // +/- 2% public StockPrice[] getPrices(String[] symbols) { Random rnd = new Random(); StockPrice[] prices = new StockPrice[symbols.length]; for (int i=0; i<symbols.length; i++) { double price = rnd.nextDouble() * MAX_PRICE; double change = price * MAX_PRICE_CHANGE * (rnd.nextDouble() * 2f - 1f); prices[i] = new StockPrice(symbols[i], price, change); } return prices; } }
将服务器端代码包含在 GWT 模块中
嵌入式 servlet 容器(Jetty)可以托管包含您的服务实现的 servlet。这意味着您可以在测试和调试服务器端 Java 代码时利用在开发模式下运行您的应用的优势。
要设置此功能,请将 <servlet>
和 <servlet-mapping>
元素添加到 Web 应用部署描述符 (web.xml) 中,并指向实现类 (StockPriceServiceImpl)。
从 GWT 1.6 开始,servlet 应该在 Web 应用程序部署描述符 (web.xml) 中定义,而不是在 GWT 模块 (StockWatcher.gwt.xml) 中定义。
在 <servlet-mapping>
元素中,url-pattern 可以是绝对目录路径的形式(例如,/spellcheck
或 /common/login
)。如果在服务接口上使用 @RemoteServiceRelativePath 注释指定默认服务路径(如 StockPriceService 中所做),则确保 url-pattern 与注释值匹配。
由于你将 StockPriceService 映射到“stockPrices”,并且 StockWatcher.gwt.xml 中的模块 rename-to 属性为“stockwatcher”,因此完整的 URL 将为
https://127.0.0.1:8888/stockwatcher/stockPrices
编辑 Web 应用程序部署描述符 (StockWatcher/war/WEB-INF/web.xml)。
- 由于 greetServlet 不再需要,因此可以删除其定义。
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE web-app PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN" "http://java.sun.com/dtd/web-app_2_3.dtd"> <web-app> <!-- Default page to serve --> <welcome-file-list> <welcome-file>StockWatcher.html</welcome-file> </welcome-file-list> <!-- Servlets --> <servlet> <servlet-name>stockPriceServiceImpl</servlet-name> <servlet-class>com.google.gwt.sample.stockwatcher.server.StockPriceServiceImpl</servlet-class> </servlet> <servlet-mapping> <servlet-name>stockPriceServiceImpl</servlet-name> <url-pattern>/stockwatcher/stockPrices</url-pattern> </servlet-mapping> </web-app>
从客户端调用服务
对服务器进行异步调用
你需要在所有服务方法中添加一个 AsyncCallback 参数。
要向所有服务方法添加 AsyncCallback 参数,必须定义一个新的接口,如下所示
- 它必须与服务接口具有相同的名称,并在后面附加 Async(例如,StockPriceServiceAsync)。
- 它必须位于与服务接口相同的包中。
- 每个方法必须与服务接口中的方法具有相同的名称和签名,但有一个重要的区别:该方法没有返回值,最后一个参数是 AsyncCallback 对象。
- 在客户端子包中,创建一个接口并将其命名为 StockPriceServiceAsync。
- 用以下代码替换存根。
package com.google.gwt.sample.stockwatcher.client;
import com.google.gwt.user.client.rpc.AsyncCallback;
public interface StockPriceServiceAsync {
void getPrices(String[] symbols, AsyncCallback<StockPrice[]> callback);
}
提示:当 Eclipse 的 Google 插件找到没有匹配的异步接口的同步远程服务时,它将生成错误。你可以右键单击 Eclipse 中的错误,选择“快速修复”,然后选择“创建异步 RemoteService 接口”选项来自动生成异步接口。
进行远程过程调用
回调方法
当你调用远程过程时,你指定一个回调方法,该方法在调用完成时执行。
通过将一个 AsyncCallback 对象传递给服务代理类来指定回调方法。
AsyncCallback 对象包含两个方法,其中一个方法根据调用失败或成功而被调用:onFailure(Throwable) 和 onSuccess(T)。
创建服务代理类。
在 StockWatcher 类中,通过调用 GWT.create(Class) 创建服务代理类的实例。
private ArrayList<String> stocks = new ArrayList<String>(); private StockPriceServiceAsync stockPriceSvc = GWT.create(StockPriceService.class);
Eclipse 标记 GWT。
初始化服务代理类,设置回调对象,并对远程过程进行调用。
将现有的 refreshWatchList 方法替换为以下代码。
private void refreshWatchList() { // Initialize the service proxy. if (stockPriceSvc == null) { stockPriceSvc = GWT.create(StockPriceService.class); } // Set up the callback object. AsyncCallback<StockPrice[]> callback = new AsyncCallback<StockPrice[]>() { public void onFailure(Throwable caught) { // TODO: Do something with errors. } public void onSuccess(StockPrice[] result) { updateTable(result); } }; // Make the call to the stock price service. stockPriceSvc.getPrices(stocks.toArray(new String[0]), callback); }
Eclipse 标记 AsyncCallback。
包含导入声明。
import com.google.gwt.core.client.GWT;
import com.google.gwt.user.client.rpc.AsyncCallback;
测试远程过程调用
此时,你已经创建了一个服务并在模块 XML 文件中指向它,设置了进行异步调用的机制,并在客户端生成了一个服务代理来调用服务。但是,有一个问题。
- 使用 Eclipse 调试器在开发模式下启动 StockWatcher。
- 检查开发 Shell 窗口中的错误日志。
[ERROR] Type 'com.google.gwt.sample.stockwatcher.client.StockPrice' was not serializable
and has no concrete serializable subtypes
在服务实现(StockPriceServiceImpl)中,你继承了通过扩展 RemoteServiceServlet 类来序列化和反序列化 Java 对象的代码。但是问题是我们也没有编辑 StockPrice 类来指示它是可序列化的。
序列化 Java 对象
序列化是将对象内容打包的过程,以便可以将其从一个应用程序移动到另一个应用程序或存储以备后用。每当你通过 GWT RPC 在网络上传输对象时,都必须对其进行序列化。具体来说,GWT RPC 要求所有服务方法参数和返回值都必须是可序列化的。
如果满足以下条件之一,则类型是可序列化的,可以在服务接口中使用
- 所有原始类型(int、char、boolean 等)及其包装对象默认情况下都是可序列化的。
- 可序列化类型的数组是通过扩展方式可序列化的。
如果类满足以下三个要求,则它是可序列化的
- 它直接实现 Java Serializable 或 GWT IsSerializable 接口,或者因为它是实现该接口的超类的派生类。
- 其非 final、非 transient 实例字段本身是可序列化的,并且
- 它有一个默认的(零参数)构造函数,可以使用任何访问修饰符(例如,private Foo(){} 将起作用)
GWT 遵守 transient 关键字,因此这些字段中的值不会被序列化(因此,在 RPC 调用中使用时不会通过网络发送)。
注意:GWT 序列化与基于 Java Serializable 接口的序列化不同。
有关 GWT 中哪些可序列化和哪些不可序列化的更多信息,请参阅开发者指南,可序列化类型。
序列化 StockPrice
根据序列化要求,你需要执行哪些操作才能使 StockPrice 类准备好进行 GWT RPC?由于所有实例字段都是原始类型,因此在这种情况下,你只需实现 Serializable 或 IsSerializable 接口。
package com.google.gwt.sample.stockwatcher.client;
import java.io.Serializable;
public class StockPrice implements Serializable {
private String symbol;
private double price;
private double change;
...
- 在开发模式下刷新 StockWatcher。
- 添加一个股票。
StockWatcher 将股票添加到表格中;Price 和 Change 字段包含数据,并且没有错误。
- 虽然 StockWatcher 在表面上看起来没有区别,但在底层它现在从嵌入式 servlet 容器上的服务器端 StockPriceService servlet 获取其股票价格更新,而不是在客户端生成它们。
此时,基本的 RPC 机制正在运行。但是,还剩下一个 TODO。你需要在回调失败时编写错误处理代码。
处理异常
当远程过程调用失败时,原因可以归为两类:意外异常或受检异常。在任何情况下,你都希望处理异常,并在必要时向用户提供反馈。
意外异常:任何数量的意外事件都可能导致对远程过程的调用失败:网络可能已断开;另一端的 HTTP 服务器可能没有在监听;DNS 服务器可能着火了,等等。
如果 GWT 能够调用服务方法,但服务实现抛出了未声明的异常,则还会发生另一种类型的意外异常。例如,错误可能会导致 NullPointerException。
当服务实现中发生意外异常时,你可以在开发模式日志中找到完整的堆栈跟踪。在客户端,onFailure(Throwable) 回调方法将接收一个 InvocationException,其中包含以下通用消息:服务调用在服务器上失败;有关详细信息,请参阅服务器日志。
受检异常:如果你知道服务方法可能会抛出特定类型的异常,并且希望客户端代码能够处理它,则可以使用受检异常。GWT 支持 throws 关键字,因此你可以根据需要将其添加到服务接口方法中。当 RPC 服务方法中发生受检异常时,GWT 将序列化异常并将其发送回客户端上的调用者以进行处理。
创建受检异常
要了解如何在 RPC 中处理错误,你将在用户添加已定义为“已退市”的股票代码时抛出错误,从而导致调用失败。然后,你将通过向用户显示错误消息来处理失败。
检查并抛出异常
创建一个类:DelistedException
- 创建一个类来识别已退市的股票代码。
- 在 Eclipse 中,打开新的 Java 类向导(文件 > 新建 > 类)。
- 在“名称”中,输入
DelistedException
- 扩展 java.lang.Exception 类。
- 实现 java.io.Serializable 接口。
- 此异常旨在由 RPC 发送;因此,此类必须是可序列化的。
- 继承抽象方法。
- 按
完成
- Eclipse 创建一个 DelistedException 类存根。
- 将存根替换为以下代码。
package com.google.gwt.sample.stockwatcher.client;
import java.io.Serializable;
public class DelistedException extends Exception implements Serializable {
private String symbol;
public DelistedException() {
}
public DelistedException(String symbol) {
this.symbol = symbol;
}
public String getSymbol() {
return this.symbol;
}
}
更新股票价格服务接口:StockPriceService
在服务接口 (StockPriceService) 中,将 throws 声明附加到 getPrices(String[]) 方法。
在 StockPriceService.java 中,进行如下所示的更改。
package com.google.gwt.sample.stockwatcher.client;
import com.google.gwt.user.client.rpc.RemoteService;
import com.google.gwt.user.client.rpc.RemoteServiceRelativePath;
@RemoteServiceRelativePath("stockPrices")
public interface StockPriceService extends RemoteService {
StockPrice[] getPrices(String[] symbols) throws DelistedException;
}
更新股票价格服务实现:StockPriceServiceImpl
你需要对服务实现 (StockPriceServiceImpl) 进行两个更改。
- 对 getPrices(String[]) 方法进行相应的更改。
- 附加一个 throws 声明。
- 包含 DelistedException 的导入声明。
- 添加抛出 Delisted Exception 的代码。
- 为了本示例的简单起见,只需在将股票代码 ERR 添加到观察列表时抛出异常。
- 此清单显示了已完成的 StockPriceServiceImpl 类。
import com.google.gwt.sample.stockwatcher.client.DelistedException;
...
public StockPrice[] getPrices(String[] symbols) throws DelistedException {
Random rnd = new Random();
StockPrice[] prices = new StockPrice[symbols.length];
for (int i=0; i<symbols.length; i++) {
if (symbols[i].equals("ERR")) {
throw new DelistedException("ERR");
}
double price = rnd.nextDouble() * MAX_PRICE;
double change = price * MAX_PRICE_CHANGE * (rnd.nextDouble() * 2f - 1f);
prices[i] = new StockPrice(symbols[i], price, change);
}
return prices;
}
此时,你已经创建了将抛出异常的代码。你无需将 throws 声明添加到 StockPriceServiceAsync.java 中的服务方法。该方法将始终立即返回(请记住,它是异步的)。相反,你将在 GWT 调用 onFailure(Throwable) 回调方法时收到任何抛出的受检异常。
显示错误消息
为了显示错误,你需要一个新的 UI 组件。首先考虑一下你想要如何显示错误。一个显而易见的解决方案是使用 Window.alert(String) 显示一个弹出式警报。这在用户在输入股票代码时收到错误的情况下可能有效。但在更真实的示例中,如果用户在将股票添加到观察列表后该股票退市,或者你需要显示多个错误,会发生什么?在这些情况下,弹出式警报不是最佳方法。
因此,为了显示有关无法检索股票数据的任何消息,你将实现一个 Label 小部件。
为错误消息定义一个样式,以便它能吸引用户的注意。
序列化 StockPrice
规则适用于任何具有 class 属性为 errorMessage 的元素。
.negativeChange {
color: red;
}
.errorMessage {
color: red;
}
为了保存错误消息的文本,请添加一个 Label 小部件。
在 StockWatcher.java 中,添加以下实例字段。
private StockPriceServiceAsync stockPriceSvc = GWT.create(StockPriceService.class); private Label errorMsgLabel = new Label();
在 StockWatcher 启动时初始化 errorMsgLabel。
- 在 onModuleLoad 方法中,将一个辅助 class 属性添加到 errorMsgLabel,并且在 StockWatcher 加载时不显示它。
- 将错误消息添加到 stocksFlexTable 上方的 Main 面板中。
// Assemble Add Stock panel. addPanel.add(newSymbolTextBox); addPanel.add(addButton); addPanel.addStyleName("addPanel"); // Assemble Main panel. errorMsgLabel.setStyleName("errorMessage"); errorMsgLabel.setVisible(false); mainPanel.add(errorMsgLabel); mainPanel.add(stocksFlexTable); mainPanel.add(addPanel); mainPanel.add(lastUpdatedLabel);
处理错误
现在你已经构建了一个 Label 小部件来显示错误,你可以完成错误处理代码的编写。
如果错误得到纠正(例如,如果用户从股票表格中删除了 ERR 代码),你可能还想隐藏错误消息。
指定如果回调失败要执行的操作。
- 在 StockWatcher.java 中,在 refreshWatchList 方法中,填写 onFailure 方法,如下所示。
public void onFailure(Throwable caught) { // If the stock code is in the list of delisted codes, display an error message. String details = caught.getMessage(); if (caught instanceof DelistedException) { details = "Company '" + ((DelistedException) caught).getSymbol() + "' was delisted"; } errorMsgLabel.setText("Error: " + details); errorMsgLabel.setVisible(true); }
如果错误得到纠正,则隐藏错误消息小部件。
- 在 updateTable(StockPrice[] prices) 方法中,清除所有错误。
private void updateTable(StockPrice[] prices) { for (int i=0; i < prices.length; i++) { updateTable(prices[i]); } // Display timestamp showing last refresh. lastUpdatedLabel.setText("Last update : " + DateTimeFormat.getMediumDateTimeFormat().format(new Date())); // Clear any errors. errorMsgLabel.setVisible(false); }
测试 RPC 中的异常处理
此时,你已经创建并检查了异常——已退市的股票代码“ERR”。当 StockWatcher 对已退市的股票代码进行远程过程调用以检索股票数据时,调用失败,抛出异常,然后通过显示错误消息来处理该异常。
- 在开发模式下刷新 StockWatcher。
- 添加股票代码 ERR。
- StockWatcher 显示红色错误消息。有效的股票代码数据停止刷新。
删除股票代码 ERR。
- 错误消息被删除。有效的股票代码数据恢复刷新。
下一步
此时,你已经对服务器进行了远程过程调用以获取股票数据。
有关 GWT RPC 的更多信息,请参阅开发者指南,远程过程调用。
将服务部署到生产环境中
在测试期间,您可以使用开发模式的内置 servlet 容器来测试您的 RPC 服务的服务器端代码,就像在本教程中一样。当您部署 GWT 应用程序时,可以使用任何 servlet 容器来托管您的服务。确保您的客户端代码使用 web.xml 配置文件中映射到 servlet 的 URL 来调用服务。
要了解如何将 GWT RPC servlet 部署到生产服务器,请参阅开发者指南,部署 RPC