检索 JSON
此时,您已创建 StockWatcher 应用程序的初始实现,在客户端代码中模拟股票数据。
在本页中,您将调用本地服务器来检索 JSON 格式的股票数据。
以下为补充信息
注意: 有关 GWT 应用程序中客户端-服务器通信的更广泛指南,请参阅 与服务器通信。
什么是 JSON?
JSON 是一种通用的、与语言无关的数据格式。从这种意义上说,它类似于 XML。XML 使用标签,而 JSON 则基于 JavaScript 的对象字面量表示法。因此,这种格式比 XML 更简单。一般来说,JSON 编码的数据比 XML 中的等效数据更简洁,因此 JSON 数据的下载速度比 XML 数据更快。
当您将 StockWatcher 的股票数据编码为 JSON 格式时,它将看起来像这样(但空格会被去除)。
[
{
"symbol": "ABC",
"price": 87.86,
"change": -0.41
},
{
"symbol": "DEF",
"price": 62.79,
"change": 0.49
},
{
"symbol": "GHI",
"price": 67.64,
"change": 0.05
}
]
在本地服务器上创建 JSON 数据源
查看现有实现
在原始的 StockWatcher 实现中,您创建了一个 StockPrice 类,并使用 refreshWatchList 方法生成随机股票数据,然后调用 updateTable 方法来填充 StockWatcher 的 flex 表格。
/**
* 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);
}
在本教程中,您将创建一个 servlet 来以 JSON 格式生成股票数据。然后,您将发出 HTTP 调用以从服务器检索 JSON 数据。您将使用 JSNI 和 GWT 覆盖类型在编写客户端代码时处理 JSON 数据。
编写 servlet
要以 JSON 格式提供假设的股票报价,您将创建一个 servlet。要使用嵌入式 servlet 容器(Jetty)提供数据,请将 JsonStockData 类添加到 StockWatcher 项目的 server 目录中,并在 Web 应用程序部署描述符 (web.xml) 中引用该 servlet。
注意:如果您在本地安装了 Web 服务器(Apache、IIS 等)并安装了 PHP,则可以改为 编写一个 PHP 脚本以生成股票数据 并调用您的本地服务器。本示例重要的是股票数据是 JSON 编码的,并且服务器是本地的。
- 创建 servlet。
- 在包资源管理器中,选择 client 包:
com.google.gwt.sample.stockwatcher.client
- 在 Eclipse 中,打开“新建 Java 类”向导(文件 > 新建 > 类)。
- 在包资源管理器中,选择 client 包:
- 在“包”中,将名称从
.client
更改为.server
- 在“名称”中,输入
JsonStockData
。 - Eclipse 将为服务器端代码创建一个包,并为 JsonStockData 类创建一个存根。
- 在“名称”中,输入
- 用以下代码替换存根。
package com.google.gwt.sample.stockwatcher.server;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Random;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
public class JsonStockData extends HttpServlet {
private static final double MAX_PRICE = 100.0; // $100.00
private static final double MAX_PRICE_CHANGE = 0.02; // +/- 2%
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
Random rnd = new Random();
PrintWriter out = resp.getWriter();
out.println('[');
String[] stockSymbols = req.getParameter("q").split(" ");
boolean firstSymbol = true;
for (String stockSymbol : stockSymbols) {
double price = rnd.nextDouble() * MAX_PRICE;
double change = price * MAX_PRICE_CHANGE * (rnd.nextDouble() * 2f - 1f);
if (firstSymbol) {
firstSymbol = false;
} else {
out.println(" ,");
}
out.println(" {");
out.print(" \"symbol\": \"");
out.print(stockSymbol);
out.println("\",");
out.print(" \"price\": ");
out.print(price);
out.println(',');
out.print(" \"change\": ");
out.println(change);
out.println(" }");
}
out.println(']');
out.flush();
}
}
在 GWT 模块中包含服务器端代码
嵌入式 servlet 容器(Jetty)可以托管以 JSON 格式生成股票数据的 servlet。
要进行设置,请将 <servlet>
和 <servlet-mapping>
元素添加到 Web 应用程序部署描述符 (web.xml) 中,并指向 JsonStockData。
从 GWT 1.6 开始,servlet 应在 Web 应用程序部署描述符 (web.xml) 中定义,而不是在 GWT 模块 (StockWatcher.gwt.xml) 中定义。
在 <servlet-mapping>
元素中,url-pattern 可以是绝对目录路径的形式(例如,/spellcheck
或 /common/login
)。如果您在服务接口上使用 @RemoteServiceRelativePath 注释指定了默认服务路径(如您对 StockPriceService 所做的),那么请确保 path 属性与注释值匹配。
因为您已将 StockPriceService 映射到“stockPrices”,并且 StockWatcher.gwt.xml 中的 module 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>jsonStockData</servlet-name>
<servlet-class>com.google.gwt.sample.stockwatcher.server.JsonStockData</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>jsonStockData</servlet-name>
<url-pattern>/stockwatcher/stockPrices</url-pattern>
</servlet-mapping>
</web-app>
测试您从服务器检索 JSON 数据的能力
- 在开发模式下调试 StockWatcher。
- 此时,股票数据仍然来自客户端代码。
- 测试股票报价服务器。
- 确保开发模式代码服务器正在运行,并将股票代码传递到 servlet URL
https://127.0.0.1:8888/stockwatcher/stockPrices?q=ABC+DEF
- 确保开发模式代码服务器正在运行,并将股票代码传递到 servlet URL
- servlet 将生成以 JSON 格式编码的模拟股票数据数组。
在客户端代码中操作 JSON 数据
概述
此时,您已验证您能够从服务器获取 JSON 数据。在本节的后面,您将编写对服务器的 HTTP GET 请求。首先,重点关注处理返回到客户端代码的 JSON 编码文本。您将使用的两种技术是 JSNI(JavaScript 本机接口)和 GWT 覆盖类型。
这是从服务器返回的 JSON 数据。
[
{
"symbol": "ABC",
"price": 47.65563005127077,
"change": -0.4426563818062567
},
]
首先,您将使用 JsonUtils.safeEval() 将 JSON 字符串转换为 JavaScript 对象。然后,您将能够编写方法来访问这些对象。
// JSNI methods to get stock data.
public final native String getSymbol() /*-{ return this.symbol; }-*/;
public final native double getPrice() /*-{ return this.price; }-*/;
public final native double getChange() /*-{ return this.change; }-*/;
对于后者,您将使用 JSNI。当客户端代码编译为 JavaScript 时,Java 方法将用您在标记之间编写的 JavaScript 代码替换。
使用 JSNI 进行编码
如上面的示例所示,使用 JSNI,您可以在 GWT 模块中调用手写(而不是 GWT 生成的)JavaScript 方法。
JSNI 方法声明为 native 并包含在参数列表末尾和尾随分号之间的特殊格式的注释块中的 JavaScript 代码。JSNI 注释块以 /*-{
标记开始,以 }-*/
标记结束。JSNI 方法的调用方式与任何普通 Java 方法相同。它们可以是静态方法或实例方法。
深入: 有关将手写 JavaScript 与 Java 源代码混合的技巧、窍门和注意事项,请参阅开发人员指南的 JavaScript 本机接口 (JSNI) 部分。
将 JSON 转换为 JavaScript 对象
首先,您需要将来自服务器的 JSON 文本转换为 JavaScript 对象。这可以使用静态 JsonUtils.safeEval()
方法轻松完成。我们稍后将看到如何使用它。
JSON 数据类型
正如您可能期望的那样,JSON 数据类型对应于 JavaScript 的内置类型。JSON 可以编码字符串、数字、布尔值和空值,以及由这些类型组成的对象和数组。与 JavaScript 中一样,对象实际上只是一组无序的名称/值对。但是,在 JSON 对象中,值只能是其他 JSON 类型(不能是包含可执行 JavaScript 代码的函数)。
将 JSON 字符串转换为可以处理的内容的另一种技术是使用静态 JSONParser.parse(String) 方法。GWT 包含一组完整的 JSON 类型,用于在 com.google.gwt.json.client 包中操作 JSON 数据。如果您更愿意解析 JSON 数据,请参阅开发人员指南的 使用 JSON 部分。
创建覆盖类型
您的下一个任务是用 StockData 类型替换现有的 StockPrice 类型。
您不仅希望访问 JSON 对象数组,而且还希望能够像处理 Java 对象一样处理它们,同时进行编码。GWT 的覆盖类型允许您这样做。
新的 StockData 类将是一个覆盖类型,它覆盖了现有的 JavaScript 对象。
创建 StockData 类。
注意: 注释的数字指的是下面的实现说明。您可以删除它们。
package com.google.gwt.sample.stockwatcher.client;
import com.google.gwt.core.client.JavaScriptObject;
class StockData extends JavaScriptObject { // (1)
// Overlay types always have protected, zero argument constructors.
protected StockData() {} // (2)
// JSNI methods to get stock data.
public final native String getSymbol() /*-{ return this.symbol; }-*/; // (3)
public final native double getPrice() /*-{ return this.price; }-*/;
public final native double getChange() /*-{ return this.change; }-*/;
// Non-JSNI method to return change percentage. // (4)
public final double getChangePercent() {
return 100.0 * getChange() / getPrice();
}
}
实现说明
(1) StockData 是 JavaScriptObject 的子类,JavaScriptObject 是 GWT 用来表示 JavaScript 对象的标记类型。
JavaScriptObject 在 GWT 编译器和开发模式代码服务器中得到特殊处理。它的目的是为 Java 代码提供本机 JavaScript 对象的不透明表示。
(2) 覆盖类型始终具有受保护的、无参数构造函数。
(3) 通常,覆盖类型上的方法是 JSNI。
这些 getter 直接访问您知道的 JSON 字段。
根据设计,覆盖类型上的所有方法都是 final 和 private;因此,每个方法都可以在编译时静态解析,因此在运行时不需要动态调度。
(4) 但是,覆盖类型上的方法不需要是 JSNI。
就像你在 StockPrice 类中所做的那样,你根据价格和变化值计算变化百分比。
使用覆盖类型的优势
使用覆盖类型可以创建一个外观正常的 Java 类型,你可以使用代码补全、重构和编译时检查与它交互。然而,你也可以灵活地与任意 JavaScript 对象交互,这使得使用 RequestBuilder 访问 JSON 服务变得更加简单(你将在下一节中进行)。
GWT 现在理解 StockData 的任何实例都是来自此 GWT 模块外部的真实 JavaScript 对象。你可以像它在 JavaScript 中存在的那样与它交互。在这个例子中,你可以直接访问你知道存在的 JSON 字段:this.Price 和 this.Change。
由于覆盖类型上的方法可以由 GWT 编译器静态解析,因此它们是自动内联的候选对象。内联代码运行速度明显更快。这使得 GWT 编译器能够为应用程序的客户端代码创建高度优化的 JavaScript。
发出 HTTP 请求以从服务器检索数据
现在你已经有了处理 JSON 数据的机制,你将编写从服务器获取数据的 HTTP 请求。
在这个例子中,你将用一个使用 HTTP 的新实现替换当前的 refreshWatchList 方法。
指定 URL
首先,指定 servlet 所在的 URL,即:https://127.0.0.1:8888/stockwatcher/stockPrices?q=ABC+DFD
注意:如果你正在做 php 示例,请替换相应的 URL。
然后,将监视列表中的股票代码追加到基本模块 URL。与其硬编码 JSON 服务器的 URL,不如在 StockWatcher 类中添加一个常量。
- 在 StockWatcher 类中添加一个常量,指定 JSON 数据的 URL。
- 注意:之前你曾在模块 XML 文件中指定了 /stockPrices 的路径。
private static final String JSON_URL = GWT.getModuleBaseURL() + "stockPrices?q=";
- Eclipse 标记 GWT。
- 包含导入声明。
import com.google.gwt.core.client.GWT;
- 将股票代码追加到查询 URL 并对其进行编码。
- 用以下代码替换现有的 refreshWatchList 方法。*
private void refreshWatchList() {
if (stocks.size() == 0) {
return;
}
String url = JSON_URL;
// Append watch list stock symbols to query URL.
Iterator<String> iter = stocks.iterator();
while (iter.hasNext()) {
url += iter.next();
if (iter.hasNext()) {
url += "+";
}
}
url = URL.encode(url);
// TODO Send request to server and handle errors.
}
- Eclipse 标记 Iterator 和 URL。
- 包含导入声明。
import com.google.gwt.http.client.URL;
import java.util.Iterator;
异步 HTTP
要从服务器获取 JSON 文本,你将使用 com.google.gwt.http.client 包中的 HTTP 客户端类。这些类包含进行异步 HTTP 请求的功能。
HTTP 类型包含在 StockWatcher 需要继承的单独的 GWT 模块中。
- 要继承其他 GWT 模块,请编辑模块 XML 文件。
- 在 StockWatcher.gwt.xml 中,添加
<inherits>
标签并指定 HTTP 模块。
- 在 StockWatcher.gwt.xml 中,添加
<!-- Other module inherits -->
<inherits name="com.google.gwt.http.HTTP" />
2. * 打开 StockWatcher.java 并包含以下导入声明。声明以下 Java 类型的导入。
import com.google.gwt.http.client.Request;
import com.google.gwt.http.client.RequestBuilder;
import com.google.gwt.http.client.RequestCallback;
import com.google.gwt.http.client.RequestException;
import com.google.gwt.http.client.Response;
构建自定义 HTTP 请求
要发送请求,你将创建一个 RequestBuilder 对象的实例。你在构造函数中指定 HTTP 方法(GET、POST 等)和 URL。如有必要,你还可以设置要在 HTTP 请求中使用的用户名、密码、超时和标头。在这个例子中,你不需要这样做。
当你准备好发出请求时,你调用 sendRequest(String, RequestCallback)。
你传递的 RequestCallback 参数将在其 onResponseReceived(Request, Response) 方法中处理响应,该方法在 HTTP 调用成功完成时(如果完成)被调用。如果调用失败(例如,如果 HTTP 服务器没有响应),则改为调用 onError(Request, Throwable) 方法。RequestCallback 接口类似于 GWT 远程过程调用中的 AsyncCallback 接口。
发出 HTTP 请求并解析 JSON 响应。
- 在 refreshWatchList 方法中,用以下代码替换 TODO 注释。
``` // 向服务器发送请求并捕获任何错误。 RequestBuilder builder = new RequestBuilder(RequestBuilder.GET, url);
try { Request request = builder.sendRequest(null, new RequestCallback() { public void onError(Request request, Throwable exception) { displayError(“无法检索 JSON”); }
}); } catch (RequestException e) { displayError(“无法检索 JSON”); } ```public void onResponseReceived(Request request, Response response) { if (200 == response.getStatusCode()) { updateTable(JsonUtils.<JsArray<StockData>>safeEval(response.getText())); } else { displayError("Couldn't retrieve JSON (" + response.getStatusText() + ")"); } }
- 在这里,我们使用
JsonUtils.safeEval()
将 JSON 字符串转换为 JavaScript 对象。
你会收到两个编译错误,你将在稍后解决它们。
修改 updateTable 方法
要修复编译错误,请修改 updateTable 方法。
<li>Change: StockPrice[] prices
至:JsArray
1. * 用以下代码替换现有的 update updateTable(StockPrice[]) 方法。
updateTable(JsArray<StockData> prices)
- Eclipse 标记 JsArray。
- 声明导入。
import com.google.gwt.core.client.JsArray;
- 在 updateTable(StockPrice price) 方法中进行相应的更改。
- 将对 StockPrice 类的引用更改为 StockData 类。
private void updateTable(StockData price) {
// Make sure the stock is still in the stock table.
if (!stocks.contains(price.getSymbol())) {
return;
}
...
}
处理 GET 错误
如果在此过程中出现问题(例如,如果服务器脱机或 JSON 格式错误),你将捕获错误并向用户显示一条消息。为此,你将创建一个 Label 小部件并编写一个新的方法 displayError(String)。
- 通过在 StockWatcher 类中创建一个存根方法来清除编译错误。
- 设置错误消息的文本并使 Label 小部件可见。
/**
* If can't get JSON, display error message.
* @param error
*/
private void displayError(String error) {
errorMsgLabel.setText("Error: " + error);
errorMsgLabel.setVisible(true);
}
- Eclipse 标记 errorMsgLabel。
- 暂时忽略编译错误;你将在下一步中实现一个 Label 小部件来显示错误文本。
- 如果错误已更正,请隐藏 Label 小部件。
- 在 updateTable(JsArray
prices) 方法中,清除任何错误消息。
- 在 updateTable(JsArray
private void updateTable(JsArray<StockData> prices) {
for (int i=0; i < prices.length(); i++) {
updateTable(prices.get(i));
}
// Display timestamp showing last refresh.
lastUpdatedLabel.setText("Last update : " +
DateTimeFormat.getMediumDateTimeFormat().format(new Date()));
// Clear any errors.
errorMsgLabel.setVisible(false);
}
显示错误消息
为了显示错误,你需要一个新的 UI 组件;你将实现一个 Label 小部件。
- 为错误消息定义一个样式,以便它能引起用户的注意。
- 在 StockWatcher.css 中,创建一个样式规则,该规则应用于具有 errorMessage 类属性的任何元素。
.negativeChange {
color: red;
}
.errorMessage {
color: red;
}
- 要保存错误消息的文本,请添加一个 Label 小部件。
- 在 StockWatcher.java 中,添加以下实例字段。
private ArrayList<String> stocks = new ArrayList<String>();
private Label errorMsgLabel = new Label();
- 在 StockWatcher 启动时初始化 errorMsgLabel。
- 在 onModuleLoad 方法中,将一个辅助类属性添加到 errorMsgLabel,并且在 StockWatcher 加载时不显示它。
- 将错误消息添加到 Main 面板上的 stocksFlexTable 之上。
// 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);
测试 HTTP 请求和错误处理
- 在开发模式下刷新 StockWatcher。
- 输入多个股票代码。StockWatcher 应该显示每个股票的价格和变化数据。股票数据不是从程序中生成的,而是来自你本地服务器的 JSON 格式。
通过调用无效 URL 来测试 HTTP 错误消息。
- 在 StockWatcher,java 中,编辑 URL。* 更改:
private static final String JSON_URL = GWT.getModuleBaseURL()
- “stockPrices?q=”;`
To: `private static final String JSON_URL = GWT.getModuleBaseURL()
- “BADURL?q=”;`
- 在 StockWatcher,java 中,编辑 URL。* 更改:
- 在开发模式下刷新 StockWatcher。
- 输入一个股票代码。
- StockWatcher 显示一条红色的错误消息。有效的股票代码数据停止刷新。
- 在 StockWatcher,java 中,更正 URL。
- 在开发模式下刷新 StockWatcher。
- 输入多个股票代码。
- StockWatcher 应该再次显示每个股票的价格和变化数据。
关于 JSON、JSNI、覆盖类型和 HTTP 的更多信息
到目前为止,你已经从本地服务器检索了 JSON 编码的股票数据,并用它更新了监视列表中股票的价格和变化字段。如果你想了解如何从另一个域上的 Web 服务器检索 JSON,请参阅 发出跨站点请求。
要了解有关客户端-服务器通信的更多信息,请参阅开发人员指南“与服务器通信”。主题包括
要了解有关 JSNI 的更多信息,请参阅开发人员指南的 JavaScript 本机接口 (JSNI)。主题包括
- 编写本机 JavaScript 方法
- 从 JavaScript 访问 Java 方法和字段
- 在 Java 源代码和 JavaScript 之间共享对象
- 异常和 JSNI
- JavaScript 覆盖类型