Chinaunix首页 | 论坛 | 博客
  • 博客访问: 173672
  • 博文数量: 41
  • 博客积分: 1679
  • 博客等级: 上尉
  • 技术积分: 730
  • 用 户 组: 普通用户
  • 注册时间: 2010-12-24 10:24
文章分类

全部博文(41)

文章存档

2011年(34)

2010年(7)

分类: Python/Ruby

2011-08-29 15:26:51

App Engine的开发模型简单到不能再简单了:
  1. 创建应用程序;
  2. 在你自己的电脑上测试一下这个应用程序(使用App Engine开发包自带的那个Web 服务器软件);
  3. 将做好的应用程序上传到App Engine。
    我们将在本章中详细地了解整个工作流程:新建一个应用程序,利用开发服务器对其进行测试,注册一个新的应用程序ID,设置一个域名,将应用程序上传至App Engine。
    我们将会看一下Python和Java软件开发包(SDK)以及App Engine管理控制台的部分功能。此外,我们还要讨论一下有关应用程序开发和部署方面的工作流程。
    利用这个机会,我们还将介绍Web应用程序的一个经典模式:管理用户偏好数据。这个模式将会用到App Engine的多个服务和功能。

安装SDK

    应用程序开发工作所需的全部工具和库都能在App Engine SDK中找到。Python和Java都有各自专门的SDK,它们都含有许多对开发工作大有裨益的功能。这两个SDK都能用于不同的平台,包括Windows、Mac OS X以及Linux。
    Python和Java的SDK都含有一个能够在本地计算机上运行的Web服务器,其目的是在模拟的运行时环境中运行应用程序。开发服务器能够强制执行完整运行时环境的沙盒约束,还可以模拟所有的App Engine服务。启动这个开发服务器之后,你可以在开发应用程序的同时让它一直运行,这样,只需在浏览器中重新加载页面就可以立刻看到修改后的效果了。
    这两个SDK都自带一个实用工具,专门用于跟那些正在App Engine上运行着的应用程序交互。你可以使用该工具上传应用程序的代码、静态文件以及配置文件。该工具还可以管理数据存储区索引、任务队列以及计划任务等。此外,它还能下载由在线应用程序所产生的日志消息,以便对应用程序的负载和行为进行分析。
    由于首先支持的是Python而不是Java,因此Python SDK中有一些Java SDK所没有的工具。最重要的一点是,Python SDK含有向数据存储区上传数据以及从数据存储区下载数据的工具。这对于制作备份、改变已有数据的结构、离线处理数据等工作而言是很有用的。如果你用的是Java也没有关系,只要稍微花点功夫就可以用上这些基于Python的数据工具了。
    针对Windows和Mac OS X平台的Python SDK中都带有一个“启动器”应用程序,它使你能够通过一个简单的图形界面创建、编辑、测试和上传应用程序。如果再配上一款较好的编程型文本编辑器(比如Windows上的Notepad++或者Mac OS X上的extMate),那么你就可以得到一个非常快速、直观的Python开发体验了。
    对于Java开发人员Google公司提供了一个用于Eclipse集成开发环境的插件,它实现了完整的App Engine开发流程。该插件含有一个用于新建App Engine Java应用程序的模板,还有一个用于在Eclipse调试器中运行应用程序和开发服务器的配置文件。若要将项目部署到App Engine上,你只需单击Eclipse工具条上的一个按钮就可以了。
    此外,这两个SDK中还含有一组支持上述全部功能的跨平台的命令行工具。你可以在命令行中使用这些工具,也可以将它们集成到你自己的开发环境中去,随你高兴。
    我们将先讨论Python SDK,然后在本章后面的“安装Java SDK”一节中介绍Java SDK。如果某节所使用的语言不合你的意,直接跳过就是了。

安装Python SDK
    针对Python运行时环境的App Engine SDK可以运行在任何安装了Python 2.5的电脑上。如果你用的是Mac OS X或Linux,又或者你以前就用过Python,那么你的系统中可能已经安装有Python了。通过在命令行提示符(在Windows中叫做命令行提示符,即Command Prompt,在Mac OS X中叫做终端,即Terminal)中执行下面这条命令即可检查系统中是否已经安装了Python及其版本号:
  1. python -V
(其中V为大写。)如果已经安装了Python,其版本号就会显示出来,如下所示:
  1. Python 2.5.2
    也可以从Python的网站上下载并安装适用于其他平台的Python 2.5:
务必保证你在该网站的“Download”区下载的是2.5版(比如2.5.4)的Python。到编写本书时为止,Python最新的主版本号是3.1,最新的2.x兼容版是2.6。虽然App Engine的Python SDK可以使用Python 2.6,不过在开发的时候还是用跟App Engine一样的版本会比较好,因为这样你就不会被那些莫名其妙的兼容性问题搞得抓耳挠腮了。
注意: App Engine暂时还不支持Python 3。Python 3含有一些新的语言和库特性,而这些特性都不能向下兼容早期的版本。当App Engine添加了Python 3支持时,很可能是在Python 2运行时环境之外再添加一个新的运行时环境。可以通过应用程序配置文件中的一个设置来控制应用程序应该使用哪个运行时环境,也就是说,在新版运行时环境发布之后,你的应用程序仍然可以一如既往地运行。
    可以在GAE的网站上下载到与你的操作系统相匹配的App Engine Python SDK。
    下载并安装适合于你的操作系统的文件:
  • 对于Windows,Python SDK是一个.msi(Microsoft Installer)文件。单击相应的链接即可下载,然后双击文件即可启动安装进程。这将会安装GAE Launcher应用程序,同时向开始菜单中添加一个图标,还会把命令行工具添加到命令行路径中。
  • 对于Mac OS X,Python SDK是一个.dmg(磁盘镜像)文件形式的Mac应用程序。单击相应的链接即可下载,然后双击文件即可安装该磁盘镜像。将GAE Launcher 图标拖拽到你的“Applications”文件夹中。为了安装命令行工具,可以先双击这个图标以启动GAE Launcher,然后在弹出的对话框中允许其创建“symlinks”。
  • 如果你用的是Linux或别的平台,则可以使用.zip文件形式的Python SDK。下载并解压(一般使用unzip命令)以创建一个名为google_appengine的目录。命令行工具都在这个目录中。按需调整你的命令路径即可。
    为了测试一下刚刚安装的这个App Engine Python SDK,在命令提示符那里运行下面这个命令即可:
  1. dev_appserver.py --help
    该命令将会输出帮助信息并退出。如果你看到的是“找不到该命令”之类的消息,检查一下安装程序是否成功完成,以及dev_appserver.py命令的位置信息是否写到了命令路径中。
    Windows的用户们,如果你们在运行这个命令时弹出了一个上面写着“Windows不能打开此文件……要打开此文件,Windows需要知道哪个程序创建了它”的对话框,你们必须告诉Windows用Python去打开这个文件。选中该对话框中的“从列表中选择程序”并单击确定。单击浏览,然后找到你的Python安装目录(比如C:\Python25)。在这个文件夹中选中python并单击打开。选中“始终使用选择的程序打开这种文件”,单击确定。然后就会有一个窗口打开并尝试运行该命令,接着很快就自己关掉了。然后你就可以在命令行提示符那里运行这个命令了。

走马观花看GAE Launcher

    Windows和Mac OS X版的Python SDK都含有一个叫做GAE Launcher(下文中将简称为Launcher)的应用程序。有了Launcher,你就可以在图形界面下创建和管理多个App Engine Python项目了。图2-1展示了在Mac OS X中的Launcher窗口。
图2-1:GAE Launcher for Mac OS X的主窗口,其中选中了一个项目
    单击“File”菜单中的“Create New Application”即可创建一个新项目(也可以单击窗口下方那个加号按钮)。选定你想要把这个项目放到什么位置,然后为其输入一个名字。然后Launcher就会在那个地方创建一个新目录并以项目的名称命名(该目录用于存放该项目的文件),然后再创建一堆初始文件。现在,这个项目就会出现在Launcher主窗口的项目列表中了。
    为了启动开发服务器,首先要选中一个项目,然后单击“Run”按钮。可以通过“Stop”按钮来停止开发服务器。单击“Browse”按钮即可在浏览器中打开运行着的应用程序的主页。“Logs”按钮用于显示应用程序在开发服务器中记录的日志消息。
    “SDK Console”按钮用于打开开发服务器的Web界面,这个界面上有一些用于检视应用程序运行状态的功能,包括查看(模拟的)数据存储区和内存缓存中的内容的工具, 以及一个用于执行Python语句并显示结果的交互式控制台。
    “Edit”按钮用于在默认文本编辑器中打开指定项目的文件。在Mac OS X版中,这对于那些能够打开整个目录中的文件的编辑器(比如TextMate或Emacs)而言是尤为有用的。在Windows版本中,就只能打开app.yaml。
    “Deploy”按钮用于将项目上传到AppEngine。在部署项目之前,你必须先在AppEngine上注册一个应用程序ID,并把这个ID放到应用程序的配置文件中。“Dashboard”按钮用于在浏览器窗口中打开已部署应用程序的App Engine管理控制台。在本章部稍后的分中,我们将会讨论配置文件、注册过程以及管理控制台。
整个App Engine Python SDK(包括命令行工具)都位于Launcher的应用程序目录中。在Windows版中,安装程序会将相应的目录添加到命令路径中,这样你就可以通过命令提示符来运行这些工具了。
    在Mac OS X中,当你第一次启动Launcher时,它会要求创建“symlink”的权限。该操作将会在/usr/local/bin/目录中创建一些指向该应用程序包中的命令行工具的符号链接。有了这些链接,你只需在Terminal提示符中输入命令的名字就可以运行它了。如果之前没有创建symlink,也可以通过GAE Launcher菜单中的“Make Symlinks”来创建。
    你还可以在Launcher中为开发服务器设置命令行参数。做法是这样的:选中应用程序, 然后单击“Edit”菜单中的“Application Settings”,在Extra Flags文本框中输入所需的命令行参数,然后单击“Update”。
注意: Mac OS X版的Launcher会安装Google公司的软件更新功能,它用于检查新版的App Engine SDK。当新版本发布之后,该功能就会向你发出提醒并实施更新。
在更新完毕后,你会发现相关的symlink都变得无效了。重新打开Launcher并按照提示进行操作即可解决此问题。由于symlink的创建操作需要得到你的许可,因此更新不会自动完成此操作。

安装Java SDK
    针对Java运行时环境的App Engine SDK可以运行在任何安装了Java SE Development Kit(JDK)的电脑上。App Engine支持JDK 5和JDK 6。当运行在App Engine上时,Java运行时环境将使用Java 6 JVM。
    如果你还没有安装Java 6 JDK,可以从Sun的网站下载,绝大多数平台都有对应的版本可供下载(Mac用户请看下一节):
    通过在命令提示符中运行如下所示的命令即可测试系统中是否已经安装了Java开发包及其具体的版本号:
  1. javac -version
    如果安装的是Java 6 JDK,该命令将会显示类似于“javac 1.6.0”这样的版本号。如果安装的是Java 5 JDK,则该命令将会显示类似于“javac 1.5.0”这样的版本号。实际的输出值取决于你所安装的具体版本。
    App Engine的Java应用程序会用到Java Enterprise Edition(Java EE)中的接口和功能。App Engine SDK含有Java EE相关功能的具体实现,因此你没必要安装独立的Java EE 实现。
    针对Java的App Engine SDK的安装步骤取决于你是否想要在Eclipse IDE中使用Google公司的插件。我们将分别讨论这两种情况。

Mac OS X上的Java
    如果你用的是Mac OS X,那么Java和JDK就算是已经安装好了。具体的使用方法取决于操作系统的版本,以及你的电脑用的是32位处理器(比如Intel Core Duo)还是64位处理器(比如Intel Core 2 Duo、Intel Xeon)。通过“Apple”菜单中的“About This Mac” 就可以看到你究竟用的是哪种处理器了。
    Mac OS X 10.6 Snow Leopard含有Java 6及其JDK,而且有专门的32位处理器版和64位处理器版。如果是64位处理器,那么默认就是64位的Java 6。如果是32位处理器,那么默认就是32位的Java 6。
    Mac OS X 10.5 Leopard既有Java 5也有Java 6,但默认的是Java 5。这是因为Leopard上的Java 6只能工作于64位处理器。如果你用的就是64位处理器,则可以把64位的Java 6设置为默认的。
    通过Java Preferences实用工具(可以在/Applications/Utilities/中找到)可以修改系统所使用的Java版本。在“Java Applications”列表中,将所需的版本(比如“Java SE 6, 64-bit”) 拖到列表顶部即可。OS X将使用该列表最上面的那个(与系统兼容的)版本。
    如果运行Leopard的是一台32位的Mac机,那就只能用Java 5了。在Java 5版本下,App Engine SDK也是没问题的,而且用Java 5构建的应用程序在App Engine上也能正常运行。使用Java 5时唯一需要注意的就是:你所找到的那些App Engine代码示例所采用的可能会是Java 6。
    如果你正在使用Eclipse,则还必须保证其版本与你的处理器和Java的版本相对应。Eclipse IDE for Java EE Developers分别有对应的32位和64位处理器的版本。
    更多关于Java和Mac OS X的信息,请参见苹果公司的开发人员网站:http://developer.apple.com/java/

安装Java SDK以及GPlugin for Eclipse

    用Java开发App Engine应用程序最简单的方法之一就是使用Eclipse IDE和GPlugin for Eclipse。该插件可用于Eclipse 3.3(Europa)、Eclipse 3.4(Ganymede)以及Eclipse 3.5 (Galileo)。可以从Eclipse网站免费下载到适用于各种平台的Eclipse:
    如果你只打算用Eclipse来进行App Engine开发,下载Eclipse IDE for Java EE Developers软件包即可。该软件包中有许多对开发Web应用程序有帮助的组件,比如Eclipse Web Tools Platform(WTP)包。
可以通过“Preferences”窗口告诉Eclipse使用你已经安装好的JDK。在Eclipse 3.5中,打开“Preferences”(如果是Windows或Linux,该项在Window菜单中;如果是Mac OS X,该项则在Eclipse菜单中)。在Java大类中,选中“Installed JREs”。如果需要,则可以把SDK的位置添加到列表中,并确保复选框是选中的。
    使用Eclipse的软件安装功能即可安装App Engine Java SDK和GPlugin。在Eclipse 3.5中,单击“Help”菜单中的“New Software...”,然后在“Work with”文本框中输入URL并单击“Add...”按钮(这个URL在浏览器中是无效的,它只对Eclipse的软件安装器起作用,具体的URL参见使用说明)。
    在弹出的对话框窗口的“Name”文本框中输入Google公司的英文LOGO,并单击“OK”。然后将会有两个条目被添加到列表中,一个针对插件(Plugin),另一个则针对AppEngine和GWeb Toolkit SDK(SDK)。图2-2展示了Install Software窗口的样子,其中选中了相应的项。

图2-2:Eclipse 3.5(Galileo)的“Install Software”窗口,其中选中了GPlugin
    选中这两项前面的复选框。单击“Next >”按钮并按照提示进行操作。
    更多有关安装GPlugin for Eclipse的信息(包括针对Eclipse 3.3或3.4的指引信息),请参阅该插件的网站。
    安装完成之后,Eclipse的工具栏中将会出现三个新按钮,如图2-3所示。

图2-3:在安装完GPlugin 之后,Ecl ipse 3.5的工具栏中将会出现三个新按钮:New Web Application Project、GWT Compile Project以及Deploy App Engine Project
    该插件将会向Eclipse的界面中添加一些新东西:
  • 工具栏中的三个新按钮:New Web Application Project、GWT Compile Project以及Deploy App Engine Project;
  • 在“File”菜单的“New”下面会多出一个“Web Application Project”;
  • Web应用程序调试配置文件。有了它,就能通过Eclipse调试器在开发Web服务器中运行应用程序了。
你可以利用Eclipse来开发应用程序,并将其部署到App Engine上去。为了使用App Engine SDK的其他功能(如下载日志数据),你必须使用其中的命令行工具。Eclipse会将这个SDK安装到你的Eclipse应用程序目录中(在eclipse/plugins/中)。实际的目录名取决于你所安装的SDK的版本,不过看上去跟下面这个长得差不多:
com.google.appengine.eclipse.sdkbundle_1.2.5.v200909021031/appengine-java-sdk-1.2.5/
该目录含有一个叫做bin/的子目录,用于存放命令行工具。在Mac OS X或Linux中,为了能在命令行中使用这些工具,你可能需要将这些文件的权限修改为可执行:
chmod 755 bin/*
虽然可以把bin/目录添加到命令行路径中,不过请时刻记住:每当你更新了SDK之后,这个路径就会发生变化。

单独安装Java SDK

如果你没有使用Eclipse IDE,又或者你不愿意使用GPlugin,则可以从App Engine网站下载.z i p文件形式的App Engine Java SDK。该文件会解压成一个目录,其名字类似于appengine-java-sdk-1.2.5。
这个SDK的bin/子目录中含有命令行启动脚本。为了在使用这些命令时能够更加轻松,你可以将该目录添加到命令行路径中。
注意: AppCfg和开发服务器都是通过执行Java类的方式来实现其功能的。在将这些工具集成到IDE中或是创建脚本时,既可以调用启动脚本,也可以直接调用Java类。看启动脚本的内容就能知道具体的语法了。
这个App Engine SDK中还有一个Apache Ant插件,该插件使你能够在Ant的构建脚本(build script)中执行App Engine SDK功能。更多有关Ant和App Engine配合使用方面的信息,请参阅App Engine文档。
    在命令行提示符中运行下面这条命令即可检查App Engine Java SDK是否已经成功安装:
dev_appserver --help
    Mac OS X和Linux的用户请使用dev_appserver.sh作为命令名。
    该命令先显示一些信息,然后退出。如果出现了“该命令找不到”之类的消息,则应该首先检查一下文件是否解压成功,然后再看看SDK的bin/目录是否在你的命令行路径中。

开发应用程序
    App Engine应用程序会响应Web请求。它是通过调用请求处理器(request handler,这是一段接受请求参数并返回响应的程序)来完成此工作的。对于来自某个URL的请求,App Engine会判断应该使用哪个请求处理器,其依据是应用程序中的一个配置文件(其中有URL和请求处理器之间的映射关系)。
    应用程序也可以包含静态文件,比如图片、CSS样式表以及浏览器JavaScript等。对于来自相应URL的请求,App Engine会在其响应中直接将这些文件发送给客户端,无须调用任何代码。应用程序的配置文件会指出哪些文件是静态的,以及哪些URL是用来访问这些文件的。
    应用程序的配置文件中还会包含有关该应用程序的元数据,比如应用程序ID和版本号等。在将应用程序部署到App Engine时,所有的应用程序文件(包括代码、配置文件和静态文件)都会被上传,并与配置文件中所提到的应用程序ID和版本号关联起来。应用程序还可以拥有专门针对服务(比如数据存储区索引、任务队列以及计划任务等)的配置文件。这些文件是与整个应用程序关联的,而不是与某个特定版本关联的。
    虽然Python应用程序跟Java应用程序的代码和配置文件的结构、格式均不同,不过其概念是类似的。在接下来的几节中,我们将用Python和Java分别为一个简单应用程序创建一些必要的文件,并看看如何使用这两个SDK中的工具和库。

用户偏好模式
    我们将在本节中创建的应用程序是一个简单的时钟。当用户访问该网站时,应用程序将根据服务器的系统时钟显示当前时间。默认情况下,应用程序将显示协调世界时间(Coordinated Universal Time,UTC)时区的当前时间。用户通过GAccount登录并设置一个偏好之后即可实现对时区的自定义。
    该应用程序演示了App Engine的三个功能:
  • 数据存储区。它是用户配置数据的主要存储区,是持久的、可靠的、可扩展的。
  • 内存缓存(也就是memcache)。它是次要存储区,比数据存储区快,但不能长期保持数据。
  • GAccount。利用Google公司的用户账户系统对用户进行鉴权和识别。
    GAccount的工作方式跟大多数用户账户系统类似。如果用户没有登录时钟应用程序,则只会看到一个带有默认设置(UTC时区)的普通界面,当然,该界面上面还有登录或创建新账户的链接。如果用户选择登录或注册,应用程序就会将其转向至一个由GAccount管理的登录表单。登录或创建账户之后,用户又会被带回到应用程序。
    当然,你也可以不用GAccount而自己实现一个账户机制。使用GAccount有利有弊,最主要的好处就是你不用亲自去实现账户机制。如果你的某个用户已经有了一个GAccount,那么他就可以直接用这个账户登录了,而不用再专门为你的应用程序去创建一个新的。
    如果用户在访问应用程序的时候已经登录过了,应用程序就会加载该用户的偏好数据并用它来呈现页面。应用程序获取偏好数据的步骤有两个。首先,它将尝试从快速的次要存储区(即内存缓存)中获取数据。如果数据不在内存缓存中,应用程序就会尝试从主要存储区(数据存储区)中获取数据。如果成功,应用程序就会把找到的数据放入内存缓存中以便今后使用。
    也就是说,对于大多数请求而言,应用程序都能从memcache中得到用户的偏好数据,而无需访问数据存储区。虽然从数据存储区中读取数据已经够快的了,不过从memcache中读取数据还要比这快得多。如果用户每次在访问页面时都必须读取相同数据,这个差别就比较大了。
    我们的时钟应用程序有两个请求处理器。一个用于显示当前时间,以及登录和注销的链接。它还将显示一个能让登录用户调整时区的Web表单。另一个请求处理器用于处理被提交的时区表单。用户在提交了偏好表单之后,应用程序会保存修改并将浏览器转回到主页面。
    应用程序通过它所在的服务器的系统时钟获取当前时间。有一点需要注意,App Engine不保证它的所有Web服务器的系统时钟是同步的。由于本应用程序的任何两个请求都可能由不同的服务器处理,因此不同的请求可能会有不同的时钟。服务器时钟虽然不像真实应用程序的时间数据源那么和谐,不过对本例而言还是足够了。
    在下一节中,我们将使用Python来实现这个应用程序。在“开发一个Java应用程序” 一节中,我们会用Java再做一次。跟之前一样,如果某节的内容不适合你,直接跳过就是了。

开发一个Python应用程序
    为App Engine开发的最简单的Python应用程序只有一个目录,其中有两个文件:一个叫做app.yaml的配置文件,以及一个实现了请求处理器的Python代码文件。含有app.yaml文件的目录是应用程序的根目录。在使用工具的时候,你将会经常用到这个目录。
注意: 如果使用Launcher,则可以通过“File”菜单中的“Create New Application....”来新建项目。Launcher会创建一个新项目以及几个文件,随着对这个示例应用程序逐步深入的讲解,我们将会编辑所有这些文件。另外,你也可以手工创建项目目录和那些文件,然后通过“File”菜单中的“Add Existing Application....”将其添加到Launcher中。
    创建一个名为clock的目录以存放本项目。使用你最喜欢的文本编辑器在这个目录中创建一个名为app.yaml的文件,其内容如示例2-1所示。
    示例2-1:这个简单应用程序的app.yaml配置文件
  1. application: clock
  2. version: 1
  3. runtime: python
  4. api_version: 1
  5. handlers:
  6. - url: /.*
  7. script: main.py
    该配置文件的格式叫做YAML,这是一种用于配置文件和网络消息的开放格式。目前,你还不需要了解关于此格式的更多知识,只要能看明白这个配置文件就行了。
    在本例中,该配置文件将告诉App Engine:这是一个版本号为1的名叫clock的应用程序,它用的是版本号为1的Python运行时环境(API version),该应用程序的每个请求(所有URL都匹配正则表达式/.*)都由名为main.py的Python脚本进行处理。
    在app.yaml的同一个目录中,再创建一个名为main.py的文件,其内容如示例2-2所示。
示例2-2:一个简单的Python请求处理器脚本
  1. import datetime
  2. print 'Content-Type: text/html'
  3. print ''
  4. print '

    The time is: %s

    '
    % str(datetime.datetime.now())
    这个简单的Python程序从Python标准库中引入了datetime模块,输出一个声明了文档类型(HTML)的HTTP头,然后再输出一个包含当前时间(根据Web服务器的时钟)的消息。
    Python请求处理器使用CGI协议与App Engine进行通信。当App Engine接收到来自Python应用程序的请求时,会根据该请求的数据在环境变量中建立一个运行时环境,根据配置文件判断应该运行哪段处理器脚本,然后再根据请求体(如果有)在标准输入流上运行该脚本。处理器脚本用于执行请求所需的全部动作,然后把响应(包括一个有效的HTTP头)输出到标准输出流。这个简单示例会忽略掉请求数据,输出一个声明了响应数据的类型的头,然后再输出一段带有当前时间的消息。
    现在,我们来测试一下刚才所做的这个应用程序。运行dev_appserver.py命令以打开开发服务器,注意把项目目录(clock)的路径作为参数:
dev_appserver.py clock
注意: 如果当前工作目录就是刚才所创建的那个clock目录,则在运行该命令时可以使用一个点(.)作为项目的路径:
  1. dev_appserver.py .
    服务器启动之后会在控制台输出一些消息。如果这是你第一次在命令行中运行该服务器,它可能会问你要不要检查更新,输入你的答案,然后按Enter键。不用理会“Could not read datastore data”和“Could not initialize images API”之类的警告。如果你按照之前所讲的那些安装步骤安装,这些警告都是正常的。最后一条消息应该是这样的:
  1. INFO ... Running application clock on port 8080: http://localhost:8080
    这条消息说明服务器启动成功了。如果没看到这条消息,则可以在其他消息里面看看有没有什么提示,还有就是再看看app.yaml文件中的语法是不是对的。
    通过在Web浏览器中访问服务器的URL可测试你的应用程序:
    浏览器将会显示出与图2-4类似的页面。

图2-4:第1版的时钟应用程序在浏览器中的样子
    在开发应用程序的时候,你可以一直把这个Web服务器开着。它会一直盯着那些文件,在你对文件作出修改之后,它就会根据需要自动重新加载修改后的文件。
注意:在Launcher中,你可以通过单击“Run”按钮的方式来启动开发服务器。当服务器成功启动之后,项目旁边的那个图标就会变绿。单击“Browse”按钮即可在浏览器中打开这个项目。
webapp框架简介
    示例2-2中的代码试图直接实现CGI协议,不过在真实的应用程序中禁止这样做。Python标准库中就有这样的模块(还有名字就叫cgi的),它们不仅实现了CGI协议,而且还能完成一些其他的Web应用程序任务。这些实现不仅是完整的、快速的,而且是彻底测试过的,所以一般来说,直接使用这些模块要比自己重新做一个好。
    Web应用程序框架不仅仅是一堆模块库那么简单,它通过一组和谐的工具、组件和模式为我们提供了最好的Web应用程序开发实践:数据建模接口、模板系统、请求处理机制、项目管理工具以及开发环境等,这些东西组合到一起就能降低原本需要由你亲自编写和维护的代码量了。用Python编写的Web框架有很多,其中不少已经很成熟了,不仅有着漂亮的文档,而且还有人气旺盛的开发人员社区。Django、web2py和Pylons就是这类Python Web框架中的优秀典范。
    并不是每个Python Web框架都完全适用于App Engine的Python运行时环境。App Engine的沙盒原则所强制要求的那些约束(含有C代码的模块被约束得尤为厉害)限制了那些需要使用沙盒外部功能的框架。目前已知的能够良好运行的Web框架是Django(),其他的都需要额外软件的配合才行。我们将在第14章讨论如何在App Engine中使用Django。
    为了方便大家学习,App Engine还提供了一个名为“webapp”的简单框架。webapp框架小巧且易于使用。它虽然没有大型框架中那么完备的功能,不过对小项目而言已经是足够好的了。如果你是通过Launcher创建出这个clock项目,则可以发现那些初始文件所用的就是webapp框架。
    为了简单起见,本书中大部分的Python示例都将使用webapp框架。虽然我们不会在本书中详细介绍webapp框架,但是会介绍它的一些功能。对于更大的应用程序,你可能会希望使用像Django这样的拥有更多功能的框架。
    让我们用webapp框架来更新一下这个时钟应用程序。根据示例2-3的内容,对main.py做一些修改。在浏览器中重新加载该页面即可看到新版本的运行效果了。(除时间被更新了之外,你将看不到有其他任何区别。这个示例跟前一个版本是等效的。)
示例2-3:一个简单使用了webapp框架的请求处理器
  1. from google.appengine.ext import webapp
  2. from google.appengine.ext.webapp.util import run_wsgi_app
  3. import datetime
  4. class MainPage(webapp.RequestHandler):
  5.     def get(self):
  6.         time = datetime.datetime.now()

  7.         self.response.headers['Content-Type'] = 'text/html'
  8.         self.response.out.write('

    The time is: %s

    '
    % str(time))
  9. application = webapp.WSGIApplication([('/', MainPage)],
  10.                         debug=True)

  11. def main():
  12.      run_wsgi_app(application)

  13. if __name__ == '__main__':
  14.      main()
示例2-3引入了模块google.appengine.ext.webapp,然后定义了一个叫做MainPage的请求处理器类(它是webapp.RequestHandler的一个子类)。该类将为请求处理器所支持的每个HTTP方法定义单独的方法,在本例中,就只有一个针对HTTP GET的叫做get()的方法。当应用程序在处理请求时,首先将处理器类实例化,并设置self.request和self.response,然后再调用适当的处理器方法(本例中就是get())。当处理器方法退出之后,应用程序会将self.response的值作为HTTP响应。
应用程序本身是由webapp.WSGIApplication类的一个实例来表示的。其初始化过程依赖于一组映射信息,即处理器类与URL之间的映射关系。如果应用程序是在开发服务器上运行,那么debug参数的作用是:告诉应用程序当请求处理器返回异常时是否要在浏览器窗口中输出一条出错信息。webapp会自动侦测它现在到底是在开发服务器上运行还是在App Engine上运行。如果是在App Engine上运行,就算debug值为True,也不会把出错信息输出到浏览器上。若将debug设置为False,即可在开发服务器上模拟应用程序在App Engine上出错的情况。
这段脚本中还定义了一个main()函数,它通过一个实用工具方法来运行应用程序。最后,这段脚本用了一条固定形式的Python语句if__name__ == '__main__':...来实现对main()的调用;只要是由Web服务器来运行这段脚本,则该条件就永远为真。这条语句使你可以将这段脚本(包括其中定义的类和函数)作为一个模块引入别的代码中,那时就不会执行main()了。
注意: 用这种方式定义main()函数,App Engine就会将已编译的处理器脚本缓存起来,这样,后续请求执行起来就会快很多了。更多有关应用程序缓存的知识,请参见第3章。
单个WSGIApplication实例可以处理多个URL,它会根据URL的模式把不同的请求路由到不同的RequestHandler类。但是我们之前所看到的那个app.yaml文件已经把URL模式跟处理器脚本映射起来了。那么,究竟URL模式该被放到app.yaml文件中呢,还是该被放到WSGIApplication中呢?许多Web框架都有自己的一套URL调度逻辑,而且通常都会将所有的动态URL路由给它自己的调度器(app.yaml)。对webapp而言,主要还是看你想如何组织你的代码。在这个时钟应用程序中,我们还将在一个单独的脚本文件中创建第二个请求处理器,目的是利用app.yaml的用户验证功能。当然,你也可以将这个逻辑写到main.py中,也就是说,用WSGIApplication对象来路由URL。
用户和GAccount
到目前为止,我们的时钟对每个用户显示的都是一样的东西。为了使用户能够自定义显示结果(还要保存其偏好数据以便今后使用),我们需要找到一个能够识别出发起请求的用户的办法。最简单的方案之一就是GAccount。
接下来,我们给这个页面加上点能够判断“用户是否登录”的东西,再加入一个用于“登入和登出应用程序”的链接。按照示例2-4的方式编辑main.py。
示例2-4:能够显示GAccount信息和相关链接的main.py
  1. from google.appengine.api import users
  2. from google.appengine.ext import webapp
  3. from google.appengine.ext.webapp.util import run_wsgi_app
  4. import datetime

  5. class MainPage(webapp.RequestHandler):
  6.      def get(self):
  7.          time = datetime.datetime.now()
  8.          user = users.get_current_user()
  9.          if not user:
  10.              navbar = ('

    Welcome! Sign in or register to customize.

    '%
    (users.create_login_url(self.request.path)))
  11.         else:
  12.             navbar = ('

    Welcome, %s! You can sign out.

    '%
    (user.email(), users.create_logout_url(self.request.path)))

  13.         self.response.headers['Content-Type'] = 'text/html'
  14.         self.response.out.write('''
  15.        
  16.            
  17.                 The Time Is...
  18.            
  19.            
  20.             %s
  21.                

    The time is: %s


  22.            
  23.        
  24.         ''' % (navbar, str(time)))
  25. application = webapp.WSGIApplication([('/', MainPage)],debug=True)

  26. def main():
  27.     run_wsgi_app(application)

  28. if __name__ == '__main__':
  29.     main()
在真实的应用程序中,你应该用一个模板系统来进行输出,把HTML和显示逻辑与应用程序代码分开。许多Web应用程序框架都有一个模板系统,不过webapp没有。由于这个时钟应用程序只有一个页面,所以我们就直接把HTML放到请求处理器代码中(用Python的字符串格式化来组织各种信息)。
注意: Python运行时环境中带有Django,其模板系统可以用于webapp。当Google公司发布第1版Python
运行时环境时,最新的Django是0.96版,所以运行时环境中所包含的就是这个版本了。更多有关如何在webapp中使用Django模板的知识,请参阅App Engine的文档。
在浏览器中重新加载该页面。新页面如图2-5所示。

图2-5:当用户没有登录时,时钟应用程序会显示一个指向GAccount的链接
用于GAccount的Python API由google.appengine.api.users模块提供。如果用户没有登录,则该模块中的get_current_user()函数将返回None;否则将返回一个带有用户账户信息的User类实例。User对象的email()方法用于返回该用户的电子邮件地址。
create_login_url()和create_logout_url()方法用于生成指向GAccount的URL。这两个方法都以该应用程序的URL路径为参数,在执行完相关任务之后用户就会被带回到那个URL上去。登录URL所指向的GAccount页面可以让用户登录或注册一个新账户。注销URL则访问GAccount以注销当前用户,并立即转回指定的应用程序URL。
当应用程序运行于开发服务器时,单击链接“Sign in or register”将会打开GAccount登录界面的开发服务器模拟版,如图2-6所示。在这个界面上,不管你输入什么电子邮件地址,开发服务器都会接受,就好像你真的有这么一个账户一样。

图2-6:开发服务器所模拟出来的GAccount登录界面
当应用程序运行在App Engine上时,登录和注销URL将会指向真正的GAccount地址。登录或注销之后,GAccount将立刻转向在线应用程序所指定的URL。
单击“Sign in or register”,然后在模拟的GAccount页面上单击“Login”按钮,这时使用的是默认的测试电子邮件地址(test@example.com)。时钟应用程序现在看上去如图2-7所示。单击“sign out”链接即可注销。

Web表单和数据存储区
现在我们已经知道用户是谁了,接下来就可以问问她喜欢哪个时区,并保存她的偏好以便她在下次访问时可以使用。
首先,为了今后的请求能够访问到用户的偏好数据,我们需要先找到一个能够记住这些数据的办法。App Engine的数据存储区是一种可靠且可扩展的存储手段,刚好满足我们的要求。Python API中有一个能够把Python对象跟数据存储区实体映射起来的数据建模接口。我们可以用它来编写一个UserPrefs类。
创建一个名为models.py的文件,其内容如示例2-5所示。

图2-7:用户已经登录了这个时钟应用程序
示例2-5:models.py文件中有一个用于将用户偏好存放到数据存储区的类
  1. from google.appengine.api import users
  2. from google.appengine.ext import db

  3. class UserPrefs(db.Model):
  4.      tz_offset = db.IntegerProperty(default=0)
  5.      user = db.UserProperty(auto_current_user_add=True)

  6. def get_userprefs(user_id=None):
  7.      if not user_id:
  8.           user = users.get_current_user()
  9.           if not user:
  10.                return None
  11.           user_id = user.user_id()

  12.      key = db.Key.from_path('UserPrefs', user_id)
  13.      userprefs = db.get(key)
  14.      if not userprefs:
  15.           userprefs = UserPrefs(key_name=user_id)
  16.      return userprefs
Python的数据建模接口由google.appengine.ext.db模块提供。数据模型其实就是基类为db.Model的一个类。模型子类通过类属性定义出每个对象的数据结构。当值被赋给实例属性时,db.Model将会强制执行这个结构。对于UserPrefs而言,我们定义了两个属性:tz_offset(这是个整型值)以及user(由GAccount API返回的一个User对象)。
每个数据存储区实体都有一个主键。跟关系型数据库中的表主键不同,实体键是固定不变的,只能在实体创建时设置其值。系统中所有实体的键都是唯一的,由多个部分组成,其中包括实体的类别(本例中是'UserPrefs')。从这个API中可以看出,应用程序可以将键的键名(key name)部分设置为任意值。
时钟应用程序以用户的唯一ID(由User对象的user_id()方法提供)作为UserPrefs实体的键名。这就使得应用程序可以根据键来获取实体了,因为它可以从GAccount API中获知用户的ID。通过键来获取实体的方式要比执行数据存储区查询的方式快一些。
在models.py中,我们定义了一个名为get_userprefs()的函数,它用于获取指定用户的UserPrefs对象。在得到用户ID之后,该函数将通过类别'UserPrefs'和键名(等于用户ID)构造出实体的数据存储区键。如果实体在数据存储区中,则该函数就会返回UserPrefs对象。
如果实体不在数据存储区中,则该函数将使用默认设置以及对应于该用户的键名新建一个UserPrefs对象。新对象不会被自动保存到数据存储区。调用者必须调用UserPrefs实例的put()方法才能将其保存。
现在我们已经有了一个用于获取UserPrefs对象的机制,接下来对主页做两个修改。如果用户登录了,我们就可以获取其偏好数据(如果有)并调整时钟的时区。下面,我们来给主页添加一个能让用户设置时区偏好的Web表单。如示例2-6所示修改main.py即可实现这些功能。
示例2-6:新版main.py,它可以根据用户的时区调整时钟,还可以显示一个偏好表单
  1. from google.appengine.api import users
  2. from google.appengine.ext import webapp
  3. from google.appengine.ext.webapp.util import run_wsgi_app
  4. import datetime
  5. import models

  6. class MainPage(webapp.RequestHandler):
  7.     def get(self):
  8.         time = datetime.datetime.now()
  9.         user = users.get_current_user()
  10. if not user:
  11.     navbar = ('

    Welcome! Sign in or register to customize.

    '

  12.             % (users.create_login_url(self.request.path)))
  13.     tz_form = ''
  14. else:
  15.     userprefs = models.get_userprefs()
  16.     navbar = ('

    Welcome, %s! You can sign out.

    '

  17.             % (user.nickname(), users.create_logout_url(self.request.path)))
  18.             tz_form = '''
  19.                

  20.                    
  21.                         Timezone offset from UTC (can be negative):
  22.                    
  23.                    
  24.                         size="4" value="%d" />
  25.                    
  26.                
  27.             ''' % userprefs.tz_offset
  28.             time += datetime.timedelta(0, 0, 0, 0, 0, userprefs.tz_offset)
  29.         self.response.headers['Content-Type'] = 'text/html'
  30.         self.response.out.write('''
  31.        
  32.            
  33.                 The Time Is...
  34.            
  35.            
  36.             %s
  37.                

    The time is: %s


  38.             %s
  39.            
  40.        
  41.         ''' % (navbar, str(time), tz_form))

  42. application = webapp.WSGIApplication([('/', MainPage)],debug=True)
  43. def main():
  44. run_wsgi_app(application)
  45. if __name__ == '__main__':
  46. main()
为了实现偏好表单,我们还需要另外做一个专门的请求处理器来解析表单数据和更新数据存储区。我们在一个新的脚本文件中实现这个请求处理器。创建一个名为prefs.py的文件,其内容如示例2-7所示。
示例2-7:这是一个新的处理器脚本(prefs.py),它用于处理偏好表单
  1. from google.appengine.ext import webapp
  2. from google.appengine.ext.webapp.util import run_wsgi_app
  3. import models

  4. class PrefsPage(webapp.RequestHandler):
  5.      def post(self):
  6.           userprefs = models.get_userprefs()
  7.           try:
  8.                tz_offset = int(self.request.get('tz_offset'))
  9.                userprefs.tz_offset = tz_offset
  10.                userprefs.put()
  11.           except ValueError:
  12.                # User entered a value that wasn't an integer. Ignore for now.
  13.                pass

  14.           self.redirect('/')

  15. application = webapp.WSGIApplication([('/prefs', PrefsPage)], debug=True)

  16. def main():
  17.      run_wsgi_app(application)

  18. if __name__ == '__main__
这个请求处理器将会处理URL/prefs上的HTTP POST请求,也就是这个表单所使用的URL(action)和HTTP方法。处理器调用models.py中的get_userprefs()函数以获取当前用户的UserPrefs对象,这可能是一个新的由默认值填充的未保存的对象,也可能是一个已经存在的实体对象。处理器会将表单数据中的tz_offset参数转换成一个整型值,设置UserPrefs对象的属性,然后调用该对象的put()方法将其保存到数据存储区。如果对象不在数据存储区中,则put()方法将会创建一个新的,否则就更新掉现有的。
如果用户在那个表单文本框中输入的不是整数,那我们就什么也不做。若返回一个出错消息会更好一些,不过为了保持整个示例的简单性,这里就先不管了。
最后,为了将这个请求处理器跟URL /prefs映射关联起来,需要修改一下app.yaml中的handlers:节,如示例2-8所示。
示例2-8:新版的app.yaml,它加上了URL /prefs的映射(还带登录验证)
  1. application: clock
  2. version: 1
  3. runtime: python
  4. api_version: 1

  5. handlers:
  6. - url: /prefs
  7.   script: prefs.py
  8.   login: required

  9. - url: /.*
  10.   script: main.py
login:required这行的意思是说,用户在访问/prefs这个URL的时候必须先登录GAccount。如果用户在没有登录的情况下访问该URL,App Engine会自动将该用户指引到GAccount的登录页面,在完成登录之后再转回到这个URL。这就使我们可以轻松地为站点的某些部分单独添加登录验证了,即在请求处理器被调用之前先验证用户是否已经登录。
务必要把/prefs这个URL的映射信息放到/.*映射信息的前面。URL模式的匹配尝试是按顺序进行的,第一个匹配成功的请求处理器将被用于处理当前请求。由于/.*这个模式会匹配所有的URL,因此/prefs必须在它前面,不然就会被忽略掉。
重新加载页面,时钟应用程序现在是可定制的了。修改一下时区并提交表单。注销一下,然后用同一个电子邮件地址重新登录一次,之后再用别的电子邮件地址登录上去看看。这个应用程序会记住每个用户的时区偏好。

用memcache实现缓存
示例2-5中的代码用于获取用户的偏好数据,在每个登录用户(每次)访问站点的时候,它都会从数据存储区中获取一个实体。由于用户的偏好数据经常被读取而很少被修改,因此处理每个请求时都去数据存储区里取一次UserPrefs就显得有点资源浪费了。只需增加一个缓存层,我们就能降低从主存储区读取数据的成本。
我们可以用memcache服务作为用户偏好数据的次要存储区。只需对models.py做一点点修改就能加上缓存功能。如示例2-9所示修改该文件。
示例2-9:新版的models.py,它能将UserPrefs对象缓存到memcache中
  1. from google.appengine.api import memcache
  2. from google.appengine.api import users
  3. from google.appengine.ext import db

  4. class UserPrefs(db.Model):
  5.      tz_offset = db.IntegerProperty(default=0)
  6.      user = db.UserProperty(auto_current_user_add=True)

  7.      def cache_set(self):
  8.           memcache.set(self.key().name(), self, namespace=self.key().kind())

  9.      def put(self):
  10.           self.cache_set()
  11.           db.Model.put(self)

  12. def get_userprefs(user_id=None):
  13.      if not user_id:
  14.           user = users.get_current_user()
  15.           if not user:
  16.                return None
  17.           user_id = user.user_id()

  18.      userprefs = memcache.get(user_id, namespace='UserPrefs')
  19.      if not userprefs:
  20.           key = db.Key.from_path('UserPrefs', user_id)
  21.           userprefs = db.get(key)
  22.      if userprefs:
  23.           userprefs.cache_set()
  24.      else:
  25.           userprefs = UserPrefs(key_name=user_id)

  26. return userprefs
用于memcache服务的Python API由google.appengine.api.memcache模块提供。memcache存储的是键值对,另外在键上还可以有一个命名空间。值可以是任何能够通过Python中的pickle模块与扁平数据互相转换(序列化)的数据类型:其实,大部分数据对象都是这样的类型。
新的UserPrefs类重写了put()方法。当某个实体上的put()方法被调用时,该实体先是被保存到memcache中,然后再被原来的put()方法保存到数据存储区中。(db.Model.put(self)是Python中调用被重写的基类方法的一种手段。)
UserPrefs类中还有一个新的名叫cache_set()的方法,它的作用是调用memcache.set()。memcache.set()接受一个键、一个值,以及一个可选的键命名空间。这里,我们把实体的键名用作键,整个对象(self)作为值,实体的类别(UserPrefs)作为命名空间。API将会负责对UserPrefs对象进行序列化,也就是说,我们可以直接保存和获取完整的对象。
新版get_userprefs()会在检查数据存储区之前先在memcache中看看有没有想要的UserPrefs对象。如果在缓存中找到了的话,就用它。如果没找到,就去数据存储区中看看,如果在那儿找到了的话,就把它保存到缓存中之后再用。如果对象既不在memcache中也不在数据存储区中的话,get_userprefs()就会用默认值构造一个新的UserPrefs对象并将其返回。
重新加载一下页面,看看新版的效果怎么样。为了让缓存表现得更明显一点,你可以在适当的位置加上一些日志语句,比如:
  1. import logging

  2. class UserPrefs(db.Model):
  3.     # ...
  4.     def cache_set(self):
  5.         logging.info('cache set')
  6.         # ...
开发服务器会在控制台显示日志输出。如果使用Launcher,则只要单击Logs按钮就能打开一个专门显示开发服务器输出信息的窗口了。
接下来,我们将看看同一个例子用Java运行时环境该怎么弄。如果你对Java不感兴趣,可以直接跳到本章“注册应用程序”。

开发一个Java应用程序
App Engine上的Java Web应用程序通过Java Servlet标准接口与应用程序服务器交互。一个应用程序由一个或多个类组成,这些类都扩展自一个servlet基类。servlet是通过一个标准配置文件(叫做“部署描述符”)与URL映射起来的。当App Engine接收到一个请求时,首先会判断该使用哪个servlet类(根据URL和部署描述符),然后初始化该类,并调用这个servlet对象上的某个方法。
Java应用程序的所有文件(包括已编译的Java类、配置文件以及静态文件)都是以一种叫做Web应用程序档案(Web Application Archive,WAR)的标准目录结构组织起来的。
WAR目录中的所有东西都会被部署到App Engine上去。WAR中的内容通常是在开发过程中由一组源文件生成的,你可以使用自动构建过程,也可以使用支持WAR的开发工具。
如果使用的是Eclipse IDE和GPlugin,则可以通过Web Application向导来创建新项目。在“File”菜单中,单击“New”,然后单击“Web Application Project”。在打开的窗口中,输入项目名(如Clock)和包名(如clock)。
取消“Use GWeb Toolkit”复选框的选中状态,并确保“Use GAE”复选框是选中的。
( 如果选中了“GWT” 复选框, 则新项目在创建时会有一些初始文件。) 单击“Finish”即可创建该项目。
如果没有使用GPlugin for Eclipse,就要使用另一种办法来创建这些目录和文件了。如果对Java的Web开发已经很熟悉了,可以用你现有的工具和工序来生成最终的WAR。在本节余下的内容中,我们将假设该目录结构已经由GPlugin for Eclipse创建好了。
图2-8展示了该项目的文件结构,跟在Eclipse Package Explorer中显示的一样。

图2-8:一个新Java项目的结构,跟Eclipse Package Explorer中所显示的一样该项目的根目录(Clock)含有两个主要的子目录:src和war。src/目录所包含的是该项目全部的类文件,这是常见的Java包结构形式。根据clock的包路径,Eclipse将会在文件clock/ClockServlet.java中创建一个名为ClockServlet的servlet类。
war/目录所包含的是该应用程序完整的最终内容。Eclipse会自动编译src/中的源代码,
并将编译后的类文件保存到war/WEB-INF/classes/中(默认情况下,该目录在Eclipse的Package Explorer中是不可见的)。Eclipse还会自动地将src/META-INF/中的内容复制到war/WEB-INF/classes/META-INF/。其他所有的东西都会被创建在war/目录中的指定位置。
下面,我们从一个简单的servlet开始构建咱们这个时钟应用程序,其功能就是显示当前时间。打开文件src/clock/ClockServlet.java(如果需要,应先创建),然后如示例2-10所示编辑其中的内容。
示例2-10:一个简单的Java servlet
  1. package clock;

  2. import java.io.IOException;
  3. import java.io.PrintWriter;
  4. import java.text.SimpleDateFormat;
  5. import java.util.Date;
  6. import java.util.SimpleTimeZone;
  7. import javax.servlet.http.*;

  8. @SuppressWarnings("serial")
  9. public class ClockServlet extends HttpServlet {
  10.      public void doGet(HttpServletRequest req,HttpServletResponse resp)
  11.                throws IOException {
  12.           SimpleDateFormat fmt = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss.SSSSSS");
  13.           fmt.setTimeZone(new SimpleTimeZone(0, ""));

  14.           resp.setContentType("text/html");
  15.           PrintWriter out = resp.getWriter();
  16.           out.println("

    The time is: " + fmt.format(new Date()) + "

    "
    );
  17.      }
  18. }
该servlet类继承自javax.servlet.http.HttpServlet,并为它所希望支持的所有HTTP方法重写了基类中的相应方法。为了能够处理HTTP GET请求,我们的这个servlet重写了doGet()方法。服务器在调用该方法时,将会以一个HttpServletRequest对象和一个HttpServletResponse对象作为参数。HttpServletRequest含有该请求相关的信息,比如URL、表单参数以及cookie等。而该方法则通过HttpServletResponse上的一系列方
法来准备响应,如setContentType()和getWriter()等。当这个servlet方法退出时,App Engine就会将响应发送出去。
为了告诉App Engine使用这个servlet来处理请求,我们还需要一个部署描述符。打开或创建文件war/WEB-INF/web.xml,如示例2-11所示修改其内容。
示例2-11:该web.xml文件(也就是部署描述符)将所有URL都映射到了ClockServlet上
  1. <?xml version="1.0" encoding="utf-8"?>
  2. <!DOCTYPE web-app PUBLIC
  3. "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
  4. "">
  5. <web-app xmlns="" version="2.5">
  6. <servlet>
  7. <servlet-name>clock</servlet-name>
  8. <servlet-class>clock.ClockServlet</servlet-class>
  9. </servlet>
  10. <servlet-mapping>
  11. <servlet-name>clock</servlet-name>
  12. <url-pattern>
Eclipse可能会在其XML“设计”视图(这是一个表型视图,里面是元素和值)中打开该文件。点击编辑器面板底部的“Source”标签页就可以编辑XML源代码了。
web.xml是一个XML文件,其根元素是。为了将URL模式映射到servlet,你需要为每个servlet声明一个元素,然后再用一个元素来声明其映射信息。servlet映射信息中的可以使用完整的URL路径,也可以是一个由*开头或结尾(用于表示某个路径的一部分)的URL路径。在本例中,URL模式/* 将匹配所有的URL。
注意: 务必保证每个值都是由一个正斜杠(/)开头的。若忽略这个符号,可能在开发服务器上能得到预期的效果,不过在App Engine就不一定了。
App Engine还需要另一个配置文件,该文件不属于servlet标准。打开或创建文件war/WEB-INF/appengine-web.xml,如示例2-12所示修改其内容。
示例2-12:appengine-web.xml文件中含有特定于App Engine的配置信息
  1. <?xml version="1.0" encoding="utf-8"?>
  2. <appengine-web-app xmlns="">
  3. <application>clock</application>
  4. <version>1</version>
  5. </appengine-web-app>
在本例中,该配置文件告诉App Engine:这是一个叫做clock的版本号为1的应用程序。
你还可以用这个配置文件去控制一些其他的东西,比如静态文件和会话等。更多知识请参见第3章。
应用程序的WAR中必须包含App Engine SDK中的一些JAR:有关Java EE实现的JAR以及App Engine API JAR。GPlugin for Eclipse会自动将这些JAR安装到WAR中。如果你没用GPlugin for Eclipse,就必须手工复制这些JAR。找到SDK目录中的lib/user/和lib/shared/子目录,把这些文件夹中所有的.jar文件都复制到你项目的war/WEB-INF/lib/目录中去。
最后,还必须编译这个servlet类。Eclipse会自动按需编译所有的类。如果你没有用Eclipse,也可以用诸如Apache Ant之类的构建工具来编译源代码和执行其他构建任务。
有关如何使用Apache Ant生成App Engine项目的知识,请参阅App Engine的官方文档。
我料想你应该是知道如何在命令行中用javac命令去编译一个Java项目的。你完全可以这样做:把war/WEB-INF/lib/和war/WEB-INF/classes/目录中所有的JAR都放到classpath里面去,并保证编译好的类都被放到classes/中。不过在实际工作当中,你多半会让IDE或是一段Ant脚本来做这些事情。另外,当我们在接下来的几节中介绍数据存储区时,还需要添加一个用于构建项目的步骤,这就让编译工作的纯手工工艺变得更不现实了。
现在是时候用开发Web服务器来测试一下咱们这个应用程序了。GPlugin for Eclipse可以在Eclipse调试器中运行应用程序和开发服务器。单击“Run”菜单,单击“Debug As”下面的“Web Application”,就可以开始了。服务器启动了,并会向Console面板上输出以下消息:
The server is running at
如果你没用Eclipse,也可以通过dev_appserver命令(Mac OS X或Linux中应该用dev_appserver.sh)来启动开发服务器。该命令要用WAR目录的路径作为参数,就像这样:
dev_appserver war
在Web浏览器中访问服务器的URL就可以测试你的应用程序了:

浏览器中显示的页面跟前面Python版(如图2-4所示)的很像。

用户和GAccount
现在,我们这个时钟显示的是UTC时区的时间。我们希望这个应用程序能让用户自定义时区,还要记住用户的偏好以便今后使用。为了达到这个目的,我们将通过GAccount来识别究竟是哪个用户在使用应用程序。
如示例2-13所示编辑ClockServlet.java。
示例2-13:ClockServlet.java的代码,它能显示GAccount的信息和链接
  1. package clock;
  2. import java.io.IOException;
  3. import java.io.PrintWriter;
  4. import java.text.SimpleDateFormat;
  5. import java.util.Date;
  6. import java.util.SimpleTimeZone;
  7. import javax.servlet.http.*;
  8. import com.google.appengine.api.users.User;
  9. import com.google.appengine.api.users.UserService;
  10. import com.google.appengine.api.users.UserServiceFactory;
  11. @SuppressWarnings("serial")
  12. public class ClockServlet extends HttpServlet {
  13. public void doGet(HttpServletRequest req,
  14. HttpServletResponse resp)
  15. throws IOException {
  16. SimpleDateFormat fmt = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss.SSSSSS");
  17. fmt.setTimeZone(new SimpleTimeZone(0, ""));
  18. UserService userService = UserServiceFactory.getUserService();
  19. User user = userService.getCurrentUser();
  20. String navBar;
  21. if (user != null) {
  22. navBar = "

    Welcome, " + user.getNickname() + "! You can +

  23. userService.createLogoutURL("/") +
  24. "\">sign out.

    ";
  25. } else {
  26. navBar = "

    Welcome! + userService.createLoginURL("/") +

  27. "\">Sign in or register to customize.

    ";
  28. }
  29. resp.setContentType("text/html");
  30. PrintWriter out = resp.getWriter();
  31. out.println(navBar);
  32. out.println("

    The time is: " + fmt.format(new Date()) + "

    "
    );
  33. }
  34. }
在真正的应用程序中,你可不要像这样把HTML和Java源代码混到一起。你可以用Java服务器页(JavaServer Page,JSP)来呈现页面,也可以用一个模板系统来渲染输出。为了简单起见,本例剩下的部分仍然是直接在servlet代码中编写HTML,不过要记住,这可不是好习惯。
若使用Eclipse,你可以在编写代码的时候让开发服务器一直运行着。当你保存了对代码的修改之后,Eclipse就会自动编译这个类,如果编译成功,Eclipse就会将这个新类注入到正在运行的服务器中。大部分情况下,你只需在浏览器中重新加载页面即可,它会用上新代码的。
如果没使用Eclipse,也可以按下Ctrl+C来关闭开发服务器。重新编译你的项目,然后再次启动服务器。
在浏览器中重新加载一次新版的时钟应用程序。新页面跟Python版(如图2-5所示)长得一模一样。
这一版的时钟应用程序用到了com.google.appengine.api.users包所提供的GAccount接口。应用程序通过调用UserServiceFactory类的getUserService()方法获取一个UserService实例。然后再调用这个UserService的getCurrentUser()方法,该方法将返回一个User对象,如果当前用户没有登录,则返回null。User对象的getEmail()方法将返回该用户的电子邮件地址。
UserService的createLoginURL()和createLogoutURL()方法会生成指向GAccount的URL。这两个方法都接受来自应用程序的URL路径作为参数,当执行完所需的任务之后,用户就会被带回到这里。登录URL所指向的GAccount页能让用户登录或注册一个新账户。注销URL的功能是:访问GAccount,注销当前用户,并立即转回指定的应用程序URL,期间不会显示别的东西。
当应用程序运行于开发服务器中时,单击链接“Sign in or register”将会打开GAccount 登录界面的开发服务器模拟版,它与前面的Python版(如图2-6所示)一样。在这个界面上,不管你输入什么电子邮件地址,开发服务器都会接受,就好像你真的有这么一个账户似的。
如果应用程序运行在App Engine上,登录和注销URL将会指向真正的GAccount地址。登录或注销之后,GAccount将立刻转向在线应用程序所指定的URL。
单击“Sign in or register”,然后在模拟的GAccount页面上输入一个电子邮件地址(如test@example.com)并单击“Login”按钮。时钟应用程序现在看上去如图2-7所示(在前面介绍Python时提到)。单击“sign out”链接就可以注销了。
除UserService API之外,应用程序还可以通过servlet的“用户主体”(user principal)接口获取当前用户的相关信息。通过调用HttpServletRequest对象上的getUserPrincipal()方法,应用程序可以得到一个java.security.Principal对象或null(如果该用户未登录)。该对象有一个getName()方法,它等效于App Engine中User对象上的getEmail()方法。
从servlet接口获取用户信息的主要优点是,servlet接口是标准的。用标准接口编写应用程序可以让应用程序能够被轻松地移植到其他平台上去,比如其他基于servlet的Web应用程序环境或私有服务器。App Engine已经尽可能多地在其服务和功能上实现了标准接口。
标准接口也有缺点,即:不是所有标准接口都能反映App Engine的全部特性,而且AppEngine也没有实现某些接口的全部功能。所有服务都含有一个非标准的“低级”API,你可以直接用它,也可以通过它来实现其他接口的适配器。

Web表单和数据存储区
现在我们已经可以识别用户了。我们可以拿到用户的偏好数据,并将这些数据保存起来以便今后使用。我们可以把这些偏好数据保存到App Engine的数据存储区中。
App Engine SDK所支持的用于访问数据存储区的接口主要有两个:Java数据对象(Java Data Object,JDO)2.3以及Java持久化API(Java Persistence API,JPA)1.0。跟其他服务一样,数据存储区也有一个低级API。
我们用JPA接口来保存用户的时区设置信息。JPA需要一个配置文件,该文件用于指定该用哪个JPA实现以及一些其他的选项。该文件的最终位置是war/WEB-INF/classes/META-INF/persistence.xml。如果你使用的是Eclipse,则可以只创建文件src/META-INF/persistence.xml,Eclipse会自动将其复制到最终位置上。创建文件src/META-INF/persistence.xml,并如示例2-14所示编辑其内容。
示例2-14:JPA配置文件(persistence.xml),其中有一些有用的选项
  1. <?xml version="1.0" encoding="UTF-8" ?>
  2. <persistence xmlns=""
  3. xmlns:xsi=""
  4. xsi:schemaLocation="
  5. /persistence_1_0.xsd" version="1.0">
  6. <persistence-unit name="transactions-optional">
  7. <provider>org.datanucleus.store.appengine.jpa.DatastorePersistenceProvider</provider>
  8. <properties>
  9. <property name="datanucleus.NontransactionalRead" value="true"/>
  10. <property name="datanucleus.NontransactionalWrite" value="true"/>
  11. <property name="datanucleus.ConnectionURL" value="appengine"/>
  12. </properties>
  13. </persistence-unit>
  14. </persistence>
应用程序通过EntityManager对象与数据存储区进行交互,它将从EntityManagerFactory获取该对象。考虑到执行效率,最好只为每个servlet初始化一次EntityManagerFactory。你可以将该实例保存在包装器类的一个静态成员中。
在clock包中新建一个名为EMF的类(src/clock/EMF.java),并如示例2-15所示编辑其内容。
示例2-15:文件EMF.java,它是用于存放EntityManagerFactory实例的包装器类
  1. package clock;
  2. import javax.persistence.EntityManagerFactory;
  3. import javax.persistence.Persistence;
  4. public final class EMF {
  5. private static final EntityManagerFactory emfInstance =
  6. Persistence.createEntityManagerFactory("transactions-optional");
  7. private EMF() {}
  8. public static EntityManagerFactory get() {
  9. return emfInstance;
  10. }
  11. }
JPA使你的Java数据得以持久化。到目前为止,这个应用程序中的数据对象都是简单的Java对象(Plain Old Java Object,POJO),当然它们都有成员和方法。当创建数据类的实例时,你可以通过EntityManager将其变成可持久化的。从那时起,JPA将保证该对象上的所有修改都会被保存到数据存储区中去。当你以后从数据存储区中获取该对象时,它不仅有全部的数据,而且仍然有持久化功能。
当你定义一个JPA数据类时,其实就是定义了一个可持久化的类,另外还可以告诉JPA要如何保存和恢复这个类的实例。可以在类定义中使用Java标注(annotation)来达到这个目的。
示例2-16给出了一个叫做UserPrefs的用户偏好数据类的代码,其中用JPA标注将其声明成为一个可持久化的类。创建出这个类(src/clock/UserPrefs.java)。
示例2-16:UserPrefs.java的代码。这是一个数据类,其中的JPA标注使其实例可持久化
  1. package clock;
  2. import javax.persistence.Basic;
  3. import javax.persistence.Entity;
  4. import javax.persistence.EntityManager;
  5. import javax.persistence.Id;
  6. import com.google.appengine.api.users.User;
  7. import clock.EMF;
  8. import clock.UserPrefs;
  9. @Entity(name = "UserPrefs")
  10. public class UserPrefs {
  11. @Id
  12. private String userId;
  13. private int tzOffset;
  14. @Basic
  15. private User user;
  16. public UserPrefs(String userId) {
  17. this.userId = userId;
  18. }
  19. public String getUserId() {
  20. return userId;
  21. }
  22. public int getTzOffset() {
  23. return tzOffset;
  24. }
  25. public void setTzOffset(int tzOffset) {
  26. this.tzOffset = tzOffset;
  27. }
  28. public User getUser() {
  29. return user;
  30. }
  31. public void setUser(User user) {
  32. this.user = user;
  33. }
  34. public static UserPrefs getPrefsForUser(User user) {
  35. UserPrefs userPrefs = null;
  36. EntityManager em = EMF.get().createEntityManager();
  37. try {
  38. userPrefs = em.find(UserPrefs.class, user.getUserId());
  39. if (userPrefs == null) {
  40. userPrefs = new UserPrefs(user.getUserId());
  41. userPrefs.setUser(user);
  42. }
  43. } finally {
  44. em.close();
  45. }
  46. return userPrefs;
  47. }
  48. public void save() {
  49. EntityManager em = EMF.get().createEntityManager();
  50. try {
  51. em.persist(this);
  52. } finally {
  53. em.close();
  54. }
  55. }
  56. }
UserPrefs类有三个成员:用户ID、用户的时区偏好以及代表当前用户的User对象(它含有用户的电子邮件地址)。该类被@Entity标注声明为可持久化。name参数指定的是JPA查询中所用到的名称,通常与类名相同。默认情况下,数据存储区实体的类别就是直接由类名得来的,本例中就是UserPrefs。
user字段上有一个@Basic标注,这是因为:在默认情况下,JPA将不认为其字段类型(User)是可持久化的。而JPA默认String和int是可持久化的。
字段userId是该对象的主键,由@Id标注声明。跟关系型数据库不同,这里的键不是记录上的一个字段,而仅仅只是数据存储区实体上的一个固定元素而已,只能在初次保存对象时设置其值。如果数据类的主键是一个String成员,则当初次保存该对象时,JPA需要该成员被设置为键名(key name),也就是一个在该类所有对象中唯一的值。对象被保存之后,这个值就不能再被修改了。
这个应用程序会为每个用户创建一个UserPrefs对象(偏好集),每个对象的键的键名都使用由GAccount提供的唯一用户ID。对我们这个时钟应用程序而言,当用户访问它时,它将尝试用这个键(由用户ID构造出来的)来获取UserPrefs对象,如果找到了这样一个对象,就据此来调整时钟的显示。
用JPA和App Engine声明主键的办法还有不少,包括让数据存储区自动指派一个唯一ID。我们将在第8章中对此进行讨论。
当数据类被编译之后,JPA会将一些额外代码附加到它上面去,这个步骤叫做“增强”(enhancement)。标注将会告诉增强过程如何修改已编译类的字节码:在适当的地方添加对JPA API的调用以保证对象的持久化成员能够被保存到数据存储区中去。如果你用的是Eclipse和GPlugin for Eclipse,则JPA增强过程将会在编译数据类时自动发生。App Engine SDK中也有一个执行此步骤的Ant插件;此外,还可以通过某个工具在你自己的构建过程中执行增强处理。更多关于通过构建脚本(build script)执行JPA类增强步骤的知识,请参见App Engine的官方文档。
考虑到等会要加上缓存逻辑,我们还给这个类添加了两个分别用于获取和保存UserPrefs对象的方法。静态方法getPrefsForUser()接受一个User对象(由GAccount API返回),并尝试从数据存储区中获取该用户的UserPrefs对象。实例方法save()用于将对象保存到数据存储区:如果相应键的数据存储区实体不存在,那么就新建一个;如果存在,则进行更新之。(这个save()方法有悖于JPA的自动对象持久化思想,不过却是一种便于集成memcache的办法,只要很少的代码就可以解决问题了。)
为了让用户能够自定义时区,下面该升级一下我们的这个时钟应用程序了。示例2-17给出了一个新版的ClockServlet类,它将获取当前登录用户的UserPrefs对象,如果有,那么就直接用这个对象来自定义时钟显示。它还会显示一个Web表单,用户可以用它来修改时区偏好设置。
示例2-17:新版的ClockServlet.java,它根据用户的时区调整时钟,还会显示一个偏好表单
  1. // ...
  2. import clock.UserPrefs;
  3. @SuppressWarnings("serial")
  4. public class ClockServlet extends HttpServlet {
  5. public void doGet(HttpServletRequest req,
  6. HttpServletResponse resp)
  7. throws IOException {
  8. SimpleDateFormat fmt = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss.SSSSSS");
  9. UserService userService = UserServiceFactory.getUserService();
  10. User user = userService.getCurrentUser();
  11. String navBar;
  12. String tzForm;
  13. if (user == null) {
  14. navBar = "

    Welcome! + userService.createLoginURL("/") +

  15. "\">Sign in or register to customize.

    ";
  16. tzForm = "";
  17. fmt.setTimeZone(new SimpleTimeZone(0, ""));
  18. } else {
  19. UserPrefs userPrefs = UserPrefs.getPrefsForUser(user);
  20. int tzOffset = 0;
  21. if (userPrefs != null) {
  22. tzOffset = userPrefs.getTzOffset();
  23. }
  24. navBar = "

    Welcome, " + user.getEmail() + "! You can +

  25. userService.createLogoutURL("/") +
  26. "\">sign out.

    ";
  27. tzForm = "
    " +
  28. " +
  29. "Timezone offset from UTC (can be negative):" +
  30. "" +
  31. " +
  32. "value=\"" + tzOffset + "\" />" +
  33. "" +
  34. "";
  35. fmt.setTimeZone(new SimpleTimeZone(tzOffset * 60 * 60 * 1000, ""));
  36. }
  37. resp.setContentType("text/html");
  38. PrintWriter out = resp.getWriter();
  39. out.println(navBar);
  40. out.println("

    The time is: " + fmt.format(new Date()) + "

    "
    );
  41. out.println(tzForm);
  42. }
  43. }
为了启用这个偏好表单,我们需要一个能够解析表单数据并更新数据存储区的servlet。
我们使用一个新的servlet类来实现这个功能。创建一个名为PrefsServlet的类(src/clock/PrefsServlet.java),如示例2-18所示编辑其内容。
示例2-18:PrefsServlet.java的代码。它是一个用于处理偏好表单的servlet
  1. package clock;
  2. import java.io.IOException;
  3. import javax.servlet.http.HttpServlet;
  4. import javax.servlet.http.HttpServletRequest;
  5. import javax.servlet.http.HttpServletResponse;
  6. import com.google.appengine.api.users.User;
  7. import com.google.appengine.api.users.UserService;
  8. import com.google.appengine.api.users.UserServiceFactory;
  9. import clock.UserPrefs;
  10. @SuppressWarnings("serial")
  11. public class PrefsServlet extends HttpServlet {
  12. public void doPost(HttpServletRequest req,
  13. HttpServletResponse resp)
  14. throws IOException {
  15. UserService userService = UserServiceFactory.getUserService();
  16. User user = userService.getCurrentUser();
  17. UserPrefs userPrefs = UserPrefs.getPrefsForUser(user);
  18. try {
  19. int tzOffset = new Integer(req.getParameter("tz_offset")).intValue();
  20. userPrefs.setTzOffset(tzOffset);
  21. userPrefs.save();
  22. } catch (NumberFormatException nfe) {
  23. // User entered a value that wasn't an integer. Ignore for now.
  24. }
  25. resp.sendRedirect("/");
  26. }
  27. }
接下来,我们需要修改一下web.xml,让它能够将这个servlet映射到/prefs这个URL上去。编辑该文件,并加上如示例2-19所示的XML。
示例2-19:在web.xml中,用一个安全约束把/prefs这个URL跟PrefsServlet映射关联(片段)
  1. <servlet>
  2. <servlet-name>prefs</servlet-name>
  3. <servlet-class>clock.PrefsServlet</servlet-class>
  4. </servlet>
  5. <servlet-mapping>
  6. <servlet-name>prefs</servlet-name>
  7. <url-pattern>/prefs</url-pattern>
  8. </servlet-mapping>
  9. <security-constraint>
  10. <web-resource-collection>
  11. <web-resource-name>prefs</web-resource-name>
  12. <url-pattern>/prefs</url-pattern>
  13. </web-resource-collection>
  14. <auth-constraint>
  15. <role-name>*</role-name>
  16. </auth-constraint>
  17. </security-constraint>
该文件中的URL映射的排列顺序是无关紧要的。较长的模式(不算通配符)是先于较短的模式匹配的。
这里的块告诉App Engine:用户只有在登录了GAccount之后才能访问/prefs这个URL。如果用户在没有登录的情况下访问该URL,App Engine就会将其转向到GAccount以要求其登录。用户在登录之后,会被转回到原来想要访问的那个URL上面去。安全约束是实现一组URL上的GAccount验证的便捷方式。在本例中,它意味着PrefsServlet无须处理“未登录用户试图向该URL提交数据”的问题。
servlet通过传递给doPost()方法的HttpServletRequest对象访问表单数据。我们现在暂时不管用户在那个文本框中输入的是不是整数。在后面我们再另外可以实现一个出错消息。
如果表单数据可用,则servlet将把这个值设置给UserPrefs对象(通过getPrefsForUser()方法获取),然后再调用save()方法。save()方法首先将打开一个PersistenceManager,把这个UserPrefs对象附加上去(如果是第一次,那么还要使其能够持久化),然后关闭PersistenceManager以便将该对象保存到数据存储区。
最后,PrefsServlet会将用户带回到主页面上去。表单提交之后的转向操作使用户能够重新加载主页面(无须重新提交表单)。
重新启动你的开发服务器,然后刷新页面来看看咱们这个可定制的时钟应用程序的运行效果。修改一下时区并提交表单。注销一下,然后用同一个电子邮件地址再登录进去,之后再用别的电子邮件账户登录一下。该应用程序能记住每个用户的时区偏好。

用memcache实现缓存
到目前为止,我们的应用程序在每个登录用户(每次)访问站点的时候都会从数据存储区中获取一个实体。由于用户的偏好数据不会被频繁修改,因此我们可以通过内存缓存(次要存储区)来提高请求的数据访问速度。
跟其他服务一样,App Engine SDK中用于访问memcache服务的接口也有两个:一个是功能丰富的专用API,另一个是遵循JCache(JSR 107,这是一个广受推荐的Java标准)的API。虽然我们这个示例用哪一种都行,但这里我们将使用那个专用的API。
由于我们只用了两个方法来分别进行UserPrefs对象的获取和保存操作,因此只需很少的改动就能实现对UserPrefs对象的缓存。示例2-20给出了UserPrefs类上所需的修改。
示例2-20:在UserPrefs.java中做这些改动即可实现对UserPrefs对象的缓存
  1. import java.io.Serializable;
  2. import com.google.appengine.api.memcache.MemcacheService;
  3. import com.google.appengine.api.memcache.MemcacheServiceException;
  4. import com.google.appengine.api.memcache.MemcacheServiceFactory;
  5. // ...
  6. @SuppressWarnings("serial")
  7. @Entity(name = "UserPrefs")
  8. public class UserPrefs implements Serializable {
  9. // ...
  10. @SuppressWarnings("unchecked")
  11. public static UserPrefs getPrefsForUser(User myUser) {
  12. UserPrefs userPrefs = null;
  13. String cacheKey = "UserPrefs:" + myUser.getUserId();
  14. try {
  15. MemcacheService memcache = MemcacheServiceFactory.getMemcacheService();
  16. if (memcache.contains(cacheKey)) {
  17. userPrefs = (UserPrefs) memcache.get(cacheKey);
  18. return userPrefs;
  19. }
  20. // If the UserPrefs object isn't in memcache,
  21. // fall through to the datastore.
  22. } catch (MemcacheServiceException e) {
  23. // If there is a problem with the cache,
  24. // fall through to the datastore.
  25. }
  26. EntityManager em = EMF.get().createEntityManager();
  27. try {
  28. userPrefs = em.find(UserPrefs.class, myUser.getUserId());
  29. if (userPrefs == null) {
  30. userPrefs = new UserPrefs(myUser.getUserId());
  31. userPrefs.setUser(myUser);
  32. } else {
  33. try {
  34. MemcacheService memcache = MemcacheServiceFactory.getMemcacheService();
  35. memcache.put(cacheKey, userPrefs);
  36. } catch (MemcacheServiceException e) {
  37. // Ignore cache problems, nothing we can do.
  38. }
  39. }
  40. } finally {
  41. em.close();
  42. }
  43. return userPrefs;
  44. }
  45. public void save() {
  46. EntityManager em = EMF.get().createEntityManager();
  47. try {
  48. em.persist(this);
  49. } finally {
  50. em.close();
  51. }
  52. }
  53. }
存储在memcache中的任何对象都必须可以被序列化(也就是说,它必须实现java.io包中的Serializable接口)。对UserPrefs而言,它是完全可以实现这个接口的,因为它的所有成员都已经是可序列化的了。
新版的getPrefsForUser()静态方法会在检查数据存储区之前先在缓存中看看有没有特定用户的UserPrefs对象。每个缓存值都是跟一个键一起保存的,这个键可以是任何可序列化的对象。我们给UserPrefs对象所使用缓存键是这样构成的:在字符串"UserPrefs:"后面再加上从User对象中得到的电子邮件地址。如果指定的键值对不在缓存中,又或者访问缓存时出现了问题,该方法就会去数据存储区中找,并调用那个新的辅助方法(cacheSet())将其保存到缓存中去。
同样,新版的save()方法既会把对象保存到数据存储区中,也会将其保存到缓存中。对于缓存和数据存储区这两个服务,如果一个崩溃而另外一个却没事,则根本就没办法保证它们含有相同的值。一般的做法是先保存到数据存储区,然后再保存到缓存。为了更加安全,我们可以设置缓存值在一段时间后过期,这样,就算它们之间出现了不和谐,也不会是很长时间的事情。前面已经介绍过了,只要系统不崩溃,缓存值在内存中持久化的时间要多长有多长。
重新加载页面,看看这个新版本的工作效果如何。为了使缓存行为更明显,你可以在适当的地方添加一些日志语句,如下所示:
  1. import java.util.logging.*;
  2. import javax.persistence.Transient;
  3. // ...
  4. public class UserPrefs implements Serializable {
  5. @Transient
  6. private static Logger logger = Logger.getLogger(UserPrefs.class.getName());
  7. // ...
  8. if (cache.containsKey(cacheKey)) {
  9. logger.warning("CACHE HIT");
  10. userPrefs = (UserPrefs) cache.get(cacheKey);
  11. return userPrefs;
  12. }
  13. logger.warning("CACHE MISS");
开发服务器会向控制台输出日志信息。如果你用的是Eclipse,则这些消息将会出现在Console面板中。

开发控制台
Python和Java的开发服务器中都有一个非常方便的功能(一个基于Web的开发控制台),其作用是在本地计算机上检测和调试你的应用程序。当开发服务器在运行的时候,在浏览器中打开下面这个URL即可访问开发控制台:
_ah/admin
在Python的GAE Launcher中,直接单击“SDK Console”按钮即可在浏览器中打开这个控制台。
目前,Java的开发控制台在功能上暂时还落后于Python的开发控制台,不过它正在迎头赶上。图2-9展示了Python控制台中的数据存储区查看器。

图2-9:开发控制台的数据存储区查看器(Python版)
在Python开发控制台的数据存储区查看器中,你可以按类别列出并检查实体、编辑实体,还可以创建新实体。你可以编辑现有属性上的值,但是不能删除或添加属性,也不能改变其值的类型。对于新实体,控制台将根据现有实体的类别推测出它所拥有的属性,然后显示一个用以填写这些属性的表单。同样,在这个控制台中,你只能创建已有类别的新实体,而不能创建新的类别。
Python版的控制台上还有一个针对memcache的查看器。你可以看到缓存的统计信息,还可以检查、创建和删除键。通过序列化(pickle过的)形式显示和编辑值。
Python版控制台的一个非常强大的功能就是“交互式控制台” (Interactive Console)。
该功能使你能够在一个Web表单中输入任意Python代码,并在浏览器中查看其运行结果。有了它,你就可以在本地的开发服务器上编写临时Python代码来测试和操作数据存储区、memcache以及全局数据了。
这里给出一个例子:运行时钟应用程序,随便找个电子邮件地址登录,然后设置一下时区偏好(比如-8)。现在打开Python开发控制台,然后单击“Interactive Console”。在左边的文本框中输入下面这段代码,其中的-8是你所用到的时区偏好值。
  1. from google.appengine.ext import db
  2. import models
  3. q = models.UserPrefs.gql("WHERE tz_offset = -8")
  4. for prefs in q:
  5. print prefs.user
单击“Run Program”按钮。然后,代码就可以运行了,你所用到的那个电子邮件地址会出现在右边的文本框中。
开发控制台中运行的代码跟应用程序代码没什么两样。如果你执行了一条数据存储区查询,且该查询需要一个自定义索引,那么开发服务器一样会把该索引的配置信息添加到应用程序的index.yaml配置文件中去。有关数据存储区索引配置的知识,我们将在第5章中讨论。
有了Python版的开发控制台,你还可以在浏览器中查看应用程序的任务队列和计划任务的配置情况。通过任务队列查看器,你可以查看队列中的当前任务(在应用程序的本地实例中),还可以运行或删除它们。(开发服务器不会在后台运行任务队列,你只能通过这个控制台来运行它们。详情请参见第13章。)
此外,还可以通过在这个控制台中向应用程序发送模拟消息的方式来测试其是否能够正确接收电子邮件和XMPP消息。
Java版的开发服务器也有一个控制台。它也有一个让你可以按类别列出和检索数据存储区实体的数据存储区查看器,而且它也能运行任务队列以及向应用程序发送电子邮件和XMPP消息。

注册应用程序
在将应用程序上传到App Engine并与全世界共同分享它之前,你还必须先创建一个开发人员账户,并注册一个应用程序ID。如果打算使用自定义域名(而不是每个应用程序都有的那个appspot.com免费域名),你还必须为该域名设置Apps服务。所有这些都能在App Engine的管理控制台中完成。
在浏览器中访问下面这个URL即可打开管理控制台:

找一个你打算拿来做开发人员账户的GAccount登录。如果还没有GAccount(比如Gmail账户),也可以随便找个其他的电子邮件地址去创建一个。
登录之后,该控制台会显示出你已经创建好的应用程序列表(如果有),还有一个“Create an Application”按钮,如图2-10所示。在这个界面上,你可以创建和管理多个应用程序,每个应用程序都有其自己的URL、配置信息以及资源限制。

图2-10:管理控制台的应用程序列表(这里就一个应用程序)
在注册你的第一个应用程序ID时,管理控制台会通过手机短信的方式验证你的开发人员账户。在输入你的手机号码之后,Google公司会向你发送一条带有确认码的短信。输入这个确认码即可开始注册流程。每个手机号码只能验证一个账户,因此如果你只有一部手机(大部分人都是这样),务必保证它所验证的就是你想要在App Engine上使用的那个账户。
如果没有手机号码,也可以填写一个Web表单向网站申请人工验证。这个过程需要大约一个礼拜。有关人工验证的信息,请参阅App Engine的官方网站。
每个开发人员账户至多可以创建10个活动的应用程序ID。如果你不再想要某个应用程序ID的时候,可以在管理控制台中将其禁用掉,这样就能回收一个应用程序名额了。禁用某个应用程序之后,它就不能被公众访问了,而且管理控制台中与该应用程序有关的那些东西也将被禁用。禁用应用程序不会释放其应用程序ID,因此别人仍然不能注册那个应用程序ID。不过,你可以通过将其永久删除来达到这个目的。
要禁用或删除应用程序,先进入管理控制台的“Application Settings”,然后单击“Disable Application...”按钮。当你提请删除的时候,该应用程序的所有开发人员都会收到一封提醒邮件,如果没有人反对,将在24小时后删除该应用程序。

应用程序的ID和标题
在单击“Create an Application”按钮之后,管理控制台就会要求你提供一个应用程序ID。应用程序ID必须在App Engine的所有应用程序中唯一,就像账户的用户名一样。
当你通过开发工具与App Engine进行交互时,应用程序ID可以起到标识你的应用程序的作用。开发工具将从应用程序的配置文件中获得其应用程序ID。例如Python应用程序,其应用程序ID是在app.yaml文件中的application:行上指定的。而对于Java应用程序,你可以将其应用程序ID放到appengine-web.xml文件的元素中。
注意: 在之前的示例代码中,我们随便选了一个应用程序ID —— clock。如果你想要把这个应用程序上传到App Engine,记得要在注册了应用程序之后,把相应配置文件中的应用程序ID改成你所选的那个。
应用程序ID是在App Engine上运行的应用程序的域名的一部分。每个应用程序都会得到一个免费的域名,就像这样:
app-id.appspot.com
应用程序ID也是应用程序用于接收消息的电子邮件地址和XMPP地址的一部分。详情请参见第11章。
由于应用程序ID会被用在域名当中,因此它只能含有小写字母、数字或短横线,而且必须小于32个字符。此外,Google公司把每个Gmail用户名都预留成了一个应用程序ID,只有相应的Gmail用户才能注册这个应用程序ID。与大部分受欢迎的网站上的用户名一样,要想得到一个对用户友好的应用程序ID是很困难的。
当你在注册一个新应用程序时,管理控制台还会要求你提供一个“应用程序标题”(application title)。这个标题将用于在管理控制台和系统的其他部分表示你的应用程序。更重要的是,当应用程序把用户转向到GAccount以要求其登录的时候,用户面前所显示的就是这个标题。所以一定要想办法让这个标题成为你希望让用户看到的那个样子。
一旦注册好一个应用程序之后,其ID就不能被修改了,但是你可以把它删掉再重新建一个。任何时候你都可以在管理控制台中修改应用程序的标题。

设置域名
如果你正在开发一款专业的或商业的应用程序,那么你很有可能希望其正式地址是你自己的域名而不是appspot.com。可以通过Google公司的“软件即服务”(software as a service,SaaS)服务(即Apps)为你的App Engine应用程序设置一个自定义域名。
Apps为你的公司或组织提供了一系列的托管应用程序,包括电子邮件(通过Gmail和POP/IMAP接口)、日历(GCalendar)、聊天(GTalk)、托管的文字处理/电子表格/演示文稿(GDocs)、易于编辑的网站(GSites)、视频托管……你可以把这些服务跟你的组织的域名关联起来,只需在DNS记录中将其与Google公司的服务器映射起来就可以了。
可以让Google公司去管理该域名的DNS,也可以在你的自己的DNS配置中将子域名指向Google公司。组织中的成员通过你的域名即可访问这些托管服务。
通过App Engine,可以把你自己的应用程序添加到自定义域名的子域名上去。就算没有用到其他的Apps服务,也一样可以用Apps去关联自定义域名和应用程序。
注意: Apps的网站上说,标准版账户是“有广告的”。这说的是广告会出现在诸如Gmail之类的Google公司产品上,而并不会涉及App Engine:即使App Engine应用程序用的是免费账户,Google公司也不会在其页面上放置广告。当然,你可以在自己的应用程序中置入广告,这完全由你来定(比如根据广告收益而定)。
如果还没有为你自己的域名设置Apps,也可以在应用程序ID的注册过程中完成域名设置工作。此外,还可以在注册完应用程序ID之后,在管理控制台中设置Apps。如果你目前还没有购买一个域名,也可以在设置Apps时购买,这样就可以免费地在Google公司的域名服务器上托管该域名了。如果要使用之前已经购买的域名,按照网站上的指示将其指向Google公司的服务器即可。
在为某个域名设置好Apps之后,就可以通过下面这样的URL访问Apps的仪表板了:

为了把App Engine应用程序添加为一项服务,我们可以这样做:单击“Add more services”链接,然后在列表中找到GAE;输入你的应用程序的ID,然后单击“Add it now”;在接下来的设置界面上,可以把自定义域名上的某个子域名配置给应用程序。
这样,该子域名上的所有Web流量就都会被路由到应用程序那里去了。
注意: Apps不支持将顶级域名(比如)上的Web流量直接路由给App Engine应用程序。如果你的域名是通过Google公司购买的话,那么顶级域名上的HTTP请求将会被转向至,因此你可以将www这个子域名分配给App Engine应用程序。
如果你的域名不是由Google公司维护的话,那么就需要自己去设置转向了,可以用一台与顶级域名相关联的Web服务器来实现。
默认情况下,子域名www是分配给GS的,就算你没有Sites应用程序也是一样。为了让App Engine能用得上这个子域名,首先需要启用Sites服务,然后编辑Sites的设置并移除www子域名。

Apps和身份验证
Apps允许组织成员(雇员、承包商、志愿者)通过带有自定义域名(比如juliet@example.com)的电子邮件地址创建用户账户。成员们可以通过这些账户登录以访问组织的私有服务,比如电子邮件或文字处理。通过Apps账户,你可以让某些文档和服务只能被组织中的成员访问到,这就好像是一个能让其成员在任何地方访问的托管企业内网。
你还可以只让那些账户域名与应用程序域名相同的用户访问应用程序。这就使你可以用App Engine来开发一些内部应用程序了,比如项目管理或销售报表之类的。当App Engine应用程序被限制在组织的域名上时,只有该组织的成员才能在该应用程序的GAccount页面上登录。其他的GAccount将会被拒绝访问。
身份验证限制必须在注册应用程序的时候设置,就在注册表单的“Authentication Options”那里。默认设置允许任何拥有GAccount的用户登录该应用程序,如何对各用户进行响应完全由应用程序自己决定。当应用程序被限制到某个Apps域名上时,就只有该域名上的GAccount能登录了。
在应用程序ID已经注册好之后,身份验证选项就不能被修改了。如果你想要给应用程序设置另一个身份验证选项,那么只能重新注册一个应用程序ID。
这个约束只在应用程序使用GAccount时有效。如果应用程序还有别的不用登录GAccount即可访问的URL(比如欢迎页),那么这些URL还是可以被所有人看到的。要限制某个URL上的访问,最简单的办法之一就是应用程序的配置文件。比如,某个Python应用程序的所有URL都需要登录才能访问,其app.yaml文件应该是这个样子的:
  1. handlers:
  2. - url: /.*
  3. script: main.py
  4. login: required
Java应用程序也可以通过其部署描述符文件(web.xml)来实现同样的效果。详情请参见第3章。
即使应用程序用的是appspot.com域名,登录约束也同样是有效的。不过,用户在访问GApps域名的应用程序时,没必要进行强制的身份验证限制。
如果你或组织中的其他成员想用Apps账户作为开发人员账户,那么在访问管理控制台时就必须使用一个特殊的URL。比如说,你的Apps域名为example.com,那么就需要用下面这个URL来访问管理控制台了:
a/example.com
可以用你的Apps账户(比如juliet@example.com)登录该域名的管理控制台。
如果在创建应用程序时使用的是非Apps账户,但是又在其域名上设置了身份验证限制,虽然你仍然可以用那个非Apps账户访问管理控制台,但是却不能用该账户登录应用程序,就算是那些专门为管理员准备的URL也不行。

上传应用程序
在传统的Web应用程序环境中,向外界发布应用程序是一件非常费力的事情。为了最小化停机时间和预防出错,我们需要在正确的时间按顺序把最新的软件和配置文件放到不同的服务器和后台服务中去,然而一般来说,这都是很困难的。在App Engine中,部署工作非常简单,只需点一下鼠标或是执行一条命令把文件上传上去就行了。你可以上传和测试应用程序的多个版本,还可以将任何已经上传的版本设置为当前工作版本。
对于Python应用程序,你可以用GAE Launcher进行上传,也可以在命令提示符中完成上传工作。在GAE Launcher中,选中需要部署的应用程序,然后单击“Deploy”按钮即可。如果是命令提示符,则执行下面这条appcfg.py命令即可,注意要用你的应用程序目录的路径替换掉clock:
appcfg.py update clock
跟dev_appserver.py一样,这里的clock只是目录的路径而已。如果当前工作目录就是clock/目录的话,可以直接使用相对路径,即一个点(.)。
对Java应用程序,你可以在Eclipse中用插件进行上传,也可以用命令提示符完成上传工作。在Eclipse中,单击工具栏中的“Deploy to App Engine”按钮(那个小小的App Engine徽标)。如果用命令提示符,像下面这样执行SDK中bin/目录下的appcfg(或appcfg.sh)命令即可,注意要用应用程序的WAR目录的路径替换掉war:
appcfg update war
在弹出的对话框中,输入你的开发人员账户的电子邮件地址和密码。这些工具都会记住这些信息以便今后直接使用,这样你就不必每次部署时都重新输入一遍了。
上传进程首先将从应用程序的配置文件(Python应用程序是app.yaml,Java应用程序则是appengine-web.xml)中找出应用程序ID和版本号,然后再上传并安装这些文件。在上传完应用程序之后,你立刻就能访问到它(可以通过.appspot.com子域名,也可以是自定义的Apps域名)。比如说,应用程序ID是clock,那么你可以通过下面这个URL来进行访问:

注意: 应用程序的文件在上传到App Engine之后是没有办法下载回来的。所以,一定要确保留有该应用程序文件的副本,可以通过版本控制系统或是例行的备份工作来达到这个目的。

管理控制台简介

利用App Engine的管理控制台,你可以在浏览器中管理在线应用程序。在你注册应用程序的时候就已经见过管理控制台了,不过还是要提醒一下,通过下面这个URL即可访问管理控制台:

如果应用程序使用的是Apps域名,而你又用该域名上的某个Apps账户作为开发人员账户,那么就只能通过管理控制台的Apps地址进行访问:
a/example.com
选中你的应用程序(单击其ID)即可进入其管理控制台。
你所看到的第一个界面是“仪表板” (dashboard),如图2-11所示。仪表板总结了应用程序当前和过去的状态,包括流量和负载、资源使用量以及出错率等。你可以看到一些图表,比如请求率、每次请求所花费的时间、错误率、带宽和CPU使用量,以及应用程序是否已经触及其资源上限。

图2-11:一个新应用程序的管理控制台仪表板
如果你刚刚测试了一下这个新应用程序,那么就应该在“每秒请求数”那个图表中看到一个尖峰。该图表的范围会与其中最高的点齐平,因此就算你只是访问了这个应用程序几次而已,那个尖峰也会达到整个图表的顶部。
管理控制台就是你管理在线应用程序的中军帐。在这里,你可以观察应用程序是如何使用资源的,可以浏览应用程序的请求和消息日志,可以查询数据存储区,还可以检查其索引状态。此外,还可以管理应用程序的多个版本,因此你可以在把新上传的版本设置成“默认版本”之前先对其测试一番了。你可以邀请其他人成为该应用程序的开发人员,让他们也能访问管理控制台和上传新文件。在准备好迎接大流量时,你可以建立一个缴费账户,设置一个每日预算并监视具体开支。
花点时间看看这个控制台,尤其是Dashboard(仪表板)、Quota Details(配额明细)以及Logs(日志)这几项。在本书中,我们将讨论应用程序是如何消费系统资源的,以及如何才能优化应用程序的速度和经济效益。在跟踪资源使用量和诊断问题时,管理控制台将是你最主要的工具。
阅读(2651) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~