第 14 章:使用 Django 的 Web 应用程序

Django 是现代 Python Web 框架之一,它重新定义了 Python 世界中的 Web 领域。全栈方法、务实的設計和出色的文档是其成功的部分原因。

如果您觉得使用 Python 语言进行快速 Web 开发听起来不错,那么使用 Python 语言进行快速 Web 开发,并与整个 Java 世界(在企业 Web 空间中拥有强大的影响力)集成,听起来就更棒了。在 Jython 上运行 Django 就可以做到这一点!

对于 Java 世界来说,Django 作为一种快速构建 Web 应用程序的选择,同时仍然有机会使用现有的 Java API 和技术,非常有吸引力。

在本章中,我们将从快速介绍开始,让您在几个步骤内将 Django 与 Jython 安装一起运行,然后我们将构建一个简单的 Web 应用程序,以感受该框架。在本章的后半部分,我们将探讨 Django Web 应用程序与 JavaEE 世界之间集成的众多机会。

获取 Django

严格来说,要在 Jython 上使用 Django,您只需要获取 Django 本身,不需要其他任何东西。但是,如果没有第三方库,您将无法连接到任何数据库,因为内置的 Django 数据库后端依赖于用 C 编写的库,这些库在 Jython 上不可用。

实际上,您至少需要两个包:Django 本身和“django-jython”,顾名思义,它是一组 Django 附加组件,如果您碰巧在 Jython 之上运行 Django,这些附加组件非常有用。特别是它包含数据库后端,这是您充分利用 Django 强大功能所必需的。

由于获取这两个库的过程根据您的平台略有不同,而且这是一个手动、枯燥的任务,我们将使用一个实用程序来自动获取和安装这些库。该实用程序称为“setuptools”。有关如何在 Jython 上安装 setuptools 的说明,请参阅附录 A。

安装 setuptools 后,easy_install 命令将可用。有了它,我们就可以安装 Django 了

$ easy_install Django==1.0.3

注意

我假设 Jython 安装的 bin 目录位于您的 PATH 中。如果不是,您将不得不显式地键入该路径,并在每个命令之前加上该路径,例如 jythoneasy_install(即,您需要键入类似 /path/to/jython/bin/easy_install 的内容,而不是仅仅键入 easy_install

通过阅读 easy_install 的输出,您可以了解它如何完成查找正确包、下载和安装的繁琐工作。

Searching for Django==1.0.3
Reading http://pypi.python.org/simple/Django/
Reading http://www.djangoproject.com/
Reading http://www.djangoproject.com/download/1.0.1-beta-1/tarball/
Best match: Django 1.0.3
Downloading http://media.djangoproject.com/releases/1.0.3/Django-1.0.3.tar.gz
Processing Django-1.0.3.tar.gz
Running Django-1.0.3/setup.py -q bdist_egg --dist-dir
/tmp/easy_install-nTnmlU/Dj  ango-1.0.3/egg-dist-tmp-L-pq4s
zip_safe flag not set; analyzing archive contents...
Unable to analyze compiled code on this platform.
Please ask the author to include a 'zip_safe' setting (either True or False)
in the package's setup.py
Adding Django 1.0.3 to easy-install.pth file
Installing django-admin.py script to /home/lsoto/jython2.5.0/bin

Installed /home/lsoto/jython2.5.0/Lib/site-packages/Django-1.0.3-py2.5.egg
Processing dependencies for Django==1.0.3
Finished processing dependencies for Django==1.0.3

然后我们安装 django-jython。

$ easy_install django-jython

同样,您将获得与之前案例类似的输出。完成此操作后,您就可以开始了。

如果您想了解幕后情况,请查看 Jython 安装目录下的 Lib/site-packages 子目录,您将看到我们刚刚安装的库的条目。这些条目也列在 easy-install.pth 文件中,默认情况下使它们成为 sys.path 的一部分。

为了确保一切顺利,启动 jython 并尝试以下语句,这些语句导入 Django 和 django-jython 的顶级包。

>>> import django
>>> import doj

如果您没有在屏幕上看到任何错误输出,那么一切正常。让我们开始我们的第一个应用程序。

Django 快速入门

注意

如果您已经熟悉 Django,那么在本节的其余部分中您不会发现任何特别新颖的内容。您可以直接跳到 J2EE 部署和集成,查看在 Jython 上运行 Django 时真正特别之处。

Django 是一个全栈框架。这意味着它涵盖了从与数据库的通信到 URL 处理和网页模板的所有方面。您可能知道,有完整的书籍详细介绍了 Django。我们不会深入探讨太多细节,但我们会接触框架中包含的许多功能,这样您就可以对它的优势有一个很好的了解,以防您以前没有机会了解或尝试过 Django。这样您就知道何时 Django 是完成工作的正确工具。

要全面了解像 Django 这样功能丰富的框架,唯一的方法是用它构建一些非常简单的东西,然后随着我们深入了解框架提供的功能,逐步对其进行扩展。因此,我们将大致遵循官方 Django 教程中使用的内容(一个简单的民意调查网站),然后将其扩展到以后触及框架的大多数功能。换句话说:您在本节中看到的大多数代码都直接来自您可以在 https://docs.django.ac.cn/en/1.0/intro/tutorial01/ 上找到的优秀的 Django 教程。

现在,正如我在上一段中所说,Django 处理与数据库的通信。目前,Django/Jython 最稳定的后端是 PostgreSQL 的后端。因此,我建议您在您的机器上安装 PostgreSQL 并设置一个用户和一个空数据库,以便在本教程中使用它。

启动项目(和“应用程序”)

Django 项目通常是指完整的网站(或“子网站”),它们由一个设置文件、一个 URL 映射文件和一组提供网站实际功能的“应用程序”组成。正如您肯定已经意识到,许多网站共享许多功能:管理界面、用户身份验证/注册、评论系统、新闻提要、联系表格等。这就是为什么 Django 将实际的网站功能与“应用程序”概念分离的原因:应用程序旨在在不同的项目(网站)之间可重用

由于我们将从小处着手,因此我们的项目最初只包含一个应用程序。我们将把我们的项目命名为“pollsite”。因此,让我们为将在本节中构建的内容创建一个干净的新目录,移动到该目录并运行

$ django-admin.py startproject pollsite

您之前创建的目录下将创建一个名为“pollsite”的 Python 包。此时,我们需要对新项目的默认设置进行的最重要的更改是填写信息,以便 Django 可以与我们为本教程创建的数据库进行通信。因此,使用您选择的文本编辑器打开 pollsite/settings.py 文件,并将以 DATABASE 开头的行更改为类似于以下内容

DATABASE_ENGINE = 'doj.backends.zxjdbc.postgresql'
DATABASE_NAME = '<the name of the empty database you created>'
DATABASE_USER = '<the name of the user with R/W access to that database>'
DATABASE_PASSWORD = '<the password of such user>'

这样,您就告诉 Django 使用 doj 包提供的 PostgreSQL 驱动程序(如果您还记得 获取 Django 部分,这是 django-jython 项目的包名称),并使用给定的凭据进行连接。现在,此后端需要 PostgreSQL JDBC 驱动程序,您可以从 https://jdbc.postgresql.ac.cn/download.html 下载。

下载 JDBC 驱动程序后,需要将其添加到 Java 的 CLASSPATH 中。在 Linux/Unix/MacOSX 中,对于当前会话,可以通过以下方式添加:

$ export CLASSPATH=$CLASSPATH:/path/to/postgresql-jdbc.jar

如果您使用的是 Windows,则命令有所不同

$ set CLASSPATH=%CLASSPATH%:\path\to\postgresql-jdbc.jar

完成此操作后,我们将创建一个作为项目核心基础的单个应用程序。确保您位于 pollsite 目录中,并运行以下命令:

$ jython manage.py startapp polls

这将创建 Django 应用程序的基本结构。请注意,应用程序是在项目包内部创建的,因此我们有 pollsite 项目和 pollsite.polls 应用程序。

现在我们将了解 Django 应用程序内部的内容。

模型

在 Django 中,您使用 Python 类在 Python 代码中定义数据模式。此中心模式用于生成创建数据库模式所需的 SQL 语句,并在操作这些特殊 Python 类的对象时动态生成 SQL 查询。

现在,在 Django 中,您不会在单个中心位置定义整个项目的模式。毕竟,由于应用程序是功能的真正提供者,因此整个项目的模式不过是每个应用程序模式的组合。顺便说一下,我们现在将切换到 Django 术语,不再谈论数据模式,而是谈论模型(实际上比模式更复杂,但目前区别并不重要)。

如果您查看 pollsite/polls 目录,您会发现有一个 models.py 文件,该文件是定义应用程序模型的地方。以下代码包含简单投票的模型,每个投票包含多个选项

from django.db import models

class Poll(models.Model):
    question = models.CharField(max_length=200)
    pub_date = models.DateTimeField('date published')

    def __unicode__(self):
        return self.question

class Choice(models.Model):
    poll = models.ForeignKey(Poll)
    choice = models.CharField(max_length=200)
    votes = models.IntegerField()

    def __unicode__(self):
        return self.choice

如您所见,从 models.Model 继承的类与数据库表之间的映射很清楚,并且每个 Django 字段如何转换为 SQL 字段也比较明显。实际上,Django 字段可以承载比 SQL 字段更多的信息,如您在 pub_date 字段中看到的,该字段包含更适合人类阅读的描述:“发布日期”。Django 还为当今 Web 应用程序中常见的类型提供了更专业的字段,例如 EmailFieldURLFieldFileField。它们让您无需一遍又一遍地编写相同的代码来处理这些字段将包含的数据的验证或存储管理等问题。

定义完模型后,我们希望创建用于保存数据库数据的表。首先,您需要将应用程序添加到项目设置文件(是的,应用程序“位于”项目包下并不足够)。编辑 pollsite/settings.py 文件,并将 'pollsite.polls' 添加到 INSTALLED_APPS 列表中。它将如下所示

INSTALLED_APPS = (
   'django.contrib.auth',
   'django.contrib.contenttypes',
   'django.contrib.sessions',
   'django.contrib.sites',
   'pollsite.polls',
)

注意

如您所见,您的项目中已经包含了几个应用程序。这些应用程序默认情况下包含在每个 Django 项目中,提供框架的一些基本功能,例如会话。

之后,确保您位于项目目录中,并运行以下命令:

$ jython manage.py syncdb

如果数据库连接信息正确指定,Django 将为我们的模型以及默认情况下也包含在 INSTALLED_APPS 中的其他应用程序的模型创建表和索引。这些额外的应用程序之一是 django.contrib.auth,它处理用户身份验证。这就是为什么您还会被要求输入网站初始管理员用户的用户名和密码的原因

Creating table auth_permission
Creating table auth_group
Creating table auth_user
Creating table auth_message
Creating table django_content_type
Creating table django_session
Creating table django_site
Creating table polls_poll
Creating table polls_choice

You just installed Django's auth system, which means you don't have any
superusers defined.
Would you like to create one now? (yes/no):

回答是,并提供所需信息。

Username (Leave blank to use u'lsoto'): admin
E-mail address: admin@mailinator.com
Warning: Problem with getpass. Passwords may be echoed.
Password: admin
Warning: Problem with getpass. Passwords may be echoed.
Password (again): admin
Superuser created successfully.

之后,Django 将继续将您的模型映射到 RDBMS 工件,为您的表创建一些索引。

Installing index for auth.Permission model
Installing index for auth.Message model
Installing index for polls.Choice model

如果我们想知道 Django 在幕后做了什么,我们可以使用 sqlall 管理命令(这是 manage.py 识别的命令的名称,例如最近使用的 syncdb)来询问。此命令需要一个应用程序标签作为参数,并打印与应用程序中包含的模型相对应的 SQL 语句。顺便说一下,对标签的强调是有意的,因为它对应于应用程序“完整名称”的最后部分,而不是完整名称本身。在我们的例子中,“pollsite.polls” 的标签只是“polls”。因此,我们可以运行

$ jython manage.py sqlall polls

我们将得到以下输出

BEGIN;
CREATE TABLE "polls_poll" (
    "id" serial NOT NULL PRIMARY KEY,
    "question" varchar(200) NOT NULL,
    "pub_date" timestamp with time zone NOT NULL
)
;
CREATE TABLE "polls_choice" (
    "id" serial NOT NULL PRIMARY KEY,
    "poll_id" integer NOT NULL
        REFERENCES "polls_poll" ("id") DEFERRABLE INITIALLY DEFERRED,
    "choice" varchar(200) NOT NULL,
    "votes" integer NOT NULL
)
;
CREATE INDEX "polls_choice_poll_id" ON "polls_choice" ("poll_id");
COMMIT;

这里需要注意两点。首先,每个表都获得了一个 id 字段,该字段在我们模型定义中没有明确指定。这是自动的,是一个明智的默认值(如果您确实需要不同类型的主键,可以覆盖它,但这超出了本快速浏览的范围)。其次,请注意 sql 是针对我们使用的特定 RDBMS(在本例中为 PostgreSQL)定制的,因此如果您使用不同的数据库后端,它可能会发生变化。

好的,让我们继续。我们已经定义了模型,并准备存储投票。这里典型的下一步是创建一个 CRUD 管理界面,以便可以创建、编辑、删除等投票。哦,当然,我们可能会设想这个管理界面的一些搜索和过滤功能,提前知道一旦投票数量增长过多,管理起来就会非常困难。

好吧,不。我们不会从头开始编写这个管理界面。我们将使用 Django 最有用的功能之一:Admin 应用程序。

奖励:Admin

这是我们对 Django 项目主要架构要点(即:模型、视图和模板)的巡回演出中的一个插曲,但这是一个非常好的插曲。我们前面几段提到的管理界面的代码将不到 20 行!

首先,让我们启用 Admin 应用程序。为此,编辑 pollsite/settings.py 并将 'django.contrib.admin' 添加到 INSTALLED_APPS 中。然后编辑 pollsite/urls.py,它看起来像这样

from django.conf.urls.defaults import *

# Uncomment the next two lines to enable the admin:
# from django.contrib import admin
# admin.autodiscover()

urlpatterns = patterns('',
    # Example:
    # (r'^pollsite/', include('pollsite.foo.urls')),

    # Uncomment the admin/doc line below and add 'django.contrib.admindocs'
    # to INSTALLED_APPS to enable admin documentation:
    # (r'^admin/doc/', include('django.contrib.admindocs.urls')),

    # Uncomment the next line to enable the admin:
    # (r'^admin/(.*)', admin.site.root),
)

并取消启用 Admin(但不是 admin/doc!)的行的注释,因此文件将看起来像这样

from django.conf.urls.defaults import *

# Uncomment the next two lines to enable the admin:
from django.contrib import admin
admin.autodiscover()

urlpatterns = patterns('',
    # Example:
    # (r'^pollsite/', include('pollsite.foo.urls')),

    # Uncomment the admin/doc line below and add 'django.contrib.admindocs'
    # to INSTALLED_APPS to enable admin documentation:
    # (r'^admin/doc/', include('django.contrib.admindocs.urls')),

    # Uncomment the next line to enable the admin:
    (r'^admin/(.*)', admin.site.root),
)

现在您可以删除所有剩余的注释行,因此 urls.py 最终将包含以下内容

from django.conf.urls.defaults import *

from django.contrib import admin
admin.autodiscover()

urlpatterns = patterns('',
    (r'^admin/(.*)', admin.site.root),
)

我知道我还没有解释这个 urls.py 文件,但相信我,我们将在下一节中看到它。

最后,让我们创建 Admin 应用程序所需的数据库工件,运行

$ jython manage.py syncdb

现在我们将看到这个 Admin 的样子。让我们通过执行以下命令在开发模式下运行我们的网站

$ jython manage.py runserver

注意

开发 Web 服务器是测试您的 Web 项目的简便方法。它将无限期地运行,直到您中止它(例如,按 Ctrl + C),并且当您更改服务器已加载的源文件时,它将重新加载自身,从而提供几乎即时的反馈。但是,请注意,在生产环境中使用此开发服务器是一个非常非常糟糕的主意。

使用 Web 浏览器,导航到 http://localhost:8000/admin/。您将看到一个登录屏幕。使用您在上一节中首次运行 syncdb 时创建的用户凭据。登录后,您将看到一个类似于图 Django Admin 中所示的页面。

../_images/chapter14-tour-admin.png

Django Admin

如您所见,Admin 的中心区域显示了两个框,标题分别为“Auth”和“Sites”。这些框对应于 Django 中内置的“auth”和“sites”应用程序。“Auth”框包含两个条目:“Groups”和“Users”,每个条目对应于 auth 应用程序中包含的一个模型。如果您单击“Users”链接,您将看到添加、修改和删除用户的典型选项。这是 Admin 可以为任何其他 Django 应用程序提供的界面类型,因此我们将向其中添加我们的 polls 应用程序。

这样做需要在你的应用下创建一个名为 admin.py 的文件(也就是 pollsite/polls/admin.py),并声明性地告诉管理员你希望如何在管理员界面中展示你的模型。为了管理投票,以下代码将实现这一功能。

# polls admin.py
from pollsite.polls.models import Poll, Choice
from django.contrib import admin

class ChoiceInline(admin.StackedInline):
    model = Choice
    extra = 3

class PollAdmin(admin.ModelAdmin):
    fieldsets = [
        (None,               {'fields': ['question']}),
        ('Date information', {'fields': ['pub_date'],
                              'classes': ['collapse']}),
    ]
    inlines = [ChoiceInline]

admin.site.register(Poll, PollAdmin)

这可能看起来像是魔法,但请记住,我正在快速前进,因为我希望你看看 Django 可以做些什么。让我们先看看编写完这段代码后会得到什么。启动开发服务器,访问 http://localhost:8000/admin/,你会看到一个新的“Polls”框。如果你点击“Polls”条目中的“Add”链接,你会看到一个类似于图 使用管理员添加投票 的页面。

../_images/chapter14-tour-addpoll.png

使用管理员添加投票

玩一玩这个界面:创建几个投票,删除一个,修改它们。注意,用户界面分为三个部分,一个用于问题,另一个用于日期(最初隐藏),另一个用于选项。前两个由 PollAdmin 类的 fieldsets 定义,它允许你定义每个部分的标题(其中 None 表示没有标题),包含的字段(当然可以有多个)以及提供行为的附加 CSS 类,例如 'collapse'

很明显,我们已经将两个模型(PollChoice)的管理合并到同一个用户界面中,因为选项应该与其对应的投票“内联”编辑。这是通过 ChoiceInline 类实现的,它声明将内联哪个模型以及将显示多少个空槽。内联稍后将连接到 PollAdmin(因为你可以在任何 ModelAdmin 类中包含多个内联)。

最后,PollAdmin 使用 admin.site.register() 注册为 Poll 模型的管理界面。正如你所看到的,一切都是完全声明性的,并且运行得很好。

细心的读者可能想知道关于我之前几段中提到的搜索/过滤功能。嗯,我们将实现它,在投票列表界面中,你可以通过点击主界面中“Polls”的“Change”链接(或者点击“Polls”链接,或者添加投票后)访问该界面。

所以,将以下几行添加到 PollAdmin 类中。

search_fields = ['question']
list_filter = ['pub_date']

再次玩一玩管理员(这就是在最后一步创建几个投票是一个好主意的原因)。图 在 Django 管理员中搜索 显示了搜索工作,使用“django”作为搜索字符串。

../_images/chapter14-tour-adminsearch.png

在 Django 管理员中搜索

现在,如果你尝试按发布时间进行过滤,会感觉有点别扭,因为投票列表只显示投票的名称,所以你无法看到被过滤的投票的发布时间,无法检查过滤器是否按预期工作。这很容易解决,只需将以下行添加到 PollAdmin 类中。

list_display = ['question', 'pub_date']

在 Django 管理员中过滤和列出更多字段 显示了添加所有这些内容后界面的外观。

../_images/chapter14-tour-adminfilter.png

在 Django 管理员中过滤和列出更多字段

再次,你可以看到管理员如何几乎免费地为你提供所有这些常用功能,你只需要以纯粹的声明方式说出你想要什么。但是,如果你有更多特殊需求,管理员提供了钩子,你可以使用它们来定制其行为。它非常强大,有时会发生一个完整的 Web 应用程序可以完全基于管理员构建。有关更多信息,请参阅官方文档 https://docs.django.ac.cn/en/1.0/ref/contrib/admin/

视图和模板

现在你已经了解了管理员,我将无法使用 CRUD 来展示 Web 框架其余主要架构。没关系:CRUD 是几乎所有数据驱动 Web 应用程序的一部分,但它们并不是让你的网站与众不同的东西。因此,现在我们已经将繁琐的任务委托给了管理员应用程序,我们将专注于投票,这是我们的业务。

我们已经有了模型,现在该编写视图了,视图是面向 HTTP 的函数,它们将使我们的应用程序与外部进行通信(毕竟,这是创建Web应用程序的意义所在)。

注意

Django 开发人员半开玩笑地说 Django 遵循“MTV”模式:模型、模板和视图。这 3 个组件直接映射到其他现代框架称为模型、视图和控制器的组件。Django 采用这种看似非正统的命名方案,因为严格来说,控制器是框架本身。在其他框架中称为“控制器”的代码实际上与 HTTP 和输出模板相关联,因此它实际上是视图层的一部分。如果你不喜欢这种观点,只需记住在脑海中将 Django 模板映射到“视图”,将 Django 视图映射到“控制器”。

按照惯例,视图的代码放在应用程序的 views.py 文件中。视图是简单的函数,它们接收 HTTP 请求,进行一些处理并返回 HTTP 响应。由于 HTTP 响应通常涉及构建 HTML 页面,因此模板通过更易于维护的方式(而不是手动粘贴字符串)来帮助视图创建 HTML 输出(和其他基于文本的输出)。

投票应用程序将有一个非常简单的导航。首先,用户将看到一个“索引”,其中包含访问最新投票列表的链接。他将选择一个投票,我们将显示投票的“详细信息”,即一个包含可用选项的表单和一个按钮,以便他提交自己的选择。一旦做出选择,用户将被定向到一个页面,显示他刚刚投票的投票的当前结果。

在编写视图代码之前:设计 Django 应用程序的一个好方法是设计它的 URL。在 Django 中,你使用正则表达式将 URL 映射到视图。现代 Web 开发非常重视 URL,并且漂亮的 URL(即没有像“DoSomething.do”或“ThisIsNotNice.aspx”这样的垃圾)是常态。Django 没有使用 URL 重写来修补难看的名称,而是提供了一个间接层,在触发视图的 URL 和你碰巧给该视图起的内部名称之间。此外,由于 Django 重视可以在多个项目中重复使用的应用程序,因此有一种模块化的方法来定义 URL,以便应用程序可以定义其视图的相对 URL,并且这些 URL 可以在以后包含在不同的项目中。

让我们从修改 pollsite/urls.py 文件开始,修改后的内容如下

from django.conf.urls.defaults import *

from django.contrib import admin
admin.autodiscover()

urlpatterns = patterns('',
    (r'^admin/(.*)', admin.site.root),
    (r'^polls/', include('pollsite.polls.urls')),
)

请注意,我们添加了模式,该模式表示:如果 URL 以 polls/ 开头,则继续匹配它,遵循模块 pollsite.polls.urls 中定义的模式。因此,让我们创建文件 pollsite/polls/urls.py(请注意,它将位于应用程序内部)并将以下代码放入其中

from django.conf.urls.defaults import *

urlpatterns = patterns('pollsite.polls.views',
    (r'^$', 'index'),
    (r'^(\d+)/$', 'detail'),
    (r'^(\d+)/vote/$', 'vote'),
    (r'^(\d+)/results/$', 'results'),
)

第一个模式表示:如果没有其他内容要匹配(请记住,polls/ 已经由前面的模式匹配),则使用 index 视图。其他模式包括一个用于数字的占位符,在正则表达式中写为 \d+,并且它被捕获(使用括号),因此它将作为参数传递给它们各自的视图。最终结果是,像 polls/5/results/ 这样的 URL 将调用 results 视图,并将字符串 '5' 作为第二个参数传递(第一个视图参数始终是 request 对象)。如果你想了解更多关于 Django URL 分派的知识,请参见 https://docs.django.ac.cn/en/1.0/topics/http/urls/

因此,从我们刚刚创建的 URL 模式可以看出,我们需要编写名为 indexdetailvoteresults 的视图函数。以下是 pollsite/polls/views.py 的代码

from django.shortcuts import get_object_or_404, render_to_response
from django.http import HttpResponseRedirect
from django.core.urlresolvers import reverse
from pollsite.polls.models import Choice, Poll

def index(request):
    latest_poll_list = Poll.objects.all().order_by('-pub_date')[:5]
    return render_to_response('polls/index.html',
                              {'latest_poll_list': latest_poll_list})

def detail(request, poll_id):
    poll = get_object_or_404(Poll, pk=poll_id)
    return render_to_response('polls/detail.html', {'poll': poll})

def vote(request, poll_id):
    poll = get_object_or_404(Poll, pk=poll_id)
    try:
        selected_choice = poll.choice_set.get(pk=request.POST['choice'])
    except (KeyError, Choice.DoesNotExist):
        # Redisplay the poll voting form.
        return render_to_response('polls/detail.html', {
            'poll': poll,
            'error_message': "You didn't select a choice.",
        })
    else:
        selected_choice.votes += 1
        selected_choice.save()
        # Always return an HttpResponseRedirect after successfully dealing
        # with POST data. This prevents data from being posted twice if a
        # user hits the Back button.
        return HttpResponseRedirect(
            reverse('pollsite.polls.views.results', args=(poll.id,)))

def results(request, poll_id):
    poll = get_object_or_404(Poll, pk=poll_id)
    return render_to_response('polls/results.html', {'poll': poll})

我知道这有点快,但请记住,我们正在进行一次快速浏览。这里重要的是要掌握高级概念。此文件中定义的每个函数都是一个视图。你可以通过以下方式识别它们:它们是在 views.py 文件中定义的。但也许更重要的是,因为它们接收 request 作为第一个参数。

因此,我们定义了名为 indexdetailsvoteresults 的视图,它们将在 URL 与之前定义的模式匹配时被调用。除了 vote 之外,它们都很直观,并遵循相同的模式:它们搜索一些数据(使用 Django ORM 和辅助函数,如 get_object_or_404,即使您不熟悉它们,也很容易直观地想象它们的作用),然后最终调用 render_to_response,传递模板的路径和一个包含传递给模板的数据的字典。

注意

上面描述的三个简单视图代表了 Web 开发中非常常见的用例,Django 提供了一个抽象来用更少的代码实现它们。这个抽象被称为“通用视图”,您可以在 https://docs.django.ac.cn/en/1.0/ref/generic-views/ 上了解它们,以及在 Django 教程中 https://docs.django.ac.cn/en/1.0/intro/tutorial04/#use-generic-views-less-code-is-better

vote 视图稍微复杂一些,它也应该如此,因为它执行了有趣的操作,即注册投票。它有两个路径:一个用于用户未选择任何选项的特殊情况,另一个用于用户已选择选项的情况。请注意,在第一种情况下,视图最终会渲染与 detail 视图渲染的相同模板:polls/detail.html,但我们向模板传递了一个额外的变量来显示错误消息,以便用户知道为什么他仍然在查看同一页面。在用户选择选项的成功情况下,我们增加投票数并重定向用户到 results 视图。

我们本可以通过简单地调用视图来实现重定向(类似于 return results(request, poll.id)),但是,正如注释所说,在 POST 提交后进行实际 HTTP 重定向是一个好习惯,以避免浏览器后退按钮(或刷新按钮)出现问题。由于视图代码不知道它们映射到哪些 URL(因为这在您重用应用程序时预计会从一个站点更改到另一个站点),因此 reverse 函数为您提供给定视图和参数的 URL。

在查看模板之前,需要注意的是,Django 模板语言非常简单,并且有意像编程语言那样强大。您不能执行任意 Python 代码,也不能调用任何函数。这样设计是为了保持模板的简单性和 Web 设计师友好性。模板语言的主要功能是表达式,用双括号 ({{}}) 分隔,以及指令(称为“模板标签”),用括号和百分号 ({%%}) 分隔。表达式可以包含点,它们既可以进行属性访问,也可以进行项目访问(因此您可以编写 {{ foo.bar }},即使在 Python 中您会编写 foo['bar']),还可以使用管道将过滤器应用于表达式(例如,在给定的最大长度处截断字符串表达式)。仅此而已。您可以在以下模板中看到它们是多么明显,但在介绍一些不明显的模板标签时,我会做一些解释。

现在,是时候查看我们视图的模板了。正如您从我们刚刚编写的视图代码中推断的那样,我们需要三个模板:polls/index.htmlpolls/detail.htmlpolls/results.html。我们将在 polls 应用程序中创建 templates 子目录,然后在其中创建模板。以下是 pollsite/polls/templates/polls/index.html 的内容

{% if latest_poll_list %}
<ul>
  {% for poll in latest_poll_list %}
  <li><a href="{{ poll.id }}/">{{ poll.question }}</a></li>
  {% endfor %}
</ul>
{% else %}
<p>No polls are available.</p>
{% endif %}

非常简单,如您所见。让我们转到 pollsite/polls/templates/polls/detail.html

<h1>{{ poll.question }}</h1>

{% if error_message %}<p><strong>{{ error_message }}</strong></p>{% endif %}

<form action="./vote/" method="post">
{% for choice in poll.choice_set.all %}
    <input type="radio" name="choice" id="choice{{ forloop.counter }}"
value="{    { choice.id }}" />
    <label for="choice{{ forloop.counter }}">{{ choice.choice }}</label><br />
{% endfor %}
<input type="submit" value="Vote" />
</form>

此模板中一个可能令人惊讶的结构是 {{ forloop.counter }} 表达式,它只是公开围绕 {% for %} 循环的内部计数器。

还要注意,当此模板从 detail 视图调用时,{% if %} 模板标签将对未定义的表达式求值为 false,就像 error_message 一样。

最后,这里是 pollsite/polls/templates/polls/results.html

<h1>{{ poll.question }}</h1>

<ul>
{% for choice in poll.choice_set.all %}
    <li>{{ choice.choice }} -- {{ choice.votes }}
    vote{{ choice.votes|pluralize }}</li>
{% endfor %}
</ul>

在此模板中,您可以看到过滤器在表达式 {{ choice.votes|pluralize }} 中的使用。如果投票数大于 1,它将输出一个“s”,否则什么也不输出。要了解有关 Django 中默认提供的模板标签和过滤器的更多信息,请参阅 https://docs.django.ac.cn/en/1.0/ref/templates/builtins/。要了解有关其工作原理以及如何创建新的过滤器和模板标签的更多信息,请参阅 https://docs.django.ac.cn/en/1.0/ref/templates/api/

此时,我们拥有一个完全可用的投票网站。它并不漂亮,需要很多打磨。但它确实有效!尝试导航到 http://localhost:8000/polls/

在没有“include”的情况下重用模板:模板继承

与许多其他模板语言一样,Django 也具有“include”指令。但它的使用非常少见,因为有一个更好的解决方案来重用模板:继承。

它的工作原理与类继承完全相同。您定义一个基本模板,其中包含许多“块”。每个块都有一个名称。然后其他模板可以从基本模板继承并覆盖或扩展这些块。您可以自由地构建任意长度的继承链,就像类层次结构一样。

您可能已经注意到,我们的模板没有生成有效的 HTML,而只是片段。当然,专注于模板的重要部分很方便。但它也恰好是,通过非常小的修改,它们将生成完整的、漂亮的 HTML 页面。正如您可能已经猜到的,它们将从一个站点范围内的基本模板继承。

由于我对网页设计不太擅长,我们将从 http://www.freecsstemplates.org/ 获取一个现成的模板。特别是,我们将修改此模板:http://www.freecsstemplates.org/preview/exposure/

请注意,基本模板将是站点范围内的,因此它属于项目,而不是应用程序。我们将在项目目录下创建一个 templates 子目录。以下是 pollsite/templates/base.html 的内容

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
"http://www.w3.org/TR/    xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
  <head>
    <meta http-equiv="content-type" content="text/html; charset=utf-8" />
    <title>Polls</title>
    <link rel="alternate" type="application/rss+xml"
          title="RSS Feed"  href="/feeds/polls/" />
    <style>
      /* Irrelevant CSS code, see book sources if you are interested */
    </style>
  </head>
  <body>
    <!-- start header -->
    <div id="header">
      <div id="logo">
        <h1><a href="/polls/">Polls</a></h1>
        <p>an example for the Jython book</a></p>
      </div>
      <div id="menu">
        <ul>
          <li><a href="/polls/">Home</a></li>
          <li><a href="/contact/">Contact Us</a></li>
          <li><a href="/admin/">Admin</a></li>
        </ul>
      </div>
    </div>
<!-- end header -->
<!-- start page -->
    <div id="page">
    <!-- start content -->
      <div id="content">
        {% block content %} {% endblock %}
      </div>
<!-- end content -->
      <br style="clear: both;" />
    </div>
<!-- end page -->
<!-- start footer -->
    <div id="footer">
      <p> <a href="/feeds/polls/">Subscribe to RSS Feed</a> </p>
      <p class="legal">
        &copy;2009 Apress. All Rights Reserved.
        &nbsp;&nbsp;&bull;&nbsp;&nbsp;
        Design by
        <a href="http://www.freecsstemplates.org/">Free CSS Templates</a>
        &nbsp;&nbsp;&bull;&nbsp;&nbsp;
        Icons by <a href="http://famfamfam.com/">FAMFAMFAM</a>. </p>
    </div>
<!-- end footer -->
  </body>
</html>

如您所见,模板只声明了一个名为“content”的块(在模板末尾的页脚之前)。您可以定义任意数量的块,但为了简单起见,我们只做一项。

现在,为了让 Django 找到此模板,我们需要调整设置。编辑 pollsite/settings.py 并找到 TEMPLATE_DIRS 部分。用以下内容替换它

import os
TEMPLATE_DIRS = (
    os.path.dirname(__file__) + '/templates',
    # Put strings here, like "/home/html/django_templates" or
    # "C:/www/django/templates".
    # Always use forward slashes, even on Windows.
    # Don't forget to use absolute paths, not relative paths.
)

这是一个避免硬编码项目根目录的技巧。这个技巧可能不适用于所有情况,但对我们来说是有效的。现在,我们已经有了这个 base.html 模板,我们将在 pollsite/polls/templates/polls/index.html 中从它继承

{% extends 'base.html' %}
{% block content %}
{% if latest_poll_list %}
<ul>
  {% for poll in latest_poll_list %}
  <li><a href="{{ poll.id }}/">{{ poll.question }}</a></li>
  {% endfor %}
</ul>
{% else %}
<p>No polls are available.</p>
{% endif %}
{% endblock %}

如您所见,更改仅限于添加前两行和最后一行。实际意义是模板正在覆盖“内容”块并继承所有其他内容。对投票应用程序的其他两个模板执行相同的操作,然后再次测试应用程序,访问 http://localhost:8000/polls/。它将显示在图 应用模板后的投票网站 中。

../_images/chapter14-tour-aftertemplate.png

应用模板后的投票网站

此时,我们可以认为我们的示例 Web 应用程序已完成。但我希望强调 Django 中包含的一些其他功能,这些功能可以帮助您开发 Web 应用程序(就像管理员一样)。为了展示它们,我们将向我们的网站添加以下功能

  1. 联系表格(请注意,该链接已包含在我们的通用基本模板中)
  2. 最新投票的 RSS 提要(请注意,该链接已添加到页脚)
  3. 投票的用户评论。

表单

Django 提供了一些帮助来处理 HTML 表单,这总是有点乏味。我们将使用此帮助来实现“联系我们”功能。由于它听起来像一个可以在将来重复使用的通用功能,因此我们将为它创建一个新应用程序。移动到项目目录并运行

$ jython manage.py startapp contactus

请记住在 pollsite/settings.py 中的 INSTALLED_APPS 列表中添加此应用程序的条目,作为 'pollsite.contactus'

然后,我们将通过修改 pollsite/urls.py 并为其添加一行来委托 URL 与 /contact/ 模式匹配

from django.conf.urls.defaults import *

from django.contrib import admin
admin.autodiscover()

urlpatterns = patterns('',
    (r'^admin/(.*)', admin.site.root),
    (r'^polls/', include('pollsite.polls.urls')),
    (r'^contact/', include('pollsite.contactus.urls')),
)

稍后,我们创建 pollsite/contactus/urls.py。为简单起见,我们将只使用一个视图来显示和处理表单。因此,文件 pollsite/contactus/urls.py 将仅包含

from django.conf.urls.defaults import *

urlpatterns = patterns('pollsite.contactus.views',
    (r'^$', 'index'),
)

以及 pollsite/contactus/views.py 的内容是

from django.shortcuts import render_to_response
from django.core.mail import mail_admins
from django import forms

class ContactForm(forms.Form):
    name = forms.CharField(max_length=200)
    email = forms.EmailField()
    title = forms.CharField(max_length=200)
    text = forms.CharField(widget=forms.Textarea)


def index(request):
    if request.method == 'POST':
        form = ContactForm(request.POST)
        if form.is_valid():
            mail_admins(
                "Contact Form: %s" % form.title,
                "%s <%s> Said: %s" % (form.name, form.email, form.text))
            return render_to_response("contactus/success.html")
    else:
        form = ContactForm()
    return render_to_response("contactus/form.html", {'form': form})

这里的重要部分是 ContactForm 类,其中声明性地定义了表单,并封装了验证逻辑。我们只需在视图上调用 is_valid() 方法即可调用该逻辑并相应地采取行动。请参阅 https://docs.django.ac.cn/en/1.0/topics/email/#mail-admins 了解 Django 中包含的 main_admins 函数以及如何调整项目设置以使其正常工作。

表单还提供快速的方法在模板中呈现它们。我们现在将尝试一下。这是 pollsite/contactus/templates/contactus/form.html 的代码,它是我们刚刚编写的视图使用的模板

{% extends "base.html" %}
{% block content %}
<form action="." method="POST">
<table>
{{ form.as_p }}
</table>
<input type="submit" value="Send Message" >
</form>
{% endblock %}

在这里,我们利用了 Django 表单的 as_table() 方法,该方法还负责呈现验证错误。Django 表单还提供其他便利函数来呈现表单,但如果它们都不适合您的需要,您始终可以以自定义方式呈现表单。有关表单处理的详细信息,请参阅 https://docs.django.ac.cn/en/1.0/topics/forms/

在测试此联系表格之前,我们需要编写模板 pollsite/contactus/templates/contactus/success.html,它也来自 pollsite.contactus.views.index:: 此模板非常简单

{% extends "base.html" %}
{% block content %}
<h1> Send us a message </h1>
<p><b>Message received, thanks for your feedback!</p>
{% endblock %}

我们完成了。通过导航到 http://localhost:8000/contact/ 来测试它。尝试在没有数据的情况下提交表单,或者使用错误的数据(例如使用无效的电子邮件地址)。您将获得类似于图 Django 表单验证正在运行 中显示的内容。无需编写太多代码,您就可以获得所有这些验证,几乎是免费的。当然,表单框架是可扩展的,因此您可以创建具有自己的验证或呈现代码的自定义表单字段类型。同样,我将您推荐到 https://docs.django.ac.cn/en/1.0/topics/forms/ 以获取详细信息。

../_images/chapter14-tour-formvalidation.png

Django 表单验证正在运行

提要

现在是实现我们在页脚之前的链接中提供的提要的时候了。您一定不会感到惊讶地知道 Django 包含声明性地声明提要并快速编写它们的方法。让我们从修改 pollsite/urls.py 开始,使其如下所示

from django.conf.urls.defaults import *
from pollsite.polls.feeds import PollFeed

from django.contrib import admin
admin.autodiscover()

urlpatterns = patterns('',
    (r'^admin/(.*)', admin.site.root),
    (r'^polls/', include('pollsite.polls.urls')),
    (r'^contact/', include('pollsite.contactus.urls')),
    (r'^feeds/(?P<url>.*)/$', 'django.contrib.syndication.views.feed',
     {'feed_dict': {'polls': PollFeed}}),
)

更改包括导入 PollFeed 类(我们还没有编写)以及以 /feeds/ 开头的 URL 的最后一个模式,它将映射到一个内置视图,该视图接受一个包含 feed 的字典作为参数(在我们的例子中,PollFeed 是唯一的)。编写这个描述 feed 的类非常容易。让我们创建文件 pollsite/polls/feeds.py 并将以下代码放入其中

from django.contrib.syndication.feeds import Feed
from django.core.urlresolvers import reverse
from pollsite.polls.models import Poll

class PollFeed(Feed):
    title = "Polls"
    link = "/polls"
    description = "Latest Polls"

    def items(self):
        return Poll.objects.all().order_by('-pub_date')

    def item_link(self, poll):
        return reverse('pollsite.polls.views.detail', args=(poll.id,))

    def item_pubdate(self, poll):
        return poll.pub_date

我们几乎准备好了。当 Django 接收到对 URL /feeds/polls/ 的请求时,它将使用此 feed 描述来构建所有 XML 数据。缺少的部分是如何在 feed 上显示民意调查的内容。为此,我们需要创建一个模板。按照惯例,它必须命名为 feeds/<feed_name>_description.html,其中 <feed_name> 是我们在 pollsite/urls.py 中的 feed_dict 中指定的键。因此,我们创建文件 pollsite/polls/templates/feeds/polls_description.html,其内容非常简单

 <ul>
   {% for choice in obj.choice_set.all %}
   <li>{{ choice.choice }}</li>
   {% endfor %}
</ul>

这个想法很简单:Django 将 PollFeed.items() 返回的每个对象传递给此模板,在其中它将名称设置为 obj。然后,您生成一个 HTML 片段,该片段将嵌入到 feed 结果中。

就这样。通过将浏览器指向 http://localhost:8000/feeds/polls/ 或使用您喜欢的 feed 阅读器订阅该 URL 来测试它。例如,Opera 会显示图 Opera 浏览器中显示的民意调查 feed 所示的 feed。

../_images/chapter14-tour-feed.png

Opera 浏览器中显示的民意调查 feed

评论

由于评论是当前网站的常见功能,Django 包含一个迷你框架,可以轻松地将评论合并到任何项目或应用程序中。我将向您展示如何在我们的项目中使用它。首先,为 Django 评论应用程序添加一个新的 URL 模式,因此 pollsite/urls.py 文件将如下所示

from django.conf.urls.defaults import *
from pollsite.polls.feeds import PollFeed

from django.contrib import admin
admin.autodiscover()

urlpatterns = patterns('',
    (r'^admin/(.*)', admin.site.root),
    (r'^polls/', include('pollsite.polls.urls')),
    (r'^contact/', include('pollsite.contactus.urls')),
    (r'^feeds/(?P<url>.*)/$', 'django.contrib.syndication.views.feed',
     {'feed_dict': {'polls': PollFeed}}),
    (r'^comments/', include('django.contrib.comments.urls')),
)

然后,将 'django.contrib.comments' 添加到 pollsite/settings.py 上的 INSTALLED_APPS。之后,我们将让 Django 通过运行以下命令创建必要的表格

$ jython manage.py syncdb

评论将添加到民意调查页面,因此我们必须编辑 pollsite/polls/templates/polls/detail.html。我们将在当前是文件最后一行 {% endblock %} 行之前添加以下代码

{% load comments %}
{% get_comment_list for poll as comments %}
{% get_comment_count for poll as comments_count %}

{% if comments %}
<p>{{ comments_count }} comments:</p>
{% for comment in comments %}
<div class="comment">
  <div class="title">
    <p><small>
    Posted by <a href="{{ comment.user_url }}">{{ comment.user_name }}</a>,
        {{ comment.submit_date|timesince }} ago:
    </small></p>
  </div>
  <div class="entry">
    <p>
    {{ comment.comment }}
    </p>
  </div>
</div>

{% endfor %}

{% else %}
<p>No comments yet.</p>
{% endif %}

<h2>Left your comment:</h2>
{% render_comment_form for poll %}

基本上,我们正在导入“comments”模板标签库(通过执行 {% load comments %}),然后我们直接使用它。它支持将评论绑定到任何数据库对象,因此我们不需要做任何特殊的事情来使其工作。图 评论驱动的民意调查 显示了我们为此简短代码片段所获得的内容。

../_images/chapter14-tour-comments.png

评论驱动的民意调查

如果您自己尝试该应用程序,您会注意到在提交评论后,您会看到一个显示成功消息的丑陋页面。或者,如果您没有输入所有数据,则会显示一个丑陋的错误表单。这是因为我们正在使用评论模板。对此的快速有效修复是创建文件 pollsite/templates/comments/base.html,其内容如下

{% extends 'base.html' %}

是的,只有一行!它展示了模板继承的强大功能:我们所需要做的就是将评论框架的基模板更改为从我们的全局基模板继承。

等等…

到目前为止,我希望你已经学会欣赏 Django 的优势:它本身就是一个非常好的 Web 框架,但它也采用了“自带电池”的理念,并为 Web 开发中的许多常见问题提供了解决方案。这通常会大大加快创建新网站的过程。而且我们还没有触及 Django 开箱即用的其他常见主题,例如用户身份验证(从登录对话框到轻松声明哪些视图需要经过身份验证的用户,或者具有某些特殊特征的用户,例如网站管理员),或者通用视图。

但本书是关于Jython的,我们将利用本章的剩余部分来展示在Jython上运行 Django 时出现的有趣可能性。如果你想了解更多关于 Django 本身的信息,我建议你再次参考官方文档,该文档可以在 https://docs.django.ac.cn/ 上找到。

J2EE 部署和集成

虽然你可以使用 Django 内置的开发服务器来部署你的应用程序,但这绝对不是一个好主意。开发服务器不是为在高负载下运行而设计的,这实际上是一项更适合于适当的应用程序服务器的工作。我们将安装 Glassfish v2.1 - 来自 Sun Microsystems 的一个开源高性能 JavaEE 5 应用程序服务器,并展示如何将其部署到该服务器上。

现在让我们安装 Glassfish - 从以下地址获取发行版

https://glassfish.dev.java.net/public/downloadsindex.html

在撰写本文时,Glassfish v3.0 正在准备发布,它将开箱即用地支持 Django 和 Jython,但我们将坚持使用稳定版本,因为其文档和稳定性已经得到很好的验证。下载 v2.1 版本(目前为 v2.1-b60e)。我强烈建议你使用 JDK6 来进行部署。

获得安装 JAR 文件后,你可以通过以下命令进行安装

% java -Xmx256m -jar glassfish-installer-v2.1-b60e-windows.jar

如果你的 Glassfish 安装文件具有不同的名称,只需使用该名称代替上述示例中列出的文件名即可。但要小心你在哪里调用此命令 - Glassfish 会将应用程序服务器解压缩到你在其中启动安装程序的目录中的“glassfish”子目录中。

在我急于安装 Glassfish 的过程中,有一个步骤让我很头疼,那就是你实际上需要调用 ant 来完成安装。在 UNIX 上,你需要调用

% chmod -R +x lib/ant/bin
% lib/ant/bin/ant -f setup.xml

或者在 Windows 上

% lib\ant\bin\ant -f setup.xml

这将完成设置 - 你会发现一个包含“asadmin”或“asadmin.bat”的 bin 目录,这将表明应用程序服务器已安装。你可以通过以下命令启动服务器

% bin/asadmin start_domain -v

在 Windows 上,这将在前台启动服务器 - 该进程不会成为守护进程并在后台运行。在 UNIX 操作系统上,该进程将自动成为守护进程并在后台运行。无论哪种情况,一旦服务器启动并运行,你就可以通过浏览器访问 Web 管理屏幕,方法是访问 http://localhost:5000/。默认登录名为“admin”,密码为“adminadmin”。

目前,Jython 上的 Django 仅官方支持 PostgreSQL 数据库,但也有一个 SQL Server 后端的预发布版本,以及一个 SQLite3 后端。让我们让 PostgreSQL 后端工作起来 - 你需要从 http://jdbc.PostgreSQL.org 获取 PostgreSQL JDBC 驱动程序。

在撰写本文时,最新版本是 postgresql-8.4-701.jdbc4.jar,将该 jar 文件复制到你的 GLASSFISH_HOME/domains/domain/domain1/lib 目录中。这将使你托管在应用程序服务器中的所有应用程序都能使用相同的 JDBC 驱动程序。

你现在应该有一个包含以下内容的 GLASSFISH_HOME/domains/domain1/lib 目录

applibs/
classes/
databases/
ext/
postgresql-8.3-604.jdbc4.jar

你需要停止并启动应用程序服务器,以让这些库加载起来。

% bin/asadmin stop_domain
% bin/asadmin start_domain -v

部署你的第一个应用程序

Jython 上的 Django 包含一个内置命令来支持 WAR 文件的创建,但首先,你需要进行一些配置,以确保一切顺利运行。首先,我们将设置一个简单的 Django 应用程序,该应用程序启用了管理应用程序,以便我们有一些模型可以用来玩。创建一个名为“hello”的项目,并确保你将“django.contrib.admin”和“doj”应用程序添加到 INSTALLED_APPS 中。

现在通过编辑 urls.py 并取消注释 admin 行来启用用户管理。您的 urls.py 现在应该看起来像这样

from django.conf.urls.defaults import *
from django.contrib import admin
admin.autodiscover()
urlpatterns = patterns('',
    (r'^admin/(.*)', admin.site.root),
)

禁用 PostgreSQL 登录

在开发机器上使用 PostgreSQL 时,我不可避免地要做的第一件事是禁用对数据库的身份验证检查。最快的解决方法是通过编辑 pg_hba.conf 文件来仅启用对数据库的本地连接。对于 PostgreSQL 8.3,此文件通常位于 c:PostgreSQL8.3datapg_hba.conf,在 UNIX 系统上,它通常位于 /etc/PostgreSQL/8.3/data/pg_hba.conf

在文件底部,您会找到连接配置信息。注释掉所有行并启用来自 localhost 的受信任连接。您编辑后的配置应该看起来像这样

# TYPE  DATABASE    USER        CIDR-ADDRESS          METHOD
host    all         all         127.0.0.1/32          trust

这将允许任何用户名密码连接到数据库。您不希望在面向公众的生产服务器上执行此操作。您应该查阅 PostgreSQL 文档以获取更合适设置的说明。编辑连接配置后,您需要重新启动 PostgreSQL 服务器。

现在使用 createdb 命令创建您的 PostgreSQL 数据库

> createdb demodb

设置数据库很简单 - 只需在 Jython 上从 Django 启用 pgsql 后端即可。请注意,即使我们在 PostgreSQL 中禁用了它们,后端也需要用户名和密码对。您可以为 DATABASE_NAME 和 DATABASE_USER 设置填充任何您想要的内容。现在,您的 settings 模块的数据库部分应该看起来像这样

DATABASE_ENGINE = 'doj.backends.zxjdbc.postgresql'
DATABASE_NAME = 'demodb'
DATABASE_USER = 'ngvictor'
DATABASE_PASSWORD = 'nosecrets'

现在初始化您的数据库

> jython manage.py syncdb Creating table django_admin_log Creating table auth_permission Creating table auth_group Creating table auth_user Creating table auth_message Creating table django_content_type Creating table django_session Creating table django_site

您刚刚安装了 Django 的身份验证系统,这意味着您没有定义任何超级用户。您现在想创建一个吗?(yes/no): yes Username: admin E-mail address: admin@abc.com Warning: Problem with getpass. Passwords may be echoed. Password: admin Warning: Problem with getpass. Passwords may be echoed. Password (again): admin Superuser created successfully. Installing index for admin.LogEntry model Installing index for auth.Permission model Installing index for auth.Message model

到目前为止,所有这些都应该是回顾,现在我们将获取应用程序并将其部署到正在运行的 Glassfish 服务器中。这实际上是最简单的一部分。Jython 上的 Django 带有一个自定义的“war”命令,该命令会构建一个自包含文件,您可以使用它部署到任何 Java servlet 容器中。

关于 WAR 文件的说明

对于 JavaEE 服务器,部署应用程序的常见方法是部署“WAR”文件。这只是一个用于 zip 文件的奇特名称,其中包含您的应用程序及其所需的任何依赖项,这些依赖项应用程序服务器尚未作为共享资源提供。这是一种可靠的方法,可以确保您最大程度地减少库版本控制更改的影响,如果您想在应用程序服务器中部署多个应用程序。

考虑一下您的 Django 应用程序随着时间的推移 - 您无疑会升级 Django 的版本,您可能会升级数据库驱动程序的版本 - 您甚至可能会决定升级您希望部署的 Jython 语言的版本。如果您将所有依赖项捆绑到 WAR 文件中,这些选择最终取决于您。通过将所有依赖项捆绑到 WAR 文件中,您可以确保您的应用程序在您部署它时“正常工作”。服务器会自动将每个应用程序划分为自己的空间,并使用同一代码的并发运行版本。

要启用 war 命令,请将“doj”应用程序添加到 INSTALLED_APPS 列表中的设置中。接下来,您需要启用站点的媒体目录和媒体的上下文相关根目录。编辑您的 settings.py 模块,以便您的媒体文件被正确配置以供提供服务。war 命令会自动配置您的媒体文件,以便它们使用静态文件 servlet 提供服务,并且 URL 将重新映射到上下文根目录之后。

编辑您的 settings 模块并配置 MEDIA_ROOT 和 MEDIA_URL 行。

MEDIA_ROOT = ‘c:\dev\hello\media_root’ MEDIA_URL = ‘/site_media/’

现在,您需要在“hello”项目下创建 media_root 子目录,并放入一个示例文件,以便您可以验证静态内容服务是否正常工作。将文件“sample.html”放入您的 media_root 目录中。将您想要的内容放入其中 - 我们只是使用它来确保静态文件被正确提供服务。

用英语来说 - 这意味着当使用上述配置时 - “hello”将部署到您的 servlet 容器中,并且容器会分配一些 URL 路径作为 Glassfish 中的“上下文根目录” - 这意味着您的应用程序将位于“http://localhost:8000/hello/”。site_media 目录将在“http://localhost:8000/hello/site_media”中可见。DOJ 会自动将静态内容设置为由 Glassfish 的 fileservlet 提供服务,该文件已经非常高效。对于大多数部署,无需为静态文件服务器设置单独的服务器。

现在使用标准的 manage.py 脚本构建您的 war 文件,并使用 asadmin 工具部署

c:\dev\hello>jython manage.py war

Assembling WAR on c:\docume~1\ngvictor\locals~1\temp\tmp1-_snn\hello

Copying WAR skeleton...
Copying jython.jar...
Copying Lib...
Copying django...
Copying media...
Copying hello...
Copying site_media...
Copying doj...
Building WAR on C:\dev\hello.war...
Cleaning c:\docume~1\ngvictor\locals~1\temp\tmp1-_snn...

Finished.

Now you can copy C:\dev\hello.war to whatever location your application server wants it.

C:\dev\hello>cd \glassfish
C:\glassfish>bin\asadmin.bat deploy hello.war
Command deploy executed successfully.

C:\glassfish>

就是这样。您现在应该能够看到您的应用程序正在运行

http://localhost:8080/hello/

管理屏幕也应该在以下位置可见

您可以通过访问以下地址来验证您的静态媒体是否正常提供服务

就是这样。您对 servlet 容器的基本部署现在正在运行。

扩展安装

doj 中的 war 命令为您提供了额外的选项,用于指定要与您的应用程序一起包含的额外 JAR 文件,这些文件可以缩小 WAR 文件的大小。默认情况下,'war' 命令将捆绑以下项目

  • Jython
  • Django 及其管理媒体文件
  • 您的项目和媒体文件
  • 您在 site-packages 中的所有库

您可以专门化您的 WAR 文件以包含特定的 JAR 文件,并且您可以指示 doj 仅使用您需要的 python 包来组装 WAR 文件。 “manage.py war” 的相应选项是 “–include-py-packages” 和 “–include-jar-libs”。基本用法很简单,只需将您自定义 python 包的位置和 JAR 文件传递给这两个参数,distutils 就会自动解压缩这些压缩卷的内容并将它们重新压缩到您的 WAR 文件中。

要捆绑 JAR 文件,您需要指定一个文件列表到 “–include-java-libs”。

以下示例将 jTDS JAR 文件和一个名为 urllib3 的常规 python 模块捆绑到我们的 WAR 文件中。

$ jython manage.py war --include-java-libs=$HOME/downloads/jtds-1.2.2.jar \
        --include-py-package=$HOME/PYTHON_ENV/lib/python2.5/site-packages/urllib3

您可以列出多个 JAR 文件或 python 包,但必须使用操作系统的路径分隔符分隔它们。对于 UNIX 系统 - 这意味着 “:” 对于 Windows,它是 “;”。

鸡蛋也可以使用 “–include-py-path-entries” 使用鸡蛋文件名进行安装。例如

$ jython manage.py war --include-py-path-entries=$HOME/PYTHON_ENV/lib/python2.5/site-packages/urllib3

使用 JavaEE 连接池

每当您的 Web 应用程序从数据库中获取数据时,该数据都必须通过数据库连接返回。一些数据库具有“廉价”的数据库连接,例如 MySQL,但对于许多数据库而言,创建和释放连接非常昂贵。在高负载条件下,在每个请求上打开和关闭数据库连接会很快消耗掉太多文件句柄,您的应用程序将崩溃。

对此的通用解决方案是使用数据库连接池。虽然您的应用程序将继续创建新连接并关闭它们,但连接池将从可重用集中管理您的数据库连接。当您要关闭连接时,连接池只会简单地回收您的连接以供以后使用。使用池意味着您可以对数据库的并发连接数量施加强制上限限制。有了这个上限,您就可以推断出当达到数据库连接的上限时您的应用程序将如何执行。

虽然 Django 本身不支持使用 CPython 的数据库连接池,但您可以在 Jython 上的 Django 的 PostgreSQL 驱动程序中启用它们。在 Glassfish 中,创建对 Django/Jython 可见的连接池是一个两步过程。首先,我们需要创建一个 JDBC 连接池,然后我们需要将 JNDI 名称绑定到该池。在 JavaEE 容器中,JNDI - Java 命名和目录接口 - 是一个绑定到对象的名称注册表。它实际上最好被认为是一个哈希表,它通常抽象出一个发出对象的工厂。

在数据库连接的情况下,JNDI 抽象出一个 ConnectionFactory,它提供行为类似于数据库连接的代理对象。这些代理会自动为我们管理所有池行为。现在让我们在实践中看看这一点。

首先,我们需要创建一个 JDBC ConnectionFactory。转到 Glassfish 的管理屏幕,然后向下转到 Resources/JDBC/JDBC Resources/Connection Pools。从那里,您可以单击“新建”按钮并开始配置您的池。

将名称设置为“pgpool-demo”,资源类型应为“javax.sql.ConnectionPoolDataSource”,数据库供应商应为 PostgreSQL。单击“下一步”。

在下一页的底部,您将看到一个带有“附加属性”的部分。您需要设置四个参数以确保连接正常工作,假设数据库配置为 ngvictor/nosecrets 的用户名/密码 - 以下是连接到数据库所需的内容。

名称
databaseName demodb
服务器名称 localhost
密码 nosecrets
用户 ngvictor

您可以安全地删除所有其他属性 - 它们不再需要。单击“完成”。

您的连接池现在将在连接池列表中左侧的树形控件中可见。选择它并尝试对其进行 ping 测试以确保它正常工作。如果一切正常,Glassfish 将向您显示一条成功的 Ping 消息。

现在,我们需要将 JNDI 名称绑定到连接工厂,以提供一种机制让 Jython 看到连接池。转到 JDBC 资源,然后单击“新建”。使用 JNDI 名称:“jdbc/pgpool-demo”,选择“pgpool-demo”作为您的池名称,然后点击“确定”。

从命令行验证资源是否可用

glassfish\bin $ asadmin list-jndi-entries --context jdbc
Jndi Entries for server within jdbc context:
pgpool-demo__pm: javax.naming.Reference
__TimerPool: javax.naming.Reference
__TimerPool__pm: javax.naming.Reference
pgpool-demo: javax.naming.Reference
Command list-jndi-entries executed successfully.

现在,我们需要启用 Django 应用程序在应用程序服务器中运行时使用基于 JNDI 名称的查找,如果找不到 JNDI,则回退到常规数据库连接绑定。编辑您的 settings.py 模块并添加额外的配置以启用 JNDI。

DATABASE_ENGINE = 'doj.backends.zxjdbc.postgresql'
DATABASE_NAME = 'demodb'
DATABASE_USER = 'ngvictor'
DATABASE_PASSWORD = 'nosecrets'
DATABASE_OPTIONS  = {'RAW_CONNECTION_FALLBACK': True, \
                     'JNDI_NAME': 'jdbc/pgpool-demo' }

请注意,我们正在复制连接到数据库的配置。这是因为我们希望能够在 JNDI 查找失败的情况下回退到常规连接绑定。这使我们在测试或开发环境中运行时更容易。

就这样。

您已完成数据库连接池的配置。现在还不错吧?

处理长时间运行的任务

在构建复杂的 Web 应用程序时,您最终将不得不处理需要在后台处理的进程。如果您是在 CPython 和 Apache 之上构建的,那么您就走运了 - 没有可用的标准基础设施供您处理这些任务。幸运的是,这些服务在 Java 世界中已经为您完成了多年的工程工作。我们将研究两种处理长时间运行任务的不同策略。

线程池

第一个策略是利用 JavaEE 容器中的托管线程池。当您的 Web 应用程序在 Glassfish 中运行时,每个 HTTP 请求都由包含线程池的 HTTP 服务处理。您可以更改线程数量以影响 Web 服务器的性能。Glassfish 还允许您创建自己的线程池来为您执行任意工作单元。

线程池的基本 API 很简单

  • WorkManager 提供了对线程池的抽象接口
  • Work 是一个接口,它封装了您的工作单元
  • WorkListener 是一个接口,它允许您监控 Work 任务的进度。

首先,我们需要告诉 Glassfish 为我们提供一个线程池。在管理屏幕中,向下滚动到配置/线程池。单击“新建”以创建一个新的线程池。为您的线程池命名为“backend-workers”。将所有其他设置保留为默认值,然后单击“确定”。

您现在拥有了一个可以使用线程池。线程池公开了一个接口,您可以在其中向池提交作业,池将在线程中同步执行作业,或者您可以安排作业异步运行。只要您的工作单元实现了 javax.resource.spi.work.Work 接口,线程池就会很乐意运行您的代码。一个类单元可能与以下代码片段一样简单

from javax.resource.spi.work import Work

class WorkUnit(Work):
    """
    This is an implementation of the Work interface.
    """
    def __init__(self, job_id):
        self.job_id = job_id

    def release(self):
        """
        This method is invoked by the threadpool to tell threads
        to abort the execution of a unit of work.
        """
        logger.warn("[%d] Glassfish asked the job to stop quickly" % self.job_id)

    def run(self):
        """
        This method is invoked by the threadpool when work is
        'running'
        """
        for i in range(20):
            logger.info("[%d] just doing some work" % self.job_id)

上面的 WorkUnit 类没有做任何有趣的事情,但它确实说明了工作单元所需的基本结构。我们只是将消息记录到磁盘,以便我们可以直观地看到线程执行。

WorkManager 实现了几种方法,这些方法可以运行您的作业并阻塞,直到线程池完成您的工作,或者它可以异步运行作业。通常,我更喜欢异步运行事物,并且只是定期检查工作状态。这使我能够一次向线程池提交多个作业并检查每个作业的状态。

为了监控工作的进度,我们需要实现 WorkListener 接口。此接口在任务在线程池中执行的 3 个阶段中进行时向我们提供通知。这些状态是

  1. 已接受
  2. 已启动
  3. 已完成

所有作业必须进入已完成或已拒绝状态。最简单的方法是简单地构建捕获事件的列表。当已完成列表和已拒绝列表的长度之和与我们提交的作业数量相同时,我们就知道我们已经完成了。通过使用列表而不是简单的计数器,我们可以更详细地检查工作对象。

以下是我们 SimpleWorkListener 的代码

from javax.resource.spi.work import WorkListener
class SimpleWorkListener(WorkListener):
    """
    Just keep track of all work events as they come in
    """
    def __init__(self):
        self.accepted = []
        self.completed = []
        self.rejected = []
        self.started = []

    def workAccepted(self, work_event):
        self.accepted.append(work_event.getWork())
        logger.info("Work accepted %s" % str(work_event.getWork()))

    def workCompleted(self, work_event):
        self.completed.append(work_event.getWork())
        logger.info("Work completed %s" % str(work_event.getWork()))

    def workRejected(self, work_event):
        self.rejected.append(work_event.getWork())
        logger.info("Work rejected %s" % str(work_event.getWork()))

    def workStarted(self, work_event):
        self.started.append(work_event.getWork())
        logger.info("Work started %s" % str(work_event.getWork()))

要访问线程池,您只需要知道我们要访问的池的名称并安排我们的作业。每次安排工作单元时,我们都需要告诉池等待多长时间直到我们超时作业,并提供对 WorkListener 的引用,以便我们可以监控作业的状态。

执行此操作的代码如下所示

from com.sun.enterprise.connectors.work import CommonWorkManager
from javax.resource.spi.work import Work, WorkManager, WorkListener
wm = CommonWorkManager('backend-workers')
listener = SimpleWorkListener()
for i in range(5):
    work = WorkUnit(i)
    wm.scheduleWork(work, -1, None, listener)

您可能会注意到 scheduleWork 方法在第三个参数中接受了一个 None。这是执行上下文 - 就我们而言,最好忽略它并将其设置为 None。scheduleWork 方法将立即返回,并且当我们的工作对象通过时,侦听器将获得回调通知。要验证我们所有作业是否已完成(或被拒绝) - 我们只需检查侦听器的内部列表即可。

while len(listener.completed) + len(listener.rejected) < num_jobs:
    logger.info("Found %d jobs completed" % len(listener.completed))
    time.sleep(0.1)

这涵盖了您访问线程池和监控每个工作单元状态所需的所有代码。忽略实际的 WorkUnit 类,管理线程池的实际代码大约只有十行。

JavaEE 标准和线程池

不幸的是,此 API 尚未在 JavaEE 5 规范中标准化,因此此处列出的代码仅在 Glassfish 中有效。并行处理的 API 正在为 JavaEE 6 标准化,在此之前,您需要了解特定应用程序服务器的一些内部机制才能使线程池正常工作。如果您使用的是 Weblogic 或 Websphere,则需要使用 CommonJ API 来访问线程池,但逻辑基本相同。

跨进程边界传递消息

虽然线程池提供了对后台作业处理的访问,但有时跨进程边界传递消息可能会有益。每周似乎都会出现一个新的 Python 包试图解决这个问题,对于 Jython,我们很幸运地可以利用 Java 的 JMS。JMS 指定了一种消息代理技术,您可以在其中定义发布/订阅或点对点消息传递,以便在不同的服务之间传递消息。消息是异步发送的,以提供松散耦合,代理处理所有繁琐的工程细节,例如传递保证、安全性、服务器崩溃之间的消息持久性和集群。

虽然您可以使用手工制作的 RESTful 消息传递实现 - 使用 OpenMQ 和 JMS 具有许多优势。

  1. 它很成熟。您真的认为您的消息传递实现处理了所有边缘情况吗?服务器崩溃?网络连接错误?可靠性保证?集群?安全?OpenMQ 背后有近 10 年的工程经验。这是有原因的。
  2. JMS 标准就是这样 - 标准。您获得了在任何 JavaEE 代码之间发送和接收消息的能力。
  3. 互操作性。JMS 不是城里唯一的消息代理。流式文本定向消息协议 (STOMP) 是另一个在非 Java 开发人员中很流行的标准。您可以使用 stompconnect 将 JMS 代理转换为 STOMP 代理。这意味着您可以有效地在任何消息传递客户端和任何消息传递代理之间传递消息,使用十多种不同的语言。

在 JMS 中,有两种类型的消息传递机制

  • 发布/订阅:当我们想要向一个或多个订阅者发送有关正在发生的事件的消息时,可以使用此方法。这是通过 JMS 的“主题”完成的。
  • 点对点消息传递:这些是单发送方、单接收方消息队列。适当地,JMS 将这些称为“队列”。

我们需要在 Glassfish 中配置几个对象才能使 JMS 运行。简而言之,我们需要创建一个连接工厂,客户端将使用它来连接到 JMS 代理。我们将创建一个发布/订阅资源和一个点对点消息队列。在 JMS 术语中,它们被称为“目标”。可以将它们视为您将邮件发送到的邮箱。

转到 Glassfish 管理屏幕,然后转到资源/JMS 资源/连接工厂。创建一个新的连接工厂,JNDI 名称: “jms/MyConnectionFactory”。将资源类型设置为 javax.jms.ConnectionFactory。删除屏幕底部的用户名和密码属性,并添加一个属性:‘imqDisableSetClientID’,其值为‘false’。单击“确定”。

# TODO 屏幕截图显示属性设置

通过将 imqDisableSetClientID 设置为 false,我们强制客户端在使用 ConnectionFactory 时声明用户名和密码。OpenMQ 使用登录名来唯一标识 JMS 服务的客户端,以便它可以正确地执行目标的传递保证。

现在我们需要创建实际的目标 - 一个用于发布/订阅的主题和一个用于点对点消息传递的队列。转到资源/JMS 资源/目标资源,然后单击“新建”。将 JNDI 名称设置为‘jms/MyTopic’,目标名称设置为‘MyTopic’,资源类型设置为‘javax.jms.Topic’。单击“确定”保存主题。

# TODO: 创建主题图片

现在我们需要为点对点消息创建 JMS 队列。创建一个新的资源,将 JNDI 名称设置为“jms/MyQueue”,目标名称设置为“MyQueue”,资源类型设置为“javax.jms.Queue”。点击确定保存。

# TODO: 创建队列图片

与之前讨论的数据库连接类似,JMS 服务也是通过使用 JNDI 名称查找在 JavaEE 容器中获取的。与数据库代码不同,我们需要手动完成一些工作来获取命名上下文,我们针对它进行查找。当我们的应用程序在 Glassfish 中运行时,获取上下文非常简单。我们只需导入类并实例化它。上下文提供了一个 lookup() 方法,我们使用它来获取 JMS 连接工厂并访问我们感兴趣的特定目标。在以下示例中,我将发布一条消息到我们的主题。让我们先看一些代码,我会详细介绍正在发生的事情。

from javax.naming import InitialContext, Session
from javax.naming import DeliverMode, Message
context = InitialContext()

tfactory = context.lookup("jms/MyConnectionFactory")

tconnection = tfactory.createTopicConnection('senduser', 'sendpass')
tsession = tconnection.createTopicSession(False, Session.AUTO_ACKNOWLEDGE)
publisher = tsession.createPublisher(context.lookup("jms/MyTopic"))

message = tsession.createTextMessage()
msg = "Hello there : %s" % datetime.datetime.now()
message.setText(msg)
publisher.publish(message, DeliveryMode.PERSISTENT,
        Message.DEFAULT_PRIORITY, 100)
tconnection.close()
context.close()

在此代码片段中,我们通过连接工厂获取主题连接。重申一下 - 主题用于发布/订阅场景。我们创建一个主题会话 - 一个上下文,我们可以在其中发送和接收消息。传递给创建主题会话的两个参数指定了事务标志以及我们的客户端将如何确认消息的接收。我们将禁用事务并让会话在收到消息时自动将确认发送回代理。

获取发布者的最后一步是 - 创建发布者。从那里我们可以开始将消息发布到代理。

在这一点上 - 重要的是要区分持久消息和持久消息。JMS 如果代理接收到的消息被持久化,则将消息称为“持久”。这保证了发送者知道代理已收到消息。它不保证消息实际上会传递给最终接收者。

持久订阅者保证在他们暂时断开与代理的连接并在稍后重新连接的情况下接收消息。JMS 代理将使用客户端 ID、用户名和密码的组合来唯一标识订阅者客户端,以唯一标识客户端并管理每个客户端的消息队列。

现在我们需要创建订阅者客户端。我们将编写一个独立的客户端来表明您的代码不必驻留在应用程序服务器中才能接收消息。我们在这里要应用的唯一技巧是,虽然我们可以简单地使用空构造函数为应用程序服务器中的代码创建 InitialContext,但存在于应用程序服务器之外的代码必须知道在哪里找到 JNDI 命名服务。Glassfish 通过 CORBA(通用对象请求代理体系结构)公开命名服务。简而言之 - 我们需要知道一个工厂类名来创建上下文,并且我们需要知道对象请求代理所在的 URL。

以下侦听器客户端可以在与 Glassfish 服务器相同的主机上运行

"""
This is a standalone client that listens messages from JMS
"""
from javax.jms import TopicConnectionFactory, MessageListener, Session
from javax.naming import InitialContext, Context
import time

def get_context():
    props = {}
    props[Context.INITIAL_CONTEXT_FACTORY]="com.sun.appserv.naming.S1ASCtxFactory"
    props[Context.PROVIDER_URL]="iiop://127.0.0.1:3700"
    context = InitialContext(props)
    return context

class TopicListener(MessageListener):
    def go(self):
        context = get_context()
        tfactory = context.lookup("jms/MyConnectionFactory")
        tconnection = tfactory.createTopicConnection('recvuser', 'recvpass')
        tsession = tconnection.createTopicSession(False, Session.AUTO_ACKNOWLEDGE)
        subscriber = tsession.createDurableSubscriber(context.lookup("jms/MyTopic"), 'mysub')
        subscriber.setMessageListener(self)
        tconnection.start()
        while True:
            time.sleep(1)
        context.close()
        tconnection.close()

    def onMessage(self, message):
        print message.getText()

if __name__ == '__main__':
    TopicListener().go()

JMS 主题的订阅者和发布者方面只有几个关键区别。首先,订阅者使用唯一的客户端 ID 创建 - 在这种情况下 - 它为“mysub”。JMS 使用它来确定在客户端断开 JMS 连接并在稍后重新绑定时要发送给客户端的待处理消息。如果我们不关心接收错过的消息,我们可以使用“createSubscriber”而不是“createDurableSubscriber”创建一个非持久订阅者,并且我们不必传入客户端 ID。

其次,侦听器对传入消息使用回调模式。当收到消息时,onMessage 将由订阅者对象自动调用,并且消息对象将被传入。

现在我们需要在代理上创建我们的发送用户和接收用户。转到命令行并转到 GLASSFISH_HOME/imq/bin。我们将创建两个用户 - 一个发送者和一个接收者。

GLASSFISH_HOME/imq/bin $ imqusermgr add -u senduser -p sendpass
User repository for broker instance: imqbroker
User senduser successfully added.

GLASSFISH_HOME/imq/bin $ imqusermgr add -u recvuser -p recvpass
User repository for broker instance: imqbroker
User recvuser successfully added.

现在我们有了两个新用户,用户名/密码对为 senduser/sendpass 和 recvuser/recvpass。

您现在拥有足够的代码来在您的代码中启用发布/订阅消息模式,以向驻留在应用程序服务器之外的应用程序发出信号。我们可能有多个侦听器附加到 JMS 代理,JMS 将确保所有订阅者以可靠的方式接收消息。

现在让我们看一下通过队列发送消息 - 这提供了可靠的点对点消息传递,并且它保证消息以安全的方式持久化,以防服务器崩溃。这次,我们将构建我们的发送和接收客户端作为与 JMS 代理通信的独立客户端。

from javax.jms import Session
from javax.naming import InitialContext, Context
import time

def get_context():
    props = {}
    props[Context.INITIAL_CONTEXT_FACTORY]="com.sun.appserv.naming.S1ASCtxFactory"
    props[Context.PROVIDER_URL]="iiop://127.0.0.1:3700"
    context = InitialContext(props)
    return context

def send():
    context = get_context()
    qfactory = context.lookup("jms/MyConnectionFactory")
    # This assumes a user has been provisioned on the broker with
    # username/password of 'senduser/sendpass'
    qconnection = qfactory.createQueueConnection('senduser', 'sendpass')
    qsession = qconnection.createQueueSession(False, Session.AUTO_ACKNOWLEDGE)
    qsender = qsession.createSender(context.lookup("jms/MyQueue"))
    msg = qsession.createTextMessage()
    for i in range(20):
        msg.setText('this is msg [%d]' % i)
        qsender.send(msg)

def recv():
    context = get_context()
    qfactory = context.lookup("jms/MyConnectionFactory")
    # This assumes a user has been provisioned on the broker with
    # username/password of 'recvuser/recvpass'
    qconnection = qfactory.createQueueConnection('recvuser', 'recvpass')
    qsession = qconnection.createQueueSession(False, Session.AUTO_ACKNOWLEDGE)
    qreceiver = qsession.createReceiver(context.lookup("jms/MyQueue"))
    qconnection.start()  # start the receiver

    print "Starting to receive messages now:"
    while True:
        msg = qreceiver.receive(1)
        if msg <> None and isinstance(msg, TextMessage):
            print msg.getText()

send() 和 recv() 函数与用于管理主题的发布/订阅代码几乎相同。一个细微的差别是 JMS 队列 API 不使用回调对象来接收消息。假设客户端应用程序会主动从 JMS 队列中出队对象,而不是充当被动订阅者。

这段 JMS 代码的妙处在于,您可以将消息发送到代理,并确保即使服务器宕机,您的消息也不会丢失。当服务器恢复运行并且您的端点客户端重新连接时,它仍然会收到所有待处理的消息。

我们可以进一步扩展这个例子。Codehaus.org 有一个名为 STOMP 的消息传递项目,即流式文本面向消息协议。STOMP 比原始 JMS 消息更简单,但性能更低,但权衡是客户端存在于十多种不同的语言中。STOMP 还提供了一个名为“stomp-connect”的适配器,它允许我们将 JMS 代理转换为 STOMP 消息代理。

这将使我们能够用几乎任何语言编写的应用程序通过 JMS 与我们的应用程序进行通信。有时我会使用现有的 CPython 代码,它利用各种 C 库(如 Imagemagick 或 NumPy)来进行计算,而这些计算在 Jython 或 Java 中根本不支持。

通过使用 stompconnect,我可以通过 JMS 发送工作消息,通过 STOMP 桥接这些消息,并让 CPython 客户端处理我的请求。完成的工作然后通过 STOMP 发送回来,桥接到 JMS,并由我的 Jython 代码接收。

首先,您需要从 codehaus.org 获取最新版本的 stomp-connect。从这里下载 stompconnect-1.0.zip

解压缩 zip 文件后,您需要配置一个 JNDI 属性文件,以便 STOMP 可以充当 JMS 客户端。配置与我们的 Jython 客户端相同。创建一个名为“jndi.properties”的文件,并将其放在您的 stompconnect 目录中。内容应包含以下两行

java.naming.factory.initial=com.sun.appserv.naming.S1ASCtxFactory
java.naming.provider.url=iiop://127.0.0.1:3700

现在您需要从 Glassfish 中提取一些 JAR 文件,以访问 JNDI、JMS 和 STOMP 所需的一些日志记录类。将以下 JAR 文件从 GLASSFISH_HOME/lib 复制到 STOMPCONNECT_HOME/lib

  • appserv-admin.jar
  • appserv-deployment-client.jar
  • appserv-ext.jar
  • appserv-rt.jar
  • j2ee.jar
  • javaee.jar

将 imqjmsra.jar 文件从 GLASSFISH_HOME/imq/lib/imqjmsra.jar 复制到 STOMPCONNECT_HOME/lib。

现在您应该可以使用以下命令行启动连接器

java -cp "lib\*;stompconnect-1.0.jar" \
    org.codehaus.stomp.jms.Main tcp://0.0.0.0:6666 \
    "jms/MyConnectionFactory"

如果它有效,您应该看到一堆输出,最后是一条消息,表明服务器正在监听 tcp://0.0.0.0:6666 上的连接。恭喜,您现在拥有一个 STOMP 代理,它充当 OpenMQ JMS 代理的双向代理。

接收来自 Jython+JMS 的 CPython 中的消息与以下代码一样简单。

import stomp
serv = stomp.Stomp('localhost', 6666)
serv.connect({'client-id': 'reader_client', \
                  'login': 'recvuser', \
               'passcode': 'recvpass'})
serv.subscribe({'destination': '/queue/MyQueue', 'ack': 'client'})
frame = self.serv.receive_frame()
if frame.command == 'MESSAGE':
    # The message content will arrive in the STOMP frame's
    # body section
    print frame.body
    serv.ack(frame)

发送消息也同样简单。从我们的 CPython 代码中,我们只需要导入 stomp 客户端,就可以将消息发送回我们的 Jython 代码。

import stomp
serv = stomp.Stomp('localhost', 6666)
serv.connect({'client-id': 'sending_client', \
                  'login': 'senduser', \
               'passcode': 'sendpass'})
serv.send({'destination': '/queue/MyQueue', 'body': 'Hello world!'})

结论

我们在这里涵盖了很多内容。我已经向您展示了如何让 Django 在 Jython 上使用数据库连接池来强制执行对应用程序可以使用的数据库资源的限制。我们已经研究了如何设置 JMS 队列和主题,以便在 Jython 进程之间提供点对点和发布/订阅消息。然后,我们利用这些消息传递服务,在 Jython 代码和非 Java 代码之间提供互操作性。

根据我的经验,能够混合使用精心挑选的技术集合是赋予 Jython 如此强大功能的原因。您可以使用 JavaEE 中的技术,利用多年来积累的宝贵经验,并获得使用更轻量级、更现代的 Web 应用程序堆栈(如 Django)的好处。

Jython 和 Django 在应用程序服务器中的未来非常光明。Websphere 现在使用 Jython 作为其官方脚本语言,Glassfish 的版本 3 将提供对 Django 应用程序的一流支持。您将能够部署您的 Web 应用程序,而无需构建 WAR 文件。只需从您的源目录直接部署,您就可以开始使用。