在介绍使用 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 contact) {
this.formGrid.setVisible(true);
currentContact = contact;
this.emailField.setText(contact.getEmail());
this.phoneField.setText(contact.getPhone());
this.nameField.setText(contact.getName());
}
private void copyFieldDateToContact() {
currentContact.setEmail(emailField.getText());
currentContact.setName(nameField.getText());
currentContact.setPhone(phoneField.getText());
}
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);
}
}
public void gui_eventAddButtonClicked() {
addNewButton.setVisible(true);
formGrid.setVisible(false);
copyFieldDateToContact();
this.phoneField.getText();
this.contactService.addContact(currentContact);
}
public void gui_eventUpdateButtonClicked() {
addNewButton.setVisible(true);
formGrid.setVisible(false);
copyFieldDateToContact();
this.contactService.updateContact(currentContact);
}
public void gui_eventAddNewButtonClicked() {
this.addNewButton.setVisible(false);
this.updateButton.setVisible(false);
this.addButton.setVisible(true);
this.emailField.setReadOnly(false);
loadForm(new Contact());
}
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 ++;
}
}
public void service_eventAddContactSuccessful() {
status.setText("Contact was successfully added");
this.contactService.listContacts();
}
public void service_eventUpdateSuccessful() {
status.setText("Contact was successfully updated");
this.contactService.listContacts();
}
public void service_eventRemoveContactSuccessful() {
status.setText("Contact was removed");
this.contactService.listContacts();
}
public void service_eventUpdateContactFailed(Throwable caught) {
status.setText("Update contact failed");
}
public void service_eventAddContactFailed(Throwable caught) {
status.setText("Unable to update contact");
}
public void service_eventRemoveContactFailed(Throwable caught) {
status.setText("Remove contact failed");
}
public void service_eventListContactsFailed(Throwable caught) {
status.setText("Unable to get contact list");
}
}
|
|
结束语
这个共包含三部分的 Google App Engine for Java 系列文章的第二部分向您介绍了如何使用 App Engine for Java 的 Eclipse 插件工具创建定制 GWT 应用程序。在构建简单的联系人管理应用程序的过程中,您学会了以下内容:
- 构建可以异步工作的远程服务
- 组织 GUI 代码以避免嵌套的内部类声明
- 利用 GWT 为两个关键用例实现功能
敬请期待本文的第 3 部分,您将对联系人管理应用程序进行优化,并通过 App Engine for Java 数据存储功能添加对持久化 Contact
对象的支持。
Google App Engine 曾经一度是 Python 开发人员 的专利。那是一段黑暗的岁月。Google Inc. 在 2009 年 4 月向 Java™ 开发人员开放了其云计算平台。在这个共分三部分的系列文章中,Java 技术作家兼培训师 Rick Hightower 将带领您了解这个可靠、健壮、有趣的平台,并将它用于基于 Java 的开发。在本文中,您将了解到为什么 Google App Engine for Java 将成为您构建高度可伸缩的杀手级应用程序的开发平台,然后开始使用 Google Plugin for Eclipse 构建两个示例应用程序:一个基于 Google Web Toolkit (GWT),另一个基于 Java Servlet API。您将了解到 Google App Engine for Java 带来的巨大改变,包括从头构建应用程序以及将它部署到高达 5 百万个视图。(这仅仅是免费版提供的功能)。
头脑里出现的想法就好像是被蚊虫叮了一样:您需要抓痒痒,这样做才感觉舒服一些。作为软件开发人员,我们花了大量时间来为各种应用程序捕捉想法。很有趣,不是吗?困难的部分在于想出如何使一个软件产品获得成功。需要构想出一些东西并随后 实现它。考虑其他的问题(即没有被抓过的痒处)只会让人灰心。
许多应用程序从未获得进展的一个原因就是无法满足对基础设施的需求。一个得到良好维护的基础设施常常需要一个由系统管理员、DBA 和网络工程师组成的团队,到目前为止,这一直是企业获得成功的主因。即使雇用第三方来托管您的应用程序也绝不简单:如果应用程序大受欢迎并且突然之间获得很高的点击率,会发生什么?所谓的 Slashdot 效应 可以帮助获得一个好的想法,仅仅因为很难预测加载峰值。
但是,众所周知,事物是不断变化的。Web 服务的基础在不断演变,如今它为我们带来了许多新方式,通过云计算和强大的平台即服务/PAAS 更轻松地构建、部署和发布应用程序。现在,在编写下一个 Twitter 并将其部署到云平台上时,它将不断扩展。哇,感觉很棒!
在这份共分三部分的系列文章中,您将了解到为什么云计算/PAAS 对于软件开发来说是如此重要的一个演变,同时开始使用一种令人振奋的新平台进行 Java 开发:Google App Engine for Java,目前可以使用它的预览版。我将首先对 App Engine for Java 进行概述,包括它所提供的应用程序服务的类型。之后将直接查看第一个应用程序示例(共两个),它使用 App Engine for Java Google Plugin for Eclipse。第一个应用程序示例将利用 App Engine for Java 对 Java Servlet API 的支持,第二个示例将利用对 GWT 的支持。在 第 2 部分 中,您将利用 App Engine for Java 对 servlets 和 GWT 提供的支持创建一个小型的联系人管理应用程序。在第 3 部分中,将使用自己构建的应用程序来利用 App Engine for Java 的基于 Java 的持久性支持,这种支持的基础是 Java Data Objects (JDO) 和 Java Persistence API (JPA)。
好的,不说废话了:让我们开始吧!
关于 Google App Engine for Java
Google(同时也是一些搜索引擎的创建者)于 2008 年 4 月首度发布了 Google App Engine。令许多 Java 开发人员失望的是,初始版完全只服务于 Python 程序员 — 那些认为应该大块使用空白的人!(我曾经撰写过一本有关 Python 的书,因此我想我应该知道)。Google 响应了用户的普遍要求,于 2009 年 4 月发布了 Google App Engine for Java。
Google App Engine for Java 为企业 Java 开发提供了一个端到端解决方案:一个易于使用的基于浏览器的 Ajax GUI、Eclipse 工具支持以及后端的 Google App Engine。易于使用和工具支持是 Google App Engine for Java 优于其他云计算解决方案的两大优势。
App Engine for Java 中的应用程序开发意味着使用 Google 的资源存储和检索 Java 对象。数据存储的基础是 BigTable,但是使用的是 JDO 和 JPA 接口,这些接口允许您编写没有直接绑定到 BigTable 的代码。事实上,Google 为许多 API 提供了基于标准的支持,这样就可以编写没有全部绑定到 App Engine for Java 的代码。
App Engine for Java 依赖以下标准 Java API:
java.net.URL
,检索服务(通过使用 HTTP 和 HTTPS 协议与其他主机通信)
- JavaMail,发送邮件消息
- 一个通向 Memcache 的 JCache (JSR 107) 接口,提供快速、临时的分布式存储,用于缓存查询和计算
此外,App Engine for Java 为以下应用程序服务提供了支持:
- 用户身份验证和授权
- CRON
- 数据导入/导出
- 访问防火墙数据
对于将数据从其他来源移动到您的 App Engine for Java 应用程序,数据导入/导出十分重要。这也不需要绑定到 App Engine for Java。Google 的 CRON 支持基于对某个调度的内部 URL 命中率,从而使它成为不需要绑定 App Engine for Java 的出色服务。用户身份验证和授权机制是 特定于 App Engine for Java 的,但是您可以编写一个 ServletFilter
、aspect 或 Spring Security 插件,以便最小化这种紧密耦合。
创建 App Engine for Java 应用程序
如果您已经阅读了前面的内容,那么已经准备好开始构建第一个 App Engine for Java 应用程序。首先需要 安装 Google Plugin for Eclipse for App Engine for Java;安装之后,您就有了好的起点。
打开 Eclipse IDE,您将看到在 Eclipse IDE 中,Printer 按钮旁边出现了三个新按钮:一个 G 显示在蓝色小球中,另一个 G 显示在红色工具箱中,还有一个 App Engine for Java 迷你喷气式飞机,如图 1 所示:
图 1. Eclipse IDE 中的新按钮
下面列出了这些按钮的功能:
- 蓝色小球让您能够访问 App Engine for Java 项目创建向导。
- 红色工具箱让您编译一个 GWT 项目。
- 迷你喷气式飞机图标让您能够部署一个 App Engine 项目。
您将使用项目创建向导创建两个新项目:一个基于 servlets,另一个使用 GWT 构建。将使用工具箱功能编译 GWT 项目。当您准备好部署 App Engine 项目时,将启动迷你喷气式分机,激活项目。
首先创建一个 App Engine for Java 项目。第一步,单击蓝色小球以访问项目创建向导。然后使用名称为 gaej.example 的包创建名为 SimpleServletApp 的应用程序,如图 2 所示:
图 2. 开始一个新项目
注意,对于这第一个简单示例,没有选择 GWT 支持。完成了这一步后,项目创建向导将创建一个简单的基于 servlet 的应用程序,提供了一个 Hello World 类型的 servlet。图 3 展示了这个项目的屏幕截图。
图 3. SimpleServletApp 项目
注意,这个新的基于 servlet 的项目自动包含了 JAR 文件:
- datanucleus-*.jar:用于使用标准 JDO 或低级 DataNucleus API访问 App Engine for Java 数据库
- appengine-api-sdk.1.2.0.jar:用于使用非标准 App Engine for Java 应用程序服务,比如 App Engine for Java Security
- geronimo-*.jar:用于使用标准 Java API,比如 Java Transaction Management API (JTA) 和 JPA
- jdo2-api-2.3-SNAPSHOT.jar:用于使用 JDO API
在本系列 第 2 部分 中,您将了解到如何使用 App Engine for Java 的持久化 API 和 App Engine for Java 的一些应用程序服务。
还需注意用于为 Google App Engine 配置运行时容器的文件,名为 appengine.xml。在本例中,appengine.xml 用于配置 logging.properties 文件,以使用 App Engine for Java 完成登录。
App Engine for Java servlet 应用程序初探
在项目创建向导中完成所有配置后,App Engine for Java 将向您显示一个 Hello World 风格的 servlet 应用程序的骨架。查看代码并看看如何使用 App Engine for Java Eclipse 工具运行应用程序。该应用程序的主要入口点为 SimpleServletAppServlet
,如清单 1 所示:
清单 1. SimpleServletAppServlet
package gaej.example;
import java.io.IOException;
import javax.servlet.http.*;
@SuppressWarnings("serial")
public class SimpleServletAppServlet extends HttpServlet {
public void doGet(HttpServletRequest req, HttpServletResponse resp)
throws IOException {
resp.setContentType("text/plain");
resp.getWriter().println("Hello, world");
}
}
|
servlet 在 web.xml 中的 URI /simpleservletapp
下完成映射,如清单 2 所示:
清单 2. web.xml
<?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 xmlns="http://java.sun.com/xml/ns/javaee" version="2.5">
<servlet>
<servlet-name>simpleservletapp</servlet-name>
<servlet-class>gaej.example.SimpleServletAppServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>simpleservletapp</servlet-name>
<url-pattern>/simpleservletapp</url-pattern>
</servlet-mapping>
<welcome-file-list>
<welcome-file>index.html</welcome-file>
</welcome-file-list>
</web-app>
|
项目创建还提供了一个 index.html 文件,包含了连接新 servlet 的链接,如清单 3 所示:
清单 3. 项目创建向导生成的 index.html
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<!-- The HTML 4.01 Transitional DOCTYPE declaration-->
<!-- above set at the top of the file will set -->
<!-- the browser's rendering engine into -->
<!-- "Quirks Mode". Replacing this declaration -->
<!-- with a "Standards Mode" doctype is supported, -->
<!-- but may lead to some differences in layout. -->
<html>
<head>
<meta http-equiv="content-type" content="text/html; charset=UTF-8">
<!-- -->
<!-- Any title is fine -->
<!-- -->
<title>Hello App Engine</title>
</head>
<!-- -->
<!-- The body can have arbitrary html, or -->
<!-- you can leave the body empty if you want -->
<!-- to create a completely dynamic UI. -->
<!-- -->
<body>
<h1>Hello App Engine!</h1>
<table>
<tr>
<td colspan="2" style="font-weight:bold;">Available Servlets:</td>
</tr>
<tr>
<td><a href="simpleservletapp"/>SimpleServletAppServlet</td>
</tr>
</table>
</body>
</html>
|
您现在已经使用了一些 Java API 构建了一个简单的 servlet 应用程序。并且重点在于:App Engine for Java 使用标准 Java API 封装 App Engine 功能,使 App Engine 能够支持面向 Java 平台的丰富框架。
部署应用程序
要使用 App Engine for Java Eclipse 工具运行我们的基于 servlet 的应用程序,首先右键单击该项目并选择 Run As 菜单,然后选择旁边有一个蓝色小球的 “Web Application”,如图 4 所示:
图 4. 运行 App Engine for Java 部署服务器
现在您应当能够在浏览器中导航到 http://localhost:8080/simpleservletapp 并看到应用程序显示出 Hello World 消息。
创建 App Engine for Java/GWT 应用程序
您已经了解了一个简单 App Engine for Java servlet 应用程序的工作原理,那么接下来让我们探讨一下面向 GWT 应用程序的 App Engine for Java Eclipse 工具。首先单击 Eclipse IDE 工具栏中的蓝色小球来激活 Google 项目创建向导。这一次,选择 GWT 支持,如图 5 所示:
图 5. 使用 App Engine for Java 项目创建向导创建一个简单的 GWT 应用程序
如图 6 所示,与简单的基于 servlet 的应用程序相比,App Engine for Java 为 GWT 应用程序提供了更多的代码工件。示例应用程序是一个在 GWT 执行的 GUI,它可以与一个问候(greeting)服务应用程序通信。
图 6. 为 GWT 应用程序提供的代码工件
为 GWT 应用程序提供的一个额外 JAR 对于基于 servlet 的应用程序并不是必须的,这个 JAR 文件就是 gwt-servlet.jar。
其他工件包括:
- src/gaej/example:SimpleGWTApp.gwt.xml:GWT 模块描述符
- src/gaej.example.server:GreetingServiceImpl.java:问候服务的实现
- src/gaej.example.client:GreetingService.java:问候服务的同步 API
- src/gaej.example.client:GreetingServiceAsync.java:问候服务的异步 API
- src/gaej.example.client:SimpleGWTApp.java:构建启动 GUI 的主要入口点
- war/WEB-INF:web.xml:配置
GreetingServiceImpl
的部署描述符
- war:SimpleGWTApp.html:显示 GWT GUI 的 HTML 页面
- war:SimpleGWTApp.css:GWT GUI 的样式表
在深入研究应用程序的架构和源代码之前,看看运行它的时候会发生什么状况。要运行应用程序,单击工具栏中的红色工具箱,然后单击 Compile 按钮。现在右键单击项目并像刚才一样选择 Run As—> Web Application 菜单项。这一次,由于您处理的是一个 GWT 应用程序,将显示一个 GWT Hosted Mode Console 和浏览器。继续并使用 Web 应用程序输入您的名字并观察响应。我收到如图 7 所示的响应:
图 7. 运行样例 GWT 应用程序
在下一小节中,我将带领您遍历样例 GWT 应用程序。如果希望了解更多关于 GWT 的信息(或者获得 GWT 教程),请参见 参考资料。
详细了解 GWT 应用程序
根据已经提供的配置,Eclipse 的 GWT 工具创建了一个启动应用程序,包含一个 HTML 前端(SimpleGWTApp.html,如 清单 10 所示),可以加载 simplegwtapp.js 和 simplegwtapp.nocache.js。这是由 GWT 从 Java 代码中生成的 JavaScript 代码;也就是说,位于 gaej.example.client 包中的 src 目录下的代码(参见清单 6、7 和 8)。
创建 GUI 的主要入口点是 gaej.example.client.SimpleGWTApp
,如 清单 8 所示。该类创建 GWT GUI 元素并将它们与 SimpleGWTApp.html 中的 HTML DOM 元素关联起来(参见 清单 10)。SimpleGWTApp.html 定义了两个 DOM 元素,分别命名为 nameFieldContainer
和 sendButtonContainer
(表中的两列)。SimpleGWTApp
类使用 RootPanel.get("nameFieldContainer")
访问与这些 DOM 元素关联的面板并使用 GUI 元素替换它们。SimpleGWTApp
类随后定义一个文本框和按钮,可以使用它们输入某人的名字并发送一句问候语(参见 清单 10)。
GWT 知道 SimpleGWTApp
类是应用程序的主要入口点,因为 SimpleGWTApp.gwt.xml 使用入口点元素进行了指定。
SimpleGWTApp
封装了名为 sendButton
的按钮,这样当它被单击时,SimpleGWTApp
将对 GreetingService
调用 greetServer
方法。GreetingService
接口在 src/gaej.example.client.GreetingService.java 中被定义(参见 清单 6)。
由于 Ajax 天生就具有异步性,因此 GWT 定义了一个异步接口来访问远程服务。SimpleGWTApp 使用 src/gaej.example.client.GreetingServiceAsync.java 中定义的异步接口(参见 清单 7)。GreetingServiceImpl
(src/gaej.example.server.GreetingServiceImpl.java)实现了 GreetingService
中定义的 greetServer
方法(参见 清单 5)。GreetingServiceImpl.greetServer
方法返回一条问候消息 String
,SimpleGWTApp 用它在所创建的对话框中显示问候消息。
GWT 模块描述符声明了 GUI 应用程序的主要入口点,即 gaej.example.client.SimpleGWTApp
,如清单 4 所示:
清单 4. GWT 模块描述符(src/gaej/example/SimpleGWTApp.gwt.xml)
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE module PUBLIC "-//Google Inc.//DTD Google Web Toolkit 1.6.4//EN"
"http://google-web-toolkit.googlecode.com/svn/tags/1.6.4/
distro-source/core/src/gwt-module.dtd">
<module rename-to='simplegwtapp'>
<!-- Inherit the core Web Toolkit stuff. -->
<inherits name='com.google.gwt.user.User'/>
<!-- Inherit the default GWT style sheet. You can change -->
<!-- the theme of your GWT application by uncommenting -->
<!-- any one of the following lines. -->
<inherits name='com.google.gwt.user.theme.standard.Standard'/>
<!-- <inherits name='com.google.gwt.user.theme.chrome.Chrome'/> -->
<!-- <inherits name='com.google.gwt.user.theme.dark.Dark'/> -->
<!-- Other module inherits -->
<!-- Specify the app entry point class. -->
<entry-point class='gaej.example.client.SimpleGWTApp'/>
</module>
|
GreetingServiceImpl
是问候服务应用程序的实际实现,如清单 5 所示。它运行在服务器端,并且客户机代码通过一个远程过程调用来调用它。
清单 5. greeting-service 应用程序的实现(src/gaej.example.server.GreetingServiceImpl.java)
package gaej.example.server;
import gaej.example.client.GreetingService;
import com.google.gwt.user.server.rpc.RemoteServiceServlet;
/**
* The server side implementation of the RPC service.
*/
@SuppressWarnings("serial")
public class GreetingServiceImpl extends RemoteServiceServlet implements
GreetingService {
public String greetServer(String input) {
String serverInfo = getServletContext().getServerInfo();
String userAgent = getThreadLocalRequest().getHeader("User-Agent");
return "Hello, " + input + "!<br><br>I am running " + serverInfo
+ ".<br><br>It looks like you are using:<br>" + userAgent;
}
}
|
如清单 6 所示,GreetingService
是客户机代码使用的远程过程调用的接口:
清单 6. 同步 API (src/gaej.example.client.GreetingService.java)
package gaej.example.client;
import com.google.gwt.user.client.rpc.RemoteService;
import com.google.gwt.user.client.rpc.RemoteServiceRelativePath;
/**
* The client side stub for the RPC service.
*/
@RemoteServiceRelativePath("greet")
public interface GreetingService extends RemoteService {
String greetServer(String name);
}
|
GreetingServiceAsync
是客户机代码将使用的实际接口,如清单 7 所示。每个方法都提供了一个回调对象,这样就可以在完成远程过程调用后收到异步通知。至于内部原理,GWT 使用了 Ajax。在客户机上使用 Ajax 时,最好不要阻塞客户机,这样就不会阻塞异步调用。阻塞会违背使用 Ajax 的初衷:
清单 7. 异步 API(src/gaej.example.client.GreetingServiceAsync.java)
package gaej.example.client;
import com.google.gwt.user.client.rpc.AsyncCallback;
/**
* The async counterpart of <code>GreetingService</code>.
*/
public interface GreetingServiceAsync {
void greetServer(String input, AsyncCallback<String> callback);
}
|
SimpleGWTApp
中包含了最多的操作。它注册了 GUI 事件,然后将 Ajax 请求发送到 GreetingService
。
清单 8. 应用程序的主入口点还构建了启动 GUI(src/gaej.example.client.SimpleGWTApp.java)
package gaej.example.client;
import com.google.gwt.core.client.EntryPoint;
import com.google.gwt.core.client.GWT;
import com.google.gwt.event.dom.client.ClickEvent;
import com.google.gwt.event.dom.client.ClickHandler;
import com.google.gwt.event.dom.client.KeyCodes;
import com.google.gwt.event.dom.client.KeyUpEvent;
import com.google.gwt.event.dom.client.KeyUpHandler;
import com.google.gwt.user.client.rpc.AsyncCallback;
import com.google.gwt.user.client.ui.Button;
import com.google.gwt.user.client.ui.DialogBox;
import com.google.gwt.user.client.ui.HTML;
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.VerticalPanel;
/**
* Entry point classes define <code>onModuleLoad()</code>.
*/
public class SimpleGWTApp implements EntryPoint {
/**
* The message displayed to the user when the server cannot be reached or
* returns an error.
*/
private static final String SERVER_ERROR = "An error occurred while "
+ "attempting to contact the server. Please check your network "
+ "connection and try again.";
/**
* Create a remote service proxy to talk to the server-side Greeting service.
*/
private final GreetingServiceAsync greetingService = GWT
.create(GreetingService.class);
/**
* This is the entry point method.
*/
public void onModuleLoad() {
final Button sendButton = new Button("Send");
final TextBox nameField = new TextBox();
nameField.setText("GWT User");
// You can add style names to widgets
sendButton.addStyleName("sendButton");
// Add the nameField and sendButton to the RootPanel
// Use RootPanel.get() to get the entire body element
RootPanel.get("nameFieldContainer").add(nameField);
RootPanel.get("sendButtonContainer").add(sendButton);
// Focus the cursor on the name field when the app loads
nameField.setFocus(true);
nameField.selectAll();
// Create the popup dialog box
final DialogBox dialogBox = new DialogBox();
dialogBox.setText("Remote Procedure Call");
dialogBox.setAnimationEnabled(true);
final Button closeButton = new Button("Close");
// You can set the id of a widget by accessing its Element
closeButton.getElement().setId("closeButton");
final Label textToServerLabel = new Label();
final HTML serverResponseLabel = new HTML();
VerticalPanel dialogVPanel = new VerticalPanel();
dialogVPanel.addStyleName("dialogVPanel");
dialogVPanel.add(new HTML("<b>Sending name to the server:</b>"));
dialogVPanel.add(textToServerLabel);
dialogVPanel.add(new HTML("<br><b>Server replies:</b>"));
dialogVPanel.add(serverResponseLabel);
dialogVPanel.setHorizontalAlignment(VerticalPanel.ALIGN_RIGHT);
dialogVPanel.add(closeButton);
dialogBox.setWidget(dialogVPanel);
// Add a handler to close the DialogBox
closeButton.addClickHandler(new ClickHandler() {
public void onClick(ClickEvent event) {
dialogBox.hide();
sendButton.setEnabled(true);
sendButton.setFocus(true);
}
});
// Create a handler for the sendButton and nameField
class MyHandler implements ClickHandler, KeyUpHandler {
/**
* Fired when the user clicks on the sendButton.
*/
public void onClick(ClickEvent event) {
sendNameToServer();
}
/**
* Fired when the user types in the nameField.
*/
public void onKeyUp(KeyUpEvent event) {
if (event.getNativeKeyCode() == KeyCodes.KEY_ENTER) {
sendNameToServer();
}
}
/**
* Send the name from the nameField to the server and wait for a response.
*/
private void sendNameToServer() {
sendButton.setEnabled(false);
String textToServer = nameField.getText();
textToServerLabel.setText(textToServer);
serverResponseLabel.setText("");
greetingService.greetServer(textToServer,
new AsyncCallback<String>() {
public void onFailure(Throwable caught) {
// Show the RPC error message to the user
dialogBox
.setText("Remote Procedure Call - Failure");
serverResponseLabel
.addStyleName("serverResponseLabelError");
serverResponseLabel.setHTML(SERVER_ERROR);
dialogBox.center();
closeButton.setFocus(true);
}
public void onSuccess(String result) {
dialogBox.setText("Remote Procedure Call");
serverResponseLabel
.removeStyleName("serverResponseLabelError");
serverResponseLabel.setHTML(result);
dialogBox.center();
closeButton.setFocus(true);
}
});
}
}
// Add a handler to send the name to the server
MyHandler handler = new MyHandler();
sendButton.addClickHandler(handler);
nameField.addKeyUpHandler(handler);
}
}
|
Web 部署描述符(web.xml,如清单 9 所示)将 GreetingService
映射为一个基于 servlet 的 Web 资源。它在 /simplegwtapp/greet
名称下映射 GreetingService
servlet,这样 SimpleGWTApp
就可以加载它并对它发起调用。Web 部署描述符还可以将 SimpleGWTApp.html 指定为应用程序的欢迎页面,这样就会始终加载它。
清单 9. 配置 GreetingServiceImpl (war/WEB-INF/web.xml) 的部署描述符
<?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>SimpleGWTApp.html</welcome-file>
</welcome-file-list>
<!-- Servlets -->
<servlet>
<servlet-name>greetServlet</servlet-name>
<servlet-class>gaej.example.server.GreetingServiceImpl</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>greetServlet</servlet-name>
<url-pattern>/simplegwtapp/greet</url-pattern>
</servlet-mapping>
</web-app>
|
HTML 前端为 SimpleGWTApp.html,如清单 10 所示。这是加载 simplegwtapp.js 和 simplegwtapp.nocache.js 的页面,是由 GWT 从 Java 代码中生成的 JavaScript 代码。如前所述,这段代码位于 gaej.example.client 包的 src 目录中(来自清单 6、7 和 8)。
清单 10. 显示 GWT GUI (war/SimpleGWTApp.html) 的 HTML 页面
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<!-- The HTML 4.01 Transitional DOCTYPE declaration-->
<!-- above set at the top of the file will set -->
<!-- the browser's rendering engine into -->
<!-- "Quirks Mode". Replacing this declaration -->
<!-- with a "Standards Mode" doctype is supported, -->
<!-- but may lead to some differences in layout. -->
<html>
<head>
<meta http-equiv="content-type" content="text/html; charset=UTF-8">
<!-- -->
<!-- Consider inlining CSS to reduce the number of requested files -->
<!-- -->
<link type="text/css" rel="stylesheet" href="SimpleGWTApp.css">
<!-- -->
<!-- Any title is fine -->
<!-- -->
<title>Web Application Starter Project</title>
<!-- -->
<!-- This script loads your compiled module. -->
<!-- If you add any GWT meta tags, they must -->
<!-- be added before this line. -->
<!-- -->
<script type="text/javascript" language="javascript"
src="simplegwtapp/simplegwtapp.nocache.js"></script>
</head>
<!-- -->
<!-- The body can have arbitrary html, or -->
<!-- you can leave the body empty if you want -->
<!-- to create a completely dynamic UI. -->
<!-- -->
<body>
<!-- OPTIONAL: include this if you want history support -->
<iframe src="javascript:''" id="__gwt_historyFrame" tabIndex='-1'
style="position:absolute;width:0;height:0;border:0"></iframe>
<h1>Web Application Starter Project</h1>
<table align="center">
<tr>
<td colspan="2" style="font-weight:bold;">Please enter your name:</td>
</tr>
<tr>
<td id="nameFieldContainer"></td>
<td id="sendButtonContainer"></td>
</tr>
</table>
</body>
</html>
|
使用 GWT,您可以通过 CSS 控制应用程序的观感,如清单 11 所示:
清单 11. GWT GUI (war/SimpleGWTApp.css) 的样式表
/** Add css rules here for your application. */
/** Example rules used by the template application (remove for your app) */
h1 {
font-size: 2em;
font-weight: bold;
color: #777777;
margin: 40px 0px 70px;
text-align: center;
}
.sendButton {
display: block;
font-size: 16pt;
}
/** Most GWT widgets already have a style name defined */
.gwt-DialogBox {
width: 400px;
}
.dialogVPanel {
margin: 5px;
}
.serverResponseLabelError {
color: red;
}
/** Set ids using widget.getElement().setId("idOfElement") */
#closeButton {
margin: 15px 6px 6px;
}
|
部署到 Google App Engine
创建了下一代杀手级应用程序后(因为我们确实需要一个用户友好的问候应用程序),您需要部署它。使用 Google App Engine 的重点就是可以将应用程序部署到 Google 提供的可靠基础设施中,使它更易于扩展。Google App Engine 的设计初衷就是为构建可伸缩应用程序提供一个平台,可伸缩应用程序就是指 “能够在不触动基础设施的情况下将用户轻松增长到数百万”(正如 App Engine 主页中描述的那样)。为使用这种基础设施,您需要一个 Google App Engine for Java 帐户。
就像许多其他产品一样,第一次体验总是免费的。App Engine for Java 的免费版提供了一个已部署的应用程序,提供了足够的 CPU、带宽和存储来为 500 万个页面访问次数提供服务。超过使用次数开始收费(同样需要注意,在撰写本文时可以获得 App Engine for Java 平台的预览版)。
获得帐户之后,您应该可以在 App Engine for Java 站点 看到一个空的应用程序列表。单击 Create New Application 按钮,应当出现一个如图 8 所示的表单。输入一个独特的应用程序名称和描述,之后您将看到一条显示有应用程序标识符的确认消息。
该标识符也位于应用程序的 app.yaml 文件中。注意,该标识符不可修改。如果对您的应用程序使用 Google 身份验证,那么在访问应用程序时,“GAEj Article For Rick Part 1” 将显示在 Sign In 页面中。您将使用 gaejarticleforrick
和 App Engine for Java Eclipse 插件来将应用程序部署到 Google App Engine。
图 8. 创建一个新的 App Engine for Java 应用程序
设置好应用程序 ID 后,可以从 Eclipse 中部署您的应用程序。首先,单击看上去类似 Google App Engine 徽标的工具栏按钮(显示机翼和尾翼的喷气式发动机),如图 9 所示:
图 9. App Engine for Java Eclipse 插件
在单击图 10 所示的对话框中的 Deploy 之前,可能需要确保 App Engine for Java 项目被选中。将要求您输入 Google 凭证,即您的电子邮件地址和用户名。
图 10. 部署项目
图 10 中的对话框包含一个 “App Engine Project setting” 链接。单击此链接(也可以从项目设置文件访问)并输入应用程序 ID(在本例中为 gaejarticleforrick
),如图 11 所示。填充好应用程序 ID 后,单击 OK,然后单击 Deploy。
图 11. Google App Engine 的项目设置
部署完应用程序后,该应用程序可以通过 http://<application id>.appspot.com/
访问。您还会看到应用程序出现在 http://gaejarticleforrick.appspot.com/ 中。
结束语
Google App Engine for Java 系列的第一部分到此结束。至此,您已经大致了解了 App Engine for Java 是什么,并通过使用 App Engine for Java Google Plugin for Eclipse 迈出了第一步。您创建了两个启动应用程序(一个基于 servlet,另一个基于 GWT)并随后将 GWT 应用程序部署到 Google App Engine 平台。
本文的示例到目前为止演示了可以更轻松地创建和部署基于 Java 的应用程序的工具和功能 — 这些应用程序有可能扩展到 YouTube 或 Facebook 那样的规模。在 第 2 部分 中,您将继续为 Java 开发人员介绍使用 App Engine for Java 的机会。从本文演示的示例应用程序出发,您将构建一个定制的联系人管理应用程序。该应用程序将成为第 3 部分的中心内容,第 3 部分将进一步了解 App Engine for Java 的数据存储及其 GUI 前端。