使用 Oracle JDeveloper 构建您的第一个 GWT Web 应用程序
到目前为止,您已经了解了 GWT 的工作方式;现在,让我们编码示例 Web 应用程序。
示例应用程序是一个工作列表管理器。其特性十分简单:创建、编辑、删除工作列表并对其进行优先级排列。我们选择了该示例是因为它很容易理解,然而其实施涵盖了大量 GWT 的特性。
下面是最终应用程序的屏幕快照:
第 1 步:安装 GWT
从 Google 的 Web 站点 http://code.google.com/webtoolkit/ 下载 GWT。在写本文时,GWT 推出的是 Windows 和 Linux 版本。GWT 是特定于平台的,因为其托管模式在 Firefox 的修改版本中工作,该版本本身依赖于平台。(我们可以在 Apple 计算机上成功地使用 GWT 的 Linux 版本,但是托管模式不起作用。)
GWT 下载形式是一个归档文件,您必须使用 Linux 上的 tar -xvf 命令或者 Windows 上的解压缩工具进行解压缩。这就是您安装该工具包需要做的所有工作。
第 2 步:运行 applicationCreator 脚本
打开命令行,转至 GWT 的安装目录。该目录包含 applicationCreator 脚本,我们将使用该脚本启动我们的应用程序。由于我们希望应用程序存储在 Oracle Technology Network 目录中,因此我们将“-out otn”作为参数添加到脚本中。在 Linux 上,键入:
./applicationCreator -out otn otn.todo.client.TodoApp
在 Windows 上,使用:
applicationCreator -out otn otn.todo.client.TodoApp该脚本生成基本的项目结构 — 请求的应用程序类中的示例“Hello word”代码以及两个脚本:TodoApp-shell(用于在托管模式下运行应用程序)和 TodoApp-compile(用于打包应用程序以便在 Web 模式下使用)。
第 3 步:在 JDeveloper 中打开项目
启动 JDeveloper 并创建一个新的 Web 项目:
单击 Next 按钮。JDeveloper 将询问新项目的位置。使用应用程序的名称作为 Project Name,选择应用程序根目录(如步骤 2 的定义)作为 Directory Name:
单击 Next 按钮,并验证您的应用程序是 J2EE 1.4 应用程序:
单击 Next 按钮,并选择您的项目 Web 属性:Document Root 是当前项目的 www 目录,J2EE Web Application Name 和 J2EE Context Root 都是项目名称:这将创建 JDeveloper 项目,但是将出现某些编译错误,因为 GWT 的库未包含在项目类路径中。在项目属性中,选择左侧端树的 Libraries 节点,并添加 gwt-user.jar 库:
您的项目现在应该可以编译,看起来与以下内容相似:
编写客户端代码
上面的 applicationCreator 脚本创建了一个基本的“Hello world”应用程序,可在 otn.todo.client 程序包中使用。下面是其主要方法:
public void onModuleLoad() {该方法将创建一个按钮“Click Me”。单击该按钮后,将显示“Hello World”。
final Button button = new Button("Click me");
final Label label = new Label();
button.addClickListener(new ClickListener() {
public void onClick(Widget sender) {
if (label.getText().equals(""))
label.setText("Hello World!");
else
label.setText("");
}
});
RootPanel.get("slot1").add(button);
RootPanel.get("slot2").add(label);
}
}
该方法分为三部分:
- 创建 Button 和 Label 小部件
- 创建 ClickListener 对象。该代码与您用 Swing 编写的内容很接近;如果您具有桌面 Java 背景则更容易理解。
- 在 HTML 页上显示小部件:slot1 和 slot2 都是该页上的 HTML 元素
在托管模式下运行和调试
现在您已经创建了应用程序并且已经看到其生成的内容,结下来让我们来执行它。
您可以通过从命令行使用 TodoApp-shell 脚本轻松地运行该项目。虽然这是启动应用程序的很好途径,但是您可能更喜欢直接从 JDeveloper 内启动应用程序。为此,单击 Run 菜单,选择 Choose Active Run Configuration > Manage Run Configurations。编辑默认的运行配置并使用以下命令:
- 对于 Default Run Target:使用 com.google.gwt.dev.GWTShell,它在特定于平台的 GWT jar 内。在 Linux 上,它类似以下内容:
path.to.your.gwt.installation.directory/gwt-devlinux.jar!/com/google/gwt/dev/GWTShell.class
在 Windows 上,它类似以下内容:path.to.your.gwt.installation.directory/gwt-dev-windows.jar!/com/google/gwt/dev/GWTShell.class
- 对于 Program Arguments,使用:
-out path.to.your.gwt.installation.directory/otn/www otn.todo.TodoApp/TodoApp.html
- 对于 Run Directory,使用
path.to.your.gwt.installation.directory/otn
这是一个很复杂的设置,但是令人欣慰的是,您可以重新使用它对应用程序进行调试。使用 Debug 按钮而不是 Run 按钮。然后,您可以象平常一样使用调试器 — 设置断点、逐步执行代码等:
关于该特性给人印象很深的是,您可以通过标准的 JDeveloper 调试器调试用 Java 编写的客户端代码。扩展您的 GWT Web 应用程序
现在您已经创建了一个简单的 GWT Web 应用程序,让我们通过两个最常用的 GWT 特性对其进行扩展:RPC 机制(该机制允许应用程序调用服务器端代码)和 History 对象(通过该对象,用户可精确处理浏览器的 Back 按钮)。
使用 RPC 进行客户端和服务器之间的数据交换
到目前为止,您只创建了应用程序的客户端代码:使用 GWT 编译器,您已经生成了大量 HTML 和 JavaScript 文件,它们将在最终用户的浏览器中运行。但是,如果该应用程序不能与服务器通信就没有什么用处了。
使用 GWT,客户端/服务器通信就是对 servlet 进行编码并使其与应用程序通信。下面是您要做的工作。
创建一个定义您的服务的接口。该接口必须扩展 Google 的 com.google.gwt.user.client.rpc.RemoteService 接口,并可以放到客户端程序包(本例为 otn.todo.client)中。
然后,对接口进行编码以便允许您在服务器上读取和写入工作列表:
package otn.todo.client;对 Servlet 进行编码。在服务器端,您必须编码出具有以下特征的类:
import java.util.List;
import com.google.gwt.user.client.rpc.RemoteService;
public interface TodoListBackupService extends RemoteService {
/**
* Save the to-do list on the server.
*/
void saveTodoList(List todoList);
/**
* Get the to-do list on the server.
*/
List getTodoList();
}
- 扩展 Google 的 com.google.gwt.user.server.rpc.RemoteServiceServlet 类(该类反过来会扩展 Java 的 javax.servlet.http.HttpServlet,有效使其成为 servlet)
- 实施步骤 1 中编写的接口
- 位于服务器程序包(本例为 otn.todo.server)中
package otn.todo.server;该 servlet 在用户的 HttpSession 中只存储工作列表;这当然是保存数据的基本方法。在一般的应用程序中,您可以使用 JNDI 访问 EJB,或者使用任何经典模式从 servlet 访问业务服务。
import java.util.ArrayList;
import java.util.List;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
import otn.todo.client.Todo;
import otn.todo.client.TodoListBackupService;
import com.google.gwt.user.server.rpc.RemoteServiceServlet;
public class TodoListBackupServiceImpl extends RemoteServiceServlet implements
TodoListBackupService {
private static final String TODOLIST_KEY = "TODOLIST_KEY";
public void saveTodoList(List todoList) {
HttpServletRequest request = this.getThreadLocalRequest();
HttpSession session = request.getSession();
session.setAttribute(TODOLIST_KEY, todoList);
}
public List getTodoList() {
HttpServletRequest request = this.getThreadLocalRequest();
HttpSession session = request.getSession();
if (session.getAttribute(TODOLIST_KEY) == null) {
List todoList = new ArrayList();
Todo todo = new Todo("Hello from the server");
todoList.add(todo);
return todoList;
} else {
return (List) session.getAttribute(TODOLIST_KEY);
}
}
}
最后,您必须在 servlet 容器内配置该 servlet。如果您使用的是 GWT shell,您可以在 *.gwt.xml 配置文件中进行配置,本例中该配置文件为 TodoApp.gwt.xml:
<module>如果您希望在其他应用服务器(如 OC4J)中对其进行配置,只需将平常的 XML 配置添加到 WEB-INF/web.xml 文件中即可:
<!-- Inherit the core Web Toolkit stuff. -->
<inherits name='com.google.gwt.user.User'/>
<!-- Specify the app entry point class. -->
<entry-point class='otn.todo.client.TodoApp'/>
<servlet path="/todoListBackupService" class="otn.todo.server.TodoListBackupServiceImpl"/>
</module>
<servlet>添加一些粘合剂。我们需要的粘合剂是 Async 类,它必须遵循几个规则:
<servlet-name>TodoListBackupService</servlet-name>
<servlet-class>otn.todo.server.TodoListBackupServiceImpl</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>TodoListBackupService</servlet-name>
<url-pattern>/todoListBackupService</url-pattern>
</servlet-mapping>
- 位于客户端程序包(otn.todo.client)中。
- 其名称与步骤 1 中描述的接口的名称相同,最后面添加 Async。
- 其方法与步骤 1 中描述的接口的方法相同,但是它们都回调一个附加参数 com.google.gwt.user.client.rpc.AsyncCallback。
package otn.todo.client;
import java.util.List;
import com.google.gwt.user.client.rpc.AsyncCallback;
public interface TodoListBackupServiceAsync {
/**
* Save the to-do list on the server.
*/
void saveTodoList(List todoList, AsyncCallback callback);
/**
* Get the to-do list on the server.
*/
void getTodoList(AsyncCallback callback);
}
- 在应用程序内使用该类。要从客户端应用程序内访问服务器端代码,使用 com.google.gwt.core.client.GWT 类,该类可以创建一个很特殊的对象:
TodoListBackupServiceAsync todoListBackupService = (TodoListBackupServiceAsync) GWT.create(TodoListBackupService.class);这将在运行时创建一个实施两个接口的类:
- 我们刚刚在步骤 3 中进行编码的 Async 接口
- Google 的 com.google.gwt.user.client.rpc.ServiceDefTarget 接口
ServiceDefTarget endpoint = (ServiceDefTarget) todoListBackupService; endpoint.setServiceEntryPoint("/todoListBackupService");现在您已经将该对象配置为可访问服务器端服务,让我们来访问服务。如您在步骤 3 中所见,Async 接口允许您通过添加 AsyncCallback 回调参数访问在服务中定义的所有方法。该参数用于定义应用程序的行为,具体取决于服务器端调用的成功或失败:
AsyncCallback callback = new AsyncCallback() {让我们把它们全都放在一起。下面是访问 TodoListBackupService 业务服务的两个客户端方法的完整代码:一个用于在服务器端保存工作列表,另一个用于读取该列表:
public void onSuccess(Object result) {
printTodoList();
}
public void onFailure(Throwable caught) {
Window.alert("Warning : the to-do list could not be saved on the server. Maybe the server is down.");
}
};
/**示例应用程序在启动时进行服务器端调用。该调用将返回用户的 HttpSession 中保存的最新工作列表,或者包含“Hello from the server”工作的新工作列表:
* Update the to-do list with data from the server.
*/
private void updateTodoListFromServer() {
TodoListBackupServiceAsync todoListBackupService =
(TodoListBackupServiceAsync)GWT.create(TodoListBackupService.class);
ServiceDefTarget endpoint = (ServiceDefTarget)todoListBackupService;
endpoint.setServiceEntryPoint("/todoListBackupService");
AsyncCallback callback = new AsyncCallback() {
public void onSuccess(Object result) {
todoList = (List)result;
saveTodoListInHistory();
}
public void onFailure(Throwable caught) {
Todo todo =
new Todo("ERROR!! Server could not be reached.");
todoList.add(todo);
saveTodoListInHistory();
}
};
todoListBackupService.getTodoList(callback);
}
/**
* Save the to-do list on the server.
*/
private void saveTodoListOnServer() {
saveTodoListInHistory();
TodoListBackupServiceAsync todoListBackupService =
(TodoListBackupServiceAsync)GWT.create(TodoListBackupService.class);
ServiceDefTarget endpoint = (ServiceDefTarget)todoListBackupService;
endpoint.setServiceEntryPoint("/todoListBackupService");
AsyncCallback callback = new AsyncCallback() {
public void onSuccess(Object result) {
printTodoList();
}
public void onFailure(Throwable caught) {
Window.alert("Warning : the to-do list could not be saved on the server. Maybe the server is down.");
}
};
todoListBackupService.saveTodoList(todoList, callback);
}
管理 Back 按钮
在高端 Web 应用程序中,浏览器的 Back 按钮经常断开。经典的 Ajax 应用程序不支持返回前一 Web 页的标准 Web 行为。
另一方面,GWT 允许对 Back 按钮进行编程处理。这是一个功能强大却又很难处理的特性,我们将在示例应用程序中对其进行探究。提议是将 Back 按钮用作 Undo 按钮:单击该按钮将显示最新事件之前的工作列表。同样地,Forward 按钮将用作 Redo 按钮。
实施 HistoryListener 接口。要以编程方式管理 Back 按钮,GWT 应用程序必须实施 com.google.gwt.user.client.HistoryListener 接口。这将强制编写 onHistoryChanged(String _historyToken) 方法:
public class TodoApp implements EntryPoint, HistoryListener {该方法意味着当浏览器的历史记录更改时接收事件。您必须将其作为监听器添加到 GWT 的 History 对象中。该操作通常在 onModuleLoad() 方法中完成,以便 History 对象在启动时正确初始化:
/**
* This method is called whenever the application's history changes.
*/
public void onHistoryChanged(String _historyToken) {
if (Integer.parseInt(_historyToken) + 1 != historyToken) {
if (historyMap.get(_historyToken) != null) {
historyToken = Integer.parseInt(_historyToken);
todoList = (List) historyMap.get(_historyToken);
}
}
printTodoList();
}
/**现在,每次浏览器的历史记录更改时都会调用 onHistoryChanged(String _historyToken) 方法。
* This is the entry point method.
*/
public void onModuleLoad() {
History.addHistoryListener(this);
}
该方法可以根据作为参数传递的令牌重新创建应用程序的状态。本例中,您将使用该令牌作为密钥来查找存储在历史地图中的工作列表。
向历史记录中添加条目。要使 onHistoryChanged(String _historyToken) 方法起作用,您必须预先在历史记录中存储一些条目。
利用 History 对象很容易实现,可以使用其静态 newItem(String historyToken) 方法:
private void saveTodoListInHistory() {在本例中,您将应用程序状态存储在了地图中,因此使用历史记录令牌可以找到它。注意,您使用了一个数字作为历史记录令牌,也可以改用任何字符串。
List todoListClone = new ArrayList();
Iterator it = todoList.iterator();
while (it.hasNext()) {
Todo todo = (Todo) it.next();
todoListClone.add(todo.clone());
}
historyMap.put(String.valueOf(historyToken), todoListClone);
History.newItem(String.valueOf(historyToken));
historyToken++;
}
部署您的 Web 应用程序
要部署通过 GWT 构建的 Web 应用程序,您需要编译客户端代码,在 Web 应用程序的 .war 文件中打包结果,然后将 .war 文件部署到相应的应用服务器 OC4J 上。
编译客户端代码
编译客户端代码的方法有多种。使用 applicationCreator 脚本后,GWT 将创建一个名为 TodoApp-compile 的 shell 脚本。您可以从命令行启动该脚本。与 TodoApp-shell 一样,这是编译应用程序的很好方法;但是,您可能更喜欢直接从 JDeveloper 内部启动该脚本。
另一种编译代码的方法使以托管模式执行应用程序,以便直接从 JDeveloper 编译代码。您的应用程序的窗口的工具栏包含编译/浏览按钮,与下图类似:
在编译过程结束时,您的默认 Web 浏览器将打开以便您可以测试结果。GWT 开发 shell 的窗口将显示编译是否成功:最后一种编译代码的方法是使用 Ant。GWT 不提供特定的 Ant 任务,但是您可以通过标准的 Java Ant 任务启动任何 Java 类(例如 GWTCompiler)。首先,定义包含 GWT jar 的路径:
现在,定义专用于编译客户端代码的任务:
<path id="project.class.path">
<pathelement path="${java.class.path}/"/>
<pathelement location="src"/>
<pathelement path="/your/path/to/gwt-user.jar"/>
<pathelement path="/your/path/to/gwt-dev-linux.jar"/>
<!-- ... -->
</path>
<target name="GWTcompile">在属性文件中设置 gwt.output.dir 和 entry.point.class 变量,如下所示:
<java classpathref="http://www.oracle.com/technology/pub/articles/project.class.class.path"
classname="com.google.gwt.dev.GWTCompiler"
fork="true">
<arg value="-out"/>
<arg value="${gwt.output.dir}"/>
<arg value="${entry.point.class}"/>
</java>
</target>
gwt.output.dir=www最后,在 Ant 脚本中声明属性文件(此处为 build.properties),如下所示:
entry.point.class=otn.todo.TodoApp
<property file="build.properties"/>您可以通过在任务的 Context 菜单中选择 Run Target GWTCompile 直接启动该新的目标:
Apache Ant Log 窗口将显示以下结果:
GWTcompile:
[java] Output will be written into www\otn.todo.TodoApp
[java] Compilation succeeded
BUILD SUCCESSFUL
在 OC4J 中部署
编译完应用程序之后,在 OC4J 下部署它只需创建一个适当的部署配置文件。如果您按照前面描述的步骤进行操作,您应该已经具有一个默认的部署配置文件。如果没有,只需选择 File > New...> Deployment Profiles > WAR File,创建一个新的配置文件。
使用您的配置,一切都应该正常工作。但是,如果您遇到任何问题,请查看以下常见的错误:
- 在 Project Properties 的 Project Content > Web Application 中,HTML Root Directory 应该是应用程序的 www 目录(在该目录中,GWT 将对应用程序进行编译以便在托管模式下运行)。
- 在部署配置文件的 File Groups > WEB-INF/lib > Contributors 中,应该添加 gwt-user.jar。该 jar 文件包括 J2EE 规范中的 javax.servlet 程序包。这在本例中没有引起任何问题;但是,您通常不应将这些类部署在 Web 应用程序中,因为它们会引起麻烦。如果发生了这种情况,GWT 还提供有一个 gwt-servlet.jar 文件,它是没有 javax.servlet 程序包的 gwt-user.jar。
- Web 应用程序的上下文根影响 GWT 的 RPC 机制。如果 GWT 客户端应用程序要与服务器通信(如“使用 RPC 进行客户端和服务器之间的数据交换”所述),它必须可以找到服务器。这就是我们已经讨论过的 endpoint.setServiceEntryPoint("") 方法的目的。在本例中,我们将应用程序部署到了服务器的根上,这就是 GWT shell 默认的工作方式。但是,如果您将应用程序部署到 TodoApp Web 上下文(在部署配置文件的一般属性中),请将端点设置为 /TodoApp/todoListBackupService 而不要设置为 /todoListBackupService。不用忘记该 URL 还应正确映射到应用程序的 web.xml 文件中(如前所述)。
- 假设 OC4J 已在系统上正确安装,将应用程序部署到服务器是很简单的:只需右键单击部署配置文件,并选择 Deploy to OC4J。
- 客户端应用程序是一组先前编译的 HTML 和 JavaScript 文件。(请参见“编译客户端代码”部分。)OC4J 充当经典的 Web 服务器,将这些文件传递给用户。
- 服务器端应用程序基本上是一个处理 RPC 通信的 servlet。在 OC4J 中部署后,该 servlet 可以访问诸如 EJB 或 JMS 提供商等企业资源。
如该图形所显示的,在正常负载下(每秒几个请求),应用程序的服务器端部分在平均不到 4 ms 的时间内进行响应,这是很难得的结果。