诸如 Google App Engine for Java 这样的云平台的关键在于能够设计、构建和部署专业级的应用程序 —— 可以非常容易地进行伸缩。在这个包含三部分的 Google App Engine for Java 系列文章第二篇中,Rick Hightower 将通过一个分步指南,使用 Google App Engine for Java 来编写和部署一个简单的联系人管理应用程序,从而超越第1部分中提供的现成示例。
在介绍使用 App Engine for Java 构建可伸缩 Java 应用程序的第1部分中,您了解了 Google 云计算平台(即 PAAS)为 Java 开发人员提供的 Eclipse 工具和基础设施。该文章中的示例都是预先准备的,这样您可以将精力集中到 App Engine for Java 与 Eclipse 的集成中,并快速构建和部署不同类型的应用程序 — 即使用 Google Web Toolkit (GWT) 构建的应用程序和基于 servlet 的应用程序。本文将在此基础上展开,并且在本系列第 3 部分中提供了更加高级的编程实践。
您将构建的联系人管理应用程序允许用户存储基本的联系人信息,比如名称、电子邮件地址和电话号码。要创建这个应用程序,将需要使用 Eclipse GWT 项目创建向导。
从 CRUD 到联系人应用程序
正如目前您已经了解到的一样,在 App Engine for Java 中构建新应用程序的第一步就是在 Eclipse 启动项目创建向导。之后,您可以打开 GWT 项目启动向导来创建 GWT 项目(本文第1部分给出了在 App Engine for Java 中创建 GWT 项目的详细说明)。
对于这个练习,您将启动一个简单的 CRUD 应用程序,并稍后添加实际的存储。我们将使用一个具有模拟实现的数据访问对象(DAO),如清单 1 所示:
清单 1. ContactDAO 接口
package gaej.example.contact.server;import java.util.List;import gaej.example.contact.client.Contact;public interface ContactDAO { void addContact(Contact contact); void removeContact(Contact contact); void updateContact(Contact contact); List<Contact> listContacts();}
ContactDAO 添加了各种方法,可以添加联系人、删除联系人、更新联系人,并返回一个所有联系人的列表。它是一个非常基本的 CRUD 接口,可以管理联系人。Contact 类是您的域对象,如清单 2 所示:
清单 2. 联系人域对象(gaej.example.contact.client.Contact)
package gaej.example.contact.client;import java.io.Serializable;public class Contact implements Serializable { private static final long serialVersionUID = 1L; private String name; private String email; private String phone; public Contact() { } public Contact(String name, String email, String phone) { super(); this.name = name; this.email = email; this.phone = phone; } public String getName() { return name; } public void setName(String name) { this.name = name; } public String getEmail() { return email; } public void setEmail(String email) { this.email = email; } public String getPhone() { return phone; } public void setPhone(String phone) { this.phone = phone; } }
对于这个应用程序的第一个版本,您将使用一个模拟对象将联系人存储在一个内存集合中,如清单 3 所示:
清单 3. Mock DAO 类
package gaej.example.contact.server;import gaej.example.contact.client.Contact;import java.util.ArrayList;import java.util.Collections;import java.util.LinkedHashMap;import java.util.List;import java.util.Map;public class ContactDAOMock implements ContactDAO { Map<String, Contact> map = new LinkedHashMap<String, Contact>(); { map.put("rhightower@mammatus.com", new Contact("Rick Hightower", "rhightower@mammatus.com", "520-555-1212")); map.put("scott@mammatus.com", new Contact("Scott Fauerbach", "scott@mammatus.com", "520-555-1213")); map.put("bob@mammatus.com", new Contact("Bob Dean", "bob@mammatus.com", "520-555-1214")); } public void addContact(Contact contact) { String email = contact.getEmail(); map.put(email, contact); } public List<Contact> listContacts() { return Collections.unmodifiableList(new ArrayList<Contact>(map.values())); } public void removeContact(Contact contact) { map.remove(contact.getEmail()); } public void updateContact(Contact contact) { map.put(contact.getEmail(), contact); }}
创建远程服务
您现在的目标是创建一个允许您使用 DAO 的 GWT GUI。将使用 ContactDAO 接口上的所有方法。第一步是将 DAP 类(未来版本将直接与服务器端的数据存储通信,因此必须位于服务器中)的功能封装到一个服务中,如清单 4 所示:
清单 4. ContactServiceImpl
package gaej.example.contact.server;import java.util.ArrayList;import java.util.List;import gaej.example.contact.client.Contact;import gaej.example.contact.client.ContactService;import com.google.gwt.user.server.rpc.RemoteServiceServlet;public class ContactServiceImpl extends RemoteServiceServlet implements ContactService { private static final long serialVersionUID = 1L; private ContactDAO contactDAO = new ContactDAOMock(); public void addContact(Contact contact) { contactDAO.addContact(contact); } public List<Contact> listContacts() { List<Contact> listContacts = contactDAO.listContacts(); return new ArrayList<Contact> (listContacts); } public void removeContact(Contact contact) { contactDAO.removeContact(contact); } public void updateContact(Contact contact) { contactDAO.updateContact(contact); } }
注意,ContactServiceImpl 实现了 RemoteServiceServlet,随后定义方法来添加联系人、列出联系人、删除联系人,以及更新联系人。它将所有这些操作委托给 ContactDAOMock。ContactServiceImpl 不过是一个围绕 ContactDAO 的包装器,后者将 ContactDAO 功能公开给 GWT GUI。ContactServiceImpl 在 web.xml 文件中被映射到 URI /contactlist/contacts,如清单 5 所示:
清单 5. web.xml 中的 ContactService
<servlet> <servlet-name>contacts</servlet-name> <servlet-class>gaej.example.contact.server.ContactServiceImpl</servlet-class> </servlet> <servlet-mapping> <servlet-name>contacts</servlet-name> <url-pattern>/contactlist/contacts</url-pattern> </servlet-mapping>
要使 GUI 前端访问该服务,需要定义一个远程服务接口和一个异步远程服务接口,如清单 6 和 7 所示:
清单 6. ContactService
package gaej.example.contact.client;import java.util.List;import com.google.gwt.user.client.rpc.RemoteService;import com.google.gwt.user.client.rpc.RemoteServiceRelativePath;@RemoteServiceRelativePath("contacts")public interface ContactService extends RemoteService { List<Contact> listContacts(); void addContact(Contact contact); void removeContact(Contact contact); void updateContact(Contact contact);}
清单 7. ContactServiceAsync
package gaej.example.contact.client;import java.util.List;import com.google.gwt.user.client.rpc.AsyncCallback;public interface ContactServiceAsync { void listContacts(AsyncCallback<List <Contact>> callback); void addContact(Contact contact, AsyncCallback<Void> callback); void removeContact(Contact contact, AsyncCallback<Void> callback); void updateContact(Contact contact, AsyncCallback<Void> callback);}
注意,ContactService 实现了 RemoteService 接口并定义了一个 @RemoteServiceRelativePath,指定了 “联系人” 的相对路径。相对路径与您在 web.xml 文件中为服务定义的路径是对应的(必须匹配)。ContactServiceAsync 包含回调对象,因此 GWT GUI 可以收到来自服务器的调用的通知,而不会阻塞其他客户机行为。
避免编写杂乱的代码
我并不喜欢编写杂乱的代码,因此在可能的情况下会尽量避免编写这类代码。这类代码的一个例子就是一组匿名内部类,这些类的方法定义匿名内部类。这些内部类反过来执行回调,调用在某个内部类中以内联方式定义的方法。坦白说,我无法阅读或是理解这些纠缠在一起的代码,即使是我自己编写的!因此,为了将代码稍微简单化,我建议将 GWT GUI 分解为三个部分:
ContactListEntryPoint ContactServiceDelegate ContactListGUI
ContactListEntryPoint 是主要的入口点;它执行 GUI 事件连接。ContactServiceDelegate 封装 ContactService 功能并隐藏内部类回调连接。ContactListGUI 管理所有 GUI 组件并处理来自 GUI 和 Service 的事件。ContactListGUI 使用 ContactServiceDelegate 发出 ContactService 请求。
ContactList.gwt.xml 文件(位于 gaej.example.contact 下的一个资源)使用 entry-point 元素将 ContactListEntryPoint 指定为应用程序的主要入口点,如清单 8 所示:
清单 8. ContactList.gwt.xml
<entry-point class='gaej.example.contact.client.ContactListEntryPoint'/>
ContactListEntryPoint 类实现了 GWT 的 EntryPoint 接口(com.google.gwt.core.client.EntryPoint),并指定将调用该类来初始化 GUI。ContactListEntryPoint 所做的工作并不多。它创建一个 ContactListGUI 实例和一个 ContactServiceDelegate 实例,然后让它们彼此了解对方,这样就可以展开协作。ContactListEntryPoint 然后执行 GUI 事件连接。ContactListEntryPoint 如清单 9 所示:
清单 9. ContactListEntryPoint
package gaej.example.contact.client;import com.google.gwt.core.client.EntryPoint;import com.google.gwt.event.dom.client.ClickEvent;import com.google.gwt.event.dom.client.ClickHandler;import com.google.gwt.user.client.ui.HTMLTable.Cell;/** * Entry point classes define onModuleLoad(). */public class ContactListEntryPoint implements EntryPoint { private ContactListGUI gui; private ContactServiceDelegate delegate; /** * This is the entry point method. */ public void onModuleLoad() { gui = new ContactListGUI(); delegate = new ContactServiceDelegate(); gui.contactService = delegate; delegate.gui = gui; gui.init(); delegate.listContacts(); wireGUIEvents(); } private void wireGUIEvents() { gui.contactGrid.addClickHandler(new ClickHandler(){ public void onClick(ClickEvent event) { Cell cellForEvent = gui.contactGrid.getCellForEvent(event); gui.gui_eventContactGridClicked(cellForEvent); }}); gui.addButton.addClickHandler(new ClickHandler(){ public void onClick(ClickEvent event) { gui.gui_eventAddButtonClicked(); }}); gui.updateButton.addClickHandler(new ClickHandler(){ public void onClick(ClickEvent event) { gui.gui_eventUpdateButtonClicked(); }}); gui.addNewButton.addClickHandler(new ClickHandler(){ public void onClick(ClickEvent event) { gui.gui_eventAddNewButtonClicked(); }}); }}
注意,ContactListEntryPoint 为 addButton、updateButton、contactGrid 和 addNewButton 连接事件。具体做法是注册为小部件事件实现侦听器接口的匿名内部类。这与 Swing 中的事件处理非常相似。这些小部件事件来自由 GUI 创建的小部件(ContactListGUI),我将稍后进行讨论。注意,GUI 类包含 gui_eventXXX 方法来响应 GUI 事件。
ContactListGUI 创建了 GUI 小部件并响应来自它们的事件。ContactListGUI 将 GUI 事件转换为用户希望对 ContactsService 执行的操作。ContactListGUI 使用 ContactServiceDelegate 对 ContactService 调用方法。ContactServiceDelegate 对 ContactService 创建一个异步接口并使用它发出异步 Ajax 调用。ContactServiceDelegate 向 ContactListGUI 通知来自服务的事件(成功或失败)。ContactServiceDelegate 如清单 10 所示:
清单 10. ContactServiceDelegatepackage gaej.example.contact.client;
import java.util.List;import com.google.gwt.core.client.GWT;import com.google.gwt.user.client.rpc.AsyncCallback;public class ContactServiceDelegate { private ContactServiceAsync contactService = GWT.create(ContactService.class); ContactListGUI gui; void listContacts() { contactService.listContacts(new AsyncCallback<List<Contact>> () { public void onFailure(Throwable caught) { gui.service_eventListContactsFailed(caught); } public void onSuccess(List<Contact> result) { gui.service_eventListRetrievedFromService(result); } }//end of inner class );//end of listContacts method call. } void addContact(final Contact contact) { contactService.addContact(contact, new AsyncCallback<Void> () { public void onFailure(Throwable caught) { gui.service_eventAddContactFailed(caught); } public void onSuccess(Void result) { gui.service_eventAddContactSuccessful(); } }//end of inner class );//end of addContact method call. } void updateContact(final Contact contact) { contactService.updateContact(contact, new AsyncCallback<Void> () { public void onFailure(Throwable caught) { gui.service_eventUpdateContactFailed(caught); } public void onSuccess(Void result) { gui.service_eventUpdateSuccessful(); } }//end of inner class );//end of updateContact method call. } void removeContact(final Contact contact) { contactService.removeContact(contact, new AsyncCallback<Void> () { public void onFailure(Throwable caught) { gui.service_eventRemoveContactFailed(caught); } public void onSuccess(Void result) { gui.service_eventRemoveContactSuccessful(); } }//end of inner class );//end of updateContact method call. } }
注意,ContactServiceDelegate 通过以 service_eventXXX 开头的方法向 ContactListGUI 发出有关服务事件的通知。如前所述,我编写 ContactListGUI 的目标之一就是避免嵌套的内部类并创建一个相对扁平的 GUI 类(我可以非常方便地阅读和理解的类)。ContactListGUI 只有 186 行,因此非常简单。ContactListGUI 管理 9 个 GUI 小部件并与 ContactServiceDelegate 协作来管理一个 CRUD 清单,如清单 11 所示:
清单 11. ContactListGUI 的实际使用
package gaej.example.contact.client;import java.util.List;import com.google.gwt.user.client.ui.Button;import com.google.gwt.user.client.ui.Grid;import com.google.gwt.user.client.ui.Hyperlink;import com.google.gwt.user.client.ui.Label;import com.google.gwt.user.client.ui.RootPanel;import com.google.gwt.user.client.ui.TextBox;import com.google.gwt.user.client.ui.HTMLTable.Cell;public class ContactListGUI { /* Constants. */ private static final String CONTACT_LISTING_ROOT_PANEL = "contactListing"; private static final String CONTACT_FORM_ROOT_PANEL = "contactForm"; private static final String CONTACT_STATUS_ROOT_PANEL = "contactStatus"; private static final String CONTACT_TOOL_BAR_ROOT_PANEL = "contactToolBar"; private static final int EDIT_LINK = 3; private static final int REMOVE_LINK = 4; /* GUI Widgets */ protected Button addButton; protected Button updateButton; protected Button addNewButton; protected TextBox nameField; protected TextBox emailField; protected TextBox phoneField; protected Label status; protected Grid contactGrid; protected Grid formGrid; /* Data model */ private List<Contact> contacts; private Contact currentContact; protected ContactServiceDelegate contactService;
注意,ContactListGUI 跟踪表单中加载的当前联系人(currentContact)和清单中的联系人列表(contacts)。图 1 展示了小部件如何对应于创建的 GUI:
图 1. 联系人管理 GUI 中活动的小部件
清单 12 展示了 ContactListGUI 如何创建小部件和联系人表单,并将小部件放到表单中:
清单 12. ContactListGUI 创建并放置小部件
public class ContactListGUI { /* Constants. */ private static final String CONTACT_LISTING_ROOT_PANEL = "contactListing"; private static final String CONTACT_FORM_ROOT_PANEL = "contactForm"; private static final String CONTACT_STATUS_ROOT_PANEL = "contactStatus"; private static final String CONTACT_TOOL_BAR_ROOT_PANEL = "contactToolBar"; ... public void init() { addButton = new Button("Add new contact"); addNewButton = new Button("Add new contact"); updateButton = new Button("Update contact"); nameField = new TextBox(); emailField = new TextBox(); phoneField = new TextBox(); status = new Label(); contactGrid = new Grid(2,5); buildForm(); placeWidgets(); } private void buildForm() { formGrid = new Grid(4,3); formGrid.setVisible(false); formGrid.setWidget(0, 0, new Label("Name")); formGrid.setWidget(0, 1, nameField); formGrid.setWidget(1, 0, new Label("email")); formGrid.setWidget(1, 1, emailField); formGrid.setWidget(2, 0, new Label("phone")); formGrid.setWidget(2, 1, phoneField); formGrid.setWidget(3, 0, updateButton); formGrid.setWidget(3, 1, addButton); } private void placeWidgets() { RootPanel.get(CONTACT_LISTING_ROOT_PANEL).add(contactGrid); RootPanel.get(CONTACT_FORM_ROOT_PANEL).add(formGrid); RootPanel.get(CONTACT_STATUS_ROOT_PANEL).add(status); RootPanel.get(CONTACT_TOOL_BAR_ROOT_PANEL).add(addNewButton); }
ContactListGUI init 方法由 ContactListEntryPoint.onModuleLoad 方法创建。init 方法调用 buildForm 方法来创建新的表单网格并使用字段填充,以编辑联系人数据。init 方法随后调用 placeWidgets 方法,随后将 contactGrid、formGrid、status 和 addNewButton 小部件放到 HTML 页面中定义的插槽中,这个 HTML 页面托管了清单 13 中定义的 GUI 应用程序:
清单 13. ContactList.html 定义了用于小部件的插槽
<h1>Contact List Example</h1> <table align="center"> <tr> <td id="contactStatus"></td> <td id="contactToolBar"></td> </tr> <tr> <td id="contactForm"></td> </tr> <tr> <td id="contactListing"></td> </tr> </table>
常量(比如 CONTACT_LISTING_ROOT_PANEL="contactListing")对应于 HTML 页面中定义的元素的 ID(类似 id="contactListing")。这允许页面设计师进一步控制应用程序小部件的布局。
对于基本的应用程序构建,让我们了解几个常见的使用场景。
展示一个有关页面加载的链接
当联系人管理应用程序的页面首次加载时,它将调用 ContactListEntryPoint 的 onModuleLoad 方法。onModuleLoad 调用 ContactServiceDelegate 的 listContacts 方法,后者异步调用服务的 listContact 方法。当 listContact 方法返回时,ContactServiceDelegate 中定义的匿名内部类将调用名为 service_eventListRetrievedFromService 的服务事件处理器方法,如清单 14 所示:
清单 14. 调用 listContact 事件处理器
public class ContactListGUI { ... public void service_eventListRetrievedFromService(List<Contact> result) { status.setText("Retrieved contact list"); this.contacts = result; this.contactGrid.clear(); this.contactGrid.resizeRows(this.contacts.size()); int row = 0; for (Contact contact : result) { this.contactGrid.setWidget(row, 0, new Label(contact.getName())); this.contactGrid.setWidget(row, 1, new Label (contact.getPhone())); this.contactGrid.setWidget(row, 2, new Label (contact.getEmail())); this.contactGrid.setWidget(row, EDIT_LINK, new Hyperlink("Edit", null)); this.contactGrid.setWidget(row, REMOVE_LINK, new Hyperlink("Remove", null)); row ++; } }
service_eventListRetrievedFromService 事件处理器方法存储由服务器发送的联系人列表。然后它将清空显示联系人列表的 contactGrid。它将重新调整行数,以匹配服务器返回的联系人列表的大小。随后遍历联系人列表,将每个联系人的姓名、电话、电子邮件数据放到每一行的前三个列中。它还为每个联系人提供了 Edit 链接和一个 Remove 链接,使用户能够轻松地删除和编辑联系人。
用户编辑现有的联系人
当用户单击联系人列表中的 Edit 链接时,gui_eventContactGridClicked 将得到调用,如清单 15 所示:
清单 15. ContactListGUI 的 gui_eventContactGridClicked 事件处理器方法
public class ContactListGUI { ... public void gui_eventContactGridClicked(Cell cellClicked) { int row = cellClicked.getRowIndex(); int col = cellClicked.getCellIndex(); Contact contact = this.contacts.get(row); this.status.setText("Name was " + contact.getName() + " clicked "); if (col==EDIT_LINK) { this.addNewButton.setVisible(false); this.updateButton.setVisible(true); this.addButton.setVisible(false); this.emailField.setReadOnly(true); loadForm(contact); } else if (col==REMOVE_LINK) { this.contactService.removeContact(contact); } } ... private void loadForm(Contact contact) { this.formGrid.setVisible(true); currentContact = contact; this.emailField.setText(contact.getEmail()); this.phoneField.setText(contact.getPhone()); this.nameField.setText(contact.getName()); }
gui_eventContactGridClicked 方法必须确定 Edit 链接或 Remove 链接是否被单击。具体做法是找到那个列被单击。随后隐藏 addNewButton 和 addButton,并使 updateButton 可见。updateButton 显示在 formGrid 中,允许用户将更新信息发送回 ContactService。它还使 emailField 变为只读,这样用户就不能编辑电子邮件字段。接下来,gui_eventContactGridClicked 调用 loadForm(如清单15所示),后者将 formGrid 设置为可见,设置正在被编辑的联系人,然后将联系人属性复制到 emailField、phoneField 和 nameField 小部件中。
当用户单击 updateButton 时,gui_eventUpdateButtonClicked 事件处理器方法被调用,如清单 16 所示。这个方法使 addNewButton 变为可见(这样用户就可以编辑新的联系人)并隐藏了 formGrid。它随后调用 copyFieldDateToContact,后者将来自 emailField、phoneField 和 nameField 小部件的文本复制回 currentContact 的属性。随后调用 ContactServiceDelegate updateContact 方法来将新更新的联系人传递回服务。
清单 16. ContactListGUI 的 gui_eventUpdateButtonClicked 事件处理器方法
public class ContactListGUI { ... public void gui_eventUpdateButtonClicked() { addNewButton.setVisible(true); formGrid.setVisible(false); copyFieldDateToContact(); this.contactService.updateContact(currentContact); } private void copyFieldDateToContact() { currentContact.setEmail(emailField.getText()); currentContact.setName(nameField.getText()); currentContact.setPhone(phoneField.getText()); }
这两个场景应当使您了解到应用程序是如何工作的,以及它如何依赖于 App Engine for Java 提供的基础设施。ContactListGUI 的完整代码如清单 17 所示:
清单 17. ContactListGUI 的完整代码
package gaej.example.contact.client;import java.util.List;import com.google.gwt.user.client.ui.Button;import com.google.gwt.user.client.ui.Grid;import com.google.gwt.user.client.ui.Hyperlink;import com.google.gwt.user.client.ui.Label;import com.google.gwt.user.client.ui.RootPanel;import com.google.gwt.user.client.ui.TextBox;import com.google.gwt.user.client.ui.HTMLTable.Cell;public class ContactListGUI { /* Constants. */ private static final String CONTACT_LISTING_ROOT_PANEL = "contactListing"; private static final String CONTACT_FORM_ROOT_PANEL = "contactForm"; private static final String CONTACT_STATUS_ROOT_PANEL = "contactStatus"; private static final String CONTACT_TOOL_BAR_ROOT_PANEL = "contactToolBar"; private static final int EDIT_LINK = 3; private static final int REMOVE_LINK = 4; /* GUI Widgets */ protected Button addButton; protected Button updateButton; protected Button addNewButton; protected TextBox nameField; protected TextBox emailField; protected TextBox phoneField; protected Label status; protected Grid contactGrid; protected Grid formGrid; /* Data model */ private List<Contact> contacts; private Contact currentContact; protected ContactServiceDelegate contactService; public void init() { addButton = new Button("Add new contact"); addNewButton = new Button("Add new contact"); updateButton = new Button("Update contact"); nameField = new TextBox(); emailField = new TextBox(); phoneField = new TextBox(); status = new Label(); contactGrid = new Grid(2,5); buildForm(); placeWidgets(); } private void buildForm() { formGrid = new Grid(4,3); formGrid.setVisible(false); formGrid.setWidget(0, 0, new Label("Name")); formGrid.setWidget(0, 1, nameField); formGrid.setWidget(1, 0, new Label("email")); formGrid.setWidget(1, 1, emailField); formGrid.setWidget(2, 0, new Label("phone")); formGrid.setWidget(2, 1, phoneField); formGrid.setWidget(3, 0, updateButton); formGrid.setWidget(3, 1, addButton); } private void placeWidgets() { RootPanel.get(CONTACT_LISTING_ROOT_PANEL).add(contactGrid); RootPanel.get(CONTACT_FORM_ROOT_PANEL).add(formGrid); RootPanel.get(CONTACT_STATUS_ROOT_PANEL).add(status); RootPanel.get(CONTACT_TOOL_BAR_ROOT_PANEL).add(addNewButton); } private void loadForm(Contact co