内容


GWT 应用,第 1 部分

使用 Google Web Toolkit 实现 places 应用程序

使用 Java 代码实现类似桌面的 Web 应用程序

Comments

系列内容:

此内容是该系列 # 部分中的第 # 部分: GWT 应用,第 1 部分

敬请期待该系列的后续内容。

此内容是该系列的一部分:GWT 应用,第 1 部分

敬请期待该系列的后续内容。

我从上世纪 90 年代开始使用 Swing。我钟爱 Swing 的原因是它能够实现您的一切想法。对我而言,这就是开发软件的奇妙之处:能够在屏幕上实现自己的想法。借助 Swing API,您可以轻松各种各样的应用程序,从拖放式工具到街机游戏,无所不能。

然后,服务器端 Java 和 Struts 等原始框架的出现让软件开发又回到 20 世纪 60 年代的水平,它们只能类似于大型机的窗体。没有拖放操作,没有街机游戏,没有任何乐趣,我们感觉就像又回到了编程的石器时代。

这正是我钟情于 Google Web Toolkit 的原因。通过使用这个类似于 Swing 的 API,您可以再一次在浏览器中实现任何能想像到的功能。当然,Web 应用程序框架的形势自从 Struts 1.0 之后已经发生了显著变化,借助 JSF 2、Ruby on Rails 和 Lift 等框架,开发人员可以实现的功能已经不再仅限于类似于大型机的窗体。但是,GWT 仍然是其他框架所无法比拟的,它允许开发人员通过熟悉的语言和 API 发挥 JavaScript 的强大功能。如果您想在浏览器中实现类似于桌面的应用程序,则 GWT 是非常值得考虑的一种选择,至少在客户端方面是这样的。

在本系列文章中,我将实现一个类似于桌面的应用程序,为您的 GWT 学习之路打下基础。本文的内容包括:

  • 小部件(Widgets)
  • 远程过程调用(Remote procedure calls,RPC)和数据库集成
  • 复合小部件
  • 事件处理程序
  • Ajax 测试

在第 2 部分中,我将更加详细地讨论如何实现自定义小部件,以及一些高级技巧,比如说在计时器中使用事件预览和动画图像。您可以 下载 源代码,获取完整的示例应用程序。

Places:一个支持 Ajax 和数据库的 Web 服务 mashup

我将使用 GWT 构建一个 places 应用程序,可用于查看位置(places)。我将位置定义为某特定位置的地图与天气信息的结合,如图 1 所示:

图 1. places 应用程序:查看某个位置
places 应用程序:查看某个位置
places 应用程序:查看某个位置

places 应用程序随带 6 个内置地址,应用程序将在启动时从数据库获取它们。然后,应用程序在一个列表框中显示这些地址。单击列表框中的某个项目时,应用程序将使用所选地址更新列表框右侧的表格。

当您单击表格的 Show 按钮时,应用程序将打开一个窗口。该窗口分别在左侧和右侧显示地图和天气信息,中间通过一个垂直分隔面板 隔开。应用程序将从 Yahoo! Web Services 获取地图和天气信息。您可以同时显示多个窗口并调整分隔面板的尺寸,如图 2 所示:

图 2. places 应用程序:查看多个 places
places 应用程序:查看多个 places
places 应用程序:查看多个 places

图 1图 2 的静态截图并不能显示一项操作:地图位于视区 内部,因此您可以在分隔面板中拖动地图。如果快速拖动地图 — 时间少于 1 秒 — 则视区会按您拖动鼠标的方向持续移动视区,从而实现动画效果。当图像到达视区的边缘时,它会从边缘反弹并继续移动,直到您在视区中单击鼠标。如果想亲自尝试,请 下载 应用程序。

在第 2 部分中,我将讨论视区并借此演示一些高级 GWT 技巧,比如说为动画使用计时器以及使用事件预览。

小部件

在本文的其余部分,我将从头开始实现这个 places 应用程序。首先,我需要从数据库获取地址并在列表框中显示它们,如图 3 所示:

图 3. 小部件和数据库访问
小部件和数据库访问
小部件和数据库访问

清单 1 显示了 图 3 所示应用程序的代码:

清单 1. Places.java, take 1
package com.clarity.client;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

import com.google.gwt.core.client.EntryPoint;
import com.google.gwt.core.client.GWT;
import com.google.gwt.user.client.rpc.AsyncCallback;
import com.google.gwt.user.client.ui.HorizontalSplitPanel;
import com.google.gwt.user.client.ui.Label;
import com.google.gwt.user.client.ui.ListBox;
import com.google.gwt.user.client.ui.RootPanel;

public class Places implements EntryPoint {
  final ListBox addresses = new ListBox();
  final ArrayList<Address> addressList = new ArrayList<Address>();
  final HorizontalSplitPanel hsp = new HorizontalSplitPanel();

  public void onModuleLoad() {
    hsp.add(addresses);
    hsp.add(new Label("Address grid goes here"));
    hsp.setSplitPosition("175px");
    
    getAddresses();
    
    RootPanel.get().add(hsp);
  }

  public void getAddresses() {
    // Instantiate the address service
    AddressServiceAsync as = (AddressServiceAsync) GWT
        .create(AddressService.class);

   // Use the address service to fetch addresses and populate the listbox
    ...
  }  
}

所有的 GWT 应用程序都是 GWT 模块,并且所有的 GWT 模块都实现了 EntryPoint 接口,其中定义了一个方法:public void onModuleLoad()。该方法类似于桌面应用程序中的 main() 方法。在 places 应用程序中,onModuleLoad() 方法用于在垂直分隔面板中添加地址列表框和一个标签。然后,它从数据库获取地址并将分隔面板添加到应用程序的根面板中。(根面板表示应用程序 HTML 页面的主体)。

创建小部件和配置它们都可以通过 GWT 轻松实现。获取和显示数据库中的值则稍微复杂一些。

RPC 和数据库集成

要从数据库获取地址,我需要使用 GWT RPC。首先,我使用 GWT.create() 方法实例化一个 AddressServiceAsync 实例,然后使用该实例调用 RPC 并捕获调用的结果,如清单 2 所示:

清单 2. Places.java, take 2
package com.clarity.client;

public class Places implements EntryPoint {
  final ListBox addresses = new ListBox();
  final final ArrayList<Address> addressList = new ArrayList<Address>();
  final HorizontalSplitPanel hsp = new HorizontalSplitPanel();

  public void onModuleLoad() {
    hsp.add(addresses);
    hsp.add(new Label("Address grid goes here"));
    hsp.setSplitPosition("175px");
    
    getAddresses();
    
    RootPanel.get().add(hsp);
  }

  public void getAddresses() {
    // Instantiate the address service
    AddressServiceAsync as = (AddressServiceAsync) GWT
        .create(AddressService.class);

    as.getAddresses(new AsyncCallback<List<Address>>() {
      public void onFailure(Throwable caught) {
        GWT.log("Can't access database", caught);
      }

      public void onSuccess(List<Address> result) {
        Iterator<Address> it = result.iterator();
        
        while (it.hasNext()) {
          Address address = it.next();
          addresses.addItem(address.getAddress());
          addressList.add(address);
        }
        addresses.setVisibleItemCount(result.size());
      }
    });  
}

GWT RPC 由两个接口定义:对客户机调用的异步接口,以及 GWT 对服务器调用的远程接口。对于地址服务,这些接收分别是 AddressServiceAsyncAddressService。我在 清单 2 中调用了 GWT.create,将远程接口类传递给它,并且 GWT 将返回一个异步接口的实例。

清单 3 显示了 AddressService 接口:

清单 3. AddressService.java
package com.clarity.client;

import java.util.List;

import com.google.gwt.user.client.rpc.RemoteService;
import com.google.gwt.user.client.rpc.RemoteServiceRelativePath;

@RemoteServiceRelativePath("address")
public interface AddressService extends RemoteService {
  public List<Address> getAddresses();
}

清单 4 显示了 AddressServiceAsync 接口:

清单 4. AddressServiceAsync.java
package com.clarity.client;

import java.util.List;

import com.google.gwt.user.client.rpc.AsyncCallback;

public interface AddressServiceAsync {
  public void getAddresses(AsyncCallback<List<Address>> callback);
}

当我通过异步接口对客户机调用 getAddresses() 方法时,GWT 将通过服务的远程接口调用相应的方法。GWT 将等待服务器上的方法完成,然后在异步实现的回调中调用它,如 清单 2 所示。

最后,我在 Web 应用程序的部署描述符中声明远程 servlet,如清单 5 所示:

清单 5. WEB-INF/web.xml
package com.clarity.client;
<?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>
  
  <!-- Servlets -->
  <servlet>
    <servlet-name>address</servlet-name>
    <servlet-class>com.clarity.server.AddressServiceImpl</servlet-class>
  </servlet>

  ...
  
  <servlet-mapping>
    <servlet-name>address</servlet-name>
    <url-pattern>/places/address</url-pattern>
  </servlet-mapping>
  
  ...
</web-app>

注意,servlet 的名称(address)与我在 清单 3 中为 @RemoteServiceRelativePath 注释指定的值相匹配。这种匹配连接了 清单 3 中定义的远程 RPC 接口和清单 6 中实现的 servlet。

服务器端数据库代码

AddressServiceAsyncAddressService 接口位于客户机上。在服务器上,我使用 POJO 和 Hibernate 访问数据库中的地址,实现了一些普通的代码。POJO 如清单 6 所示:

清单 6. Address.java
package com.clarity.client;

import java.io.Serializable;

public class Address implements Serializable {
  private static final long serialVersionUID = 1L;
  private Long id;
  private String description, address, city, state, zip;

  public Address() {
    // you must implement a no-arg constructor
  }
  
  public Address(Long id, String address, String city) {
    this.address = address;
    this.city = city;
    this.id = id;
  }

  public String toString() {
    return address + " " + city + ", " + state + zip;
  }
 
  // Setters and getters for String properties are omitted in the interest of brevity 
}

清单 7 显示了相应的 Hibernate 代码:

清单 7. AddressServiceImpl.java
package com.clarity.server;

import java.util.List;

import org.hibernate.HibernateException;
import org.hibernate.Session;

import com.clarity.client.AddressService;
import com.clarity.client.Address;
import com.google.gwt.user.server.rpc.RemoteServiceServlet;

@SuppressWarnings("unchecked")
public class AddressServiceImpl extends RemoteServiceServlet
  implements AddressService {
  private static final long serialVersionUID = 1L;

  public List<Address> getAddresses() {
    List<Address> addresses = null;
    try {
      Session session = HibernateUtil.getSessionFactory()
        .getCurrentSession(); 
      session.beginTransaction();
      addresses = (List<Address>)session.createQuery("from Address Address ")
        .list();
      session.getTransaction().commit();   
    } catch (HibernateException e) {
      e.printStackTrace();
    }
    return addresses;
  }
}

清单 7 所示,AddressServiceImpl 类实现了地址服务的远程接口 — AddressService — 并扩展了 GWT 的 RemoteServiceServletgetAddresses() 方法返回数据库中的所有地址。

RPC 的异步特性

第一次使用数据库中的地址填充地址表格时,您可能会考虑采用以下方法:

public class Places implements EntryPoint {
final ListBox addresses = new ListBox();
final private AddressGrid addressGrid = 
    new AddressGrid(addresses, addressList);
  ...

  public void onModuleLoad() {
    ...
    getAddresses();

    // this won't work
    addressGrid.setAddress(addressList.get(0));
    ... 
    RootPanel.get().add(hsp);
  }
  ...
}

但这不起作用,因为对 getAddresses() 的调用是异步的。您应该在 RPC 返回之后再填充地址表格,如下所示:

public class Places implements EntryPoint {
  ...
  public void onModuleLoad() {
    ...
    getAddresses();
    ... 
    RootPanel.get().add(hsp);
  }
  public void getAddresses() {
    AddressServiceAsync as = (AddressServiceAsync) GWT
        .create(AddressService.class);

    as.getAddresses(new AsyncCallback<List<Address>>() {
      ...
      public void onSuccess(List<Address> result) {
        ...
        addressGrid.setAddress(addressList.get(0));
      }
    });
  }  
}

复合小部件

目前为止,我已经创建了一些小部件并使用数据库中的地址填充了一个列表框。现在,我需要将地址表格添加到应用程序分隔面板的右侧,如图 4 所示:

图 4. 添加地址表格
添加地址表格
添加地址表格

图 4 右侧的地址表格是一个复合小部件。清单 8 显示了 AddressGrid 类的实现:

清单 8. AddressGrid.java
package com.clarity.client;

import com.google.gwt.event.dom.client.ClickHandler;
import com.google.gwt.user.client.ui.Button;
import com.google.gwt.user.client.ui.Composite;
import com.google.gwt.user.client.ui.Grid;
import com.google.gwt.user.client.ui.Label;
import com.google.gwt.user.client.ui.ListBox;
import com.google.gwt.user.client.ui.TextBox;

public class AddressGrid extends Composite {
  private Grid grid = new Grid(6,2);
  private Label streetAddressLabel = new Label("Address");
  private TextBox streetAddressTextBox = new TextBox();
  private Label cityLabel = new Label("City");
  private TextBox cityTextBox = new TextBox();
  private TextBox stateTextBox = new TextBox();
  private Label zipLabel = new Label("Zip");
  private TextBox zipTextBox = new TextBox();
  private Label stateLabel = new Label("State");
  private Button button = new Button();
  private Address address;

  public AddressGrid(final ListBox addresses, String buttonText,
    ClickHandler buttonClickHandler) {
    initWidget(grid);
    button.setText(buttonText);
    
    grid.addStyleName("addressGrid");

    stateTextBox.setVisibleLength(3);
    zipTextBox.setVisibleLength(5);
    cityTextBox.setVisibleLength(15);

    grid.setWidget(0, 0, streetAddressLabel); grid.setWidget(0, 1, streetAddressTextBox);
    grid.setWidget(1, 0, cityLabel);          grid.setWidget(1, 1, cityTextBox);
    grid.setWidget(2, 0, stateLabel);         grid.setWidget(2, 1, stateTextBox);
    grid.setWidget(3, 0, zipLabel);           grid.setWidget(3, 1, zipTextBox);
    grid.setWidget(5, 0, button);

    button.addClickHandler(buttonClickHandler);
  }
  
  void setAddress(Address address) {
    this.address = address;
    streetAddressTextBox.setText(address.getAddress());
    cityTextBox.setText(address.getCity());
    stateTextBox.setText(address.getState());
    zipTextBox.setText(address.getZip());
  }
  
  public Address getAddress() {
    return address;
  }
  public Button getButton() {
    return button;
  }
}

顾名思义,复合小部件是由其他小部件组成的小部件。地址表格包括一个 Grid,其中填充了一些标签、文本框和一个按钮。在构建地址表格时,需要提供在按钮上显示的文本以及按钮的单击处理程序。

地址表格是对某个地址的可重用描述,您可以向它附加一些功能。在本例中,该功能是在 places 应用程序中使用一个事件处理程序实现的。

事件处理程序

places 应用程序有两个事件处理程序:一个用于在您从地址列表框中选择地址时填充地址表格,另一个用于在您单击地址表格的 Show 按钮时创建一个窗口。这两个事件处理程序都显示在清单 9 中,它是 清单 1 的更新版本:

清单 9. Places.java, take 3
package com.clarity.client;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

import com.google.gwt.core.client.EntryPoint;
import com.google.gwt.core.client.GWT;
import com.google.gwt.event.dom.client.ChangeEvent;
import com.google.gwt.event.dom.client.ChangeHandler;
import com.google.gwt.event.dom.client.ClickEvent;
import com.google.gwt.event.dom.client.ClickHandler;
import com.google.gwt.user.client.Window;
import com.google.gwt.user.client.rpc.AsyncCallback;
import com.google.gwt.user.client.ui.HorizontalSplitPanel;
import com.google.gwt.user.client.ui.ListBox;
import com.google.gwt.user.client.ui.RootPanel;

public class Places implements EntryPoint {
  final ListBox addresses = new ListBox();
  final HorizontalSplitPanel hsp = new HorizontalSplitPanel();
  final ArrayList<Address> addressList = new ArrayList<Address>();
  final AddressGrid addressGrid = new AddressGrid("Show", new ShowPlaceHandler());

  public void onModuleLoad() {
    hsp.add(addresses);
    hsp.add(addressGrid);
    hsp.setSplitPosition("175px");
    
    getAddresses();
    
    addresses.addChangeHandler(new ChangeHandler() {
      public void onChange(ChangeEvent e) {
        addressGrid.setAddress(addressList.get(addresses.getSelectedIndex()));
      }
    });

    RootPanel.get().add(hsp);
  }

  public void getAddresses() {
    AddressServiceAsync as = (AddressServiceAsync) GWT
        .create(AddressService.class);

    as.getAddresses(new AsyncCallback<List<Address>>() {
      public void onFailure(Throwable caught) {
        GWT.log("Can't access database", caught);
      }

      public void onSuccess(List<Address> result) {
        Iterator<Address> it = result.iterator();
        
        while (it.hasNext()) {
          Address address = it.next();
          addresses.addItem(address.getAddress());
          addressList.add(address);
        }
        addresses.setVisibleItemCount(result.size());
        addressGrid.setAddress(addressList.get(0));
      }
    });
  }  

  private class ShowPlaceHandler implements ClickHandler {
    private String[] urls;

    public void onClick(ClickEvent event) {
      final WeatherServiceAsync ws = (WeatherServiceAsync) 
        GWT.create(WeatherService.class);
      
      final MapServiceAsync ms = (MapServiceAsync) GWT.create(MapService.class);
      final Address address = addressGrid.getAddress();

      addressGrid.getButton().setEnabled(false);
      
      ms.getMap(address.getAddress(), address.getCity(), address.getState(), 
        new AsyncCallback<String[]>() {
          public void onFailure(Throwable arg0) {
            Window.alert(arg0.getMessage());            
          }
          public void onSuccess(final String[] urls) {
            addresses.setEnabled(true);
            ws.getWeatherForZip(address.getZip(), true, 
              new AsyncCallback<String>() {
              public void onFailure(Throwable arg0) {
                Window.alert(arg0.getMessage());
                done();
              }
            
              public void onSuccess(String weatherHTML) {
                PlaceDialog dialog = new PlaceDialog(addressGrid.getAddress(), 
                    urls, weatherHTML);
                dialog.setPopupPosition(200, 200);
                dialog.show();
                done();
              }            
            });
          }
        });
    }
    private void done() {
      addressGrid.getButton().setEnabled(true);
    }
  }
}

清单 9 所示,修改后的 places 应用程序在地址列表框中添加了一个变化处理程序。该变化处理程序用于更新地址表格,以反映用户从列表框中选择的地址。

应用程序还使用表格按钮的文本和单击处理程序实例化了一个 AddressGrid 实例。该单击处理程序是在 ShowPlaceHandler 类中实现的,这个类中还将 RPC 嵌入到了地图和天气 Web 服务中。当两个 RPC 都完成之后,事件处理程序将创建一个对话框,在其中显示所选地址的地图和天气预报。该对话框是 PlaceDialog 的一个实例,我将在第 2 部分中讨论这一方面的内容。

注意,清单 9 使用的是 handlers,而不是 listeners。GWT 最初实现了 Swing 的熟悉的事件处理监听模式:您实现了一个监听器接口,有时需要使用一个实现了一些空方法的适配器类,这样便不需要实现所有接口方法。从 GWT 1.6 开始,监听器便被处理程序所替代。处理程序与监听器极为类似,唯一的区别就是处理程序只定义了一个方法,并且始终只接受一个事件对象。事件处理程序只监听单一类型的事件,因此比监听器具有更好的粒度性。此外,监听器还接受触发事件的小部件,而处理程序需要通过一个包含事件相关信息的事件对象来获取更多信息,包括触发事件的小部件。

现在,places 应用程序已经具备了一定的功能。我应该对该功能进行一些测试。因此,接下来我将介绍如何使用 GWT 测试 Ajax 调用。

Ajax 测试

GWT 提供了与 JUnit 的集成,这使您可以轻松地测试应用程序。最简单的方法是运行 GWT 的 junitCreator 脚本,这将为您的应用程序创建一些框架测试。我使用它为 Places 应用程序创建框架测试的方法如下:

junitCreator -junit /Developer/Java/Tools/junit-4.5/junit-4.5.jar 
   -module com.clarity.Places 
   -eclipse Places  com.clarity.client.PlacesTest

junitCreator 脚本将创建一个测试目录以及一些框架测试,如图 5 所示:

图 5. 测试目录结构
测试目录结构

注意,我故意在相同的包(但不是同一个目录)中创建了测试类,作为应用程序用户界面的实现。这使测试类能够更加轻松地访问 UI 类的成员变量,同时保证测试代码与 UI 代码是分开的。将测试类放在与 UI 相同的包但不同目录中是一种常用的 GWT 技巧。

现在,GWT 已经创建了一个框架测试类。接下来,我将提供实现。我将创建一个应用程序实例,编写代码来操作小部件,然后验证结果。清单 10 显示了如何测试 places 应用程序从数据库中获取地址的能力:

清单 10. 测试 RPC
package com.clarity.client;

import com.google.gwt.junit.client.GWTTestCase;
import com.google.gwt.user.client.Timer;
import com.google.gwt.user.client.ui.ListBox;

public class PlacesTest extends GWTTestCase {

  public String getModuleName() {
    return "com.clarity.Places";
  }

  public void testGetAddresses() {
    Places demo = new Places();
    demo.getAddresses();
    final ListBox addresses = demo.addresses;

    new Timer() {
      public void run() {
        assert (addresses.getItemCount() == 6);
        assert (demo.addressList.size() == 6);
        finishTest();
      }
    }.schedule(10000);

    delayTestFinish(20000);
  }
}

清单 10 实现了一种常见的 GWT 技巧,即在使用计时器时,通过调用 finishTest()delayFinishTest() 来测试对服务器的异步调用。testGetAddresses() 方法:

  1. 将创建一个 places 应用程序实例。
  2. 调用应用程序的 getAddresses() 方法。
  3. 断言应用程序随后已使用 6 个地址填充了地址列表框。
  4. 断言应用程序随后已使用 6 个地址填充了应用程序的地址列表。

由于对 getAddresses() 的调用是异步时,因此我必须等待它完成,然后才能检查地址列表框中的项目数量。考虑到这种情况,我将这种检查工作放在了一个计时器中,它会在计时器创建 10 秒之后运行。在这 10 秒时间内,应用程序足以完成 RPC。delayTestFinish() 调用将通知 GWT 等待 20 秒,以便我能够调用 finishTest()。如果我没有在 20 秒内调用 finishTest(),则测试将会超时并报错。

结束语

GWT 非常适合用于创建类似于桌面的应用程序,并为它添加各种有趣的功能,比如说拖放功能、窗口、对话框以及各种交互式小部件。虽然非常简单,但 places 应用程序演示了使用 GWT 创建这种应用程序的潜力。目前为止,我已经向您展示了 GWT 的基本功能,包括 RPC 和数据库访问,并实现了一些复合小部件、事件处理和 Ajax 测试。在第 2 部分中,您将了解一些高级的 GWT 特性,包括 sinking 事件,以及如何实现 GWT 模块和使用事件预览。


下载资源


相关主题


评论

添加或订阅评论,请先登录注册

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=Java technology, Web development
ArticleID=431783
ArticleTitle=GWT 应用,第 1 部分: 使用 Google Web Toolkit 实现 places 应用程序
publish-date=09292009