部署到 GAE

至此,您已创建了 StockWatcher 应用程序的初始实现,并在客户端代码中模拟了股票数据。

在本节中,您将在 Google App Engine 上部署此应用程序。您还将了解一些 App Engine 服务 API,并使用它们个性化 StockWatcher 应用程序,以便用户可以登录其 Google 帐户并检索其股票列表。

  1. App Engine 入门
  2. 将应用程序部署到 App Engine
  3. 使用用户服务个性化应用程序
  4. 在数据存储区中存储数据

注意:有关部署的更广泛指南,请参见 部署 GWT 应用程序

本教程基于在 构建示例 GWT 应用程序 教程中创建的 GWT 概念和 StockWatcher 应用程序。它还使用 GWT RPC 教程中介绍的技术。如果您尚未完成这些教程并且熟悉基本的 GWT 概念,您可以按照以下说明导入 StockWatcher 项目,该项目已编码到此点。

App Engine 入门

注册 App Engine 帐户

注册 App Engine 帐户。帐户激活后,登录并创建应用程序。记下您选择的应用程序 ID,因为您在配置 StockWatcher 项目时需要此信息。完成本教程后,您可以将此应用程序 ID 重用于其他应用程序。

下载 App Engine SDK

如果您打算使用 Eclipse,您可以使用 适用于 Eclipse 的 Google 插件 下载 App Engine SDK。或者 下载 适用于 Java 的 App Engine SDK。

设置项目

设置项目(使用 Eclipse)

如果您最初使用 Google 适用于 Eclipse 的插件创建了 StockWatcher Eclipse 项目,并且同时启用了 GWT 和 Google App Engine,则您的项目已准备好运行在 App Engine 上。如果不是

  1. 如果您还没有,请安装 适用于 Eclipse 的 Google 插件,同时启用 GWT 和 App Engine SDK,然后重新启动 Eclipse。
  2. 完成 构建示例 GWT 应用程序 教程,确保同时启用 GWT 和 Google App Engine 创建项目。或者,如果您想跳过构建示例 GWT 应用程序教程,则 下载、解压缩并导入 StockWatcher Eclipse 项目。要导入项目

    1. 在“文件”菜单中,选择“导入...”菜单选项。
    2. 选择导入源“常规”>“将现有项目导入工作区”。单击“下一步”按钮。
    3. 在“选择根目录”中,浏览并选择 StockWatcher 目录(来自解压缩的文件)。单击“完成”按钮。
    4. 将 Google Web Toolkit 和 App Engine 功能添加到新创建的项目中(右键单击您的项目>“Google”>“Web Toolkit/App Engine 设置...”)。这会将 Google 插件功能添加到您的项目,并将必需的库自动复制到您的项目 WEB-INF/lib 目录中。

设置项目(不使用 Eclipse)

  1. 如果您还没有,请下载适用于 Java 的 App Engine SDK
  2. 完成 构建示例 GWT 应用程序 教程,使用 webAppCreator 创建 GWT 应用程序。或者,如果您想跳过构建示例 GWT 应用程序教程,请下载并解压缩 此文件。编辑 StockWatcher/build.xml 中的 gwt.sdk 属性,然后继续进行以下修改。
  3. App Engine 需要自己的 Web 应用程序部署描述符。创建一个文件 StockWatcher/war/WEB-INF/appengine-web.xml,其中包含以下内容

    <?xml version="1.0" encoding="utf-8"?>
    <appengine-web-app xmlns="http://appengine.google.com/ns/1.0">
      <application><!-- Your App Engine application ID goes here --></application>
      <version>1</version>
    </appengine-web-app>
    

    在第二行替换您的 App Engine 应用程序 ID。详细了解 appengine-web.xml

  4. 由于我们将在稍后使用 Java 数据对象 (JDO) 存储数据,因此创建一个文件 StockWatcher/src/META-INF/jdoconfig.xml,其中包含以下内容

    <?xml version="1.0" encoding="utf-8"?>
    <jdoconfig xmlns="http://java.sun.com/xml/ns/jdo/jdoconfig"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:noNamespaceSchemaLocation="http://java.sun.com/xml/ns/jdo/jdoconfig">
      <persistence-manager-factory name="transactions-optional">
        <property name="javax.jdo.PersistenceManagerFactoryClass" value="org.datanucleus.store.appengine.jdo.DatastoreJDOPersistenceManagerFactory"/>
        <property name="javax.jdo.option.ConnectionURL" value="appengine"/>
        <property name="javax.jdo.option.NontransactionalRead" value="true"/>
        <property name="javax.jdo.option.NontransactionalWrite" value="true"/>
        <property name="javax.jdo.option.RetainValues" value="true"/>
        <property name="datanucleus.appengine.autoCreateDatastoreTxns" value="true"/>
      </persistence-manager-factory>
    </jdoconfig>
    

    您将在稍后通过其名称“transactions-optional”引用此配置。详细了解 jdoconfig.xml

  5. GWT ant 构建文件需要修改以支持 DataNucleus JDO 编译和使用 App Engine 开发服务器。编辑 StockWatcher/build.xml 并添加以下内容

  6. 添加 App Engine SDK 目录的属性。

    <!-- Configure path to GWT SDK -->
    <property name="gwt.sdk" location="_Path to GWT_" />
    <!-- Configure path to App Engine SDK -->
    <property name="appengine.sdk" location="_Path to App Engine SDK_" />
    
  7. 添加 App Engine 工具类路径的属性。

    <path id="project.class.path">
      <pathelement location="war/WEB-INF/classes"/>
      <pathelement location="${gwt.sdk}/gwt-user.jar"/>
      <fileset dir="${gwt.sdk}" includes="gwt-dev*.jar"/>
        <!-- Add any additional non-server libs (such as JUnit) -->
      <fileset dir="war/WEB-INF/lib" includes="**/*.jar"/>
    </path>
    
    <path id="tools.class.path">
      <path refid="project.class.path"/>
      <pathelement location="${appengine.sdk}/lib/appengine-tools-api.jar"/>
      <fileset dir="${appengine.sdk}/lib/tools">
        <include name="**/asm-*.jar"/>
        <include name="**/datanucleus-enhancer-*.jar"/>
      </fileset>
    </path>
    
  8. 修改“libs”ant 目标,以便将必需的 jar 文件复制到 WEB-INF/lib。

    <target name="libs" description="Copy libs to WEB-INF/lib">
      <mkdir dir="war/WEB-INF/lib" />
      <copy todir="war/WEB-INF/lib" file="${gwt.sdk}/gwt-servlet.jar" />
      <!-- Add any additional server libs that need to be copied -->
      <copy todir="war/WEB-INF/lib" flatten="true">
        <fileset dir="${appengine.sdk}/lib/user" includes="**/*.jar"/>
      </copy>
    </target>
    
  9. JDO 使用 DataNucleus Java 字节码增强来实现。修改“javac”ant 目标以添加字节码增强。

    <target name="javac" depends="libs" description="Compile java source">
      <mkdir dir="war/WEB-INF/classes"/>
      <javac srcdir="src" includes="**" encoding="utf-8"
          destdir="war/WEB-INF/classes"
          source="1.5" target="1.5" nowarn="true"
          debug="true" debuglevel="lines,vars,source">
        <classpath refid="project.class.path"/>
      </javac>
      <copy todir="war/WEB-INF/classes">
        <fileset dir="src" excludes="**/*.java"/>
      </copy>
      <taskdef name="datanucleusenhancer"
          classpathref="tools.class.path"
          classname="org.datanucleus.enhancer.tools.EnhancerTask" />
      <datanucleusenhancer classpathref="tools.class.path"
          failonerror="true">
        <fileset dir="war/WEB-INF/classes" includes="**/*.class" />
      </datanucleusenhancer>
    </target>
    
  10. 修改“devmode”ant 目标以使用 App Engine 开发服务器,而不是 GWT 附带的 servlet 容器。

    <target name="devmode" depends="javac" description="Run development mode"">
      <java failonerror="true" fork="true" classname="com.google.gwt.dev.DevMode"">
        <classpath>
          <pathelement location="src"/>
          <path refid="project.class.path"/>
          <path refid="tools.class.path"/>
        </classpath>
        <jvmarg value="-Xmx256M"/>
        <arg value="-startupUrl"/>
        <arg value="StockWatcher.html"/>
        <!-- Additional arguments like -style PRETTY or -logLevel DEBUG -->
        <arg value="-server"/>
        <arg value="com.google.appengine.tools.development.gwt.AppEngineLauncher"/>
        <arg value="com.google.gwt.sample.stockwatcher.StockWatcher"/>
      </java>
    </target>
    

本地测试

我们将以 GWT 开发模式运行应用程序,以验证项目是否已成功设置。但是,应用程序将运行在 App Engine 开发服务器中,而不是使用 GWT 附带的 servlet 容器,它是 App Engine SDK 附带的 servlet 容器。有什么区别?App Engine 开发服务器配置为模拟 App Engine 生产环境。

以开发模式运行应用程序(使用 Eclipse)

  1. 在“包资源管理器”视图中,选择 StockWatcher 项目。
  2. 在工具栏中,单击“运行”按钮(以 Web 应用程序运行)。

以开发模式运行应用程序(不使用 Eclipse)

  1. 从命令行,更改到 StockWatcher 目录。
  2. 执行
ant devmode

将应用程序部署到 App Engine

现在,我们已经验证了 StockWatcher 项目在本地以 GWT 开发模式和 App Engine 开发服务器运行,我们可以将应用程序运行在 App Engine 上。

将应用程序部署到 App Engine(使用 Eclipse)

  1. 在“包资源管理器”视图中,选择 StockWatcher 项目。
  2. 在工具栏中,单击“部署 App Engine 项目”按钮 icon
  3. (首次使用)单击“App Engine 项目设置...”链接以指定您的应用程序 ID。完成后,单击“确定”按钮。
  4. 输入您的 Google 帐户电子邮件和密码。单击“部署”按钮。您可以在 Eclipse 控制台中查看部署进度。

将应用程序部署到 App Engine(不使用 Eclipse)

  1. 从命令行,更改到 StockWatcher 目录。
  2. 通过执行以下操作编译应用程序
ant build

提示:将 ant bin 目录添加到您的环境 PATH 中,以避免必须指定 ant 的完整路径。

  1. appcfg 是 App Engine SDK 附带的命令行工具。通过执行以下操作上传应用程序
appcfg.sh update war

从 Windows 命令提示符,该命令为 appcfg update war。第一个参数是要执行的操作。第二个参数是包含更新的目录,在本例中为包含静态文件和 GWT 编译器输出的相对目录。在提示时,输入您的 Google 帐户电子邮件和密码。

提示:将 App Engine SDK bin 目录添加到您的环境 PATH 中,以避免必须指定 appcfg.sh 的完整路径。

在 App Engine 上测试

通过在 Web 浏览器中打开 http://_application-id_.appspot.com/ 来测试已上传的应用程序,其中 application-id 是您之前创建的 App Engine 应用程序 ID。StockWatcher 应用程序现在在您的应用程序 ID 下运行在 App Engine 上。

使用用户服务个性化应用程序

概览

现在 StockWatcher 已经部署到 App Engine 上,我们可以开始使用一些可用的服务来丰富应用程序。我们将从每个用户的基础上持久化股票报价列表开始。这是由于数据存储服务,它允许我们保存应用程序数据,以及用户服务,它允许我们让用户登录并为每个用户保存股票报价列表。为了持久化,我们将使用 App Engine SDK 提供的 Java 数据对象 (JDO) 接口。

为了实现登录功能,我们将使用用户服务。有了此服务,任何拥有 Google 帐户的用户都可以使用他们的帐户登录以访问 StockWatcher 应用程序。在本节中,您将使用 App Engine 用户 API 将用户登录添加到应用程序中。

App Engine 用户服务非常易于使用。首先,您需要实例化 UserService 类,如下面的代码片段所示。

UserService userService = UserServiceFactory.getUserService();

接下来,您需要获取访问 StockWatcher 应用程序的当前用户。

User user = userService.getCurrentUser();

如果访问应用程序的当前用户已登录他们的 Google 帐户,则 UserService 会返回一个已实例化的 User 对象。User 对象包含有用的信息,例如与帐户关联的电子邮件地址以及帐户昵称。如果访问应用程序的人未登录其帐户或没有 Google 帐户,则返回的 User 对象将为 null。在这种情况下,我们有多种方法可以处理这种情况,但出于 StockWatcher 应用程序的目的,我们将引导用户进入登录 URL,他们可以在该 URL 登录其 Google 帐户。

User API 提供了一种简单的方法来生成登录 URL。只需调用 UserService 的 createLoginURL(String requestUri) 方法,即可获得将用户重定向到 Google 帐户登录屏幕的重定向登录 URL。他们登录后,App Engine 容器将知道根据您在进行 createLoginURL() 调用时提供的 requestUri 将用户重定向到哪里。

定义登录 RPC 服务

为了使这一点更加具体,让我们为 StockWatcher 应用程序创建一个登录 RPC 服务。如果您不熟悉 GWT RPC,请参阅前面的 教程

首先,创建 LoginInfo 对象,该对象将包含来自 User 服务的登录信息。

LoginInfo.java

package com.google.gwt.sample.stockwatcher.client;

import java.io.Serializable;

public class LoginInfo implements Serializable {

  private boolean loggedIn = false;
  private String loginUrl;
  private String logoutUrl;
  private String emailAddress;
  private String nickname;

  public boolean isLoggedIn() {
    return loggedIn;
  }

  public void setLoggedIn(boolean loggedIn) {
    this.loggedIn = loggedIn;
  }

  public String getLoginUrl() {
    return loginUrl;
  }

  public void setLoginUrl(String loginUrl) {
    this.loginUrl = loginUrl;
  }

  public String getLogoutUrl() {
    return logoutUrl;
  }

  public void setLogoutUrl(String logoutUrl) {
    this.logoutUrl = logoutUrl;
  }

  public String getEmailAddress() {
    return emailAddress;
  }

  public void setEmailAddress(String emailAddress) {
    this.emailAddress = emailAddress;
  }

  public String getNickname() {
    return nickname;
  }

  public void setNickname(String nickname) {
    this.nickname = nickname;
  }
}

LoginInfo 是可序列化的,因为它是一个 RPC 方法的返回值类型。

接下来,创建 LoginService 和 LoginServiceAsync 接口。

LoginService.java

package com.google.gwt.sample.stockwatcher.client;

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

@RemoteServiceRelativePath("login")
public interface LoginService extends RemoteService {
  public LoginInfo login(String requestUri);
}

路径注释“login”将在下面配置。

LoginServiceAsync.java

package com.google.gwt.sample.stockwatcher.client;

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

public interface LoginServiceAsync {
  public void login(String requestUri, AsyncCallback<LoginInfo> async);
}

在 com.google.gwt.sample.stockwatcher.server 包中创建 LoginServiceImpl 类,如下所示。

LoginServiceImpl.java

package com.google.gwt.sample.stockwatcher.server;

import com.google.appengine.api.users.User;
import com.google.appengine.api.users.UserService;
import com.google.appengine.api.users.UserServiceFactory;
import com.google.gwt.sample.stockwatcher.client.LoginInfo;
import com.google.gwt.sample.stockwatcher.client.LoginService;
import com.google.gwt.user.server.rpc.RemoteServiceServlet;

public class LoginServiceImpl extends RemoteServiceServlet implements
    LoginService {

  public LoginInfo login(String requestUri) {
    UserService userService = UserServiceFactory.getUserService();
    User user = userService.getCurrentUser();
    LoginInfo loginInfo = new LoginInfo();

    if (user != null) {
      loginInfo.setLoggedIn(true);
      loginInfo.setEmailAddress(user.getEmail());
      loginInfo.setNickname(user.getNickname());
      loginInfo.setLogoutUrl(userService.createLogoutURL(requestUri));
    } else {
      loginInfo.setLoggedIn(false);
      loginInfo.setLoginUrl(userService.createLoginURL(requestUri));
    }
    return loginInfo;
  }

}

最后,在您的 web.xml 文件中配置 servlet。映射由 GWT 模块定义 (stockwatcher) 中的 rename-to 属性和 RemoteServiceRelativePath 注释 (login) 组成。此外,由于 greetServlet 不需要此应用程序,因此可以删除其配置。

web.xml

<?xml version="1.0" encoding="UTF-8"?>

<web-app>

  <!-- Default page to serve -->
  <welcome-file-list>
    <welcome-file>StockWatcher.html</welcome-file>
  </welcome-file-list>

  <!-- Servlets -->
  <servlet>
    <servlet-name>loginService</servlet-name>
    <servlet-class>com.google.gwt.sample.stockwatcher.server.LoginServiceImpl</servlet-class>
  </servlet>

  <servlet-mapping>
    <servlet-name>loginService</servlet-name>
    <url-pattern>/stockwatcher/login</url-pattern>
  </servlet-mapping>

</web-app>

更新 StockWatcher UI

现在登录 RPC 服务已经到位,最后要做的是从 StockWatcher 入口点类调用该服务。但是,我们必须考虑在添加登录功能后应用程序流程如何变化。在应用程序的先前版本中,您可以无条件地加载 StockWatcher,因为它不需要任何登录。现在我们要求用户登录,因此我们必须稍微更改加载逻辑。

首先,如果用户已经登录,则应用程序可以继续并加载 StockWatcher。但是,如果用户未登录,我们必须将他们重定向到登录页面。登录后,他们将被重定向回 StockWatcher 主页,在那里我们需要再次检查他们是否已通过身份验证。如果身份验证检查通过,那么我们就可以加载股票观察器。

需要注意的关键是,加载股票观察器取决于登录结果。这意味着仅在登录通过后才能调用加载 StockWatcher 的逻辑。这将需要一些重构。如果您使用的是 Eclipse,这将很容易完成。只需选择 StockWatcher onModuleLoad() 方法中的代码,选择“重构”菜单,然后单击“提取方法...”功能。从那里您可以将提取的方法声明为合适的内容,例如 private void loadStockWatcher()

您应该得到类似于以下内容的结果。

StockWatcher.java

public void onModuleLoad() {
    loadStockWatcher();
  }

  private void loadStockWatcher() {
    // Create table for stock data.
    stocksFlexTable.setText(0, 0, "Symbol");
    stocksFlexTable.setText(0, 1, "Price");
    stocksFlexTable.setText(0, 2, "Change");
    stocksFlexTable.setText(0, 3, "Remove");
    ...
  }

现在您已经将 StockWatcher 加载逻辑重构为一个可调用方法,我们可以在 onModuleLoad() 方法中进行登录 RPC 服务调用,并在登录通过时调用 loadStockWatcher() 方法。但是,如果用户未登录,则需要向他们提供一些指示,说明他们需要登录才能继续。为此,使用登录面板以及相应的标签和按钮来指示用户继续登录是有意义的。

考虑到所有这些,您应该在 StockWatcher 入口点类中添加类似于以下内容的内容。

StockWatcher.java

import com.google.gwt.user.client.ui.Anchor;

...

  private LoginInfo loginInfo = null;
  private VerticalPanel loginPanel = new VerticalPanel();
  private Label loginLabel = new Label(
      "Please sign in to your Google Account to access the StockWatcher application.");
  private Anchor signInLink = new Anchor("Sign In");

  public void onModuleLoad() {
    // Check login status using login service.
    LoginServiceAsync loginService = GWT.create(LoginService.class);
    loginService.login(GWT.getHostPageBaseURL(), new AsyncCallback<LoginInfo>() {
      public void onFailure(Throwable error) {
      }

      public void onSuccess(LoginInfo result) {
        loginInfo = result;
        if(loginInfo.isLoggedIn()) {
          loadStockWatcher();
        } else {
          loadLogin();
        }
      }
    });
  }

  private void loadLogin() {
    // Assemble login panel.
    signInLink.setHref(loginInfo.getLoginUrl());
    loginPanel.add(loginLabel);
    loginPanel.add(signInLink);
    RootPanel.get("stockList").add(loginPanel);
  }

关于登录功能的另一个重要点是能够注销应用程序。这也是您应该添加到 StockWatcher 应用程序中的内容。幸运的是,用户服务通过类似于 createLoginURL(String requestUri) 方法的调用,为我们提供了一个注销 URL。我们可以通过添加以下片段,将此添加到 StockWatcher 示例应用程序中。

StockWatcher.java

private Anchor signInLink = new Anchor("Sign In");
  private Anchor signOutLink = new Anchor("Sign Out");

...

  private void loadStockWatcher() {
    // Set up sign out hyperlink.
    signOutLink.setHref(loginInfo.getLogoutUrl());

    // Create table for stock data.
    stocksFlexTable.setText(0, 0, "Symbol");
    stocksFlexTable.setText(0, 1, "Price");
    stocksFlexTable.setText(0, 2, "Change");
    stocksFlexTable.setText(0, 3, "Remove");

  ...

    // Assemble Main panel.
    mainPanel.add(signOutLink);
    mainPanel.add(stocksFlexTable);
    mainPanel.add(addPanel);
    mainPanel.add(lastUpdatedLabel);

测试用户服务功能

您可以重复上述说明以在 本地App Engine 上运行应用程序。

如果您使用 App Engine 开发服务器在开发模式下运行应用程序,则登录页面将允许您输入任何电子邮件地址(便于测试)。如果您将应用程序部署到 App Engine,则登录页面将要求用户登录 Google 帐户才能访问应用程序。

在数据存储中存储数据

概览

App Engine Java 运行时可用的数据存储服务与 Python 运行时可用的服务相同。要访问 Java 中的此服务,您可以使用低级 数据存储 APIJava 数据对象 (JDO)Java 持久性 API (JPA)。对于此示例,我们将使用 JDO。

定义股票 RPC 服务

我们将创建一个基本的股票服务来处理用户股票的持久化。我们还将公开此服务作为 GWT RPC 服务。

StockService.java

package com.google.gwt.sample.stockwatcher.client;

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

@RemoteServiceRelativePath("stock")
public interface StockService extends RemoteService {
  public void addStock(String symbol) throws NotLoggedInException;
  public void removeStock(String symbol) throws NotLoggedInException;
  public String[] getStocks() throws NotLoggedInException;
}

StockServiceAsync.java

package com.google.gwt.sample.stockwatcher.client;

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

public interface StockServiceAsync {
  public void addStock(String symbol, AsyncCallback<Void> async);
  public void removeStock(String symbol, AsyncCallback<Void> async);
  public void getStocks(AsyncCallback<String[]> async);
}

StockWatcher.java

public class StockWatcher implements EntryPoint {

  private static final int REFRESH_INTERVAL = 5000; // ms
  private VerticalPanel mainPanel = new VerticalPanel();
  private FlexTable stocksFlexTable = new FlexTable();
  private HorizontalPanel addPanel = new HorizontalPanel();
  private TextBox newSymbolTextBox = new TextBox();
  private Button addStockButton = new Button("Add");
  private Label lastUpdatedLabel = new Label();
  private ArrayList<String> stocks = new ArrayList<String>();
    private LoginInfo loginInfo = null;
    private VerticalPanel loginPanel = new VerticalPanel();
    private Label loginLabel = new Label("Please sign in to your Google Account to access the StockWatcher application.");
    private Anchor signInLink = new Anchor("Sign In");
    private final StockServiceAsync stockService = GWT.create(StockService.class);

已检查异常将指示用户尚未登录。这种情况是可能的,因为即使没有当前用户,股票服务也可能会收到 RPC 调用。该类是可序列化的,因此可以通过 AsyncCallback 的 onFailure(Throwable error) 方法通过 RPC 调用返回它。您还可以使用 servlet 过滤器或 Spring 安全性来实现安全性。

NotLoggedInException.java

package com.google.gwt.sample.stockwatcher.client;

import java.io.Serializable;

public class NotLoggedInException extends Exception implements Serializable {

  public NotLoggedInException() {
    super();
  }

  public NotLoggedInException(String message) {
    super(message);
  }

}

Stock 类是使用 JDO 持久化的内容。持久化方式的细节由 JDO 注释决定。尤其是

  • PersistenceCapable 注释告诉 DataNucleus 字节码增强器处理此类。
  • PrimaryKey 注释指定一个用于存储其主键的 id 属性。
  • 在此类中,每个属性都将持久化。但是,您可以使用 NotPersistent 注释将属性指定为不持久化。
  • User 属性是一种特殊的 App Engine 类型,它可以允许您跨电子邮件地址更改识别用户。

Stock.java

package com.google.gwt.sample.stockwatcher.server;

import java.util.Date;
import javax.jdo.annotations.IdGeneratorStrategy;
import javax.jdo.annotations.IdentityType;
import javax.jdo.annotations.PersistenceCapable;
import javax.jdo.annotations.Persistent;
import javax.jdo.annotations.PrimaryKey;
import com.google.appengine.api.users.User;

@PersistenceCapable(identityType = IdentityType.APPLICATION)
public class Stock {

  @PrimaryKey
  @Persistent(valueStrategy = IdGeneratorStrategy.IDENTITY)
  private Long id;
  @Persistent
  private User user;
  @Persistent
  private String symbol;
  @Persistent
  private Date createDate;

  public Stock() {
    this.createDate = new Date();
  }

  public Stock(User user, String symbol) {
    this();
    this.user = user;
    this.symbol = symbol;
  }

  public Long getId() {
    return this.id;
  }

  public User getUser() {
    return this.user;
  }

  public String getSymbol() {
    return this.symbol;
  }

  public Date getCreateDate() {
    return this.createDate;
  }

  public void setUser(User user) {
    this.user = user;
  }

  public void setSymbol(String symbol) {
    this.symbol = symbol;
  }
}

此类实现股票服务,并包含对 JDO API 的调用以持久化股票数据。需要注意的是

  • 记录器记录的日志在您在 App Engine 管理控制台 中检查应用程序时可见。
  • PersistenceManagerFactory 单例是从上面 jdoconfig.xml 中名为“transactions-optional”的属性创建的。
  • 每当我们想要确保用户已登录时,都会调用 checkedLoggedIn 方法。
  • getUser 方法使用 UserService。

StockServiceImpl.java

package com.google.gwt.sample.stockwatcher.server;

import java.util.ArrayList;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.jdo.JDOHelper;
import javax.jdo.PersistenceManager;
import javax.jdo.PersistenceManagerFactory;
import javax.jdo.Query;

import com.google.appengine.api.users.User;
import com.google.appengine.api.users.UserService;
import com.google.appengine.api.users.UserServiceFactory;
import com.google.gwt.sample.stockwatcher.client.NotLoggedInException;
import com.google.gwt.sample.stockwatcher.client.StockService;
import com.google.gwt.user.server.rpc.RemoteServiceServlet;

public class StockServiceImpl extends RemoteServiceServlet implements
StockService {

  private static final Logger LOG = Logger.getLogger(StockServiceImpl.class.getName());
  private static final PersistenceManagerFactory PMF =
      JDOHelper.getPersistenceManagerFactory("transactions-optional");

  public void addStock(String symbol) throws NotLoggedInException {
    checkLoggedIn();
    PersistenceManager pm = getPersistenceManager();
    try {
      pm.makePersistent(new Stock(getUser(), symbol));
    } finally {
      pm.close();
    }
  }

  public void removeStock(String symbol) throws NotLoggedInException {
    checkLoggedIn();
    PersistenceManager pm = getPersistenceManager();
    try {
      long deleteCount = 0;
      Query q = pm.newQuery(Stock.class, "user == u");
      q.declareParameters("com.google.appengine.api.users.User u");
      List<Stock> stocks = (List<Stock>) q.execute(getUser());
      for (Stock stock : stocks) {
        if (symbol.equals(stock.getSymbol())) {
          deleteCount++;
          pm.deletePersistent(stock);
        }
      }
      if (deleteCount != 1) {
        LOG.log(Level.WARNING, "removeStock deleted "+deleteCount+" Stocks");
      }
    } finally {
      pm.close();
    }
  }

  public String[] getStocks() throws NotLoggedInException {
    checkLoggedIn();
    PersistenceManager pm = getPersistenceManager();
    List<String> symbols = new ArrayList<String>();
    try {
      Query q = pm.newQuery(Stock.class, "user == u");
      q.declareParameters("com.google.appengine.api.users.User u");
      q.setOrdering("createDate");
      List<Stock> stocks = (List<Stock>) q.execute(getUser());
      for (Stock stock : stocks) {
        symbols.add(stock.getSymbol());
      }
    } finally {
      pm.close();
    }
    return (String[]) symbols.toArray(new String[0]);
  }

  private void checkLoggedIn() throws NotLoggedInException {
    if (getUser() == null) {
      throw new NotLoggedInException("Not logged in.");
    }
  }

  private User getUser() {
    UserService userService = UserServiceFactory.getUserService();
    return userService.getCurrentUser();
  }

  private PersistenceManager getPersistenceManager() {
    return PMF.getPersistenceManager();
  }
}

现在 GWT RPC 服务已经实现,我们将确保 servlet 容器知道它。映射 /stockwatcher/stock 由 GWT 模块定义 (stockwatcher) 中的 rename-to 属性和 RemoteServiceRelativePath 注释 (stock) 组成。

web.xml

<?xml version="1.0" encoding="UTF-8"?>

<web-app>

  <!-- Default page to serve -->
  <welcome-file-list>
    <welcome-file>StockWatcher.html</welcome-file>
  </welcome-file-list>

  <!-- Servlets -->
  <servlet>
    <servlet-name>loginService</servlet-name>
    <servlet-class>com.google.gwt.sample.stockwatcher.server.LoginServiceImpl</servlet-class>
  </servlet>

  <servlet>
    <servlet-name>stockService</servlet-name>
    <servlet-class>com.google.gwt.sample.stockwatcher.server.StockServiceImpl</servlet-class>
  </servlet>

  <servlet-mapping>
    <servlet-name>loginService</servlet-name>
    <url-pattern>/stockwatcher/login</url-pattern>
  </servlet-mapping>

  <servlet-mapping>
    <servlet-name>stockService</servlet-name>
    <url-pattern>/stockwatcher/stock</url-pattern>
  </servlet-mapping>

</web-app>

更新 StockWatcher UI

检索股票

当 StockWatcher 应用程序加载时,它应该预先填充用户的股票。为了重用显示股票的现有代码,我们将重构 StockWatcher addStock(),以便显示新股票的逻辑位于一个新的 displayStock(String symbol) 方法中。

private void addStock() {
    final String symbol = newSymbolTextBox.getText().toUpperCase().trim();
    newSymbolTextBox.setFocus(true);

    // Stock code must be between 1 and 10 chars that are numbers, letters, or dots.
    if (!symbol.matches("^[0-9a-zA-Z\\.]{1,10}$")) {
      Window.alert("'" + symbol + "' is not a valid symbol.");
      newSymbolTextBox.selectAll();
      return;
    }

    newSymbolTextBox.setText("");

    // Don't add the stock if it's already in the table.
    if (stocks.contains(symbol))
      return;

    displayStock(symbol);
  }

  private void displayStock(final String symbol) {
    // Add the stock to the table.
    int row = stocksFlexTable.getRowCount();
    stocks.add(symbol);
    stocksFlexTable.setText(row, 0, symbol);
    stocksFlexTable.setWidget(row, 2, new Label());
    stocksFlexTable.getCellFormatter().addStyleName(row, 1, "watchListNumericColumn");
    stocksFlexTable.getCellFormatter().addStyleName(row, 2, "watchListNumericColumn");
    stocksFlexTable.getCellFormatter().addStyleName(row, 3, "watchListRemoveColumn");

    // Add a button to remove this stock from the table.
    Button removeStockButton = new Button("x");
    removeStockButton.addStyleDependentName("remove");
    removeStockButton.addClickHandler(new ClickHandler() {
      public void onClick(ClickEvent event) {
        int removedIndex = stocks.indexOf(symbol);
        stocks.remove(removedIndex);
        stocksFlexTable.removeRow(removedIndex + 1);
      }
    });
    stocksFlexTable.setWidget(row, 3, removeStockButton);

    // Get the stock price.
    refreshWatchList();

  }

在设置股票表格后,是加载股票的合适时机。

private void loadStockWatcher() {

  ...

    stocksFlexTable.setCellPadding(5);
    stocksFlexTable.addStyleName("watchList");
    stocksFlexTable.getRowFormatter().addStyleName(0, "watchListHeader");
    stocksFlexTable.getCellFormatter().addStyleName(0, 1, "watchListNumericColumn");
    stocksFlexTable.getCellFormatter().addStyleName(0, 2, "watchListNumericColumn");
    stocksFlexTable.getCellFormatter().addStyleName(0, 3, "watchListRemoveColumn");

    loadStocks();

  ...

  }

loadStocks() 方法调用之前定义的 StockService。RPC 返回一个股票符号数组,这些符号使用 displayStock(String symbol) 方法单独显示。

private void loadStocks() {
    stockService.getStocks(new AsyncCallback<String[]>() {
      public void onFailure(Throwable error) {
      }
      public void onSuccess(String[] symbols) {
        displayStocks(symbols);
      }
    });
  }

  private void displayStocks(String[] symbols) {
    for (String symbol : symbols) {
      displayStock(symbol);
    }
  }

添加股票

我们不会只是在添加股票时显示股票,而是会调用 StockService 将新的股票符号保存到数据存储中。

private void addStock() {
    final String symbol = newSymbolTextBox.getText().toUpperCase().trim();
    newSymbolTextBox.setFocus(true);

    // Stock code must be between 1 and 10 chars that are numbers, letters, or dots.
    if (!symbol.matches("^[0-9a-zA-Z\\.]{1,10}$")) {
      Window.alert("'" + symbol + "' is not a valid symbol.");
      newSymbolTextBox.selectAll();
      return;
    }

    newSymbolTextBox.setText("");

    // Don't add the stock if it's already in the table.
    if (stocks.contains(symbol))
      return;

    addStock(symbol);
  }

  private void addStock(final String symbol) {
    stockService.addStock(symbol, new AsyncCallback<Void>() {
      public void onFailure(Throwable error) {
      }
      public void onSuccess(Void ignore) {
        displayStock(symbol);
      }
    });
  }

  private void displayStock(final String symbol) {
    // Add the stock to the table.
    int row = stocksFlexTable.getRowCount();
    stocks.add(symbol);

...

  }

删除股票

我们不会只是从显示中删除股票,而是会调用 StockService 从数据存储中删除股票符号。

private void displayStock(final String symbol) {

  ...

    // Add a button to remove this stock from the table.
    Button removeStock = new Button("x");
    removeStock.addStyleDependentName("remove");

    removeStock.addClickHandler(new ClickHandler(){
      public void onClick(ClickEvent event) {
        removeStock(symbol);
      }
    });
    stocksFlexTable.setWidget(row, 3, removeStock);

    // Get the stock price.
    refreshWatchList();

  }

  private void removeStock(final String symbol) {
    stockService.removeStock(symbol, new AsyncCallback<Void>() {
      public void onFailure(Throwable error) {
      }
      public void onSuccess(Void ignore) {
        undisplayStock(symbol);
      }
    });
  }

  private void undisplayStock(String symbol) {
    int removedIndex = stocks.indexOf(symbol);
    stocks.remove(removedIndex);
    stocksFlexTable.removeRow(removedIndex+1);
  }

错误处理

当某个 RPC 调用导致错误时,我们希望将消息显示给用户。

此外,请记住,如果用户因某种原因不再登录到他们的 Google 帐户,则 StockService 会抛出 NotLoggedInException。

private void checkLoggedIn() throws NotLoggedInException {
    if (getUser() == null) {
      throw new NotLoggedInException("Not logged in.");
    }
  }

如果我们收到此错误,我们将把用户重定向到注销 URL。

以下是一个用于完成这两个错误处理要求的辅助方法。

private void handleError(Throwable error) {
    Window.alert(error.getMessage());
    if (error instanceof NotLoggedInException) {
      Window.Location.replace(loginInfo.getLogoutUrl());
    }
  }

我们可以将其添加到每个 AsyncCallback 的 onFailure(Throwable error) 方法中。

loginService.login(GWT.getHostPageBaseURL(), new AsyncCallback<LoginInfo>() {
      public void onFailure(Throwable error) {
        handleError(error);
      }

    ...

    }
stockService.getStocks(new AsyncCallback<String[]>() {
      public void onFailure(Throwable error) {
        handleError(error);
      }

    ...

    });
stockService.addStock(symbol, new AsyncCallback<Void>() {
      public void onFailure(Throwable error) {
        handleError(error);
      }

    ...

    });
stockService.removeStock(symbol, new AsyncCallback<Void>() {
      public void onFailure(Throwable error) {
        handleError(error);
      }

    ...

    });

测试数据存储功能

您可以重复上述说明以在 本地App Engine 上运行应用程序。

如果您遇到运行时错误,请检查 App Engine 管理控制台 中的日志。

更多资源

进一步练习

用户现在可以登录 Google 帐户并在运行在 App Engine 上的 StockWatcher 应用程序中管理他们自己的股票列表。以下是一些建议的增强功能,您可以尝试作为练习。

  • 加载股票列表存在明显的延迟。添加一个 UI 元素以指示股票列表正在加载。
  • 向 Stock 类添加更多属性。在添加这些属性之前保存的数据会发生什么变化?
  • StockService 不会检测到一个用户注销而另一个用户登录的情况。您将如何修改应用程序以处理此边缘情况?

了解有关 App Engine 的更多信息

App Engine Java 入门教程 提供了有关构建 App Engine 应用程序的更多详细信息,包括从头开始创建项目、使用 JSP、管理不同的应用程序版本以及有关 Web 应用程序描述符文件的更多详细信息。

App Engine Java 文档 更详细地介绍了用户服务和数据存储服务。特别是,它记录了如何使用 JPA 访问数据存储服务。其他记录的服务包括 Memcache、HTTP 客户端和 Java Mail。App Engine Java 运行时的限制也进行了逐项列出。