Play-框架秘籍-全-

Play 框架秘籍(全)

原文:zh.annas-archive.org/md5/3898a199ef873c2cd80ef5c3269070bb

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

在过去 5-10 年中,Web 应用已经取得了长足的进步。从 Geocities 和 Friendster 的辉煌时代,到社交媒体网站(如 Facebook 和 Twitter)的兴起,再到更实用的软件即服务(SaaS)应用(如 Salesforce 和 Github),不可否认的是,随着消费者和企业级 Web 软件的这些进步,出现了对构建在稳固的 Web 技术平台之上的需求,这不仅适用于最终用户 Web 客户端,还适用于各种复杂和高级的后端服务,所有这些都可以组成现代 Web 应用。

这就是 Play Framework 2.0 登场的地方。Play 为开发者提供了一个强大且成熟的轻量级无状态网络开发平台,其设计初衷就是考虑开发速度和 Web 应用的扩展性。

本书旨在通过基于非常常见用例和场景的简洁代码示例,让读者对 Play Framework 的不同部分有更深入的理解。您将了解 Play 中使用的基本概念和抽象,我们将深入探讨更高级和相关的主题,例如创建 RESTful API、使用第三方云存储服务存储上传的图片、向外部消息队列发送消息,以及使用 Docker 部署 Play 网络应用。

通过提供相关的食谱,本书希望为开发者提供创建下一个 Facebook 或 Salesforce 所需的必要构建块,使用 Play Framework。

本书涵盖的内容

第一章 Play Framework 基础 介绍了 Play Framework 及其功能。本章还介绍了 Play 的基本组件,例如控制器和视图模板。最后,我们讨论了如何对 Play Framework 的模型和控制器类进行单元测试。

第二章 使用控制器 深入讨论了 Play 控制器。本章演示了如何使用控制器与其他 Web 组件(如请求过滤器、会话和 JSON)一起使用。它还涉及了如何从控制器层面利用 Play 和 Akka。

第三章 利用模块 探讨了利用官方 Play 2 模块以及其他第三方模块。这应该有助于开发者通过重用和集成现有模块来加快他们的开发速度。

第四章 创建和使用 Web API 讨论了如何使用 Play 创建安全的 RESTful API 端点。本章还讨论了如何使用 Play WS 库消费其他基于 Web 的 API。

第五章 创建插件和模块 讨论了如何编写 Play 模块和插件,并告诉我们如何将 Play 模块发布到 Amazon S3 上的私有仓库。

第六章 实用模块示例 在前一章关于 Play 模块的基础上,讨论了更多关于集成模块(如消息队列和搜索服务)的实用示例。

第七章,部署 Play 2 网络应用,讨论了使用 Docker 和 Dokku 等工具在不同环境中部署 Play 网络应用的部署场景。

第八章,附加游戏信息,讨论了与开发者相关的话题,例如与 IDE 集成和使用其他第三方云服务。本章还讨论了如何使用 Vagrant 从零开始构建 Play 开发环境。

本书所需

您需要以下软件来使用本书中的食谱:

  • Java 开发工具包 1.7

  • Typesafe Activator

  • Mac OS X 终端

  • Cygwin

  • Homebrew

  • curl

  • MongoDB

  • Redis

  • boot2docker

  • Vagrant

  • Intellij IDEA

本书面向的对象

本书旨在帮助高级开发者利用 Play 2.x 的力量。本书对希望深入了解网络开发的专业人士也很有用。Play 2.x 是一个优秀的框架,可以加速您对高级主题的学习。

部分

在本书中,您会发现几个频繁出现的标题(准备就绪、如何操作、它是如何工作的、还有更多、相关内容)。

为了清楚地说明如何完成一个食谱,我们使用以下部分如下:

准备就绪

本节告诉您在食谱中可以期待什么,并描述了如何设置任何软件或任何为食谱所需的初步设置。

如何操作…

本节包含遵循食谱所需的步骤。

它是如何工作的…

本节通常包含对上一节发生情况的详细解释。

还有更多…

本节包含有关食谱的附加信息,以便让读者对食谱有更深入的了解。

相关内容

本节提供了对其他有用信息的链接,这些信息对食谱很有帮助。

惯例

在本书中,您会发现许多文本样式,用于区分不同类型的信息。以下是一些这些样式的示例及其含义的解释。

文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 处理方式如下所示:“通过在 app/Global.scala 文件中声明来使用这个新的过滤器。”

代码块按以下方式设置:

// Java
    return Promise.wrap(ask(fileReaderActor, words, 3000)).map(
      new Function<Object, Result>() {
        public Result apply(Object response) {
          return ok(response.toString());
        }
      }
    );

当我们希望您注意代码块中的特定部分时,相关的行或项目将以粗体显示:

<span class="strong"><strong>GET   /dashboard  controllers.Application.dashboard</strong></span>
GET   /login    controllers.Application.login

任何命令行输入或输出都按以下方式编写:

<span class="strong"><strong>$ curl -v http://localhost:9000/admins</strong></span>

新术语重要词汇以粗体显示。您在屏幕上看到的单词,例如在菜单或对话框中,在文本中如下所示:“再次使用相同的网络浏览器访问这个新的 URL 路由,您会看到文本 Found userPref: tw。”

注意

警告或重要注意事项以如下框的形式出现。

小贴士

小技巧和技巧如下所示。

读者反馈

读者反馈始终受到欢迎。让我们知道您对这本书的看法——您喜欢或不喜欢什么。读者反馈对我们来说很重要,因为它帮助我们开发出您真正能从中获得最大收益的标题。

要发送给我们一般反馈,只需发送电子邮件至&lt;<a class="email" href="mailto:feedback@packtpub.com">feedback@packtpub.com</a>&gt;,并在您的邮件主题中提及书籍的标题。

如果您在某个主题上具有专业知识,并且您对撰写或为书籍做出贡献感兴趣,请参阅我们的作者指南www.packtpub.com/authors

客户支持

现在您是 Packt 书籍的骄傲拥有者,我们有多个方面可以帮助您从您的购买中获得最大收益。

下载示例代码

您可以从www.packtpub.com下载您购买的所有 Packt 出版物的示例代码文件。如果您在其他地方购买了这本书,您可以访问www.packtpub.com/support并注册,以便将文件直接通过电子邮件发送给您。

下载本书的颜色图像

我们还为您提供了一个包含本书中使用的截图/图表的颜色图像的 PDF 文件。颜色图像将帮助您更好地理解输出的变化。您可以从www.packtpub.com/sites/default/files/downloads/1234OT_ColorImages.pdf下载此文件。

勘误

尽管我们已经尽一切努力确保我们内容的准确性,但错误仍然会发生。如果您在我们的某本书中发现错误——可能是文本或代码中的错误——如果您能向我们报告这一点,我们将不胜感激。通过这样做,您可以节省其他读者的挫败感,并帮助我们改进本书的后续版本。如果您发现任何勘误,请通过访问www.packtpub.com/submit-errata,选择您的书籍,点击勘误提交表单链接,并输入您的勘误详情来报告它们。一旦您的勘误得到验证,您的提交将被接受,勘误将被上传到我们的网站或添加到该标题的勘误部分下的现有勘误列表中。

要查看之前提交的勘误,请访问www.packtpub.com/books/content/support,并在搜索字段中输入书籍的名称。所需信息将出现在勘误部分下。

盗版

互联网上对版权材料的盗版是一个横跨所有媒体的持续问题。在 Packt,我们非常重视我们版权和许可证的保护。如果您在互联网上发现任何形式的我们作品的非法副本,请立即提供给我们地址或网站名称,以便我们可以寻求补救措施。

请通过链接发送至&lt;<a class="email" href="mailto:copyright@packtpub.com">copyright@packtpub.com</a>&gt;与我们联系,以提供涉嫌盗版材料的链接。

我们感谢您在保护我们作者和我们为您提供有价值内容的能力方面的帮助。

问题和建议

如果您对本书的任何方面有问题,您可以通过&lt;<a class="email" href="mailto:questions@packtpub.com">questions@packtpub.com</a>&gt;与我们联系,我们将尽力解决问题。

第一章. Play 框架基础

在本章中,我们将介绍以下菜谱:

  • 安装 Play 框架

  • 使用 Typesafe Activator 创建 Play 应用程序

  • 使用 Play 控制台

  • 与模块一起工作

  • 与控制器和路由一起工作

  • 在控制器中使用动作参数

  • 使用反向路由和重定向

  • 与视图模板一起工作

  • 使用辅助标签

  • 使用视图布局和包含

  • 与 XML 和文本文件一起工作

  • 使用 Ebean(Java)与 MySQL

  • 使用 Anorm(Scala)和 MySQL 数据库演变

  • 使用表单模板和 Web 动作

  • 使用表单验证

  • 保护表单提交

  • 使用 JUnit(Java)和 specs2(Scala)进行测试

  • 测试模型

  • 测试控制器

简介

Play 是一个既适用于 Java 也适用于 Scala 的开发者友好的现代 Web 应用程序框架。本章将带您了解安装 Play 框架进行本地开发的步骤。本章将描述 Play 应用程序项目目录结构、其各种成员及其在 Play 应用程序中的功能。

本章还将向您介绍Activator命令,它取代了旧的 Play 命令。Activator 在开发过程中的各个阶段都得到使用,包括编译、下载库依赖项、测试和构建。它实际上与其他构建工具(如 Ant 或 Maven)非常相似。

本章还将介绍如何在 Play 框架中实现模型-视图-控制器MVC)组件。这将随后是创建控制器和路由动作的源代码,使用视图模板和用于与关系数据库管理系统(如 MySQL)接口的模型组件。本章将处理基本的 HTTP 表单,认识到现代 Web 应用程序能够处理用户交互和数据的重要性,以及 Play 框架如何提供各种 API 来简化开发者的工作。

到本章结束时,您应该能够很好地掌握如何实现基本的 Web 应用程序功能,例如使用 MySQL 进行表单提交和数据访问,创建指向 Web 动作的 URL 路由,以及创建由更小、模块化和可重用视图组件组成的视图。

本章中的大多数菜谱都假设您对 Java 开发、Web 应用程序开发、命令行界面、结构化查询语言SQL)、开发构建工具、第三方库使用、依赖管理以及单元测试有一定程度的熟悉。

安装 Play 框架

本菜谱将指导您安装 Play Framework 2.3 以进行本地开发。本节将指导您进行 Play 框架的先决条件安装,例如Java 开发工具包JDK),以及确保 Play 框架可以访问 JDK 的二进制文件的必要步骤。

准备工作

Play 框架需要 6 或更高版本的 JDK。请访问 Oracle 网站,下载适合您的开发机器的 JDK,链接为 www.oracle.com/technetwork/java/javase/downloads/index.html

下载合适的 JDK 后,请确保将二进制文件夹添加到系统路径中:

<span class="strong"><strong>    $ export JAVA_PATH=/YOUR/INSTALLATION/PATH</strong></span>
<span class="strong"><strong>    $ export PATH=$PATH:$JAVA_HOME/bin</strong></span>

您还可以参考 Oracle 的在线文档以获取有关设置环境变量的更多信息,链接为 docs.oracle.com/cd/E19182-01/820-7851/inst_cli_jdk_javahome_t/index.html

这里是如何验证 JDK 是否已添加到系统路径中的方法:

<span class="strong"><strong>    $ javac -version</strong></span>
<span class="strong"><strong>    javac 1.7.0_71</strong></span>
 <span class="strong"><strong>    $ java -version</strong></span>
<span class="strong"><strong>    java version "1.7.0_71"</strong></span>
<span class="strong"><strong>    Java(TM) SE Runtime Environment (build 1.7.0_71-b14)</strong></span>
<span class="strong"><strong>    Java HotSpot(TM) 64-Bit Server VM (build 24.71-b01, mixed mode)</strong></span>

如何操作...

截至 Play 2.3.x,Play 现在通过一个名为 Typesafe Activator 的工具进行分发,按照以下步骤安装它:typesafe.com/activator

  1. 下载 Typesafe Reactive Platform 分发版,链接为 typesafe.com/platform/getstarted,并将其解压到您有写访问权限的期望位置。

  2. 下载并解压分发版后,将 Activator 安装目录添加到您的系统路径中:

    <span class="strong"><strong>    $ export ACTIVATOR_HOME=&lt;/YOUR/INSTALLATION/PATH&gt;</strong></span>
    <span class="strong"><strong>    $ export PATH=$PATH:$ACTIVATOR_HOME</strong></span>
    
  3. 现在,验证 Activator 是否已添加到系统路径中:

    <span class="strong"><strong>    $ activator --version</strong></span>
    <span class="strong"><strong>    sbt launcher version 0.13.5</strong></span>
    
  4. 现在,您应该可以使用 activator 命令创建 Play 应用程序:

    <span class="strong"><strong>    $ activator new &lt;YOUR_APP_NAME&gt;</strong></span>
    

使用 Typesafe Activator 创建 Play 应用程序

一旦您安装并正确配置了 JDK 和 Activator,您就应该准备好创建 Play 2.3.x 应用程序。从 Play 2.0 开始,开发者现在可以创建基于 Java 或 Scala 的 Play 应用程序。Activator 为 Java 和 Scala 提供了许多 Play 项目模板。对于第一个项目,让我们使用基本项目模板。我们还将使用 Activator 的命令行界面来处理本食谱中的所有配方。

如何操作...

您需要执行以下操作以创建 Java 和 Scala 的模板:

  • 对于 Java,让我们使用 play-java 模板,并通过以下命令将我们的第一个应用程序命名为 foo_java

    <span class="strong"><strong>    $ activator new foo_java play-java</strong></span>
    
  • 对于 Scala,让我们使用 play-scala 模板,并通过以下命令将我们的第一个应用程序命名为 foo_scala

    <span class="strong"><strong>    $ activator new foo_scala play-scala</strong></span>
    

它是如何工作的…

Activator 命令创建项目的根目录(foo_javafoo_scala)并创建所有相关的子目录、配置文件和类文件:

以下截图显示了 foo_java 的根目录:

以下截图显示了 foo_scala 的根目录:

如您所见,Java 和 Scala 项目模板生成的文件列表几乎相同,只是对于 play_java 模板,生成的类文件是 .java 文件,而对于 play_scala 模板,生成的类文件是 .scala 文件。

对于项目的目录结构,Play 框架的一个重要方面是它遵循约定优于配置的概念。这最好地体现在每个 Play 应用程序遵循的标准项目目录结构中:

1st Level

2nd Level

3rd Level

描述

app/

应用程序源文件

assets/

编译后的 JavaScript 或样式表

stylesheets/

编译后的样式表(例如 LESS 或 SASS)

javascripts/

编译后的 JavaScript(例如 CoffeeScript)

controllers/

应用程序请求-响应控制器

models/

应用程序领域对象

views/

应用程序展示视图

conf/

应用程序配置文件

public/

公共可用的资源

stylesheets/

公共可用的样式表文件

javascripts/

公共可用的 JavaScript 文件

project/

构建配置文件(例如 Build.scalaplugins.sbt

lib/

未管理的库和包

logs/

日志文件

test/

测试源文件

源代码、配置文件和 Web 资源组织在预定义的目录结构中,这使得开发者能够轻松地浏览项目目录树,并在逻辑位置中找到相关文件。

更多内容...

前往 typesafe.com/activator/templates 查看可用的项目模板的完整列表。

使用 Play 控制台

Play 控制台是一个用于构建和运行 Play 应用程序的命令行界面工具。对于每个开发者来说,熟悉可用的命令,如 cleancompiledependenciesrun,以充分利用 Play 控制台的功能是非常重要的。

如何操作…

你需要执行以下操作才能使用 Play 控制台来运行 Java 和 Scala:

  1. 在 Activator 完成设置 Play 项目后,你可以进入你的 Play 应用程序的 Play 控制台。

    • 使用以下命令进行 Java:

      <span class="strong"><strong>    $ cd foo_java</strong></span>
      <span class="strong"><strong>    $ activator</strong></span>
      
    • 使用以下命令进行 Scala:

      <span class="strong"><strong>    $ cd foo_scala</strong></span>
      <span class="strong"><strong>    $ activator</strong></span>
      
  2. 一旦进入 Play 控制台,你就可以以开发模式运行你的应用程序:

    • 使用以下命令进行 Java:

      <span class="strong"><strong>    [foo_java] $ run</strong></span>
      
    • 使用以下命令进行 Scala:

      <span class="strong"><strong>    [foo_scala] $ run</strong></span>
      
  3. 现在,打开网络浏览器并转到 http://localhost:9000图片

  4. 使用以下命令行启用热重载功能启动你的 Play 应用程序:

    <span class="strong"><strong>    $ activator "~run"</strong></span>
    
  5. 使用以下命令行在不同的端口上启动你的 Play 应用程序:

    <span class="strong"><strong>    $ activator "run 9001"</strong></span>
    

备注

以开发模式运行你的应用程序将配置你的应用程序以自动重新加载运行,Play 将尝试重新编译项目文件中的任何最近更改,从而无需为每次代码编辑手动重新启动应用程序。你现在可以使用网络浏览器查看你的应用程序。

更多内容…

你还可以使用 Play 控制台通过 activator 控制台中的 compile 命令手动编译类文件(使用 activator 命令):

  • 使用以下命令进行 Java:

    <span class="strong"><strong>    [foo_java] $ compile</strong></span>
    
  • 使用以下命令进行 Scala:

    <span class="strong"><strong>    [foo_scala] $ compile</strong></span>
    

你也可以直接运行 Play 命令,而不是使用 Play 控制台:

  • 使用以下命令进行 Java:

    <span class="strong"><strong>    $ cd foo_java</strong></span>
    <span class="strong"><strong>    $ activator compile</strong></span>
    <span class="strong"><strong>    $ activator run</strong></span>
    
  • 使用以下命令进行 Scala:

    <span class="strong"><strong>    $ cd foo_scala</strong></span>
    <span class="strong"><strong>    $ activator compile</strong></span>
    <span class="strong"><strong>    $ activator run</strong></span>
    

使用以下命令使用Activator为你的现有 Play 应用程序生成 eclipse 项目文件:

<span class="strong"><strong>    $ activator eclipse</strong></span>
<span class="strong"><strong>    [info] Loading project definition from /private/tmp/foo_scala/project</strong></span>
<span class="strong"><strong>    [info] Set current project to foo_scala (in build file:/private/tmp/foo_scala/)</strong></span>
<span class="strong"><strong>    [info] About to create Eclipse project files for your project(s).</strong></span>
<span class="strong"><strong>    [info] Compiling 5 Scala sources and 1 Java source to /private/tmp/foo_scala/target/scala-2.11/classes...</strong></span>
<span class="strong"><strong>    [info] Successfully created Eclipse project files for project(s):</strong></span>
<span class="strong"><strong>    [info] foo_scala</strong></span>

使用以下命令使用Activator为你的现有 Play 应用程序生成 IntelliJ IDEA 项目文件:

<span class="strong"><strong>    $ activator idea</strong></span>
<span class="strong"><strong>    [info] Loading project definition from /private/tmp/foo_java/project</strong></span>
<span class="strong"><strong>    [info] Set current project to foo_java (in build file:/private/tmp/foo_java/)</strong></span>
<span class="strong"><strong>    [info] Creating IDEA module for project 'foo_java' ...</strong></span>
<span class="strong"><strong>    [info] Running compile:managedSources ...</strong></span>
<span class="strong"><strong>    [info] Running test:managedSources ...</strong></span>
<span class="strong"><strong>    [info] Excluding folder target</strong></span>
<span class="strong"><strong>    [info] Created /private/tmp/foo_java/.idea/IdeaProject.iml</strong></span>
<span class="strong"><strong>    [info] Created /private/tmp/foo_java/.idea</strong></span>
<span class="strong"><strong>    [info] Excluding folder /private/tmp/foo_java/target/scala-2.11/cache</strong></span>
<span class="strong"><strong>    [info] Excluding folder /private/tmp/foo_java/target/scala-2.11/classes</strong></span>
<span class="strong"><strong>    [info] Excluding folder /private/tmp/foo_java/target/scala-2.11/classes_managed</strong></span>
<span class="strong"><strong>    [info] Excluding folder /private/tmp/foo_java/target/native_libraries</strong></span>
<span class="strong"><strong>    [info] Excluding folder /private/tmp/foo_java/target/resolution-cache</strong></span>
<span class="strong"><strong>    [info] Excluding folder /private/tmp/foo_java/target/streams</strong></span>
<span class="strong"><strong>    [info] Excluding folder /private/tmp/foo_java/target/web</strong></span>
<span class="strong"><strong>    [info] Created /private/tmp/foo_java/.idea_modules/foo_java.iml</strong></span>
<span class="strong"><strong>    [info] Created /private/tmp/foo_java/.idea_modules/foo_java-build.iml</strong></span>

使用模块

你可以在你的 Play 应用程序中利用其他 Play 框架或第三方模块。这可以通过编辑构建文件(build.sbt)并以sbt依赖声明的方式声明库依赖来实现。

如何做到这一点…

你需要执行以下步骤来声明一个模块:

  1. 打开build.sbt文件,并添加以下行,在声明库依赖时使用组 ID%模块名称%版本:

    libraryDependencies ++= Seq(
          jdbc,
          "mysql" % "mysql-connector-java" % "5.1.28"
        )
    
  2. 保存对build.sbt的更改后,转到命令行,让 Activator 下载新声明的依赖项:

    <span class="strong"><strong>    $ activator clean dependencies</strong></span>
    

它是如何工作的…

在这个菜谱中,我们声明了我们的 Play 应用程序需要什么,并引用了由 Play 框架提供的Java 数据库连接JDBC)模块以及由 MySQL 提供的 MySQL Java 连接器模块。一旦我们声明了模块,我们就可以运行 activator dependencies 命令,使 Activator 从公共 Maven 仓库下载所有声明的依赖项,并将它们存储在本地开发机器上。

更多内容…

请参考 Play 框架网站以获取官方 Play 模块的完整列表(www.playframework.com/documentation/2.3.x/Modules)。你还可以参考 Typesafe 官方发布仓库以获取其他可用的插件和模块(repo.typesafe.com/typesafe/releases/)。

使用控制器和路由进行工作

Play 应用程序使用控制器来处理 HTTP 请求和响应。Play 控制器由具有特定功能的行为组成。Play 应用程序使用路由器将 HTTP 请求映射到控制器行为。

如何做到这一点…

要创建一个新页面,该页面为 Play Java 项目打印出“Hello World”,我们需要采取以下步骤:

  1. 启用热重载功能运行foo_java应用程序:

    <span class="strong"><strong>    $ activator "~run"</strong></span>
    
  2. 通过添加以下操作编辑foo_java/app/controllers/Application.java

    public static Result hello() {
          return ok("Hello World");
        }
    
  3. 通过添加以下行编辑foo_java/conf/routes

    GET    /hello    controllers.Application.hello()
    
  4. 使用网页浏览器查看你的新 hello 页面:

    <code class="literal">http://localhost:9000/hello</code>
    

对于 Scala,我们需要采取以下步骤:

  1. 启用热重载功能运行foo_scala应用程序:

    <span class="strong"><strong>    $ activator "~run"</strong></span>
    
  2. 通过添加以下操作编辑foo_scala/app/controllers/Application.scala

    def hello = Action {
          Ok("Hello World")
        }
    
  3. 通过添加以下行编辑foo_scala/conf/routes

    GET    /hello    controllers.Application.hello
    
  4. 使用网页浏览器查看你的新 hello 页面:

    <code class="literal">http://localhost:9000/hello</code>
    

它是如何工作的…

在这个菜谱中,我们列举了通过在控制器中创建一个新的网络操作来创建一个新的可访问页面的必要步骤,并通过向 conf/routes 文件中添加一个新条目来定义这个新页面的 URL 路由。我们现在应该有一个“Hello World”页面,而且无需重新加载应用程序服务器。

在控制器中使用操作参数

网络应用应该能够接受动态数据作为其规范 URL 的一部分。一个例子是 RESTful API 网络服务的 GET 操作。Play 使得开发者能够轻松实现这一点。

如何做…

对于 Java,我们需要采取以下步骤:

  1. 启用 Hot-Reloading 运行 foo_java 应用程序。

  2. 通过添加以下操作来编辑 foo_java/app/controllers/Application.java

    public static Result echo(String msg) {
          return ok("Echoing " + msg);
        }
    
  3. 通过添加以下行来编辑 foo_java/conf/routes

    GET    /echo/:msg    controllers.Application.echo(msg)
    
  4. 使用网页浏览器查看你的新 echo 页面:

    <code class="literal">http://localhost:9000/echo/foo</code>
    
  5. 你应该能够看到文本 Echoing foo

对于 Scala,我们需要采取以下步骤:

  1. 启用 Hot-Reloading 运行 foo_scala 应用程序。

  2. 通过添加以下操作来编辑 foo_scala/app/controllers/Application.scala

    def echo(msg: String) = Action {
          Ok("Echoing " + msg)
        }
    
  3. 通过添加以下行来编辑 foo_scala/conf/routes

    GET    /echo/:msg    controllers.Application.echo(msg)
    
  4. 使用网页浏览器查看你的新 echo 页面:

    <code class="literal">http://localhost:9000/echo/bar</code>
    
  5. 你应该能够看到文本 Echoing bar

它是如何工作的…

在这个菜谱中,我们只修改了两个文件,应用程序控制器 Application.javaApplication.scala 以及 routes 文件。我们在 Application.scala 中添加了一个新的网络操作,该操作接受一个 String 类型的参数 msg 并将消息的内容返回给 HTTP 响应。然后我们在 routes 文件中添加了一个新的条目,声明了一个新的 URL 路由,并将 :msg 路由参数作为规范 URL 的一部分。

使用反向路由和重定向

对于一个网络应用来说,能够重定向 HTTP 请求是一个更基本的任务,而使用 Play 框架重定向 HTTP 非常简单。这个菜谱展示了开发者如何使用反向路由来引用已定义的路由。

如何做…

对于 Java,我们需要采取以下步骤:

  1. 启用 Hot-Reloading 运行 foo_java 应用程序。

  2. 通过添加以下操作来编辑 foo_java/app/controllers/Application.java

    public static Result helloRedirect() {
          return redirect(controllers.routes.Application.echo("HelloWorldv2"));
        }
    
  3. 通过添加以下行来编辑 foo_java/conf/routes

    GET    /v2/hello    controllers.Application.helloRedirect()
    
  4. 使用网页浏览器查看你的新 echo 页面:

    <code class="literal">http://localhost:9000/v2/hello</code>
    
  5. 你应该能够看到文本 Echoing HelloWorldv2

  6. 注意,在网页浏览器中的 URL 也已经重定向到 http://localhost:9000/echo/HelloWorldv2

对于 Scala,我们需要采取以下步骤:

  1. 启用 Hot-Reloading 运行 foo_scala 应用程序。

  2. 通过添加以下操作来编辑 foo_scala/app/controllers/Application.scala

    def helloRedirect() = Action {
          Redirect(routes.Application.echo("HelloWorldv2"))
        }
    
  3. 通过添加以下行来编辑 foo_scala/conf/routes

    GET    /v2/hello    controllers.Application.helloRedirect
    
  4. 使用网页浏览器查看你的新 echo 页面:

    <code class="literal">http://localhost:9000/v2/hello</code>
    
  5. 你应该能够看到文本 Echoing HelloWorldv2

  6. 注意,在网页浏览器中的 URL 也已经重定向到 http://localhost:9000/echo/HelloWorldv2

它是如何工作的…

在这个菜谱中,我们在引用其他操作方法中的现有路由时使用了反向路由。这很方便,因为我们不需要在其他操作方法中硬编码渲染的 URL 路由。我们还通过使用我们的第一个 HTTP 重定向,一个非常常见的 Web 应用程序功能,来执行一个 302 HTTP 重定向,这是一个所有标准 Web 服务器都处理的标准 HTTP 状态码。

使用视图模板

你期望能够在 Web 应用程序中将一些数据发送回视图本身;在 Play 框架中,这非常简单直接。一个 Play 视图模板只是一个包含指令、网页标记标签和模板标签的文本文件。视图模板文件也遵循标准的命名约定,并且它们被放置在 Play 项目目录中的预定义目录中,这使得管理模板文件变得更加容易。

如何操作...

对于 Java,我们需要采取以下步骤:

  1. 使用启用热重载的功能运行foo_java应用程序。

  2. foo_java/app/views/中创建视图文件products.scala.html。添加视图文件的内容:

    @(products: Collection[String])
         &lt;h3&gt;@products.mkString(",")&lt;/h3&gt;
    
  3. 通过添加以下操作来编辑foo_java/app/controllers/Application.java

    private static final java.util.Map&lt;Integer, String&gt; productMap = new java.util.HashMap&lt;Integer, String&gt;();
         static {
          productMap.put(1, "Keyboard");
          productMap.put(2, "Mouse");
          productMap.put(3, "Monitor");
        }
         public static Result listProducts() {
          return ok(products.render(productMap.values()));
        }
    
  4. 通过添加以下行来编辑foo_java/conf/routes

    GET    /products    controllers.Application.listProducts
    
  5. 使用网络浏览器查看产品页面:

    <code class="literal">http://localhost:9000/products</code>
    

对于 Scala,我们需要采取以下步骤:

  1. 使用启用热重载的功能运行foo_scala应用程序。

  2. foo_scala/app/views/中创建视图文件products.scala.html。添加视图文件的内容:

    @(products: Seq[String])
         &lt;h3&gt;@products.mkString(",")&lt;/h3&gt;
    
  3. 通过添加以下操作来编辑foo_scala/app/controllers/Application.scala

    private val productMap = Map(1 -&gt; "Keyboard", 2 -&gt; "Mouse", 3 -&gt; "Monitor")
        def listProducts() = Action {
          Ok(views.html.products(productMap.values.toSeq))
        }
    
  4. 通过添加以下行来编辑foo_scala/conf/routes

    GET    /products    controllers.Application.listProducts
    
  5. 使用网络浏览器查看产品页面:

    <code class="literal">http://localhost:9000/products</code>
    

它是如何工作的...

在这个菜谱中,我们能够从服务器端检索数据集合,并在我们的视图模板中显示集合的内容。目前,我们使用一个静态的字符串对象集合在视图模板中显示,而不是从数据库中检索一些数据集,这将在接下来的菜谱中解决。

我们通过在视图模板的第一行代码中声明参数并在控制器中传递数据到视图模板中引入了在视图模板中声明参数的方法。

使用辅助标签

视图标签允许开发者创建可重用的视图函数和组件,从而使视图的管理变得更加简单和容易。

如何操作...

对于 Java,我们需要采取以下步骤:

  1. 使用启用热重载的功能运行foo_java应用程序。

  2. foo_java/app/views/tags中创建标签文件productsIterator.scala.html

  3. 添加标签文件的以下内容:

    @(products: Collection[String])
         &lt;ul&gt;
          @for(product &lt;- products) {
            &lt;li&gt;@product&lt;/li&gt;
          }
        &lt;/ul&gt;
    
  4. 通过添加以下块来编辑foo_java/app/views/products.scala.html

    @import tags._
    
        @productsIterator(products)
    
  5. 使用网络浏览器重新加载产品页面以查看新的产品列表,使用无序列表 HTML 标签:

    <code class="literal">http://localhost:9000/products</code>
    

对于 Scala,我们需要采取以下步骤:

  1. 使用启用热重载的功能运行foo_scala应用程序。

  2. foo_scala/app/views/tags中创建标签文件productsIterator.scala.html

  3. 添加标签文件的内容:

    @(products: Seq[String])
         &lt;ul&gt;
          @for(product &lt;- products) {
            &lt;li&gt;@product&lt;/li&gt;
      }
        &lt;/ul&gt;
    
  4. 通过添加以下代码块编辑foo_scala/app/views/products.scala.html

    @import tags._
    
        @productsIterator(products)
    
  5. 使用网页浏览器重新加载产品页面,以查看新的产品列表,使用无序列表 HTML 标签:

    <code class="literal">http://localhost:9000/products</code>
    

它是如何工作的...

在这个菜谱中,我们能够在app/views/tags中创建一个新的视图标签。然后我们继续在我们的视图模板中使用这个标签。

首先,我们创建了一个新的标签,该标签接收一组产品标题,然后将其作为无序列表在模板中显示。然后我们在产品视图模板中导入该标签,并通过调用其文件名(@productsIterator(products))来调用辅助函数。

使用视图布局和包含

对于这个菜谱,我们将创建一个包含定义好的头部和尾部视图的主布局视图模板。这将允许我们的视图模板通过包含这个主视图模板来继承一致的外观和感觉,并在单个文件中管理所有 UI 更改。在我们的产品视图中,我们将使用这个示例中的主布局视图。

如何操作...

对于 Java,我们需要采取以下步骤:

  1. 启用热重载功能运行foo_java应用程序。

  2. foo_java/app/views/common中创建主布局视图文件mainLayout.scala.html

  3. 添加主布局视图文件的内容:

    @(title: String)(content: Html)
      &lt;!DOCTYPE html&gt;
      &lt;html lang="en"&gt;
        &lt;head&gt;
          &lt;title&gt;@title&lt;/title&gt;
        &lt;/head&gt;
        &lt;body&gt;
          &lt;header&gt;@header()&lt;/header&gt;
          &lt;section class="content"&gt;@content&lt;/section&gt;
          &lt;footer&gt;@footer()&lt;/footer&gt;
        &lt;/body&gt;
      &lt;/html&gt;
    
  4. foo_java/app/views/common中创建头部视图文件header.scala.html并添加以下代码:

    &lt;div&gt;
        &lt;h1&gt;Acme Products Inc&lt;/h1&gt;
      &lt;/div&gt;
    
  5. foo_java/app/views/common中创建尾部视图文件footer.scala.html并添加以下代码:

    &lt;div&gt;
        Copyright 2014
      &lt;/div&gt;
    
  6. 编辑foo_java/app/views/products.scala.html产品视图文件,以使用主布局视图模板,将所有文件内容替换为以下代码:

    @(products: Collection[String])
       @import tags._
      @import common._
       @mainLayout(title = "Acme Products") {
        @productsIterator(products)
      }
    
  7. 使用网页浏览器重新加载更新后的产品页面:

    <code class="literal">http://localhost:9000/products</code>
    

对于 Scala,我们需要采取以下步骤:

  1. 启用热重载功能运行foo_scala应用程序。

  2. foo_scala/app/views/common中创建主布局视图文件mainLayout.scala.html

  3. 添加主布局视图文件的内容:

    @(title: String)(content: Html)
      &lt;!DOCTYPE html&gt;
      &lt;html lang="en"&gt;
        &lt;head&gt;
          &lt;title&gt;@title&lt;/title&gt;
        &lt;/head&gt;
        &lt;body&gt;
          &lt;header&gt;@header()&lt;/header&gt;
          &lt;section class="content"&gt;@content&lt;/section&gt;
          &lt;footer&gt;@footer()&lt;/footer&gt;
        &lt;/body&gt;
      &lt;/html&gt;
    
  4. foo_scala/app/views/common中创建头部视图文件header.scala.html并添加以下代码:

    &lt;div&gt;
        &lt;h1&gt;Acme Products Inc&lt;/h1&gt;
      &lt;/div&gt;
    
  5. foo_scala/app/views/common中创建尾部视图文件footer.scala.html并添加以下代码:

    &lt;div&gt;
        Copyright 2014
      &lt;/div&gt;
    
  6. 编辑foo_scala/app/views/products.scala.html产品视图文件,以使用主布局视图模板,将所有文件内容替换为以下代码:

    @(products: Seq[String])
       @import tags._
      @import common._
       @mainLayout(title = "Acme Products") {
        @productsIterator(products)
      }
    
  7. 使用网页浏览器重新加载更新后的产品页面:

    <code class="literal">http://localhost:9000/products</code>
    

它是如何工作的...

在这个菜谱中,我们创建了一个可以在整个 Play 应用程序中重用的主布局视图模板。一个常见的布局视图消除了在相关视图中重复视图逻辑的需要,并使得管理父视图和子视图变得容易得多。

处理 XML 和文本文件

使用视图模板,我们也能够响应 HTTP 请求的其他内容类型,如文本文件和 XML 数据格式。Play 框架具有原生处理 XML 和文本文件内容类型响应的处理程序。

如何操作...

对于 Java,我们需要采取以下步骤:

  1. 启用 Hot-Reloading 运行 foo_java 应用程序:

    <span class="strong"><strong>    $ activator "~run"</strong></span>
    
  2. app/views/ 中创建基于文本的视图模板文件 products.scala.txt 并添加以下内容:

    @(productMap: Map[Integer, String])
        @for((id, name) &lt;- productMap) {
          The Product '@name' has an ID of @id
        }
    
  3. app/views/ 中创建基于 XML 的视图模板文件 products.scala.xml 并添加以下内容:

    @(productMap: Map[Integer, String]) &lt;products&gt;
        @for((id, name) &lt;- productMap) {
          &lt;product id="@id"&gt;@name&lt;/product&gt;
        }
        &lt;/products&gt;
    
  4. 通过添加以下动作编辑 foo_java/app/controllers/Application.java

    public static Result listProductsAsXML() {
            return ok(views.xml.products.render(productMap));
        }
         public static Result listProductsAsTXT() {
            return ok(views.txt.products.render(productMap));
        }
    
  5. 通过添加以下行编辑 foo_java/conf/routes

    GET    /products.txt    controllers.Application.listProductsAsTXT()
        GET    /products.xml    controllers.Application.listProductsAsXML()
    
  6. 使用网页浏览器查看新的路由和动作:

    • http://localhost:9000/products.txt 和,

    • http://localhost:9000/products.xml

对于 Scala,我们需要采取以下步骤:

  1. 启用 Hot-Reloading 运行 foo_scala 应用程序:

    $ activator "~run"
    
  2. app/views/ 中创建基于文本的视图模板文件 products.scala.txt 并添加以下内容:

    @(productMap: Map[Int, String])
        @for((id, name) &lt;- productMap) {
          The Product '@name' has an ID of @id
        }
    
  3. app/views/ 中创建基于 XML 的视图模板文件 products.scala.xml 并添加以下内容:

    @(productMap: Map[Int, String]) &lt;products&gt;
        @for((id, name) &lt;- productMap) {
          &lt;product id="@id"&gt;@name&lt;/product&gt;
        }
        &lt;/products&gt;
    
  4. 通过添加以下动作编辑 foo_scala/app/controllers/Application.scala

    def listProductsAsTXT = Action {
          Ok(views.txt.products(productMap))
        }
         def listProductsAsXML = Action {
          Ok(views.xml.products(productMap))
        }
    
  5. 通过添加以下行编辑 foo_scala/conf/routes

    GET    /products.txt    controllers.Application.listProductsAsTXT
        GET    /products.xml    controllers.Application.listProductsAsXML
    
  6. 使用网页浏览器查看新的路由和动作:

    • http://localhost:9000/products.txt

    • http://localhost:9000/products.xml

它是如何工作的...

在这个菜谱中,我们利用了 Play 框架内置的其他内容类型支持。我们创建了新的 URL 路由和 Web 动作,以便能够响应 XML 或文本文件格式的数据请求。通过遵循文件命名标准和视图约定,我们能够在 HTML、XML 和文本文件格式中创建视图模板,Play 会自动处理这些模板,并在 HTTP 响应中添加适当的内容类型头。

使用 Ebean(Java)与 MySQL

Play Framework 2.x 包含一个名为 Ebean 的对象关系映射工具,用于 Java 基础的 Play 应用程序。为了能够使用 Ebean,请确保在 foo_java/build.sbt 中将 Ebean 和合适的 MySQL 驱动程序声明为项目依赖项。

对于这个菜谱,我们将利用 Ebean 进行数据库进化。Play Framework 2.x 为开发者提供了一种管理数据库迁移的方法。数据库迁移在应用程序开发过程中跟踪模式变化非常有用。数据库进化默认启用,但可以在 conf/application.conf 中通过以下设置禁用:

evolutionplugin=disabled

进化脚本存储在 conf/evolutions/default/ 目录中。有关数据库进化的更多信息,请参阅 Play 的在线文档:

www.playframework.com/documentation/2.3.x/Evolutions

如何操作...

为了利用 Ebean,你需要执行以下步骤:

  1. build.sbt 中添加 Ebean 依赖项:

    libraryDependencies ++= Seq(
            javaJdbc,   javaEbean,
            "mysql" % "mysql-connector-java" % "5.1.28"
        )
    
  2. 确保在 conf/application.conf 中正确配置了 Ebean 和 MySQL:

    db.default.driver=com.mysql.jdbc.Driver
        db.default.url="jdbc:mysql://&lt;YOUR_MYSQL_HOST&gt;/&lt;YOUR_DB&gt;"
        db.default.user=&lt;YOUR_USER&gt;
        db.default.password=&lt;YOUR_PASSWORD&gt;
         ebean.default="models.*"
    
  3. 对于接下来的菜谱,我们需要在我们的 MySQL 数据库中创建我们的产品表。在 conf/evolutions/default 中创建我们的第一个数据库进化文件 1.sql 并添加以下 SQL 语句:

    # --- !Ups
        CREATE TABLE Products (
          id INT NOT NULL AUTO_INCREMENT,
          name VARCHAR(100) NOT NULL,
          PRIMARY KEY (id)
        );
       # --- !Downs
        DROP TABLE Products;
    
  4. 下一步是创建我们实体 Product 的 Ebean 模型:

    package models;
         import java.util.*;
        import javax.persistence.*;
        import play.db.ebean.*;
        import play.data.format.*;
        import play.data.validation.*;
         @Entity
       @Table(name = "Products")
        public class Product extends Model {
    
        @Id
        public Long id;
    
        @Column
        @Constraints.Required
        public String name;
         public static Finder&lt;Long, Product&gt; find = new Finder&lt;Long, Product&gt;(
          Long.class, Product.class
        );
         public Long getId() {
          return id;
        }
        public void setId(Long id) {
          this.id = id;
        }
        public String getName() {
          return name;
        }
        public void setName(String name) {
          this.name = name;
        }
    }
    

以下展示了使用 Ebean 的各种数据库操作。

小贴士

下载示例代码

您可以从您在 www.packtpub.com 的账户中下载您购买的所有 Packt 书籍的示例代码文件。如果您在其他地方购买了这本书,您可以访问 www.packtpub.com/support 并注册以将文件直接通过电子邮件发送给您。

创建记录

以下代码片段将创建一条新记录:

Product product = new Product();
      product.name = "Apple iPhone";
      product.save();

更新记录

以下代码片段将更新一条记录:

Product forUpdate = Product.find.ref(1L);
      forUpdate.name = "Apple iPhone 6";
      forUpdate.update();Deleting a record:
  Product.find.ref(1L).delete();

查询记录

以下代码片段将查询一条记录:

Product p = Product.find.byId(1L);

检索记录

以下代码片段将检索一条记录:

List&lt;Product&gt; products = Product.find.all();

使用 Anorm(Scala)和 MySQL 数据库演变

Play Framework 2.x 包含 Anorm,这是一个用于 Scala 基础 Play 应用程序的有用数据访问库。为了能够使用 Anorm,请确保在 foo_scala/build.sbt 中将 Anorm 和合适的 MySQL 驱动程序声明为项目依赖项。

对于这个菜谱,我们将利用 Anorm 和数据库演变。Play Framework 2.x 为开发者提供了一种管理数据库迁移的方法。数据库迁移对于跟踪应用程序开发过程中的模式更改非常有用。数据库演变默认启用,但可以在 conf/application.conf 中使用以下设置禁用:

evolutionplugin=disabled

进化脚本存储在 conf/evolutions/default/ 目录中。有关数据库进化的更多信息,请参阅 Play 的在线文档,网址为 www.playframework.com/documentation/2.3.x/Evolutions

您需要执行以下步骤来利用 Anorm:

  1. 将 Anorm 依赖项添加到 build.sbt

    libraryDependencies ++= Seq(
          jdbc,
          anorm,
       "mysql" % "mysql-connector-java" % "5.1.28"
        )
    
  2. 确保在 conf/application.conf 中正确配置了 Anorm 和 MySQL:

    db.default.driver= com.mysql.jdbc.Driver
        db.default.url="jdbc:mysql://localhost/YOUR_DB"
        db.default.user=YOUR_USER
        db.default.password=YOUR_PASSWORD
    
  3. 对于接下来的菜谱,我们需要在我们的 MySQL 数据库中创建我们的产品表。在 conf/evolutions/default 中创建我们的第一个数据库演变文件 1.sql,并添加以下 SQL 语句:

    # --- !Ups
        CREATE TABLE Products (
          id INT NOT NULL AUTO_INCREMENT,
          name VARCHAR(100) NOT NULL,
          PRIMARY KEY (id)
        );
     # --- !Downs
        DROP TABLE Products;
    

还有更多...

以下部分展示了使用 Anorm 的各种数据库操作。

创建新记录

以下代码片段将创建一条新记录:

DB.withConnection { implicit c =&gt;
     SQL("INSERT INTO Products(id, name) VALUES ({id}, {name});")
        .on('id -&gt; product.id, 'name -&gt; product.name).executeInsert()
   }

更新记录

以下代码片段将更新一条记录:

   DB.withConnection { implicit c =&gt;
      SQL("UPDATE Products SET name = {name} WHERE id = {id}")
       .on('name -&gt; product.name, 'id -&gt; product.id).executeUpdate()
   }

删除记录

以下代码片段将删除一条记录:

   DB.withConnection { implicit c =&gt;
      SQL("DELETE FROM Products WHERE id={id};")
       .on('id -&gt; id).executeUpdate()
   }Querying a record

以下代码片段将查询一条记录:

DB.withConnection { implicit c =&gt;
      SQL("SELECT * FROM Products WHERE id={id};")
       .on('id -&gt; id).executeQuery().singleOpt(defaultParser)
    }

以下代码片段将检索一条记录:

DB.withConnection { implicit c =&gt;
     SQL("SELECT * FROM Products;").executeQuery().list(defaultParser)
   }

最后,我们可以将这些功能组合在一个名为 Product 的伴随对象中:

package models
     import play.api.db.DB
    import play.api.Play.current
    import anorm._
    import anorm.SqlParser.{str, int}
     case class Product(id: Long, name: String)
     object Product {
      val defaultParser = int("id") ~ str("name") map {
          case id ~ name  =&gt; Product(id, name)
         }
       def save(product: Product) = {
        DB.withConnection { implicit c =&gt;
      SQL("INSERT INTO Products(id, name) VALUES ({id}, {name});")
                  .on('id -&gt; product.id, 'name -&gt; product.name).executeInsert()
        }
          }

       def update(product: Product) = {
        DB.withConnection { implicit c =&gt;
               SQL("UPDATE Products SET name = {name} WHERE id = {id}")
                .on('name -&gt; product.name, 'id -&gt; product.id).executeUpdate()
            }
          }

      def delete(id: Long) = {
        DB.withConnection { implicit c =&gt;
               SQL("DELETE FROM Products WHERE id={id};")
           .on('id -&gt; id).executeUpdate()
        }
          }

      def get(id: Long) = {
        DB.withConnection { implicit c =&gt;
      SQL("SELECT * FROM Products WHERE id={id};")
        .on('id -&gt; id).executeQuery().singleOpt(defaultParser)
        }
          }

      def all = {
        DB.withConnection { implicit c =&gt;
      SQL("SELECT * FROM Products;").executeQuery().list(defaultParser)
        }
          }
    }

使用表单模板和 Web 动作

与大多数网络应用程序一样,总会有接受 HTTP 表单的需求,无论是注册表单还是登录表单。Play 框架提供了帮助类来管理和处理 HTTP 表单提交。在这个菜谱中,我们将介绍创建简单表单并映射处理此表单提交分配的网络操作的步骤。我们还将利用闪存作用域,它允许我们使用闪存对象在每次请求的基础上从控制器向视图模板发送消息。

如何操作...

对于 Java,我们需要采取以下步骤:

  1. 以启用热重载的方式运行 foo_java 应用程序。

  2. 创建表单视图模板文件 app/views/product/form.scala.html 并添加以下内容:

    @(productForm: Form[models.Product])
    
        @import common._
         @mainLayout(title = "New Product") {
    
          @if(flash.get("success") != null) {
            &lt;p&gt;@flash.get("success")&lt;/p&gt;
          }
           @if(productForm.hasGlobalErrors) {
            &lt;ul&gt;
            @for(error &lt;- productForm.globalErrors) {
              &lt;li&gt;@error.message&lt;/li&gt;
            }
            &lt;/ul&gt;
          }
           @helper.form(action = routes.Products.postForm()) {
            @helper.inputText(productForm("id"))
            @helper.inputText(productForm("name"))
             &lt;input type="submit"&gt;
          }
        }
    
  3. 创建产品控制器 foo_java/app/controllers/Products.java 并添加以下导入、操作和 Play 表单块:

    package controllers;
         import play.*;
        import play.mvc.*;
        import play.data.*;
        import views.html.*;
        import models.*;
         public class Products extends Controller {  
       public static Result create() {
          Form&lt;Product&gt; form = Form.form(Product.class);
               return ok(views.html.product.form.render(form));
           }
            public static Result postForm() {
              Form&lt;Product&gt; productForm = Form.form(Product.class).bindFromRequest();
              if (productForm.hasErrors()) {
              return badRequest(views.html.product.form.render(productForm));
          } else {
         Product product = productForm.get();
              product.save();
          flash("success", "Product saved!");
                  return redirect(controllers.routes.Products.create());
          }
           }
        }
    
  4. 通过添加以下行编辑 foo_java/conf/routes

    GET    /products/new    controllers.Products.create()
        POST   /products        controllers.Products.postForm()
    
  5. 使用网页浏览器查看您的新产品表单:

    <code class="literal">http://localhost:9000/product/new</code>
    
  6. 为您的新产品填写一个名称并点击 提交。您现在应该会收到成功消息:成功消息

对于 Scala,我们需要采取以下步骤:

  1. 以启用热重载的方式运行 foo_scala 应用程序。

  2. 创建表单视图模板文件 app/views/product/form.scala.html 并添加以下内容:

    @(productForm: Form[models.Product])(implicit flash: Flash)
    
        @import common._
         @mainLayout(title = "New Product") {
          @flash.get("success").map { message =&gt;
            &lt;p&gt;@message&lt;/p&gt;
          }
           @if(productForm.hasGlobalErrors) {
            &lt;ul&gt;
            @for(error &lt;- productForm.globalErrors) {
              &lt;li&gt;@error.message&lt;/li&gt;
            }
            &lt;/ul&gt;
          }
           @helper.form(action = routes.Products.postForm()) {
            @helper.inputText(productForm("id"))
            @helper.inputText(productForm("name"))
             &lt;input type="submit"&gt;
          }
        }
    
  3. 创建产品控制器 foo_scala/app/controllers/Products.scala 并添加以下导入、操作和 Play 表单块:

    import play.api._
        import play.api.mvc._
        import models._
        import play.api.data._
        import play.api.data.Forms._
         val form = Form(
          mapping(
            "id" -&gt; longNumber,
            "name" -&gt; text
          )(Product.apply)(Product.unapply)
        )
    
        def create = Action { implicit request =&gt;
          Ok(views.html.product.form(form))
        }
         def postForm = Action { implicit request =&gt;
          form.bindFromRequest.fold(
            formWithErrors =&gt; {
              BadRequest(views.html.product.form(formWithErrors))
            },
            product =&gt; {Product.save(product)
              Redirect(routes.Products.create).flashing("success" -&gt; "Product saved!")
            }
          )
        }
    
  4. 通过添加以下行编辑 foo_scala/conf/routes

    GET    /products/new    controllers.Products.create
        POST   /products        controllers.Products.postForm
    
  5. 使用网页浏览器查看您的新产品表单:

    <code class="literal">http://localhost:9000/product/new</code>
    
  6. 为您的新产品填写一个名称并点击 提交。您现在应该会收到以下成功消息:成功消息

工作原理...

在这个菜谱中,我们能够使用 Play 框架创建我们的第一个 HTTP 表单。这个菜谱包括了创建 HTML 表单视图模板和我们的产品控制器。我们声明了两个网络操作和两个 URL 路由,并创建了 Play 表单对象,我们使用它将请求参数绑定到我们的模型 Fruit。我们能够通过在网页浏览器中访问 http://localhost:9000/Products/new 来加载网络表单。填写完我们的表单详细信息后,我们提交了表单本身,并从产品控制器收到了通知。

使用表单验证

Play 框架提供了一个简单的方式来验证表单提交。对于 Play Java,我们将验证添加到模型中,这将检查提交字段的长度,如果验证条件不满足,则返回错误消息。对于 Play Scala,我们将表单验证添加到表单对象本身,并在那里定义每个表单字段的验证参数。

如何操作...

对于 Java,我们需要采取以下步骤:

  1. 编辑产品模型,foo_java/app/models/Product.java 并添加 validate() 方法:

    public String validate() {
          if (name.length() &lt; 3 || name.length() &gt; 100) {
             return "Name must be at least 3 characters or a maximum of 100 characters";
       }
          return null;
        }
    
  2. 使用网页浏览器重新加载产品表单:

    <code class="literal">http://localhost:9000/products/new</code>
    
  3. 产品表单现在应仅接受至少三个字符和最多 100 个字符的产品名称,如下截图所示!img/JI0IYz2L.jpg

对于 Scala,我们需要采取以下步骤:

  1. 编辑产品控制器foo_scala/app/controllers/Products.scala并修改表单的声明方式:

    val form = Form(
          mapping(
            "id" -&gt; longNumber,
            "name" -&gt; nonEmptyText(minLength = 3, maxLength = 100)
          )(Product.apply)(Product.unapply)
        )
    
  2. 使用网页浏览器重新加载产品表单:

    <code class="literal">http://localhost:9000/products/new</code>
    
  3. 产品表单现在应仅接受至少三个字符和最多 100 个字符的水果名称,如下截图所示!img/dQy6FzHw.jpg

它是如何工作的...

在这个菜谱中,我们为产品名称和用户提交的可接受长度添加了数据验证。对于 Java,我们在产品模型中添加了一个validate()方法。

我们可以通过使用 JSR-303 JavaBean 验证注解并定义一个 Play 在模型类中存在时调用的validate()方法来验证我们的 Java 模型。

对于 Scala,我们在控制器中的Form对象中添加了数据验证指令。我们使用了 Play 表单助手来定义产品名称属性的最小和最大字符数。

保护表单提交

Play 框架有一个 CSRF 过滤器模块,开发人员可以在 HTTP 表单提交期间验证 CSRF 令牌。这允许开发人员确信表单是使用有效的会话令牌提交的,并且没有被任何方式篡改。

如何操作...

对于 Java,我们需要采取以下步骤:

  1. 将 Play 过滤器模块作为项目依赖项添加到build.sbt

    libraryDependencies += filters
    
  2. app/目录中创建一个Global.java对象文件:

    import play.GlobalSettings;
        import play.api.mvc.EssentialFilter;
        import play.filters.csrf.CSRFFilter;
         public class Global extends GlobalSettings {
            @Override
            public &lt;T extends EssentialFilter&gt; Class&lt;T&gt;[] filters() {
                return new Class[]{CSRFFilter.class};
            }
        }
    
  3. conf/application.conf中声明Global.java对象:

    application.global=Global
    
  4. 通过添加产品表单文件app/views/product/form.scala.html的隐式请求对象来更新模板声明:

    @(productForm: Form[models.Product])
    
  5. 将 CSRF 令牌助手标签添加到产品表单文件app/views/product/form.scala.html

    @helper.form(action = routes.Products.postForm()) {
            @helper.CSRF.formField @* -Add the CSRF Token Helper Tag- *@
        }
    
  6. 使用网页浏览器重新加载产品表单:

    <code class="literal">http://localhost:9000/products/new</code>
    
  7. 产品表单现在应包含由 Play 生成的 CSRF 令牌,并使用此令牌验证表单提交,如下截图所示!img/mkHuCatJ.jpg

对于 Scala,我们需要采取以下步骤:

  1. 将 Play 过滤器模块作为项目依赖项添加到build.sbt

    libraryDependencies += filters
    
  2. app/中创建一个Global.scala对象文件:

    import play.api._
         object Global extends GlobalSettings {
        }
    
  3. conf/application.conf中声明Global.scala对象:

    application.global=Global
    
  4. 通过修改app/Global.scala中的对象声明来添加 Play 全局 CSRF 过滤器:

    import play.api.mvc._
        import play.filters.csrf._
         object Global extends WithFilters(CSRFFilter()) with GlobalSettings
    
  5. 通过添加产品表单文件app/views/product/form.scala.html的隐式请求对象来更新模板声明:

    @(productForm: Form[models.Product])(implicit flash: Flash, request: play.api.mvc.Request[Any])
    
  6. 将 CSRF 令牌助手标签添加到产品表单文件app/views/product/form.scala.html

    @helper.form(action = routes.Products.postForm()) {
            @helper.CSRF.formField @* -Add the CSRF Token Helper Tag- *@
        }
    
  7. 使用网页浏览器重新加载产品表单:

    <code class="literal">http://localhost:9000/products/new</code>
    
  8. 产品表单现在应包含由 Play 生成的 CSRF 令牌,并使用此令牌验证表单提交。

它是如何工作的...

在这个菜谱中,我们添加了 Play 框架的过滤器模块,其中包括 CSRF 辅助工具。我们通过在 Play 应用程序的全局设置类app/Global.javaapp/Global.scala中声明CSRFFilter来添加全局 CSRF 支持。最后一步是在我们的标签中插入一个 CSRF 令牌辅助标签,该标签由过滤器用于验证表单提交。

修改或篡改有效的 CSRF 令牌现在将导致错误,并被 Play 拒绝,如下截图所示:

图片

使用 JUnit(Java)和 specs2(Scala)进行测试

对于一个 Web 框架来说,尽可能无缝地将其与 Web 框架本身集成进行测试是非常重要的。这最小化了开发者在编写功能规范和编写测试以验证其工作时遇到的摩擦。对于 Play Java 项目,我们将使用流行的测试框架 JUnit。我们将使用它来进行简单的单元测试,并测试我们的模型和控制器操作。对于 Play Scala 项目,我们将使用 specs2 来进行简单的单元测试,并测试我们的模型、控制器操作和路由映射。

如何做...

对于 Java,我们需要采取以下步骤:

  1. test/目录下创建一个新的规范类ProductTest.java,并添加以下内容:

    import static org.junit.Assert.*;
        import org.junit.Test;
         public class ProductTest {
    
          @Test
          public void testString() {
            String str = "product";
            assertEquals(7, str.length());
          }
        }
    
  2. 使用 Activator 运行第一个规范,命令为test-only ProductTest

    <span class="strong"><strong>    $ activator</strong></span>
    <span class="strong"><strong>    [info] Loading project definition from /private/tmp/foo_java/project</strong></span>
    <span class="strong"><strong>    [info] Set current project to foo_java (in build file:/private/tmp/foo_java/)</strong></span>
    <span class="strong"><strong>    [foo_java] $ test-only ProductTest</strong></span>
    <span class="strong"><strong>    [info] Compiling 3 Java sources to /private/tmp/foo_java/target/scala-2.11/test-classes...</strong></span>
    <span class="strong"><strong>    [info] Passed: Total 1, Failed 0, Errors 0, Passed 1</strong></span>
    <span class="strong"><strong>    [success] Total time: 3 s, completed 09 29, 14 8:44:31 PM</strong></span>
    

对于 Scala,我们需要采取以下步骤:

  1. test/目录下创建一个新的 Spec 类ProductSpec.scala,并添加以下内容:

    import org.specs2.mutable._
         class ProductSpec extends Specification {
           "The 'product' string" should {
            "contain seven characters" in {
              "product" must have size(7)
            }
          }
        }
    
  2. 使用 Activator 运行第一个规范,命令为test-only ProductSpec

    <span class="strong"><strong>    $ activator</strong></span>
    <span class="strong"><strong>    [info] Loading project definition from /private/tmp/foo_scala/project</strong></span>
    <span class="strong"><strong>    [info] Set current project to foo_scala (in build file:/private/tmp/foo_scala/)</strong></span>
    <span class="strong"><strong>    [foo_scala] $ test-only ProductSpec</strong></span>
    <span class="strong"><strong>    [info] ProductSpec</strong></span>
    <span class="strong"><strong>    [info]</strong></span>
    <span class="strong"><strong>    [info] The 'product' string should</strong></span>
    <span class="strong"><strong>    [info] + contain seven characters</strong></span>
    <span class="strong"><strong>    [info]</strong></span>
    <span class="strong"><strong>    [info] Total for specification ProductSpec</strong></span>
    <span class="strong"><strong>    [info] Finished in 24 ms</strong></span>
    <span class="strong"><strong>    [info] 1 example, 0 failure, 0 error</strong></span>
    <span class="strong"><strong>    [info] Passed: Total 1, Failed 0, Errors 0, Passed 1</strong></span>
    <span class="strong"><strong>    [success] Total time: 2 s, completed 09 29, 14 12:22:57 PM</strong></span>
    

它是如何工作的...

在这个菜谱中,我们创建了一个全新的规范文件,该文件将包含我们的测试规范。我们将此文件放置在test/目录中,并使用activatortest-only命令运行测试。test命令用于运行测试,并显示测试结果。

测试模型

以下菜谱专注于编写对我们模型对象的测试。我们将创建一个新的记录并添加断言来验证对象的创建。然后我们将使用Activator命令来运行我们的测试。

如何做...

对于 Java,我们需要采取以下步骤:

  1. 编辑ProductTest.java文件并添加以下内容:

    // add new imports
        import static play.test.Helpers.*;
        import models.*;
        import play.test.*;
         // add new test
        @Test
        public void testSavingAProduct() {
          running(fakeApplication(), new Runnable() {
            public void run() {
    
         Product product = new Product();
              product.name = "Apple";
              product.save();
              assertNotNull(product.getId());
            }
          });
        }
    
  2. 通过运行命令test-only ProductTest来执行新的规范:

    <span class="strong"><strong>    [foo_java] $ test-only ProductTest</strong></span>
    <span class="strong"><strong>    [info] Passed: Total 2, Failed 0, Errors 0, Passed 2</strong></span>
    <span class="strong"><strong>    [success] Total time: 2 s, completed 09 29, 14 9:33:43 PM</strong></span>
    

对于 Scala,我们需要采取以下步骤:

  1. 编辑ProductSpec.scala文件并添加以下内容:

    import models._
        import play.api.test.WithApplication
         "models.Product" should {
          "create a product with save()" in new WithApplication {
                    val product = Product(1, "Apple")
            val productId = Product.save(product)
               productId must not be None
          }
        }
    
  2. 通过运行命令test-only ProductSpec来执行新的规范:

    <span class="strong"><strong>    [foo_scala] $ test-only ProductSpec</strong></span>
    <span class="strong"><strong>    [info] Compiling 1 Scala source to /private/tmp/foo_scala/target/scala-2.11/test-classes...</strong></span>
    <span class="strong"><strong>    [info] ProductSpec</strong></span>
    <span class="strong"><strong>    [info]</strong></span>
    <span class="strong"><strong>    [info] The 'product' string should</strong></span>
    <span class="strong"><strong>    [info] + contain seven characters</strong></span>
    <span class="strong"><strong>    [info]</strong></span>
    <span class="strong"><strong>    [info] models.Product should</strong></span>
    <span class="strong"><strong>    [info] + create a product with save()</strong></span>
    <span class="strong"><strong>    [info]</strong></span>
    <span class="strong"><strong>    [info] Total for specification ProductSpec</strong></span>
    <span class="strong"><strong>    [info] Finished in 1 second, 90 ms</strong></span>
    <span class="strong"><strong>    [info] 2 examples, 0 failure, 0 error</strong></span>
    <span class="strong"><strong>    [info] Passed: Total 2, Failed 0, Errors 0, Passed 2</strong></span>
    <span class="strong"><strong>    [success] Total time: 4 s, completed 09 29, 14 4:28:51 PM</strong></span>
    

它是如何工作的...

在这个菜谱中,我们添加了一个新的规范,其中我们创建了一个新产品并调用了save()方法。然后我们添加了断言语句来验证save()方法返回的值不等于 none。使用test命令来运行测试并显示测试结果。

测试控制器

以下菜谱专注于编写测试我们的控制器对象。我们将使用FakeApplication来创建一个模拟 HTTP 请求到产品 XML 列表页面,并添加断言来验证我们收到的响应确实是一个代表我们数据库中所有产品的 XML。然后,我们将使用Activator命令来运行我们的测试。

如何操作...

对于 Java,我们需要采取以下步骤:

  1. 编辑ProductTest.java文件并添加以下内容:

    // add new imports
        import play.mvc.*;
        import static org.fest.assertions.Assertions.*;
         //add new test
        @Test
        public void testProductListAsXml() {
          Result result = callAction(controllers.routes.ref.Application.listProductsAsXML());   
          assertThat(status(result)).isEqualTo(OK);
          assertThat(contentType(result)).isEqualTo("application/xml");
          assertThat(contentAsString(result)).contains("products");
        }
    
  2. 通过运行命令test-only ProductTest来执行新的 spec:

    <span class="strong"><strong>    [foo_java] $ test-only ProductTest</strong></span>
    <span class="strong"><strong>    [info] Compiling 1 Java source to /private/tmp/foo_java/target/scala-2.11/test-classes...</strong></span>
    <span class="strong"><strong>    [info] Passed: Total 3, Failed 0, Errors 0, Passed 3</strong></span>
    <span class="strong"><strong>    [success] Total time: 3 s, completed 09 29, 14 9:37:03 PM</strong></span>
    

对于 Scala,我们需要采取以下步骤:

  1. 编辑ProductSpec.scala 文件,并添加以下 spec 代码:

    import controllers._
        import play.api.test.FakeRequest
        import play.api.test.Helpers._
         "controllers.Application" should {
          "respond with XML for /products.xml requests" in new WithApplication {
            val result = controllers.Application.listProductsAsXML()(FakeRequest())
             status(result) must equalTo(OK)
            contentType(result) must beSome("application/xml")
            contentAsString(result) must contain("products")
          }
        }
    
  2. 通过运行test-only ProductSpec命令来执行新的 spec:

    <span class="strong"><strong>    $ test-only ProductSpec</strong></span>
    <span class="strong"><strong>    [info] Compiling 1 Scala source to /private/tmp/foo_scala/target/scala-2.11/test-classes...</strong></span>
    <span class="strong"><strong>    [info] ProductSpec</strong></span>
    <span class="strong"><strong>    [info]</strong></span>
    <span class="strong"><strong>    [info] The 'product' string should</strong></span>
    <span class="strong"><strong>    [info] + contain seven characters</strong></span>
    <span class="strong"><strong>    [info]</strong></span>
    <span class="strong"><strong>    [info] models.Product should</strong></span>
    <span class="strong"><strong>    [info] + create a product with save()</strong></span>
    <span class="strong"><strong>    [info]</strong></span>
    <span class="strong"><strong>    [info] controllers.Application should</strong></span>
    <span class="strong"><strong>    [info] + respond with XML for /products.xml requests</strong></span>
    <span class="strong"><strong>    [info]</strong></span>
    <span class="strong"><strong>    [info] Total for specification ProductSpec</strong></span>
    <span class="strong"><strong>    [info] Finished in 1 second, 333 ms</strong></span>
    <span class="strong"><strong>    [info] 3 examples, 0 failure, 0 error</strong></span>
    <span class="strong"><strong>    [info] Passed: Total 3, Failed 0, Errors 0, Passed 3</strong></span>
    <span class="strong"><strong>    success] Total time: 4 s, completed 09 29, 14 5:23:41 PM</strong></span>
    

它是如何工作的...

在这个菜谱中,我们创建了一个新的 spec 来测试我们之前创建的 URL 路由。然后,我们通过确保响应的内容类型是application/xml并且它包含我们的根元素 products 来验证/products.xml URL 路由。test命令用于运行测试,并显示测试结果。

第二章:使用控制器

在本章中,我们将介绍以下食谱:

  • 使用 HTTP 头

  • 使用 HTTP cookies

  • 使用会话

  • 使用自定义操作

  • 使用过滤器

  • 使用路径绑定器

  • 提供 JSON

  • 接收 JSON

  • 上传文件

  • 使用 futures 和 Akka actors

简介

在本章中,我们将更深入地探讨 Play 控制器,并讨论关于 Web 应用程序中控制器的一些高级主题。我们还将了解 Play 如何处理和解决除了数据操作和数据检索等常见用例之外更现代的 Web 应用程序需求。由于我们依赖于控制器来路由 Web 请求和响应,我们希望确保我们的控制器尽可能轻量级和松耦合,以确保页面响应性和可预测的页面加载时间。从模型和其他数据相关处理和服务中提供清晰的分离,也为开发者提供了对每一层职责的更清晰理解。

使用 HTTP 头

对于这个食谱,我们将探讨 Play 应用程序如何操作 HTTP 头。我们将使用curl工具来验证我们对 HTTP 响应头所做的更改是否正确应用。对于 Windows 用户,建议安装 Cygwin 以在 Windows 机器上获得类似 Unix 的环境(www.cygwin.com/)。

如何操作…

对于 Java,我们需要采取以下步骤:

  1. 以启用热重载的方式运行foo_java应用程序:

    <span class="strong"><strong>    activator  "~run"</strong></span>
    
  2. 通过添加以下操作修改foo_java/app/controllers/Application.java

    public static Result modifyHeaders() {
           response().setHeader("ETag", "foo_java");
          return ok("Header Modification Example");
        }
    
  3. foo_scala/conf/routes中为新增的操作添加一个新的routes条目:

    <span class="strong"><strong>  GET   /header_example    controllers.Application.modifyHeaders</strong></span>
    
  4. 请求我们的新路由并检查响应头以确认我们对 HTTP 响应头的修改:

    <span class="strong"><strong>$ curl -v http://localhost:9000/header_example</strong></span>
    <span class="strong"><strong>* Hostname was NOT found in DNS cache</strong></span>
    <span class="strong"><strong>*   Trying ::1...</strong></span>
    <span class="strong"><strong>* Connected to localhost (::1) port 9000 (#0)</strong></span>
    <span class="strong"><strong>&gt; GET /header_example HTTP/1.1</strong></span>
    <span class="strong"><strong>&gt; User-Agent: curl/7.37.1</strong></span>
    <span class="strong"><strong>&gt; Host: localhost:9000</strong></span>
    <span class="strong"><strong>&gt; Accept: */*</strong></span>
    <span class="strong"><strong>&gt;</strong></span>
    <span class="strong"><strong>&lt; HTTP/1.1 200 OK</strong></span>
    <span class="strong"><strong>&lt; Content-Type: text/plain; charset=utf-8</strong></span>
    <span class="strong"><strong>&lt; ETag: foo_java</strong></span>
    <span class="strong"><strong>&lt; Content-Length: 27</strong></span>
    <span class="strong"><strong>&lt;</strong></span>
    <span class="strong"><strong>* Connection #0 to host localhost left intact</strong></span>
    <span class="strong"><strong>Header Modification Example%</strong></span>
    

对于 Scala,我们需要采取以下步骤:

  1. 以启用热重载的方式运行foo_scala应用程序:

    <span class="strong"><strong>  activator "~run"</strong></span>
    
  2. 通过添加以下操作修改foo_scala/app/controllers/Application.scala

    def modifyHeaders = Action {
        Ok("Header Modification Example")
          .withHeaders(
            play.api.http.HeaderNames.ETAG -&gt; "foo_scala"
          )
      }
    
  3. foo_scala/conf/routes中为新增的操作添加一个新的routes条目:

    GET   /header_example    controllers.Application.modifyHeaders
    
  4. 请求我们的新routes并检查响应头以确认我们对 HTTP 响应头的修改:

    <span class="strong"><strong>$ curl -v http://localhost:9000/header_example</strong></span>
    <span class="strong"><strong>* Hostname was NOT found in DNS cache</strong></span>
    <span class="strong"><strong>*   Trying ::1...</strong></span>
    <span class="strong"><strong>* Connected to localhost (::1) port 9000 (#0)</strong></span>
    <span class="strong"><strong>&gt; GET /header_example HTTP/1.1</strong></span>
    <span class="strong"><strong>&gt; User-Agent: curl/7.37.1</strong></span>
    <span class="strong"><strong>&gt; Host: localhost:9000</strong></span>
    <span class="strong"><strong>&gt; Accept: */*</strong></span>
    <span class="strong"><strong>&gt;</strong></span>
    <span class="strong"><strong>&lt; HTTP/1.1 200 OK</strong></span>
    <span class="strong"><strong>&lt; Content-Type: text/plain; charset=utf-8</strong></span>
    <span class="strong"><strong>&lt; ETag: foo_scala</strong></span>
    <span class="strong"><strong>&lt; Content-Length: 27</strong></span>
    <span class="strong"><strong>&lt;</strong></span>
    <span class="strong"><strong>* Connection #0 to host localhost left intact</strong></span>
    <span class="strong"><strong>Header Modification Example%</strong></span>
    

它是如何工作的…

在这个食谱中,我们创建了一个新的 URL 路由和操作。在操作内部,我们添加了一个新的 HTTP 头并为其分配了一个任意值。然后我们使用命令行工具curl访问这个新操作,以便我们可以查看以纯文本形式显示的响应 HTTP 头。输出应包含我们的自定义头键及其分配的任意值。

使用 HTTP cookies

对于这个食谱,我们将探讨 Play 应用程序如何操作 HTTP cookies。我们将使用curl工具来验证我们对包含我们添加到响应中的新 cookie 的 HTTP 响应头所做的更改。

如何操作…

对于 Java,我们需要采取以下步骤:

  1. 以启用热重载的方式运行foo_java应用程序。

  2. 通过添加以下操作修改foo_java/app/controllers/Application.scala

    public static Result modifyCookies() {
            response().setCookie("source", "tw", (60*60));
            return ok("Cookie Modification Example");
         }
    
  3. foo_java/conf/routes中为新增的操作添加一个新的路由条目:

    GET   /cookie_example    controllers.Application.modifyCookies
    
  4. 请求我们的新路由并检查响应头以确认我们对 HTTP 响应头的修改:

    <span class="strong"><strong>$ curl -v http://localhost:9000/cookie_example</strong></span>
    <span class="strong"><strong>* Hostname was NOT found in DNS cache</strong></span>
    <span class="strong"><strong>*   Trying ::1...</strong></span>
    <span class="strong"><strong>* Connected to localhost (::1) port 9000 (#0)</strong></span>
    <span class="strong"><strong>&gt; GET /cookie_example HTTP/1.1</strong></span>
    <span class="strong"><strong>&gt; User-Agent: curl/7.37.1</strong></span>
    <span class="strong"><strong>&gt; Host: localhost:9000</strong></span>
    <span class="strong"><strong>&gt; Accept: */*</strong></span>
    <span class="strong"><strong>&gt;</strong></span>
    <span class="strong"><strong>&lt; HTTP/1.1 200 OK</strong></span>
    <span class="strong"><strong>&lt; Content-Type: text/plain; charset=utf-8</strong></span>
    <span class="strong"><strong>&lt; Set-Cookie: source=tw; Expires=Sun, 23 Oct 2014 10:22:43 GMT; Path=/</strong></span>
    <span class="strong"><strong>&lt; Content-Length: 27</strong></span>
    <span class="strong"><strong>&lt;</strong></span>
    <span class="strong"><strong>* Connection #0 to host localhost left intact</strong></span>
    <span class="strong"><strong>Cookie Modification Example%</strong></span>
    

对于 Scala,我们需要采取以下步骤:

  1. 以启用热重载的方式运行foo_scala应用程序。

  2. 通过添加以下操作修改foo_scala/app/controllers/Application.scala

    def modifyCookies = Action {
          val cookie = Cookie("source", "tw", Some(60*60))
          Ok("Cookie Modification Example")
            .withCookies(cookie)
         }
    
  3. foo_scala/conf/routes中为新增的操作添加一个新的路由条目:

    GET   /cookie_example    controllers.Application.modifyCookies
    
  4. 请求我们的新路由并检查响应头以确认我们对 HTTP 响应头的修改:

    <span class="strong"><strong>$ curl -v http://localhost:9000/cookie_example</strong></span>
    <span class="strong"><strong>* Hostname was NOT found in DNS cache</strong></span>
    <span class="strong"><strong>*   Trying ::1...</strong></span>
    <span class="strong"><strong>* Connected to localhost (::1) port 9000 (#0)</strong></span>
    <span class="strong"><strong>&gt; GET /cookie_example HTTP/1.1</strong></span>
    <span class="strong"><strong>&gt; User-Agent: curl/7.37.1</strong></span>
    <span class="strong"><strong>&gt; Host: localhost:9000</strong></span>
    <span class="strong"><strong>&gt; Accept: */*</strong></span>
    <span class="strong"><strong>&gt;</strong></span>
    <span class="strong"><strong>&lt; HTTP/1.1 200 OK</strong></span>
    <span class="strong"><strong>&lt; Content-Type: text/plain; charset=utf-8</strong></span>
    <span class="strong"><strong>&lt; Set-Cookie: source=tw; Expires=Sun, 23 Oct 2014 09:27:24 GMT; Path=/; HTTPOnly</strong></span>
    <span class="strong"><strong>&lt; Content-Length: 27</strong></span>
    <span class="strong"><strong>&lt;</strong></span>
    <span class="strong"><strong>* Connection #0 to host localhost left intact</strong></span>
    <span class="strong"><strong>Cookie Modification Example%</strong></span>
    

它是如何工作的…

在这个菜谱中,我们创建了一个新的 URL 路由和操作。在操作中,我们添加了一个名为source的新 cookie,并为其分配了一个任意值"tw"和一个可选的过期时间(在这个菜谱中,是一个小时):

val cookie = Cookie("source", "tw", Some(60*60))

然后,我们使用命令行工具curl访问这个新的操作,以便我们可以查看操作中分配的原始文本 HTTP 响应头。输出应该包含我们分配的 cookie 名称和值的Set-Cookie头。

使用会话

对于这个菜谱,我们将探讨 Play 应用程序如何处理会话状态。这听起来有些反直觉,因为 Play 声称是一个无状态和轻量级的 Web 框架。然而,由于会话和会话状态已成为 Web 应用程序的主要组件,Play 将会话实现为 cookie,因此实际上存储在客户端或用户浏览器上。

如何做到这一点…

对于 Java,我们需要采取以下步骤:

  1. 以启用热重载的方式运行foo_java应用程序。

  2. 通过添加以下操作修改foo_java/app/controllers/Application.scala

    public static Result modifySession() {
            final String sessionVar = "user_pref";
            final String userPref = session(sessionVar);
            if (userPref == null) {
            session(sessionVar, "tw");
            return ok("Setting session var: " + sessionVar);
            } else {
            return ok("Found user_pref: " + userPref);
            }
         }
    
  3. foo_java/conf/routes中为新增的操作添加一个新的路由条目:

    GET   /session_example    controllers.Application.modifySession
    

  4. 使用网络浏览器访问这个新的 URL 路由(http://localhost:9000/session_example)。您应该看到文本设置会话变量:user_pref

  5. 再次使用相同的网络浏览器访问这个新的 URL 路由,您会看到文本找到 userPref: tw

  6. 我们使用curl分配了新的会话变量:

    <span class="strong"><strong>$ curl -v http://localhost:9000/session_example</strong></span>
    <span class="strong"><strong>* Hostname was NOT found in DNS cache</strong></span>
    <span class="strong"><strong>*   Trying ::1...</strong></span>
    <span class="strong"><strong>* Connected to localhost (::1) port 9000 (#0)</strong></span>
    <span class="strong"><strong>&gt; GET /session_example HTTP/1.1</strong></span>
    <span class="strong"><strong>&gt; User-Agent: curl/7.37.1</strong></span>
    <span class="strong"><strong>&gt; Host: localhost:9000</strong></span>
    <span class="strong"><strong>&gt; Accept: */*</strong></span>
    <span class="strong"><strong>&gt;</strong></span>
    <span class="strong"><strong>&lt; HTTP/1.1 200 OK</strong></span>
    <span class="strong"><strong>&lt; Content-Type: text/plain; charset=utf-8</strong></span>
    <span class="strong"><strong>&lt; Set-Cookie: PLAY_SESSION="cadbcca718bbfcc11af40a2cfe8e4c76716cca1f-user_pref=tw"; Path=/; HTTPOnly</strong></span>
    <span class="strong"><strong>&lt; Content-Length: 30</strong></span>
    <span class="strong"><strong>&lt;</strong></span>
    <span class="strong"><strong>* Connection #0 to host localhost left intact</strong></span>
    <span class="strong"><strong>Setting session var: user_pref%</strong></span>
    

对于 Scala,我们需要采取以下步骤:

  1. 以启用热重载的方式运行foo_scala应用程序。

  2. 通过添加以下操作修改foo_scala/app/controllers/Application.scala

    def modifySession = Action { request =&gt;
          val sessionVar = "user_pref"
          request.session.get(sessionVar) match {
            case Some(userPref) =&gt; {
              Ok("Found userPref: %s".format(userPref))
            }
            case None =&gt; {
          Ok("Setting session var: %s".format(sessionVar))
                .withSession(
                  sessionVar -&gt; "tw"
            )
            }
          }
         }
    
  3. foo_scala/conf/routes中为新增的Action添加一个新的路由条目:

    GET   /session_example    controllers.Application.modifySession
    
  4. 使用网络浏览器访问这个新的 URL 路由(http://localhost:9000/session_example),您应该看到文本设置会话变量:user_pref

  5. 再次使用相同的网络浏览器访问这个新的 URL 路由,您会看到文本找到 userPref: tw

  6. 您也可以使用curl查看我们新的会话变量是如何分配的:

    <span class="strong"><strong>$ curl -v http://localhost:9000/session_example</strong></span>
    <span class="strong"><strong>* Hostname was NOT found in DNS cache</strong></span>
    <span class="strong"><strong>*   Trying 127.0.0.1...</strong></span>
    <span class="strong"><strong>* Connected to localhost (127.0.0.1) port 9000 (#0)</strong></span>
    <span class="strong"><strong>&gt; GET /session_example HTTP/1.1</strong></span>
    <span class="strong"><strong>&gt; User-Agent: curl/7.37.1</strong></span>
    <span class="strong"><strong>&gt; Host: localhost:9000</strong></span>
    <span class="strong"><strong>&gt; Accept: */*</strong></span>
    <span class="strong"><strong>&gt;</strong></span>
    <span class="strong"><strong>&lt; HTTP/1.1 200 OK</strong></span>
    <span class="strong"><strong>&lt; Content-Type: text/plain; charset=utf-8</strong></span>
    <span class="strong"><strong>&lt; Set-Cookie: PLAY_SESSION="64c6d2e0894a60dd28101e37b742f71ae332ed13-user_pref=tw"; Path=/; HTTPOnly</strong></span>
    <span class="strong"><strong>&lt; Content-Length: 30</strong></span>
    <span class="strong"><strong>&lt;</strong></span>
    <span class="strong"><strong>* Connection #0 to host localhost left intact</strong></span>
    <span class="strong"><strong>Setting session var: user_pref%</strong></span>
    

它是如何工作的…

在这个菜谱中,我们创建了一个新的 URL 路由和操作。在操作中,我们添加了一些逻辑来理解会话变量"user_pref"是否已经在会话中存在。如果会话变量确实已设置,我们将在响应体中打印出会话变量的值。如果当前会话中没有找到会话变量,它将添加会话变量到会话中并显示文本,通知请求者它没有找到会话变量。我们通过使用网络浏览器并请求相同的 URL 路由两次来验证这一点;首先,设置会话变量,其次,打印会话变量的值。我们还使用curl来查看会话变量是如何设置为当前会话的 HTTP cookie 头部的。

使用自定义操作

对于这个菜谱,我们将探索 Play Framework 如何提供创建可重用、自定义操作的构建块。

如何做到这一点…

对于 Java,我们需要采取以下步骤:

  1. 启用 Hot-Reloading 运行foo_java应用程序。

  2. 通过添加以下操作修改foo_java/app/controllers/Application.java

    @With(AuthAction.class)
        public static Result dashboard() {
            return ok("User dashboard");
        }
         public static Result login() {
            return ok("Please login");
        }
    
  3. 将我们的新操作类添加到foo_java/app/controllers/AuthAction.java中:

    package controllers;
     import play.*;
    import play.mvc.*;
    import play.libs.*;
    import play.libs.F.*;
     public class AuthAction extends play.mvc.Action.Simple {
        public F.Promise&lt;Result&gt; call(Http.Context ctx) throws Throwable {
            Http.Cookie authCookie = ctx.request().cookie("auth");
             if (authCookie != null) {
              Logger.info("Cookie: " + authCookie);
              return delegate.call(ctx);
             } else {
              Logger.info("Redirecting to login page");
              return Promise.pure(redirect(controllers.routes.
    Application.login()));
            }
        }
    }
    
  4. foo_java/conf/routes中为新添加的操作添加新路由:

    GET   /dashboard      controllers.Application.dashboard
      GET   /login      controllers.Application.login
    
  5. 使用网络浏览器访问仪表板 URL 路由,并注意它会重定向您到登录 URL 路由。您还会在我们的控制台中注意到一条日志条目,其中请求即将被重定向到登录页面:

    [info] application - Redirecting to login page
    

对于 Scala,我们需要采取以下步骤:

  1. 启用 Hot-Reloading 运行foo_scala应用程序。

  2. 通过添加以下操作修改foo_scala/app/controllers/Application.scala

    def dashboard = AuthAction {
           Ok("User dashboard")
         }
          def login = Action {
           Ok("Please login")
         }
    
  3. 还将我们的新操作添加到foo_scala/app/controllers/Application.scala中:

    object AuthAction extends ActionBuilder[Request] {
      import play.api.mvc.Results._
      import scala.concurrent.Future
       def invokeBlockA =&gt;     Future[Result]) = {
        request.cookies.get("auth") match {
          case Some(authCookie) =&gt; {
            Logger.info("Cookie: " + authCookie)
            block(request)  
          }
          case None =&gt; {
            Logger.info("Redirecting to login page")
            Future.successful(Redirect(routes.Application.login()))
          }
        }
      }
    }
    
  4. foo_scala/conf/routes中为新添加的操作添加新路由:

    GET   /dashboard      controllers.Application.dashboard
      GET   /login      controllers.Application.login
    
  5. 使用网络浏览器访问仪表板 URL 路由,并注意它会重定向您到登录 URL 路由。您还会在我们的控制台中注意到一条日志条目,其中请求即将被重定向到登录页面:

    [info] application - Redirecting to login page
    

它是如何工作的…

在这个菜谱中,我们创建了两个新的 URL 路由和操作;一个用于显示用户仪表板,另一个作为我们的登录页面。在仪表板操作中,我们使用了我们的新操作AuthActionAuthAction对象检查authcookie 的存在,如果它在请求中找到了该 cookie,它将调用链中的ActionBuilder

// Java
return delegate.call(ctx);
 // Scala
block(request)

如果请求中没有找到authcookie,AuthAction将重定向当前请求到loginURL 路由,并用Future.successful()包装成一个完成的Future[Result]对象:

// Java
return Promise.pure(redirect(controllers.routes.Application.login()));
 // Scala
Future.successful(Redirect(routes.Application.login()))

使用过滤器

对于这个菜谱,我们将探索 Play Framework 如何提供 HTTP 请求和响应过滤器的 API。HTTP 过滤器提供了一种透明地装饰 HTTP 请求或响应的方法,对于底层服务(如响应压缩)、收集指标和更深入的日志记录非常有用。

注意

值得注意的是,目前(截至 Play 2.3.7 版本),HTTP 过滤器最好使用 Play Scala API 中的 play.api.mvc.EssentialFilter 特性来实现。因此,对于这个菜谱,我们将为 Java 菜谱实现一个基于 Scala 的过滤器。

如何操作...

对于 Java,我们需要采取以下步骤:

  1. 启用热重载功能运行 foo_java 应用程序。

  2. 通过创建文件 foo_java/app/ResponseTimeLogFilter.scala 并添加以下内容来创建一个新的过滤器对象:

    import play.api.mvc._
       object ResponseTimeLogFilter {
          def apply(): ResponseTimeLogFilter = {
             new ResponseTimeLogFilter()
           }
         }
       class ResponseTimeLogFilter extends Filter {
          import play.api.Logger
          import scala.concurrent.Future
          import play.api.libs.concurrent.Execution.Implicits.defaultContext
            def apply(f: (RequestHeader) =&gt; Future[Result])(rh: RequestHeader): Future[Result] = {
             val startTime = System.currentTimeMillis
             val result = f(rh)
             result.map { result =&gt;
               val currDate = new java.util.Date
               val responseTime = (currDate.getTime() - startTime) / 1000F
                Logger.info(s"${rh.remoteAddress} - [${currDate}] - ${rh.method} ${rh.uri}" +
               s" ${result.header.status} ${responseTime}")
                result
             }
           }
         }
    
  3. 通过在 app/Global.java 文件中声明它来使用这个新的过滤器:

    import play.GlobalSettings;
        import play.api.mvc.EssentialFilter;
         public class Global extends GlobalSettings {
          public &lt;T extends EssentialFilter&gt; Class&lt;T&gt;[] filters() {
            return new Class[]{
        ResponseTimeLogFilter.class
         };
          }
        }
    
  4. 使用网络浏览器访问我们定义的任何之前的 URL 路由(http://localhost:9000/session_example)。你将能够看到一个包含我们响应统计的新日志条目:

    <span class="strong"><strong>[info] application - 0:0:0:0:0:0:0:1 - [Mon Oct 24 23:58:44 PHT 2014] - GET /session_example 200 0.673</strong></span>
    

对于 Scala,我们需要采取以下步骤:

  1. 启用热重载功能运行 foo_scala 应用程序。

  2. 通过创建文件 foo_scala/app/controllers/ResponseTimeLogFilter.scala 并添加以下内容来创建一个新的过滤器对象:

    import play.api.Logger
      import play.api.mvc._
      import play.api.libs.concurrent.Execution.Implicits.defaultContext
       object ResponseTimeLogFilter extends EssentialFilter {
          def apply(nextFilter: EssentialAction) = new EssentialAction {
            def apply(requestHeader: RequestHeader) = {
              val startTime = System.currentTimeMillis
              nextFilter(requestHeader).map { result =&gt;
                val currDate = new java.util.Date
                val responseTime = (currDate.getTime() - startTime) / 1000F
                 Logger.info(s"${requestHeader.remoteAddress} - [${currDate}] - ${requestHeader.method} ${requestHeader.uri}" +
                s" ${result.header.status} ${responseTime}")
                 result
              }
            }
          }
        }
    
  3. 通过在 app/Global.scala 文件中声明它来使用这个新的过滤器:

    import play.api._
        import play.api.mvc._
        import controllers.ResponseTimeLogFilter
         object Global extends WithFilters(ResponseTimeLogFilter) {
          override def onStart(app: Application) {
         Logger.info("Application has started")
          }
          override def onStop(app: Application) {
            Logger.info("Application shutdown...")
          }
        }
    
  4. 使用网络浏览器访问我们定义的任何之前的 URL 路由(http://localhost:9000/session_example)。你将能够看到一个包含我们响应统计的新日志条目:

    <span class="strong"><strong>[info] application - 0:0:0:0:0:0:0:1 - [Mon Oct 24 23:58:44 PHT 2014] - GET /session_example 200 0.673</strong></span>
    

它是如何工作的...

在这个菜谱中,我们创建了一个新的基于 Scala 的过滤器。该过滤器简单地计算请求的总响应时间并在日志文件中打印出来。然后,我们通过在全局应用程序配置类 Global.java/Global.scala 中引用它来使用过滤器。这将应用于 Play 应用程序的所有请求。

使用路径绑定

对于这个菜谱,我们将探讨 Play 应用程序如何允许我们使用自定义绑定器来处理路径参数。当您想通过在路由文件中处理模型类和方法签名而不是单独的属性和字段来简化路由和相应操作的声明时,这些绑定器非常有用。

如何操作...

对于 Java,我们需要采取以下步骤:

  1. 启用热重载功能运行 foo_java 应用程序。

  2. 创建一个新的产品控制器作为 foo_java/app/controllers/Products.scala。一旦创建,添加产品 case 类和伴随对象、产品表单对象以及两个路由(第一个用于在表单中显示所选产品,第二个作为表单更新提交的 PUT 操作):

    package controllers;
     import play.*;
    import play.mvc.*;
    import play.data.*;
    import views.html.*;
    import models.*;
     public class Products extends Controller {
      private static Form&lt;Product&gt; productForm = Form.form(Product.class);
       public static Result edit(Product product) {
        return ok(views.html.products.form.render(product.sku, productForm.fill(product)));
      }
       public static Result update(String sku) {
        return ok("Received update request");
      }
    }
    
  3. foo_java/app/models/Product.java 中添加我们的新产品模型:

    package models;
     import play.mvc.*;
     public class Product implements PathBindable&lt;Product&gt; {
      public String sku;
      public String title;
       private static final java.util.Map&lt;String, String&gt; productMap = new java.util.HashMap&lt;String, String&gt;();
      static {
        productMap.put("ABC", "8-Port Switch");
        productMap.put("DEF", "16-Port Switch");
        productMap.put("GHI", "24-Port Switch");
      }
         public static void add(Product product) {
          productMap.put(product.sku, product.title);
        }  
         public static java.util.List&lt;Product&gt; getProducts() {
          java.util.List&lt;Product&gt; productList = new java.util.ArrayList&lt;Product&gt;();
          for (java.util.Map.Entry&lt;String, String&gt; entry : productMap.entrySet()) {
            Product p = new Product();
            p.sku = entry.getKey();
            p.title = entry.getValue();
            productList.add(p);    
          }
          return productList;
        }
       public Product bind(String key, String value) {
        String product = productMap.get(value);
        if (product != null) {
          Product p = new Product();
          p.sku = value;
          p.title = product;
           return p;
        } else {
          throw new IllegalArgumentException("Product with sku " + value + " not found");
        }
      }
       public String unbind(String key) {
        return sku;
      }
       public String javascriptUnbind() {
        return "function(k,v) {\n" +
            "    return v.sku;" +
            "}";
      }
    }
    
  4. foo_java/conf/routes 中为新增的操作添加新的路由:

    GET   /products/:product   controllers.Products.edit(product: models.Product)
      PUT   /products/:sku     controllers.Products.update(sku)
    
  5. foo_java/app/views/products/form.scala.html 中创建产品表单视图模板,内容如下:

    @(sku: String, productForm: Form[models.Product])
     @helper.form(action = routes.Products.update(sku)) {
      @helper.inputText(productForm("sku"))
      @helper.inputText(productForm("title"))
       &lt;input type="submit" /&gt;
    }
    
  6. 访问我们编辑的产品 URL 路由(http://localhost:9000/products/ABC)。你应该能够查看我们第一个产品的编辑表单。访问我们的下一个编辑产品 URL 路由(http://localhost:9000/products/DEF),你应该能在表单中看到相关产品详情的加载。

  7. 访问 URL http://localhost:9000/products/XYZ,看看 Play 如何自动生成我们指定的自定义错误消息:

    For request 'GET /products/XYZ' [Product with sku XYZ not found]
    

对于 Scala,我们需要采取以下步骤:

  1. 使用 Hot-Reloading 功能运行foo_scala应用程序。

  2. 创建一个新的产品控制器作为foo_scala/app/controllers/Products.scala。一旦创建,添加一个产品 case 类和伴随对象、产品表单对象以及两个路由(第一个用于在表单中显示所选产品,第二个作为表单更新提交的 PUT 操作):

    package controllers
       import play.api._
      import play.api.data._
      import play.api.data.Forms._
      import play.api.mvc._
       case class Product(sku: String, title: String)
       object Product {
        implicit def pathBinder(implicit stringBinder: PathBindable[String]) = new PathBindable[Product] {
          override def bind(key: String, value: String): Either[String, Product] = {
            for {
              sku &lt;- stringBinder.bind(key, value).right
              product &lt;- productMap.get(sku).toRight("Product not found").right
            } yield product
          }
          override def unbind(key: String, product: Product): String = {
            stringBinder.unbind(key, product.sku)
          }
        }
         def add(product: Product) = productMap += (product.sku -&gt; product)
         val productMap = scala.collection.mutable.Map(
          "ABC" -&gt; Product("ABC", "8-Port Switch"),
          "DEF" -&gt; Product("DEF", "16-Port Switch"),
          "GHI" -&gt; Product("GHI", "24-Port Switch")
        )
      }
       object Products extends Controller {
        val productForm: Form[Product] = Form(
          mapping(
            "sku" -&gt; nonEmptyText,
            "title" -&gt; nonEmptyText
          )(Product.apply)(Product.unapply)
        )
         def edit(product: Product) = Action {
          Ok(views.html.products.form(product.sku, productForm.fill(product)))
        }
         def update(sku: String) = Action {
          Ok("Received update request")
        }
      }
    
  3. foo_scala/conf/routes中为新增的操作添加新路由:

    GET   /products/:product   controllers.Products.edit(product: controllers.Product)
      PUT   /products/:sku     controllers.Products.update(sku)
    
  4. foo_scala/app/views/products/form.scala.html中创建产品表单视图模板,内容如下:

    @(sku: String, productForm: Form[controllers.Product])
     @helper.form(action = routes.Products.update(sku)) {
      @helper.inputText(productForm("sku"))
      @helper.inputText(productForm("title"))
       &lt;input type="submit" /&gt;
    }
    
  5. 访问我们的编辑产品 URL 路由(http://localhost:9000/products/ABC),你应该能够查看我们第一个产品的编辑表单。访问我们的下一个编辑产品 URL 路由(http://localhost:9000/products/DEF),你应该能在表单中看到相关产品详情的加载。

  6. 访问 URL http://localhost:9000/products/XYZ,看看 Play 如何自动生成我们指定的自定义错误消息:

    For request 'GET /products/XYZ' [Product not found]
    

它是如何工作的...

在这个菜谱中,我们使用了 Play 的PathBindable接口来使用自定义路径绑定器。我们创建了一个新的路由、控制器和模型来表示产品。我们实现了产品的PathBindable绑定和解绑方法:

对于 Java,表单绑定相当直接:

// Java 
    private static Form&lt;Product&gt; productForm = Form.form(Product.class);
    public static Result edit(Product product) {
    return ok(views.html.products.form.render(product.sku, productForm.fill(product)));
   }

对于 Scala,我们在PathBindable类中覆盖了两个方法。在表单绑定过程中,我们首先检索产品标识符sku,然后将这个相同的sku传递给产品映射以检索对应的产品:

// Scala 
    implicit def pathBinder(implicit stringBinder: PathBindable[String]) = new PathBindable[Product] {
    override def bind(key: String, value: String): Either[String, Product] = {
      for {
        sku &lt;- stringBinder.bind(key, value).right
        product &lt;- productMap.get(sku).toRight("Product not found").right
      } yield product
    }
    override def unbind(key: String, product: Product): String = {
      stringBinder.unbind(key, product.sku)
    }
  }

我们定义了一个需要自定义路径绑定的路由:

GET   /products/:product   controllers.Products.edit(product: controllers.Product)

你会注意到在定义早期路由时,我们将产品参数映射到了一个产品实例。PathBindable类在这里完成了所有工作,将传递的sku转换为产品实例。

提供 JSON

对于这个菜谱,我们将探索 Play 框架如何让我们轻松地将我们的模型对象转换为 JSON。能够编写以 JSON 数据格式提供数据的 Web 服务,是现代 Web 应用的非常常见需求。Play 提供了一个 JSON 处理库,我们将在本菜谱中使用它。

如何做到这一点...

对于 Java,我们需要采取以下步骤:

  1. 使用 Hot-Reloading 功能运行foo_java应用程序。

  2. foo_java/app/controllers/Products.java中修改产品控制器,通过添加我们的产品列表操作:

    public static Result index() {
        return ok(Json.toJson(Product.getProducts()));
      }
    
  3. 我们需要为 Play 的 JSON 库添加以下导入语句:

    import play.libs.Json;
    
  4. foo_java/conf/routes中为产品列表操作添加一个新的路由:

    GET   /products       controllers.Products.index
    
  5. 使用curl访问我们的产品列表 URL 路由(http://localhost:9000/products):

    <span class="strong"><strong>$ curl -v http://localhost:9000/products</strong></span>
    <span class="strong"><strong>* Hostname was NOT found in DNS cache</strong></span>
    <span class="strong"><strong>*   Trying ::1...</strong></span>
    <span class="strong"><strong>* Connected to localhost (::1) port 9000 (#0)</strong></span>
    <span class="strong"><strong>&gt; GET /products HTTP/1.1</strong></span>
    <span class="strong"><strong>&gt; User-Agent: curl/7.37.1</strong></span>
    <span class="strong"><strong>&gt; Host: localhost:9000</strong></span>
    <span class="strong"><strong>&gt; Accept: */*</strong></span>
    <span class="strong"><strong>&gt;</strong></span>
    <span class="strong"><strong>&lt; HTTP/1.1 200 OK</strong></span>
    <span class="strong"><strong>&lt; Content-Type: application/json; charset=utf-8</strong></span>
    <span class="strong"><strong>&lt; Content-Length: 117</strong></span>
    <span class="strong"><strong>&lt;</strong></span>
    <span class="strong"><strong>* Connection #0 to host localhost left intact</strong></span>
    <span class="strong"><strong>[{"sku":"ABC","title":"8-Port Switch"},{"sku":"DEF","title":"16-Port Switch"},{"sku":"GHI","title":"24-Port Switch"}]%</strong></span>
    
  6. 当我们查看curl命令的输出时,你会注意到我们的内容类型被自动设置为相应的内容(application/json),并且响应体包含一个 JSON 产品数组。

对于 Scala,我们需要采取以下步骤:

  1. 使用 Hot-Reloading 启用运行foo_scala应用程序。

  2. 通过在foo_scala/app/controllers/Products.scala中添加我们的产品列表动作来修改产品控制器:

    def index = Action {
        Ok(toJson(Product.productMap.values))
      }
    
  3. 我们需要添加以下导入语句以使用 Play 的 JSON 库:

    import play.api.libs.json._
        import play.api.libs.json.Json._
    
  4. 我们还需要在我们的产品控制器中添加我们产品模型的写入实现:

    implicit val productWrites = new Writes[Product] {
          def writes(product: Product) = Json.obj(
            "sku" -&gt; product.sku,
            "title" -&gt; product.title
          )
        }
    
  5. foo_scala/conf/routes中为产品列表动作添加一个新的路由:

    GET   /products       controllers.Products.index
    
  6. 使用curl通过我们的产品列表 URL 路由(http://localhost:9000/products)进行访问:

    <span class="strong"><strong>$ curl -v http://localhost:9000/products</strong></span>
    <span class="strong"><strong>* Hostname was NOT found in DNS cache</strong></span>
    <span class="strong"><strong>*   Trying ::1...</strong></span>
    <span class="strong"><strong>* Connected to localhost (::1) port 9000 (#0)</strong></span>
    <span class="strong"><strong>&gt; GET /products HTTP/1.1</strong></span>
    <span class="strong"><strong>&gt; User-Agent: curl/7.37.1</strong></span>
    <span class="strong"><strong>&gt; Host: localhost:9000</strong></span>
    <span class="strong"><strong>&gt; Accept: */*</strong></span>
    <span class="strong"><strong>&gt;</strong></span>
    <span class="strong"><strong>&lt; HTTP/1.1 200 OK</strong></span>
    <span class="strong"><strong>&lt; Content-Type: application/json; charset=utf-8</strong></span>
    <span class="strong"><strong>&lt; Content-Length: 117</strong></span>
    <span class="strong"><strong>&lt;</strong></span>
    <span class="strong"><strong>* Connection #0 to host localhost left intact</strong></span>
    <span class="strong"><strong>[{"sku":"ABC","title":"8-Port Switch"},{"sku":"DEF","title":"16-Port Switch"},{"sku":"GHI","title":"24-Port Switch"}]%</strong></span>
    
  7. 当我们查看curl命令的输出时,你会注意到我们的内容类型已自动设置为相应值(application/json),并且响应体包含了一个 JSON 产品数组。

它是如何工作的…

在这个菜谱中,我们修改了我们的产品控制器并添加了一个新的路由,该路由以 JSON 格式返回产品数组。我们在控制器中创建了动作,并在conf/routes文件中添加了新的路由条目。然后我们声明了一个隐式的writes对象,它告诉 Play 如何以 JSON 格式渲染我们的产品模型:

implicit val productWrites = new Writes[Product] {
      def writes(product: Product) = Json.obj(
        "sku" -&gt; product.sku,
        "title" -&gt; product.title
      )
    }

在前面的代码片段中,我们明确声明了渲染 JSON 的 JSON 键标签。

动作然后将检索到的产品转换为 JSON 作为对动作请求的响应:

Ok(toJson(Product.productMap.values))

接收 JSON

对于这个菜谱,我们将探索 Play 框架如何使我们轻松接收 JSON 对象,并自动将它们转换为模型实例。

如何做到这一点…

对于 Java,我们需要采取以下步骤:

  1. 使用 Hot-Reloading 启用运行foo_java应用程序。

  2. 通过在foo_java/app/controllers/Products.java中添加我们的产品创建动作来修改产品控制器:

    @BodyParser.Of(BodyParser.Json.class)
      public static Result postProduct() {
        JsonNode json = request().body().asJson();
        String sku = json.findPath("sku").textValue();
        String title = json.findPath("title").textValue();
         Product p = new Product();
        p.sku = sku;
        p.title = title;
        Product.add(p);
        return created(Json.toJson(p));
      }
    
  3. 我们需要添加以下导入语句以使用 Play 的 JSON 库:

    import com.fasterxml.jackson.databind.JsonNode;
    
  4. foo_java/conf/routes中为产品列表动作添加一个新的路由:

    POST   /products       controllers.Products.postProduct
    
  5. 使用curl通过我们的产品创建 URL 路由(http://localhost:9000/products)进行访问:

    <span class="strong"><strong>$ curl -v -X POST http://localhost:9000/products --header "Content-type: application/json" --data '{"sku":"JKL", "title":"VPN/Router"}'</strong></span>
    <span class="strong"><strong>* Hostname was NOT found in DNS cache</strong></span>
    <span class="strong"><strong>*   Trying ::1...</strong></span>
    <span class="strong"><strong>* Connected to localhost (::1) port 9000 (#0)</strong></span>
    <span class="strong"><strong>&gt; POST /products HTTP/1.1</strong></span>
    <span class="strong"><strong>&gt; User-Agent: curl/7.37.1</strong></span>
    <span class="strong"><strong>&gt; Host: localhost:9000</strong></span>
    <span class="strong"><strong>&gt; Accept: */*</strong></span>
    <span class="strong"><strong>&gt; Content-type: application/json</strong></span>
    <span class="strong"><strong>&gt; Content-Length: 35</strong></span>
    <span class="strong"><strong>&gt;</strong></span>
    <span class="strong"><strong>* upload completely sent off: 35 out of 35 bytes</strong></span>
    <span class="strong"><strong>&lt; HTTP/1.1 201 Created</strong></span>
    <span class="strong"><strong>&lt; Content-Type: application/json; charset=utf-8</strong></span>
    <span class="strong"><strong>&lt; Content-Length: 34</strong></span>
    <span class="strong"><strong>&lt;</strong></span>
    <span class="strong"><strong>* Connection #0 to host localhost left intact</strong></span>
    <span class="strong"><strong>{"sku":"JKL","title":"VPN/Router"}%</strong></span>
    
  6. 当我们查看curl命令的输出时,你会注意到我们的响应体现在包含了我们新添加的产品。

对于 Scala,我们需要采取以下步骤:

  1. 使用 Hot-Reloading 启用运行foo_scala应用程序。

  2. 通过在foo_scala/app/controllers/Products.scala中添加我们的产品创建动作来修改产品控制器:

    def postProduct = Action(BodyParsers.parse.json) { request =&gt;
        val post = request.body.validate[Product]
        post.fold(
          errors =&gt; {
            BadRequest(Json.obj("status" -&gt;"error", "message" -&gt; JsError.toFlatJson(errors)))
          },
          product =&gt; {
            Product.add(product)
            Ok(toJson(product))
          }
        )
      }
    
  3. 我们需要添加以下导入语句以使用 Play 的 JSON 库:

    import play.api.libs.functional.syntax._
    
  4. 我们还需要在我们的产品控制器中添加我们产品模型的读取实现:

    implicit val productReads: Reads[Product] = (
        (JsPath \ "sku").read[String] and
        (JsPath \ "title").read[String]
      )(Product.apply _)
    
  5. foo_scala/conf/routes中为产品列表动作添加一个新的路由:

    POST   /products       controllers.Products.postProduct
    
  6. 使用curl通过我们的产品创建 URL 路由(http://localhost:9000/products)进行访问:

    <span class="strong"><strong>$ curl -v -X POST http://localhost:9000/products --header "Content-type: application/json" --data '{"sku":"JKL", "title":"VPN/Router"}'</strong></span>
    <span class="strong"><strong>* Hostname was NOT found in DNS cache</strong></span>
    <span class="strong"><strong>*   Trying ::1...</strong></span>
    <span class="strong"><strong>* Connected to localhost (::1) port 9000 (#0)</strong></span>
    <span class="strong"><strong>&gt; POST /products HTTP/1.1</strong></span>
    <span class="strong"><strong>&gt; User-Agent: curl/7.37.1</strong></span>
    <span class="strong"><strong>&gt; Host: localhost:9000</strong></span>
    <span class="strong"><strong>&gt; Accept: */*</strong></span>
    <span class="strong"><strong>&gt; Content-type: application/json</strong></span>
    <span class="strong"><strong>&gt; Content-Length: 35</strong></span>
    <span class="strong"><strong>&gt;</strong></span>
    <span class="strong"><strong>* upload completely sent off: 35 out of 35 bytes</strong></span>
    <span class="strong"><strong>&lt; HTTP/1.1 201 Created</strong></span>
    <span class="strong"><strong>&lt; Content-Type: application/json; charset=utf-8</strong></span>
    <span class="strong"><strong>&lt; Content-Length: 34</strong></span>
    <span class="strong"><strong>&lt;</strong></span>
    <span class="strong"><strong>* Connection #0 to host localhost left intact</strong></span>
    <span class="strong"><strong>{"sku":"JKL","title":"VPN/Router"}%</strong></span>
    
  7. 当我们查看curl命令的输出时,你会注意到我们的响应体现在包含了我们新添加的产品。

它是如何工作的…

在这个菜谱中,我们探讨了如何使用 Play 通过 JSON BodyParser 消费 JSON 对象,并将它们转换为适当的模型对象。对于 Java,我们遍历 JSON 树,检索每个属性值,并将其分配给我们的局部变量:

JsonNode json = request().body().asJson();
    String sku = json.findPath("sku").textValue();
   String title = json.findPath("title").textValue();

对于 Scala,这要简单一些,使用 Play 的 JSON BodyParser

val post = request.body.validate[Product]
     post.fold(
      errors =&gt; {
        BadRequest(Json.obj("status" -&gt;"error", "message" -&gt; JsError.toFlatJson(errors)))
      },
      product =&gt; {
        Product.add(product)
        Ok(toJson(product))
      }
    )

上传文件

对于这个菜谱,我们将学习如何在 Play 应用程序中上传文件。上传文件的能力是网络应用中更为重要的方面之一,我们将在这里看到 Play 如何使文件上传变得简单易处理。

如何操作...

对于 Java,我们需要采取以下步骤:

  1. 以启用热重载的方式运行 foo_java 应用程序。

  2. foo_java/app/views/form.scala.html 中添加一个新的视图模板,用于文件上传表单,内容如下:

    @helper.form(action = routes.Application.handleUpload, 'enctype -&gt; "multipart/form-data") {
        Profile Photo: &lt;input type="file" name="profile"&gt;
    
        &lt;div&gt;
            &lt;input type="submit"&gt;
        &lt;/div&gt;
    
    }
    
  3. 通过添加以下操作修改 foo_java/app/controllers/Application.java

    import play.mvc.Http.MultipartFormData;
        import play.mvc.Http.MultipartFormData.FilePart;
        import java.nio.file.*;
        import java.io.*;    public static Result uploadForm() {
            return ok(form.render());
        }
         public static Result handleUpload() {
            MultipartFormData body = request().body().asMultipartFormData();
            FilePart profileImage = body.getFile("profile");
             if (profileImage != null) {
                try {
                    String fileName = profileImage.getFilename();
                    String contentType = profileImage.getContentType();
                    File file = profileImage.getFile();
                     Path path = FileSystems.getDefault().getPath("/tmp/" + fileName);
                    Files.write(path, Files.readAllBytes(file.toPath()));
                    return ok("Image uploaded");
                } catch(Exception e) {
                    return internalServerError(e.getMessage());
                }
            } else {
                flash("error", "Please upload a valid file");
                return redirect(routes.Application.uploadForm());    
            }
        }
    
  4. foo_java/conf/routes 中为新增的操作添加一个新的路由条目:

    GET   /upload_form    controllers.Application.uploadForm
      POST   /upload      controllers.Application.handleUpload
    
  5. 使用网络浏览器访问上传表单 URL 路由(http://localhost:9000/upload_form)。现在你将能够在文件系统中选择要上传的文件。

  6. 你可以通过查看 /tmp 目录来验证文件确实已上传:

    <span class="strong"><strong>$ ls /tmp</strong></span>
    <span class="strong"><strong>B82BE492-0BEF-4B2D-9A68-2664FB9C2A97.png</strong></span>
    

对于 Scala,我们需要采取以下步骤:

  1. 以启用热重载的方式运行 foo_scala 应用程序。

  2. foo_scala/app/views/form.scala.html 中添加一个新的视图模板,用于文件上传表单,内容如下:

    @helper.form(action = routes.Application.handleUpload, 'enctype -&gt; "multipart/form-data") {
    
        Profile photo: &lt;input type="file" name="profile"&gt;
    
        &lt;div&gt;
            &lt;input type="submit"&gt;
        &lt;/div&gt;
    
    }
    
  3. 通过添加以下操作修改 foo_scala/app/controllers/Application.scala

    def uploadForm = Action {
        Ok(views.html.form())
      }
       def handleUpload = Action(parse.multipartFormData) { request =&gt;
        import java.io.File
         request.body.file("profile") match {
          case Some(profileImage) =&gt; {
            val filename = profileImage.filename
            val contentType = profileImage.contentType
            profileImage.ref.moveTo(new File(s"/tmp/$filename"))
            Ok("Image uploaded")
          }
          case None =&gt; {
            Redirect(routes.Application.uploadForm).flashing(
              "error" -&gt; "Please upload a valid file")
          }
        }
      }
    
  4. foo_scala/conf/routes 中为新增的操作添加一个新的路由条目:

    GET   /upload_form    controllers.Application.uploadForm
      POST   /upload      controllers.Application.handleUpload
    
  5. 使用网络浏览器访问上传表单 URL 路由(http://localhost:9000/upload_form)。现在你将能够在文件系统中选择要上传的文件。

  6. 你可以通过查看 /tmp 目录来验证文件确实已上传:

    <span class="strong"><strong>$ ls /tmp</strong></span>
    <span class="strong"><strong>B82BE492-0BEF-4B2D-9A68-2664FB9C2A97.png</strong></span>
    

它是如何工作的...

在这个菜谱中,我们创建了两个新的操作和 URL 路由;第一个用于显示我们的上传表单模板,第二个用于处理实际的文件上传操作。我们在 app/views 目录中添加了我们的上传视图表单模板 form.scala.html。然后我们通过使用 Play 的辅助方法来检索上传的文件,并将其存储在预定义的位置来处理实际的文件上传提交。

使用 Akka actors 与 futures

对于这个菜谱,我们将探讨 Play 框架如何允许我们使用 futures 和 Akka actors 创建异步控制器。创建异步控制器为开发者提供了一种触发后台作业和异步执行长时间运行操作的方法,而不会牺牲端点的响应性。将 Akka 加入其中为容错、健壮的数据服务带来了新的维度,这些服务在成熟和复杂的网络应用需求时代成为了开发者工具链中的宝贵工具。

如何操作...

对于 Java,我们需要采取以下步骤:

  1. 启用热重载功能运行foo_java应用程序。

  2. 通过添加以下内容修改foo_java/app/controllers/Application.java中的应用控制器:

    public static Promise&lt;Result&gt; asyncExample() {
          ActorRef fileReaderActor = Akka.system().actorOf(Props.create(FileReaderActor.class));
          FileReaderProtocol words = new FileReaderProtocol("/usr/share/dict/words");
           return Promise.wrap(ask(fileReaderActor, words, 3000)).map(
                new Function&lt;Object, Result&gt;() {
                    public Result apply(Object response) {
                        return ok(response.toString());
                    }
                }
            );
        }
    
  3. 我们需要添加以下导入语句以包含必要的库,特别是我们将为本菜谱使用的Akka库:

    import java.util.*;
        import play.libs.Akka;
        import play.libs.F.Function;
        import static play.mvc.Results.*;
        import static akka.pattern.Patterns.ask;
        import play.libs.F.Promise;
        import akka.actor.*;
    
  4. 我们还需要在foo_java/app/actors/FileReaderActor.java中添加我们的 Akka actor,内容如下:

    package actors;
         import java.util.*;
        import java.util.concurrent.Callable;
        import java.nio.charset.*;
        import java.nio.file.*;
        import java.io.*;
        import scala.concurrent.ExecutionContext;
        import scala.concurrent.Future;
        import scala.concurrent.Await;
        import scala.concurrent.duration.*;
        import akka.dispatch.*;
        import akka.util.Timeout;
        import akka.actor.*;
        import play.libs.Akka;
         import static akka.dispatch.Futures.future;
         public class FileReaderActor extends UntypedActor {
          public void onReceive(Object message) throws Exception {
            if (message instanceof FileReaderProtocol) {
              final String filename = ((FileReaderProtocol) message).filename;
               Future&lt;String&gt; future = future(new Callable&lt;String&gt;() {
             public String call() {
           try {
             Path path = Paths.get(filename);
              List&lt;String&gt; list = Files.readAllLines(path, StandardCharsets.UTF_8);
             String[] contents = list.toArray(new String[list.size()]);
               return Arrays.toString(contents);
            } catch(Exception e) {
          throw new IllegalStateException(e);
            }
        }
            }, Akka.system().dispatcher());
          akka.pattern.Patterns.pipe(
        future, Akka.system().dispatcher()).to(getSender());
          }
        }
      }
    
  5. 我们还需要在foo_java/app/actors/FileReaderProtocol.java中为FileReaderActor创建我们的 actor 协议类,内容如下:

    package actors;
         public class FileReaderProtocol implements java.io.Serializable {
          public final String filename;
          public FileReaderProtocol(String filename) { this.filename = filename; }
        }
    
  6. foo_java/conf/routes中为异步示例操作添加新路由:

    GET   /async_example     controllers.Application.asyncExample
    
  7. foo_java/conf/application.conf中添加默认的Akka配置:

    akka.default-dispatcher.fork-join-executor.pool-size-max = 64
        akka.actor.debug.receive = on
    
  8. 使用网络浏览器访问异步示例 URL 路由(http://localhost:9000/async_example)。你应该在浏览器中看到本地文件/usr/share/dict/words的内容。

对于 Scala,我们需要采取以下步骤:

  1. 启用热重载功能运行foo_scala应用程序。

  2. 通过添加以下内容修改foo_scala/app/controllers/Application.scala中的应用控制器:

    val fileReaderActor = Akka.system.actorOf(Props[FileReaderActor], name = "fileReader")    
          def asyncExample = Action.async {
          implicit val timeout = Timeout(3 seconds)
    
           (fileReaderActor ? FileReaderProtocol("/usr/share/dict/words")).mapTo[String].map{ words =&gt;
            Ok("Words: \n" + words)
          }
        }
    
  3. 我们还需要添加以下导入语句以包含必要的库,特别是我们将为本菜谱使用的Akka库:

    import play.api.libs.concurrent.Akka
        import play.api.Play.current
        import akka.pattern.ask
        import akka.pattern.pipe
        import akka.util.Timeout
        import akka.actor.{Props, Actor, ActorLogging}
        import scala.concurrent.duration._
        import scala.concurrent._
        import play.api.libs.concurrent.Execution.Implicits.defaultContext
    
  4. 为了方便起见,我们还在应用程序控制器中添加了我们的 Akka actor。

    case class FileReaderProtocol(filename: String)
         class FileReaderActor extends Actor with ActorLogging {
    
           def receive = {
            case FileReaderProtocol(filename) =&gt; {
              val currentSender = sender
              val contents = Future {
                scala.io.Source.fromFile(filename).mkString
              }
              contents pipeTo currentSender
            }
          }
        }
    
  5. foo_scala/conf/routes中为异步示例操作添加新路由:

    GET   /async_example     controllers.Application.asyncExample
    
  6. foo_scala/conf/application.conf中添加默认的Akka配置:

    akka.default-dispatcher.fork-join-executor.pool-size-max = 64
        akka.actor.debug.receive = on
    
  7. 使用网络浏览器访问异步示例 URL 路由(http://localhost:9000/async_example)。你应该在浏览器中看到本地文件/usr/share/dict/words的内容。

它是如何工作的...

在本菜谱中,我们修改了应用程序控制器并添加了一个新路由,该路由返回本地文件/usr/share/dict/words的内容。我们在控制器中创建了动作,并在conf/routes文件中创建了新的路由条目。然后我们创建了将执行读取文件并返回其内容的实际工作的 Akka actor 类和协议类。

对于 Java,我们需要采取以下步骤:

// Java
    Path path = Paths.get(filename);
    List&lt;String&gt; list = Files.readAllLines(path, StandardCharsets.UTF_8);

对于 Scala,我们需要采取以下步骤:

// Scala
    val contents = Future {
      scala.io.Source.fromFile(filename).mkString
    }

然后我们配置了新的动作以调用Actor,并以异步方式返回结果:

// Java
    return Promise.wrap(ask(fileReaderActor, words, 3000)).map(
      new Function&lt;Object, Result&gt;() {
        public Result apply(Object response) {
          return ok(response.toString());
        }
      }
    );
     // Scala
   (fileReaderActor ? FileReaderProtocol("/usr/share/dict/words"))       .mapTo[String].map{ words =&gt;
       Ok("Words: \n" + words)
     }

我们还在conf/application.conf中添加了默认的Akka配置设置:

akka.default-dispatcher.fork-join-executor.pool-size-max =64
    akka.actor.debug.receive = on

上述设置允许我们设置默认分发器线程池的最大大小。在本菜谱中,它被设置为 64。有关 Akka 分发器的更多信息,请参阅doc.akka.io/docs/akka/snapshot/java/dispatchers.html

第三章:利用模块

在本章中,我们将介绍以下菜谱:

  • 使用 Spring 进行依赖注入

  • 使用 Guice 进行依赖注入

  • 利用 MongoDB

  • 利用 MongoDB 和 GridFS

  • 利用 Redis

  • 将 Play 应用程序与 Amazon S3 集成

  • 将 Play 应用程序与 Typesafe Slick 集成

  • 利用 play-mailer

  • 集成 Bootstrap 和 WebJars

简介

在本章中,我们将探讨如何利用 Play 和其他第三方模块来处理现代 Web 应用程序的常见功能。随着 Web 应用程序和 Web 应用程序框架的成熟和演变,作为核心 Web 应用程序框架一部分的模块化和可扩展系统变得越来越重要。这可以通过 Play Framework 2.0 轻松实现。

使用 Spring 进行依赖注入

对于这个菜谱,我们将探讨如何将流行的 Spring 框架与 Play 应用程序集成。我们将使用 Spring 通过 Play 控制器和服务类进行 bean 实例化和注入。

如何做到这一点…

对于 Java,我们需要采取以下步骤:

  1. 启用热重载运行foo_java应用程序:

    <span class="strong"><strong>    activator "~run"</strong></span>
    
  2. build.sbt中将 Spring 声明为项目依赖项:

    "org.springframework" % "spring-context" % "3.2.2.RELEASE",
        "org.springframework" % "spring-aop" % "3.2.2.RELEASE",
        "org.springframework" % "spring-expression" % "3.2.2.RELEASE"
    
  3. foo_java/app/controllers/AdminController.java中创建一个新的管理控制器,代码如下:

    package controllers;
         import play.*;
        import play.mvc.*;
        import org.springframework.beans.factory.annotation.Autowired;
        import services.AdminService;
         @org.springframework.stereotype.Controller
        public class AdminController {
           @Autowired
          private AdminService adminService;
           public Result index() {
            return play.mvc.Controller.ok("This is an admin-only resource: " + adminService.getFoo());
          }
        }
    
  4. foo_java/app/services/AdminServices.java中创建一个管理服务接口类,并在foo_java/app/services/AdminServicesImpl.java中创建一个模拟管理服务实现类,内容如下:

    // AdminService.java
        package services;
         public interface AdminService {
          String getFoo();
        }
         // AdminServiceImpl.java
        package services;
         import org.springframework.stereotype.Service;
         @Service
        public class AdminServiceImpl implements AdminService {
          @Override
          public String getFoo() {
            return "foo";
          }
        }
    
  5. foo_java/conf/routes中为新增的操作添加一个新的路由条目:

    GET     /admins     @controllers.AdminController.index
    
  6. foo_java/app/Global.java文件中添加一个Global设置类,内容如下:

    import org.springframework.context.ApplicationContext;
        import org.springframework.context.annotation.AnnotationConfigApplicationContext;
        import play.Application;
        import play.GlobalSettings;
           public class Global extends GlobalSettings {
          private ApplicationContext ctx;
           @Override
          public void onStart(Application app) {
            ctx = new AnnotationConfigApplicationContext(SpringConfig.class);
          }
           @Override
          public &lt;A&gt; A getControllerInstance(Class&lt;A&gt; clazz) {
            return ctx.getBean(clazz);
          }
        }
    
  7. foo_java/app/SpringConfig.java中添加 Spring 配置类,内容如下:

    import org.springframework.context.annotation.ComponentScan;
        import org.springframework.context.annotation.Configuration;
         @Configuration
        @ComponentScan({"controllers", "services"})
        public class SpringConfig {
        }
    
  8. 请求我们的新路由并检查响应体以确认:

    <span class="strong"><strong>    $ curl -v http://localhost:9000/admins</strong></span>
    <span class="strong"><strong>    * Hostname was NOT found in DNS cache</strong></span>
    <span class="strong"><strong>    *   Trying ::1...</strong></span>
    <span class="strong"><strong>    * Connected to localhost (::1) port 9000 (#0)</strong></span>
    <span class="strong"><strong>    &gt; GET /admins HTTP/1.1</strong></span>
    <span class="strong"><strong>    &gt; User-Agent: curl/7.37.1</strong></span>
    <span class="strong"><strong>    &gt; Host: localhost:9000</strong></span>
    <span class="strong"><strong>    &gt; Accept: */*</strong></span>
    <span class="strong"><strong>    &gt;</strong></span>
    <span class="strong"><strong>    &lt; HTTP/1.1 200 OK</strong></span>
    <span class="strong"><strong>    &lt; Content-Type: text/plain; charset=utf-8</strong></span>
    <span class="strong"><strong>    &lt; Content-Length: 35</strong></span>
    <span class="strong"><strong>    &lt;</strong></span>
    <span class="strong"><strong>    * Connection #0 to host localhost left intact</strong></span>
    <span class="strong"><strong>    This is an admin-only resource: foo%</strong></span>
    

对于 Scala,我们需要采取以下步骤:

  1. 启用热重载运行foo_scala应用程序:

    <span class="strong"><strong>    activator "~run"</strong></span>
    
  2. build.sbt中将 Spring 声明为项目依赖项:

    "org.springframework" % "spring-context" % "3.2.2.RELEASE"
    
  3. foo_scala/app/controllers/AdminController.scala中创建一个新的管理控制器,内容如下:

    package controllers
            import play.api.mvc.{Action, Controller}
           import services.AdminService
            class AdminController(implicit adminService: AdminService) extends Controller {
             def index = Action {
               Ok("This is an admin-only resource: %s".format(adminService.foo))
             }
           }
    
  4. foo_scala/app/services/AdminServices.scala中创建一个管理服务类,内容如下:

    package services
         class AdminService {
          def foo = "foo"
        }
    
  5. foo_scala/conf/routes中为新增的操作添加一个新的路由条目:

    GET     /admins                    @controllers.AdminController.index
    
  6. foo_scala/app/Global.scala中添加一个Global设置类,内容如下:

    import org.springframework.context.ApplicationContext
        import org.springframework.context.annotation.AnnotationConfigApplicationContext
         object Global extends play.api.GlobalSettings {
           private val ctx: ApplicationContext = new AnnotationConfigApplicationContext(classOf[SpringConfig])
           override def getControllerInstanceA: A = {
            return ctx.getBean(clazz)
          }
        }
    
  7. 添加 Spring 配置类到foo_scala/app/SpringConfig.scala,内容如下:

    import org.springframework.context.annotation.Configuration
        import org.springframework.context.annotation.Bean
        import controllers._
        import services._
         @Configuration
        class SpringConfig {
          @Bean
          implicit def adminService: AdminService = new AdminService
           @Bean
          def adminController: AdminController = new AdminController
        }
    
  8. 请求我们的新路由并检查响应体以确认:

    <span class="strong"><strong>    $ curl -v http://0.0.0.0:9000/admins</strong></span>
    <span class="strong"><strong>    * Hostname was NOT found in DNS cache</strong></span>
    <span class="strong"><strong>    *   Trying 0.0.0.0...</strong></span>
    <span class="strong"><strong>    * Connected to 0.0.0.0 (127.0.0.1) port 9000 (#0)</strong></span>
    <span class="strong"><strong>    &gt; GET /admins HTTP/1.1</strong></span>
    <span class="strong"><strong>    &gt; User-Agent: curl/7.37.1</strong></span>
    <span class="strong"><strong>    &gt; Host: 0.0.0.0:9000</strong></span>
    <span class="strong"><strong>    &gt; Accept: */*</strong></span>
    <span class="strong"><strong>    &gt;</strong></span>
    <span class="strong"><strong>    &lt; HTTP/1.1 200 OK</strong></span>
    <span class="strong"><strong>    &lt; Content-Type: text/plain; charset=utf-8</strong></span>
    <span class="strong"><strong>    &lt; Content-Length: 35</strong></span>
    <span class="strong"><strong>    &lt;</strong></span>
    <span class="strong"><strong>    * Connection #0 to host 0.0.0.0 left intact</strong></span>
    <span class="strong"><strong>    This is an admin-only resource: foo%</strong></span>
    

它是如何工作的…

在这个菜谱中,我们配置了我们的 Play 应用程序,使用 Spring 在我们的控制器和服务类中进行依赖注入。我们在Global设置文件中配置了 Spring,并加载了SpringConfig类,它将包含我们的 Spring 特定配置。

使用 Guice 进行依赖注入

对于这个菜谱,我们将探讨如何将 Google Guice 与 Play 应用程序集成。我们将使用 Guice 进行 Bean 实例化和通过 Play 控制器和服务类进行注入。

如何操作...

对于 Java,我们需要采取以下步骤:

  1. 启用 Hot-Reloading 运行foo_java应用程序:

    <span class="strong"><strong>    activator "~run"</strong></span>
    
  2. build.sbt中将guice模块声明为项目依赖项:

    "com.google.inject" % "guice" % "3.0"
    
  3. 通过修改Global设置类的内容来配置 Guice:

    import com.google.inject.AbstractModule;
        import com.google.inject.Guice;
        import com.google.inject.Injector;
        import com.google.inject.Singleton;
        import play.GlobalSettings;
        import services.*;
         public class Global extends GlobalSettings {
          private Injector injector = Guice.createInjector(new AbstractModule() {
            @Override
            protected void configure() {
               bind(CategoryService.class).to(
          CategoryServiceImpl.class).in(Singleton.class);
            }
          });
           @Override
          public &lt;T&gt; T getControllerInstance(Class&lt;T&gt; clazz) {
            return injector.getInstance(clazz);
          }
       }
    
  4. foo_java/app/controllers/CategoryController.java中创建一个分类控制器,通过添加以下内容:

    package controllers;
         import com.google.inject.Inject;
        import play.libs.Json;
        import play.mvc.Controller;
        import play.mvc.Result;
        import services.CategoryService;
         public class CategoryController extends Controller {
          @Inject
          private CategoryService categoryService;
           public Result index() {
            return ok(Json.toJson(categoryService.list()));
          }
        }
    
  5. foo_java/app/services/CategoryService.java中创建一个分类服务接口,通过添加以下内容:

    package services;
         import java.util.List;
         public interface CategoryService {
          List&lt;String&gt; list();
        }
    
  6. foo_java/app/services/CategoryServicesImpl.java中创建一个分类服务实现类,通过添加以下内容:

    package services;
         import java.util.Arrays;
        import java.util.List;
         public class CategoryServiceImpl implements CategoryService {
          @Override
          public List&lt;String&gt; list() {
            return Arrays.asList(new String[] {"Manager", "Employee", "Contractor"});
          }
        }
    
  7. foo_java/conf/routes中为新增的操作添加一个新的路由条目:

    GET  /categories    @controllers.CategoryController.index
    
  8. 请求我们的新路由并检查响应头以确认我们对 HTTP 响应头的修改:

    $ curl -v http://localhost:9000/categories
        * Hostname was NOT found in DNS cache
        *   Trying ::1...
        * Connected to localhost (::1) port 9000 (#0)
        &gt; GET /categories HTTP/1.1
        &gt; User-Agent: curl/7.37.1
        &gt; Host: localhost:9000
        &gt; Accept: */*
        &gt;
        &lt; HTTP/1.1 200 OK
        &lt; Content-Type: application/json; charset=utf-8
        &lt; Content-Length: 35
        &lt;
        * Connection #0 to host localhost left intact
        ["Manager","Employee","Contractor"]%
    

对于 Scala,我们需要采取以下步骤:

  1. 启用 Hot-Reloading 运行foo_scala应用程序:

    <span class="strong"><strong>    activator "~run"</strong></span>
    
  2. build.sbt中将securesocial模块声明为项目依赖项:

    "com.google.inject" % "guice" % "3.0"
    
  3. 通过修改Global设置类的内容来配置 Guice:

    import com.google.inject.{Guice, AbstractModule}
        import play.api.GlobalSettings
        import services._
         object Global extends GlobalSettings {
          val injector = Guice.createInjector(new AbstractModule {
            protected def configure() {
              bind(classOf[CategoryService]).to(classOf[CategoryServiceImpl])
            }
          })
           override def getControllerInstanceA: A = {
            injector.getInstance(controllerClass)
          }
        }
    
  4. foo_scala/app/controllers/CategoryController.scala中创建一个分类控制器,通过添加以下内容:

    package controllers
            import play.api.mvc._
           import play.api.libs.json.Json._
           import com.google.inject._
           import services._
            @Singleton
           class CategoryController @Inject()(categoryService: CategoryService) 
          extends Controller {
           def index = Action {
        Ok(toJson(categoryService.list))
          }
           }
    
  5. foo_scala/app/services/CategoryService.scala中创建一个分类服务,通过添加以下内容:

    package services
         trait CategoryService {
       def list: Seq[String]
        }
         class CategoryServiceImpl extends CategoryService {
       override def list: Seq[String] = Seq("Manager", "Employee", "Contractor")
        }
    
  6. foo_scala/conf/routes中为新增的操作添加一个新的路由条目:

    GET  /categories    @controllers.CategoryController.index
    
  7. 请求我们的新路由并检查响应头以确认我们对 HTTP 响应头的修改:

    $ curl -v http://localhost:9000/categories
        * Hostname was NOT found in DNS cache
        *   Trying ::1...
        * Connected to localhost (::1) port 9000 (#0)
        &gt; GET /categories HTTP/1.1
        &gt; User-Agent: curl/7.37.1
        &gt; Host: localhost:9000
        &gt; Accept: */*
        &gt;
        &lt; HTTP/1.1 200 OK
        &lt; Content-Type: application/json; charset=utf-8
        &lt; Content-Length: 35
        &lt;
        * Connection #0 to host localhost left intact
        ["Manager","Employee","Contractor"]%
    

它是如何工作的...

在这个菜谱中,我们配置了我们的 Play 应用程序,使用 Google Guice 在我们的控制器和服务类中实现依赖注入。我们在Global设置文件中配置了 Guice,它将包含我们的 Guice 特定配置。

利用 MongoDB

对于这个菜谱,我们将探讨如何在 Play 应用程序中利用流行的 NoSQL 库 MongoDB。MongoDB 是最广泛使用的 NoSQL 数据库之一,它无疑已经成为许多现代 Web 应用程序的数据存储的有效选择。我们将使用 Scala 模块,play-plugins-salat,这是一个使用官方 MongoDB Scala 驱动程序 Casbah 的对象关系映射工具。这将是一个仅限 Scala 的菜谱。

有关 Casbah 的更多信息,请参阅github.com/mongodb/casbah

如何操作...

让我们采取以下步骤:

  1. 启用 Hot-Reloading 运行foo_scala应用程序:

    <span class="strong"><strong>    activator "~run"</strong></span>
    
  2. build.sbt中将 play-plugins-salat 声明为项目依赖项:

    "se.radley" %% "play-plugins-salat" % "1.5.0"
    
  3. build.sbt中添加额外的 salat 和 MongoDB 指令:

    import play.PlayImport.PlayKeys._
        import play.twirl.sbt.Import.TwirlKeys
         routesImport += "se.radley.plugin.salat.Binders._"
        TwirlKeys.templateImports += "org.bson.types.ObjectId"
    
  4. foo_scala/conf/play.plugins中声明 salat 插件:

    500:se.radley.plugin.salat.SalatPlugin
    
  5. foo_scala/conf/application.conf中声明 MongoDB 实例信息:

    mongodb.default.db = "cookbookdb"
    
  6. 通过添加以下内容修改foo_scala/app/controllers/WarehouseController.scala

    package controllers
         import models._
        import play.api.libs.json._
        import play.api.mvc.{BodyParsers, Action, Controller}
        import se.radley.plugin.salat.Binders.ObjectId
         object WarehouseController extends Controller {
          implicit val objectIdReads = se.radley.plugin.salat.Binders.objectIdReads
          implicit val objectIdWrites = se.radley.plugin.salat.Binders.objectIdWrites
          implicit val warehouseWrites = Json.writes[Warehouse]
          implicit val warehouseReads = Json.reads[Warehouse]
           def index = Action {
            val list = Warehouse.list
            Ok(Json.toJson(list))
          }
           def create = Action(BodyParsers.parse.json) { implicit request =&gt;
            val post = request.body.validate[Warehouse]
            post.fold(
              errors =&gt; {
                BadRequest(Json.obj("status" -&gt;"error", "message" -&gt; JsError.toFlatJson(errors)))
              },
              warehouse =&gt; {
                Warehouse.create(warehouse)
                Created(Json.toJson(warehouse))
              }
            )
          }
        }
    
  7. 将新添加的操作的新路由添加到 foo_scala/conf/routes

    GET     /warehouses  controllers.WarehouseController.index
        POST    /warehouses      controllers.WarehouseController.create
    
  8. 将仓库模型的集合映射添加到 foo_scala/app/models/Warehouse.scala

    package models
         import play.api.Play.current
        import com.mongodb.casbah.commons.MongoDBObject
        import com.novus.salat.dao._
        import se.radley.plugin.salat._
        import se.radley.plugin.salat.Binders._
        import mongoContext._
         case class Warehouse(id: Option[ObjectId] = Some(new ObjectId), name: String, location: String)
         object Warehouse extends ModelCompanion[Warehouse, ObjectId] {
          val dao = new SalatDAOWarehouse, ObjectId) {}
           def list = dao.find(ref = MongoDBObject()).toList
          def create(w: Warehouse) = dao.save(w)
        }
    
  9. 将 Mongo 上下文添加到 foo_scala/app/models/mongoContext.scala

    package models
         import com.novus.salat.dao._
        import com.novus.salat.annotations._
        import com.mongodb.casbah.Imports._
         import play.api.Play
        import play.api.Play.current
         package object mongoContext {
          implicit val context = {
            val context = new Context {
              val name = "global"
              override val typeHintStrategy = StringTypeHintStrategy(when = TypeHintFrequency.WhenNecessary, typeHint = "_t")
            }
            context.registerGlobalKeyOverride(remapThis = "id", toThisInstead = "_id")
            context.registerClassLoader(Play.classloader)
            context
          }
        }
    
  10. 通过使用 curl 访问仓库 post 端点来添加新的仓库记录:

    <span class="strong"><strong>    $ curl -v -X POST http://localhost:9000/warehouses --header "Content-type: application/json" --data '{"name":"Warehouse A", "location":"Springfield"}'</strong></span>
    <span class="strong"><strong>    * Hostname was NOT found in DNS cache</strong></span>
    <span class="strong"><strong>    *   Trying ::1...</strong></span>
    <span class="strong"><strong>    * Connected to localhost (::1) port 9000 (#0)</strong></span>
    <span class="strong"><strong>    &gt; POST /warehouses HTTP/1.1</strong></span>
    <span class="strong"><strong>    &gt; User-Agent: curl/7.37.1</strong></span>
    <span class="strong"><strong>    &gt; Host: localhost:9000</strong></span>
    <span class="strong"><strong>    &gt; Accept: */*</strong></span>
    <span class="strong"><strong>    &gt; Content-type: application/json</strong></span>
    <span class="strong"><strong>    &gt; Content-Length: 48</strong></span>
    <span class="strong"><strong>    &gt;</strong></span>
    <span class="strong"><strong>    * upload completely sent off: 48 out of 48 bytes</strong></span>
    <span class="strong"><strong>    &lt; HTTP/1.1 201 Created</strong></span>
    <span class="strong"><strong>    &lt; Content-Type: application/json; charset=utf-8</strong></span>
    <span class="strong"><strong>    &lt; Content-Length: 47</strong></span>
    <span class="strong"><strong>    &lt;</strong></span>
    <span class="strong"><strong>    * Connection #0 to host localhost left intact</strong></span>
    <span class="strong"><strong>    {"name":"Warehouse A","location":"Springfield"}</strong></span>
    
  11. 通过使用 curl 访问仓库索引端点来查看所有仓库记录:

    <span class="strong"><strong>    $ curl -v http://localhost:9000/warehouses</strong></span>
    <span class="strong"><strong>    * Hostname was NOT found in DNS cache</strong></span>
    <span class="strong"><strong>    *   Trying ::1...</strong></span>
    <span class="strong"><strong>    * Connected to localhost (::1) port 9000 (#0)</strong></span>
    <span class="strong"><strong>    &gt; GET /warehouses HTTP/1.1</strong></span>
    <span class="strong"><strong>    &gt; User-Agent: curl/7.37.1</strong></span>
    <span class="strong"><strong>    &gt; Host: localhost:9000</strong></span>
    <span class="strong"><strong>    &gt; Accept: */*</strong></span>
    <span class="strong"><strong>    &gt;</strong></span>
    <span class="strong"><strong>    &lt; HTTP/1.1 200 OK</strong></span>
    <span class="strong"><strong>    &lt; Content-Type: application/json; charset=utf-8</strong></span>
    <span class="strong"><strong>    &lt; Content-Length: 241</strong></span>
    <span class="strong"><strong>    &lt;</strong></span>
    <span class="strong"><strong>    * Connection #0 to host localhost left intact</strong></span>
    <span class="strong"><strong>    [{"id":"5490fde9e0820cf6df38584c","name":"Warehouse A","location":"Springfield"}]%</strong></span>
    

它是如何工作的...

在这个菜谱中,我们创建了一个新的 URL 路由和操作,用于从 MongoDB 实例中插入和检索仓库记录。我们使用了 Play 模块 play-plugins-salat,并在 foo_scala/conf/application.conf 中配置了连接。然后我们在仓库模型类中映射了我们的 Mongo 集合:

case class Warehouse(id: Option[ObjectId] = Some(new ObjectId), name: String, location: String)

接下来,我们从仓库控制器中调用了适当的仓库伴生对象方法:

val list = Warehouse.list
    Warehouse.create(warehouse)

我们还在仓库控制器中声明了我们的 JSON 绑定器,用于仓库模型和 MongoDB 的 ObjectId

implicit val objectIdReads = se.radley.plugin.salat.Binders.objectIdReads
    implicit val objectIdWrites = se.radley.plugin.salat.Binders.objectIdWrites
    implicit val warehouseWrites = Json.writes[Warehouse]
    implicit val warehouseReads = Json.reads[Warehouse]

利用 MongoDB 和 GridFS

对于这个菜谱,我们将探索如何通过使用 MongoDB 和 GridFS 来存储和交付文件,通过 Play 应用程序。我们将继续添加到之前的菜谱。与之前的菜谱一样,这个菜谱将只使用 Scala。

如何操作...

让我们采取以下步骤:

  1. 以启用热重载的方式运行 foo_scala 应用程序:

    <span class="strong"><strong>    activator "~run"</strong></span>
    
  2. 通过添加以下内容修改 foo_scala/app/controllers/WarehouseController.scala

    import java.text.SimpleDateFormat
        import play.api.libs.iteratee.Enumerator
         def upload = Action(parse.multipartFormData) { request =&gt;
            request.body.file("asset") match {
              case Some(asset) =&gt; {
                val gridFs = Warehouse.assets
                val uploadedAsset = gridFs.createFile(asset.ref.file)
                uploadedAsset.filename = asset.filename
                uploadedAsset.save()
                 Ok("Asset is available at http://localhost:9000/warehouses/assets/%s".format(uploadedAsset.id))
              }
              case None =&gt; {
                BadRequest
              }
            }
          }
           def retrieveFile(id: ObjectId) = Action {
            import com.mongodb.casbah.Implicits._
            import play.api.libs.concurrent.Execution.Implicits._
             val gridFs = Warehouse.assets
             gridFs.findOne(Map("_id" -&gt; id)) match {
              case Some(f) =&gt; Result(
                ResponseHeader(OK, Map(
                  CONTENT_LENGTH -&gt; f.length.toString,
                  CONTENT_TYPE -&gt; f.contentType.getOrElse(BINARY),
                  DATE -&gt; new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss 'GMT'", java.util.Locale.US).format(f.uploadDate)
                )),
                Enumerator.fromStream(f.inputStream)
              )
               case None =&gt; NotFound
            }
          }
    
  3. 将新添加的操作的新路由添加到 foo_scala/conf/routes

    POST /warehouses/assets/upload controllers.WarehouseController.upload
       GET  /warehouses/assets/:id controllers.WarehouseController.retrieveFile(id: ObjectId)
    
  4. 修改仓库模型在 foo_scala/app/models/Warehouse.scala 中的集合映射:

    val assets = gridFS("assets")
         def upload(asset: File) = {
          assets.createFile(asset)
        }
         def retrieve(filename: String) = {
          assets.find(filename)
        }
    
  5. 通过使用 curl 访问仓库上传端点来上传新的仓库资产文件:

    <span class="strong"><strong>    $ curl -v http://localhost:9000/warehouses/assets/upload -F "asset=@/tmp/1.jpg"</strong></span>
    <span class="strong"><strong>    * Hostname was NOT found in DNS cache</strong></span>
    <span class="strong"><strong>    *   Trying ::1...</strong></span>
    <span class="strong"><strong>    * Connected to localhost (::1) port 9000 (#0)</strong></span>
    <span class="strong"><strong>    &gt; POST /warehouses/assets/upload HTTP/1.1</strong></span>
    <span class="strong"><strong>    &gt; User-Agent: curl/7.37.1</strong></span>
    <span class="strong"><strong>    &gt; Host: localhost:9000</strong></span>
    <span class="strong"><strong>    &gt; Accept: */*</strong></span>
    <span class="strong"><strong>    &gt; Content-Length: 13583</strong></span>
    <span class="strong"><strong>    &gt; Expect: 100-continue</strong></span>
    <span class="strong"><strong>    &gt; Content-Type: multipart/form-data; boundary=------------------------4a001bdeff39c089</strong></span>
    <span class="strong"><strong>    &gt;</strong></span>
    <span class="strong"><strong>    &lt; HTTP/1.1 100 Continue</strong></span>
    <span class="strong"><strong>    &lt; HTTP/1.1 200 OK</strong></span>
    <span class="strong"><strong>    &lt; Content-Type: text/plain; charset=utf-8</strong></span>
    <span class="strong"><strong>    &lt; Content-Length: 86</strong></span>
    <span class="strong"><strong>    &lt;</strong></span>
    <span class="strong"><strong>    * Connection #0 to host localhost left intact</strong></span>
    <span class="strong"><strong>    Asset is available at http://localhost:9000/warehouses/assets/549121fbe082fc374fa6cb63%</strong></span>
    
  6. 通过在 Web 浏览器中访问前一步输出的 URL 来验证我们的文件交付 URL 路由是否正常工作:

    <code class="literal">http://localhost:9000/warehouses/assets/549121fbe082fc374fa6cb63</code>
    

它是如何工作的...

在这个菜谱中,我们创建了新的 URL 路由和操作,这些操作将用于在 MongoDB 实例中使用 GridFS 上传和检索仓库资产文件。我们在 foo_scala/app/models/Warehouse.scala 中的集合映射文件中添加了 GridFS 引用:

val assets = gridFS("assets")

然后我们添加了文件上传和检索的相关方法:

def upload(asset: File) = {
      assets.createFile(asset)
    }
     def retrieve(filename: String) = {
      assets.find(filename)
    }

接下来,我们在 foo_scala/app/controllers/WarehouseController.scala 中创建了操作,这些操作将处理实际的文件上传和检索请求。

利用 Redis

对于这个菜谱,我们将探索 Play 应用程序如何通过 Play 缓存与 Redis 集成。Redis 是一个广泛使用的键值数据库,通常用作现代 Web 应用程序的中介对象缓存。这个菜谱需要一个正在运行的 Redis 实例,我们的 Play 2 Web 应用程序可以与之交互。

如何操作...

对于 Java,我们需要采取以下步骤:

  1. 以启用热重载的方式运行 foo_java 应用程序:

    <span class="strong"><strong>    activator "~run"</strong></span>
    
  2. build.sbt 中将 Redis 声明为项目依赖项:

    "com.typesafe.play.plugins" %% "play-plugins-redis" % "2.3.1"
    
  3. build.sbt 中声明托管 Sedis 的存储库,它是 play-plugins-redis 的库依赖项:

    resolvers += "Sedis repository" at "http://pk11-scratch.googlecode.com/svn/trunk/"
    
  4. 通过在 foo_java/conf/play.plugins 中声明它来启用 play-mailer 插件:

    550:com.typesafe.plugin.RedisPlugin
    
  5. foo_java/conf/application.conf 中指定您的 Redis 主机信息:

    ehcacheplugin=disabled
        redis.uri="redis://127.0.0.1:6379"
    
  6. 通过在 foo_java/app/controllers/Application.java 中添加以下代码来修改 foo_java

    import play.cache.*;
    
         public static Result displayFromCache() {
             final String key = "myKey";
             String value = (String) Cache.get(key);
              if (value != null &amp;&amp; value.trim().length() &gt; 0) {
               return ok("Retrieved from Cache: " + value);
             } else {
               Cache.set(key, "Let's Play with Redis!");
               return ok("Setting key value in the cache");
             } 
           }
    
  7. foo_java/conf/routes 中为新增的动作添加一个新的路由条目:

    GET     /cache  controllers.Application.displayFromCache
    
  8. 请求我们的新路由并检查响应体以确认我们的 displayFromCache 动作是第一次设置键值:

    <span class="strong"><strong>    $ curl -v http://localhost:9000/cache</strong></span>
    <span class="strong"><strong>    * Hostname was NOT found in DNS cache</strong></span>
    <span class="strong"><strong>    *   Trying 127.0.0.1...</strong></span>
    <span class="strong"><strong>    * Connected to localhost (127.0.0.1) port 9000 (#0)</strong></span>
    <span class="strong"><strong>    &gt; GET /cache HTTP/1.1</strong></span>
    <span class="strong"><strong>    &gt; User-Agent: curl/7.37.1</strong></span>
    <span class="strong"><strong>    &gt; Host: localhost:9000</strong></span>
    <span class="strong"><strong>    &gt; Accept: */*</strong></span>
    <span class="strong"><strong>    &gt;</strong></span>
    <span class="strong"><strong>    &lt; HTTP/1.1 200 OK</strong></span>
    <span class="strong"><strong>    &lt; Content-Type: text/plain; charset=utf-8</strong></span>
    <span class="strong"><strong>    &lt; Content-Length: 30</strong></span>
    <span class="strong"><strong>    &lt;</strong></span>
    <span class="strong"><strong>    * Connection #0 to host localhost left intact</strong></span>
    <span class="strong"><strong>    Setting key value in the cache%</strong></span>
    
  9. 再次请求 /cache 路由以查看缓存键的值:

    <span class="strong"><strong>    $ curl -v http://localhost:9000/cache</strong></span>
    <span class="strong"><strong>    * Hostname was NOT found in DNS cache</strong></span>
    <span class="strong"><strong>    *   Trying 127.0.0.1...</strong></span>
    <span class="strong"><strong>    * Connected to localhost (127.0.0.1) port 9000 (#0)</strong></span>
    <span class="strong"><strong>    &gt; GET /cache HTTP/1.1</strong></span>
    <span class="strong"><strong>    &gt; User-Agent: curl/7.37.1</strong></span>
    <span class="strong"><strong>    &gt; Host: localhost:9000</strong></span>
    <span class="strong"><strong>    &gt; Accept: */*</strong></span>
    <span class="strong"><strong>    &gt;</strong></span>
    <span class="strong"><strong>    &lt; HTTP/1.1 200 OK</strong></span>
    <span class="strong"><strong>    &lt; Content-Type: text/plain; charset=utf-8</strong></span>
    <span class="strong"><strong>    &lt; Content-Length: 43</strong></span>
    <span class="strong"><strong>    &lt;</strong></span>
    <span class="strong"><strong>    * Connection #0 to host localhost left intact</strong></span>
    <span class="strong"><strong>    Retrieved from Cache: Let's Play with Redis!%</strong></span>
    

对于 Scala,我们需要采取以下步骤:

  1. 启用 Hot-Reloading 运行 foo_scala 应用程序:

    <span class="strong"><strong>    activator "~run"</strong></span>
    
  2. build.sbt 中声明 Redis 为项目依赖项:

    "com.typesafe.play.plugins" %% "play-plugins-redis" % "2.3.1"
    
  3. build.sbt 中声明托管 Sedis 的仓库,它是 play-plugins-redis 的库依赖项:

    resolvers += "Sedis repository" at "http://pk11-scratch.googlecode.com/svn/trunk/"
    
  4. 通过在 foo_scala/conf/play.plugins 中声明启用 play-mailer 插件:

    550:com.typesafe.plugin.RedisPlugin
    
  5. foo_scala/conf/application.conf 中指定您的 Redis 主机信息:

    ehcacheplugin=disabled
        redis.uri="redis://127.0.0.1:6379"
    
  6. 通过在 foo_scala/app/controllers/Application.scala 中添加以下代码来修改 foo_scala

    import scala.util.Random
           import play.api.cache._
           import play.api.Play.current
         def displayFromCache = Action {
             val key = "myKey"
             Cache.getAsString match {
               case Some(myKey) =&gt; {
                 Ok("Retrieved from Cache: %s".format(myKey))
               }
               case None =&gt; {
                 Cache.set(key, "Let's Play with Redis!")
                 Ok("Setting key value in the cache")
               }
             }
           }
    
  7. foo_scala/conf/routes 中为新增的 action 添加一个新的路由条目:

    GET     /cache  controllers.Application.displayFromCache
    
  8. 请求我们的新路由并检查响应体以确认我们的 displayFromCache 动作是第一次设置键值:

    <span class="strong"><strong>    $ curl -v http://localhost:9000/cache</strong></span>
    <span class="strong"><strong>    * Hostname was NOT found in DNS cache</strong></span>
    <span class="strong"><strong>    *   Trying ::1...</strong></span>
    <span class="strong"><strong>    * Connected to localhost (::1) port 9000 (#0)</strong></span>
    <span class="strong"><strong>    &gt; GET /cache HTTP/1.1</strong></span>
    <span class="strong"><strong>    &gt; User-Agent: curl/7.37.1</strong></span>
    <span class="strong"><strong>    &gt; Host: localhost:9000</strong></span>
    <span class="strong"><strong>    &gt; Accept: */*</strong></span>
    <span class="strong"><strong>    &gt;</strong></span>
    <span class="strong"><strong>    &lt; HTTP/1.1 200 OK</strong></span>
    <span class="strong"><strong>    &lt; Content-Type: text/plain; charset=utf-8</strong></span>
    <span class="strong"><strong>    &lt; Content-Length: 30</strong></span>
    <span class="strong"><strong>    &lt;</strong></span>
    <span class="strong"><strong>    * Connection #0 to host localhost left intact</strong></span>
    <span class="strong"><strong>    Setting key value in the cache%</strong></span>
    
  9. 再次请求 /cache 路由以查看缓存键的值:

    <span class="strong"><strong>    $ curl -v http://localhost:9000/cache</strong></span>
    <span class="strong"><strong>    * Hostname was NOT found in DNS cache</strong></span>
    <span class="strong"><strong>    *   Trying 127.0.0.1...</strong></span>
    <span class="strong"><strong>    * Connected to localhost (127.0.0.1) port 9000 (#0)</strong></span>
    <span class="strong"><strong>    &gt; GET /cache HTTP/1.1</strong></span>
    <span class="strong"><strong>    &gt; User-Agent: curl/7.37.1</strong></span>
    <span class="strong"><strong>    &gt; Host: localhost:9000</strong></span>
    <span class="strong"><strong>    &gt; Accept: */*</strong></span>
    <span class="strong"><strong>    &gt;</strong></span>
    <span class="strong"><strong>    &lt; HTTP/1.1 200 OK</strong></span>
    <span class="strong"><strong>    &lt; Content-Type: text/plain; charset=utf-8</strong></span>
    <span class="strong"><strong>    &lt; Content-Length: 43</strong></span>
    <span class="strong"><strong>    &lt;</strong></span>
    <span class="strong"><strong>    * Connection #0 to host localhost left intact</strong></span>
    <span class="strong"><strong>    Retrieved from Cache: Let's Play with Redis!%</strong></span>
    

它是如何工作的…

在这个菜谱中,我们创建了一个新的 URL 路由和动作,它将与我们 Redis 实例交互。为了继续这个菜谱,你需要以下正在运行的 Redis 实例来连接:

<span class="strong"><strong>$ redis-server /usr/local/etc/redis.conf</strong></span>
<span class="strong"><strong>[2407] 14 Apr 12:44:20.623 * Increased maximum number of open files to 10032 (it was originally set to 2560).</strong></span>
<span class="strong"><strong>                _._</strong></span>
<span class="strong"><strong>           _.-''__ ''-._</strong></span>
<span class="strong"><strong>      _.-''    '.  '_.  ''-._           Redis 2.8.17 (00000000/0) 64 bit</strong></span>
<span class="strong"><strong>  .-'' .-'''.  '''\/    _.,_ ''-._</strong></span>
<span class="strong"><strong> (    '      ,       .-'  | ',    )     Running in stand alone mode</strong></span>
<span class="strong"><strong> |'-._'-...-' __...-.''-._|'' _.-'|     Port: 6379</strong></span>
<span class="strong"><strong> |    '-._   '._    /     _.-'    |     PID: 2407</strong></span>
<span class="strong"><strong>  '-._    '-._  '-./  _.-'    _.-'</strong></span>
<span class="strong"><strong> |'-._'-._    '-.__.-'    _.-'_.-'|</strong></span>
<span class="strong"><strong> |    '-._'-._        _.-'_.-'    |           http://redis.io</strong></span>
<span class="strong"><strong>  '-._    '-._'-.__.-'_.-'    _.-'</strong></span>
<span class="strong"><strong> |'-._'-._    '-.__.-'    _.-'_.-'|</strong></span>
<span class="strong"><strong> |    '-._'-._        _.-'_.-'    |</strong></span>
<span class="strong"><strong>  '-._    '-._'-.__.-'_.-'    _.-'</strong></span>
<span class="strong"><strong>      '-._    '-.__.-'    _.-'</strong></span>
<span class="strong"><strong>          '-._        _.-'</strong></span>
<span class="strong"><strong>              '-.__.-'</strong></span>
 <span class="strong"><strong>[2407] 14 Apr 12:44:20.631 # Server started, Redis version 2.8.17</strong></span>
<span class="strong"><strong>[2407] 14 Apr 12:44:20.632 * DB loaded from disk: 0.001 seconds</strong></span>
<span class="strong"><strong>[2407] 14 Apr 12:44:20.632 * The server is now ready to accept connections on port 6379</strong></span>

有关安装和运行 Redis 服务器的更多信息,请参阅 redis.io/topics/quickstart

我们通过在 build.sbt 中声明必要的依赖项和仓库设置来配置了 Play Redis 模块。然后我们在 conf/application.conf 中配置了到 Redis 实例的连接。最后,我们在 conf/play.plugins 中加载了 Redis play-plugin:

550:com.typesafe.plugin.RedisPlugin

displayFromCache 动作被调用时,有两个不同的功能。首先,它尝试从缓存中检索一个值。如果它能够从 Redis 缓存中检索到一个值,它将在响应体中打印该值的正文。如果它无法从 Redis 缓存中检索到一个值,它将设置一个随机字符串值到键,并在响应体中打印一个状态消息。

我们随后使用 curl 测试这条新路由,并访问了两次该路由;动作在响应体中打印出两条不同的消息。

将 Play 应用程序与 Amazon S3 集成

对于这个菜谱,我们将探讨 Play 应用程序如何直接将文件上传到 Amazon Web Services (AWS) S3,这是一个流行的云存储解决方案。

有关 S3 的更多信息,请参阅 aws.amazon.com/s3/

如何做到这一点…

对于 Java,我们需要采取以下步骤:

  1. 启用 Hot-Reloading 运行 foo_java 应用程序:

    <span class="strong"><strong>    activator "~run"</strong></span>
    
  2. build.sbt 中将 play-s3 声明为项目依赖项:

    "com.amazonaws" % "aws-java-sdk" % "1.3.11"
    
  3. foo_java/conf/application.conf 中指定您的 AWS 凭据:

    aws.accessKeyId="YOUR S3 ACCESS KEY"
        aws.secretKey="YOUR S3 SECRET KEY"
         fooscala.s3.bucketName="YOUR S3 BUCKET NAME"
    
  4. 通过添加以下代码修改foo_java/app/controllers/Application.java

    import com.amazonaws.auth.*;
           import com.amazonaws.services.s3.*;
           import com.amazonaws.services.s3.model.*;
            public static Result s3Upload() {
             return ok(views.html.s3.render());
           }
            public static Result submitS3Upload() {
             Http.MultipartFormData body = request().body().asMultipartFormData();
             Http.MultipartFormData.FilePart profileImage = body.getFile("profile");
              if (profileImage != null) {
                try {
                    File file = profileImage.getFile();
                    String filename = profileImage.getFilename();
                     String accessKey = Play.application().configuration().getString("aws.accessKeyId");
                    String secret = Play.application().configuration().getString("aws.secretKey");
                    String bucketName = Play.application().configuration().getString("fooscala.s3.bucketName");
                     try {
                        AWSCredentials awsCredentials = new BasicAWSCredentials(accessKey, secret);
                        AmazonS3 s3Client = new AmazonS3Client(awsCredentials);
                        AccessControlList acl = new AccessControlList();
                        acl.grantPermission(GroupGrantee.AllUsers, Permission.Read);
                        s3Client.createBucket(bucketName);
                        s3Client.putObject(new PutObjectRequest(bucketName, filename, file).withAccessControlList(acl));
                         String img = "http://" + bucketName+ ".s3.amazonaws.com/" + filename;
                        return ok("Image uploaded: " + img);
                    } catch (Exception e) {
                        e.printStackTrace();
                        return ok("Image was not uploaded");
                    }
                 } catch(Exception e) {
                    return internalServerError(e.getMessage());
                }
             } else {
                return badRequest();
             }
           }
    
  5. foo_java/conf/routes中为新增的动作添加新的路由条目:

    GET     /s3_upload      controllers.Application.s3Upload
        POST    /s3_upload      controllers.Application.submitS3Upload
    
  6. 将 S3 文件上传提交视图模板添加到foo_java/app/views/s3.scala.html

    @helper.form(action = routes.Application.submitS3Upload, 'enctype -&gt; "multipart/form-data") {
          &lt;input type="file" name="profile"&gt;
          &lt;p&gt;
            &lt;input type="submit"&gt;
          &lt;/p&gt;
        }
    

对于 Scala,我们需要采取以下步骤:

  1. 以启用热重载的方式运行foo_scala应用程序:

    <span class="strong"><strong>    activator "~run"</strong></span>
    
  2. build.sbt中将 play-s3 声明为项目依赖项:

    "nl.rhinofly" %% "play-s3" % "5.0.2",
    
  3. 声明 play-s3 模块托管的自定义仓库:

    resolvers += "Rhinofly Internal Repository" at "http://maven-   repository.rhinofly.net:8081/artifactory/libs-release-local"
    
  4. foo_scala/conf/application.conf中指定您的 AWS 凭证:

    aws.accessKeyId="YOUR S3 ACCESS KEY"
        aws.secretKey="YOUR S3 SECRET KEY"
         fooscala.s3.bucketName="YOUR S3 BUCKET NAME"
    
  5. 修改foo_scala/app/controllers/Application.scala文件,添加以下代码:

    import play.api.Play.current
           import fly.play.s3._
           def s3Upload = Action {
             Ok(s3())
           }
            def submitS3Upload = Action(parse.multipartFormData) { request =&gt;
             import play.api.Play
              request.body.file("profile") match {
               case Some(profileImage) =&gt; {
                 val bucketName = Play.current.configuration.getString("fooscala.s3.bucketName").get
                 val bucket = S3(bucketName)
                  val filename = profileImage.filename
                 val contentType = profileImage.contentType
                 val byteArray = Files.toByteArray(profileImage.ref.file)
                  val result = bucket.add(BucketFile(filename, contentType.get, byteArray, Option(PUBLIC_READ), None))
                 val future = Await.result(result, 10 seconds)
                 Ok("Image uploaded to: http://%s.s3.amazonaws.com/%s".format(bucketName, filename))
              }
                case None =&gt; {
                  BadRequest
                }
              }
            }
    
  6. foo_scala/conf/routes中为新增的动作添加新的路由条目:

    GET     /s3_upload      controllers.Application.s3Upload
        POST    /s3_upload      controllers.Application.submitS3Upload
     Add the s3 file upload submission view template in foo_scala/app/views/s3.scala.html:
        @helper.form(action = routes.Application.submitS3Upload, 'enctype -&gt; "multipart/form-data") {
          &lt;input type="file" name="profile"&gt;
          &lt;p&gt;
            &lt;input type="submit"&gt;
          &lt;/p&gt;
        }
    

它是如何工作的…

在本菜谱中,我们创建了一个新的 URL 路由和动作,用于接收上传的文件。然后我们通过在conf/application.conf中提供 S3 访问密钥和秘密密钥,使用 RhinoFly S3 模块将此文件推送到 Amazon S3。我们还指定了conf/application.conf中的 S3 存储桶名称以供将来使用。我们可以通过使用 Play 的配置 API 来检索此值:

val bucketName = Play.current.configuration.getString("fooscala.s3.bucketName").get

我们随后将上传文件的存储位置打印到响应体中,以便于验证:

Ok("Image uploaded to: http://%s.s3.amazonaws.com/%s".format(bucketName, filename))

您现在应该在网页浏览器中看到以下文本的响应:

Image uploaded to: http://&lt;YOUR_BUCKET_NAME&gt;.s3.amazonaws.com/&lt;FILENAME&gt;

您还可以使用 AWS 管理控制台在 S3 部分验证文件上传:

图片

将 Typesafe Slick 集成到 Play 应用程序中

在本菜谱中,我们将探讨如何使用play-slick模块将 Typesafe Slick 与 Play 应用程序集成。Typesafe Slick 是一个基于 Scala 构建的关系映射工具,对于管理类似原生 Scala 类型的数据库对象来说非常方便。

如何做到这一点…

我们需要采取以下步骤:

  1. 以启用热重载的方式运行foo_scala应用程序:

    <span class="strong"><strong>    activator "~run"</strong></span>
    
  2. build.sbt中将 play-slick 声明为项目依赖项:

    "com.typesafe.play" %% "play-slick" % "0.8.1"
    
  3. foo_scala/conf/application.conf中指定您的数据库主机信息:

    db.default.driver=org.h2.Driver
        db.default.url="jdbc:h2:mem:play"
        db.default.user=sa
        db.default.password=""
        slick.default="models.*"
    
  4. 使用以下内容创建新的供应商控制器foo_scala/app/controllers/SupplierController.scala

    package controllers
            import play.api.mvc.{BodyParsers, Controller}
           import play.api.db.slick._
           import play.api.libs.json._
           import play.api.Play.current
           import models._
            object SupplierController extends Controller {
             implicit val supplierWrites = Json.writes[Supplier]
             implicit val supplierReads = Json.reads[Supplier]
              def index = DBAction { implicit rs =&gt;
               Ok(Json.toJson(Suppliers.list))
             }
              def create = DBAction(BodyParsers.parse.json) { implicit rs =&gt;
               val post = rs.request.body.validate[Supplier]
               post.fold(
                 errors =&gt; {
                   BadRequest(Json.obj("status" -&gt;"error", "message" -&gt; JsError.toFlatJson(errors)))
                 },
                 supplier =&gt; {
                   Suppliers.create(supplier)
                   Created(Json.toJson(supplier))
                 }
               )
             }
           }
    
  5. foo_scala/app/models/Suppliers.scala文件中创建供应商的 Slick 映射,内容如下:

    package models
         import scala.slick.driver.H2Driver.simple._
        import scala.slick.lifted.Tag
         case class Supplier(id: Option[Int], name: String, contactNo: String)
         class Suppliers(tag: Tag) extends TableSupplier {
          def id = columnInt
          def name = columnString
          def contactNo = columnString
           def * = (id.?, name, contactNo) &lt;&gt; (Supplier.tupled, Supplier.unapply)
        }
         object Suppliers {
          val suppliers = TableQuery[Suppliers]
           def list(implicit s: Session) = suppliers.sortBy(m =&gt; m.name.asc).list
          def create(supplier: Supplier)(implicit s: Session) = suppliers.insert(supplier)
        }
    
  6. foo_scala/conf/routes中添加Supplier控制器的新的路由:

    GET  /suppliers    controllers.SupplierController.index
        POST  /suppliers    controllers.SupplierController.create
    
  7. 请求新的Post路由并检查响应头和体,以确认记录已插入数据库:

    <span class="strong"><strong>    $ curl -v -X POST http://localhost:9000/suppliers --header "Content-type: application/json" --data '{"name":"Ned Flanders", "contactNo":"555-1234"}'</strong></span>
    <span class="strong"><strong>    * Hostname was NOT found in DNS cache</strong></span>
    <span class="strong"><strong>    *   Trying ::1...</strong></span>
    <span class="strong"><strong>    * Connected to localhost (::1) port 9000 (#0)</strong></span>
    <span class="strong"><strong>    &gt; POST /suppliers HTTP/1.1</strong></span>
    <span class="strong"><strong>    &gt; User-Agent: curl/7.37.1</strong></span>
    <span class="strong"><strong>    &gt; Host: localhost:9000</strong></span>
    <span class="strong"><strong>    &gt; Accept: */*</strong></span>
    <span class="strong"><strong>    &gt; Content-type: application/json</strong></span>
    <span class="strong"><strong>    &gt; Content-Length: 47</strong></span>
    <span class="strong"><strong>    &gt;</strong></span>
    <span class="strong"><strong>    * upload completely sent off: 47 out of 47 bytes</strong></span>
    <span class="strong"><strong>    &lt; HTTP/1.1 201 Created</strong></span>
    <span class="strong"><strong>    &lt; Content-Type: application/json; charset=utf-8</strong></span>
    <span class="strong"><strong>    &lt; Content-Length: 46</strong></span>
    <span class="strong"><strong>    &lt;</strong></span>
    <span class="strong"><strong>    * Connection #0 to host localhost left intact</strong></span>
    <span class="strong"><strong>    {"name":"Ned Flanders","contactNo":"555-1234"}%</strong></span>
    
  8. 请求列表路由并验证它确实正在从数据库返回记录:

    <span class="strong"><strong>    $ curl -v http://localhost:9000/suppliers</strong></span>
    <span class="strong"><strong>    * Hostname was NOT found in DNS cache</strong></span>
    <span class="strong"><strong>    *   Trying ::1...</strong></span>
    <span class="strong"><strong>    * Connected to localhost (::1) port 9000 (#0)</strong></span>
    <span class="strong"><strong>    &gt; GET /suppliers HTTP/1.1</strong></span>
    <span class="strong"><strong>    &gt; User-Agent: curl/7.37.1</strong></span>
    <span class="strong"><strong>    &gt; Host: localhost:9000</strong></span>
    <span class="strong"><strong>    &gt; Accept: */*</strong></span>
    <span class="strong"><strong>    &gt;</strong></span>
    <span class="strong"><strong>    &lt; HTTP/1.1 200 OK</strong></span>
    <span class="strong"><strong>    &lt; Content-Type: application/json; charset=utf-8</strong></span>
    <span class="strong"><strong>    &lt; Content-Length: 110</strong></span>
    <span class="strong"><strong>    &lt;</strong></span>
    <span class="strong"><strong>    * Connection #0 to host localhost left intact</strong></span>
    <span class="strong"><strong>    [{"id":1,"name":"Maud Flanders","contactNo":"555-1234"},{"id":2,"name":"Ned Flanders","contactNo":"712-1234"}]%</strong></span>
    

它是如何工作的…

在本菜谱中,我们创建了一个新的 URL 路由和动作,用于从H2数据库中创建和检索供应商。我们使用 Typesafe Slick 作为关系映射工具来创建查询和插入。我们首先在build.sbt中声明了所需的依赖项。接下来,我们在foo_scala/app/models/Supplier.scala中定义了供应商的映射属性。

在映射文件中,我们声明了我们的 case class 供应商。我们还声明了我们的 Slick 表映射类。最后,我们添加了我们的供应商对象类,该类应包含所有数据插入和查询所需的功能。我们将适当的路由添加到conf/routes文件中,并运行数据库演变。这允许 Slick 自动管理表创建和列同步。为了测试我们的实现,我们使用 curl 请求我们的POSTGET端点,以便能够查看响应头和正文。

利用 play-mailer

对于这个菜谱,我们将探讨 Play 应用程序如何发送电子邮件。我们将使用 Play 模块 play-mailer 来实现这一点。我们将使用 Mandrill,一个云电子邮件服务,来发送电子邮件。有关 Mandrill 的更多信息,请参阅mandrill.com/

如何做到这一点...

对于 Java,我们需要采取以下步骤:

  1. 启用 Hot-Reloading 后运行foo_java应用程序:

    <span class="strong"><strong>    activator "~run"</strong></span>
    
  2. build.sbt中将 play-mailer 声明为项目依赖项:

    "com.typesafe.play.plugins" %% "play-plugins-mailer" % "2.3.1"
    
  3. 通过在foo_java/conf/play.plugins中声明它来启用 play-mailer 插件:

    1500:com.typesafe.plugin.CommonsMailerPlugin
    
  4. foo_java/conf/application.conf中指定你的smtp主机信息:

    smtp.host=smtp.mandrillapp.com
        smtp.port=25
        smtp.user="YOUR OWN USER HERE"
        smtp.password="YOUR OWN PASSWORD HERE"
        smtp.mock=true
    
  5. 通过在foo_java/app/controllers/Application.java中添加以下代码来修改foo_java/app/controllers/Application.java

    import play.libs.F;
        import play.libs.F.Function;
        import play.libs.F.Promise;
        import com.typesafe.plugin.*;
         public static Promise&lt;Result&gt; emailSender() {
            Promise&lt;Boolean&gt; emailResult = Promise.promise(
                new F.Function0&lt;Boolean&gt;() {
                    @Override
                    public Boolean apply() throws Throwable {
                        try {
                            MailerAPI mail = play.Play.application().plugin(MailerPlugin.class).email();
                            mail.setSubject("mailer");
                            mail.setRecipient("ginduc@dynamicobjx.com");
                            mail.setFrom("Play Cookbook &lt;noreply@email.com&gt;");
                            mail.send("text");
                             return true;
                        } catch (Exception e) {
                            e.printStackTrace();
                            return false;
                        }
                    }
                }
            );
             return emailResult.map(
                new Function&lt;Boolean, Result&gt;() {
                    @Override
                    public Result apply(Boolean sent) throws Throwable {
                        if (sent) {
                            return ok("Email sent!");
                        } else {
                            return ok("Email was not sent!");
                        }
                    }
                }
            );
        }
    
  6. foo_java/conf/routes中为新增的操作添加一个新的路由条目:

    POST  /send_email    controllers.Application.emailSender
    
  7. 请求我们的新路由并检查响应头以确认我们对 HTTP 响应头的修改:

    <span class="strong"><strong>    $ curl -v -X POST http://localhost:9000/send_email</strong></span>
    <span class="strong"><strong>    * Hostname was NOT found in DNS cache</strong></span>
    <span class="strong"><strong>    *   Trying ::1...</strong></span>
    <span class="strong"><strong>    * Connected to localhost (::1) port 9000 (#0)</strong></span>
    <span class="strong"><strong>    &gt; POST /send_email HTTP/1.1</strong></span>
    <span class="strong"><strong>    &gt; User-Agent: curl/7.37.1</strong></span>
    <span class="strong"><strong>    &gt; Host: localhost:9000</strong></span>
    <span class="strong"><strong>    &gt; Accept: */*</strong></span>
    <span class="strong"><strong>    &gt;</strong></span>
    <span class="strong"><strong>    &lt; HTTP/1.1 200 OK</strong></span>
    <span class="strong"><strong>    &lt; Content-Type: text/plain; charset=utf-8</strong></span>
    <span class="strong"><strong>    &lt; Content-Length: 11</strong></span>
    <span class="strong"><strong>    &lt;</strong></span>
    <span class="strong"><strong>    * Connection #0 to host localhost left intact</strong></span>
    <span class="strong"><strong>    Email sent!%</strong></span>
    

对于 Scala,我们需要采取以下步骤:

  1. 启用 Hot-Reloading 后运行foo_scala应用程序:

    <span class="strong"><strong>    activator "~run"</strong></span>
    
  2. build.sbt中将 play-mailer 声明为项目依赖项:

    "com.typesafe.play.plugins" %% "play-plugins-mailer" % "2.3.1"
    
  3. 通过在foo_scala/conf/play.plugins中声明它来启用 play-mailer 插件:

    1500:com.typesafe.plugin.CommonsMailerPlugin
    
  4. foo_scala/conf/application.conf中指定你的smtp主机信息:

    smtp.host=smtp.mandrillapp.com
        smtp.port=25
        smtp.user="YOUR OWN USER HERE"
        smtp.password="YOUR OWN PASSWORD HERE"
        smtp.mock=true
    
  5. 通过在foo_scala/app/controllers/Application.scala中添加以下操作来修改foo_scala/app/controllers/Application.scala

    import scala.concurrent._
           import com.typesafe.plugin._
           import play.api.libs.concurrent.Execution.Implicits._
        import play.api.Play.current
         def emailSender = Action.async {
              sendEmail.map { messageId =&gt; 
            Ok("Sent email with Message ID: " + messageId)
             }
           }
         def sendEmail = Future {  
          val mail = use[MailerPlugin].email
           mail.setSubject("Play mailer")
          mail.setRecipient("ginduc@dynamicobjx.com")
          mail.setFrom("Play Cookbook &lt;noreply@email.com&gt;")
          mail.send("text")
        }
    
  6. foo_scala/conf/routes中为新增的操作添加一个新的路由条目:

    POST  /send_email    controllers.Application.emailSender
    
  7. 请求我们的新路由并检查响应头以确认我们对 HTTP 响应头的修改:

    <span class="strong"><strong>    $ curl -v -X POST http://localhost:9000/send_email</strong></span>
    <span class="strong"><strong>    * Hostname was NOT found in DNS cache</strong></span>
    <span class="strong"><strong>    *   Trying ::1...</strong></span>
    <span class="strong"><strong>    * Connected to localhost (::1) port 9000 (#0)</strong></span>
    <span class="strong"><strong>    &gt; POST /send_email HTTP/1.1</strong></span>
    <span class="strong"><strong>    &gt; User-Agent: curl/7.37.1</strong></span>
    <span class="strong"><strong>    &gt; Host: localhost:9000</strong></span>
    <span class="strong"><strong>    &gt; Accept: */*</strong></span>
    <span class="strong"><strong>    &gt;</strong></span>
    <span class="strong"><strong>    &lt; HTTP/1.1 200 OK</strong></span>
    <span class="strong"><strong>    &lt; Content-Type: text/plain; charset=utf-8</strong></span>
    <span class="strong"><strong>    &lt; Content-Length: 30</strong></span>
    <span class="strong"><strong>    &lt;</strong></span>
    <span class="strong"><strong>    * Connection #0 to host localhost left intact</strong></span>
    <span class="strong"><strong>    Sent email with Message ID: ()%</strong></span>
    

它是如何工作的...

在这个菜谱中,我们创建了一个新的 URL 路由和操作,该操作将调用我们新添加的sendMail函数。我们在foo_scala/build.sbt中声明了模块依赖,并在foo_scala/conf/application.conf中指定了我们的smtp服务器设置。之后,我们使用终端中的curl调用了 URL 路由来测试我们的电子邮件发送者。你现在应该能在你的电子邮件客户端软件中收到电子邮件。

集成 Bootstrap 和 WebJars

对于这个菜谱,我们将探讨如何将流行的前端框架 Bootstrap 与 Play 2 Web 应用程序集成和利用。我们将使用 WebJars 来集成 Bootstrap,这是一个将前端库打包成 JAR 文件的工具,然后可以轻松管理(在我们的情况下,通过sbt)。

如何做到这一点...

对于 Java,我们需要采取以下步骤:

  1. 启用 Hot-Reloading 后运行foo_java应用程序:

    <span class="strong"><strong>    activator "~run"</strong></span>
    
  2. build.sbt中将 Bootstrap 和 WebJars 声明为项目依赖项:

    "org.webjars" % "bootstrap" % "3.3.1",
        "org.webjars" %% "webjars-play" % "2.3.0"
    
  3. 通过添加以下代码修改foo_java/app/controllers/Application.java

    public static Result bootstrapped() {
             return ok(views.html.bootstrapped.render());
           }
    
  4. 将新的路由条目添加到foo_java/conf/routes

    GET     /webjars/*file        controllers.WebJarAssets.at(file)
        GET     /bootstrapped         controllers.Application.bootstrapped
    
  5. 创建新的布局视图模板foo_java/app/views/mainLayout.scala.html,内容如下:

    @(title: String)(content: Html)&lt;!DOCTYPE html&gt;
     &lt;html&gt;
    &lt;head&gt;
        &lt;meta charset="utf-8"&gt;
        &lt;meta http-equiv="X-UA-Compatible" content="IE=edge"&gt;
        &lt;meta name="viewport" content="width=device-width, initial-scale=1"&gt;
        &lt;meta name="description" content=""&gt;
        &lt;meta name="author" content=""&gt;
        &lt;title&gt;@title&lt;/title&gt;
        &lt;link rel="shortcut icon" type="image/png" href='@routes.Assets.at("images/favicon.png")'&gt;
        &lt;link rel="stylesheet" media="screen" href='@routes.WebJarAssets.at(WebJarAssets.locate("css/bootstrap.min.css"))' /&gt;
        &lt;link rel="stylesheet" media="screen" href='@routes.Assets.at("stylesheets/app.css")'/&gt;
        &lt;style&gt;
            body {
              padding-top: 50px;
            }
        &lt;/style&gt;
    &lt;/head&gt;
    &lt;body&gt;
    &lt;nav class="navbar navbar-inverse navbar-fixed-top" role="navigat   ion"&gt;
        &lt;div class="container-fluid"&gt;
            &lt;div class="navbar-header"&gt;
                &lt;button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#navbar" aria-expanded="false" aria-controls="navbar"&gt;
                    &lt;span class="sr-only"&gt;Toggle navigation&lt;/span&gt;
                    &lt;span class="icon-bar"&gt;&lt;/span&gt;
                    &lt;span class="icon-bar"&gt;&lt;/span&gt;
                    &lt;span class="icon-bar"&gt;&lt;/span&gt;
                &lt;/button&gt;
                &lt;a class="navbar-brand" href="#"&gt;Admin&lt;/a&gt;
            &lt;/div&gt;
            &lt;div id="navbar" class="navbar-collapse collapse"&gt;
                &lt;ul class="nav navbar-nav navbar-right"&gt;
                    &lt;li&gt;&lt;a href="#"&gt;Dashboard&lt;/a&gt;&lt;/li&gt;
                    &lt;li&gt;&lt;a href="#"&gt;Settings&lt;/a&gt;&lt;/li&gt;
                    &lt;li&gt;&lt;a href="#"&gt;Profile&lt;/a&gt;&lt;/li&gt;
                    &lt;li&gt;&lt;a href="#"&gt;Help&lt;/a&gt;&lt;/li&gt;
                &lt;/ul&gt;
            &lt;/div&gt;
        &lt;/div&gt;
    &lt;/nav&gt;
     &lt;div class="container-fluid"&gt;
        &lt;div class="row"&gt;
            &lt;div class="col-sm-3 col-md-2 sidebar"&gt;
                &lt;ul class="nav nav-sidebar"&gt;
                    &lt;li class="active"&gt;&lt;a href="#"&gt;Overview &lt;span class="sr-only"&gt;(current)&lt;/span&gt;&lt;/a&gt;&lt;/li&gt;
                    &lt;li&gt;&lt;a href="#"&gt;Reports&lt;/a&gt;&lt;/li&gt;
                    &lt;li&gt;&lt;a href="#"&gt;Analytics&lt;/a&gt;&lt;/li&gt;
                    &lt;li&gt;&lt;a href="#"&gt;Export&lt;/a&gt;&lt;/li&gt;
                &lt;/ul&gt;
                &lt;ul class="nav nav-sidebar"&gt;
                    &lt;li&gt;&lt;a href=""&gt;Users&lt;/a&gt;&lt;/li&gt;
                    &lt;li&gt;&lt;a href=""&gt;Audit Log&lt;/a&gt;&lt;/li&gt;
                &lt;/ul&gt;
                &lt;ul class="nav nav-sidebar"&gt;
                    &lt;li&gt;&lt;a href=""&gt;Sign out&lt;/a&gt;&lt;/li&gt;
                &lt;/ul&gt;
            &lt;/div&gt;
            &lt;div class="col-sm-9 col-sm-offset-3 col-md-10 col-md-offset-2 main"&gt;
                @content
            &lt;/div&gt;
        &lt;/div&gt;
    &lt;/div&gt;
     &lt;/body&gt;
    &lt;/html&gt;
    
  6. foo_java/app/views/bootstrapped.scala.html中创建自举视图模板,内容如下:

    @mainLayout("Bootstrapped") {
    &lt;div class="hero-unit"&gt;
        &lt;h1&gt;Hello, world!&lt;/h1&gt;
        &lt;p&gt;This is a template for a simple marketing or informational website. It includes a large callout called the hero unit and three supporting pieces of content. Use it as a starting point to create something more unique.&lt;/p&gt;
        &lt;p&gt;&lt;a href="#" class="btn btn-primary btn-large"&gt;Learn more &amp;raquo;&lt;/a&gt;&lt;/p&gt;
    &lt;/div&gt;
    }
    
  7. 使用网络浏览器请求我们的新自举路由(http://localhost:9000/bootstrapped)并检查使用 Bootstrap 模板渲染的页面:图片

对于 Scala,我们需要采取以下步骤:

  1. 启用热重载运行foo_scala应用程序:

    <span class="strong"><strong>    activator "~run"</strong></span>
    
  2. build.sbt中将 Bootstrap 和 WebJars 声明为项目依赖项:

    "org.webjars" % "bootstrap" % "3.3.1",
        "org.webjars" %% "webjars-play" % "2.3.0"
    
  3. 通过添加以下操作修改foo_scala/app/controllers/Application.scala

    def bootstrapped = Action {
             Ok(views.html.bootstrapped())
           }
    
  4. 将新的路由条目添加到foo_scala/conf/routes

    GET     /webjars/*file        controllers.WebJarAssets.at(file)
        GET     /bootstrapped         controllers.Application.bootstrapped
    
  5. 创建新的布局视图模板foo_scala/app/views/mainLayout.scala.html,内容如下:

    @(title: String)(content: Html)&lt;!DOCTYPE html&gt;
     &lt;html&gt;
    &lt;head&gt;
        &lt;meta charset="utf-8"&gt;
        &lt;meta http-equiv="X-UA-Compatible" content="IE=edge"&gt;
        &lt;meta name="viewport" content="width=device-width, initial-scale=1"&gt;
        &lt;meta name="description" content=""&gt;
        &lt;meta name="author" content=""&gt;
        &lt;title&gt;@title&lt;/title&gt;
        &lt;link rel="shortcut icon" type="image/png" href='@routes.Assets.at("images/favicon.png")'&gt;
        &lt;link rel="stylesheet" media="screen" href='@routes.WebJarAssets.at(WebJarAssets.locate("css/bootstrap.min.css"))' /&gt;
        &lt;link rel="stylesheet" media="screen" href='@routes.Assets.at("stylesheets/app.css")'/&gt;
        &lt;style&gt;
            body {
              padding-top: 50px;
            }
        &lt;/style&gt;
    &lt;/head&gt;
    &lt;body&gt;
    &lt;nav class="navbar navbar-inverse navbar-fixed-top" role="navigat   ion"&gt;
        &lt;div class="container-fluid"&gt;
            &lt;div class="navbar-header"&gt;
                &lt;button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#navbar" aria-expanded="false" aria-controls="navbar"&gt;
                    &lt;span class="sr-only"&gt;Toggle navigation&lt;/span&gt;
                    &lt;span class="icon-bar"&gt;&lt;/span&gt;
                    &lt;span class="icon-bar"&gt;&lt;/span&gt;
                    &lt;span class="icon-bar"&gt;&lt;/span&gt;
                &lt;/button&gt;
                &lt;a class="navbar-brand" href="#"&gt;Admin&lt;/a&gt;
            &lt;/div&gt;
            &lt;div id="navbar" class="navbar-collapse collapse"&gt;
                &lt;ul class="nav navbar-nav navbar-right"&gt;
                    &lt;li&gt;&lt;a href="#"&gt;Dashboard&lt;/a&gt;&lt;/li&gt;
                    &lt;li&gt;&lt;a href="#"&gt;Settings&lt;/a&gt;&lt;/li&gt;
                    &lt;li&gt;&lt;a href="#"&gt;Profile&lt;/a&gt;&lt;/li&gt;
                    &lt;li&gt;&lt;a href="#"&gt;Help&lt;/a&gt;&lt;/li&gt;
                &lt;/ul&gt;
            &lt;/div&gt;
        &lt;/div&gt;
    &lt;/nav&gt;
     &lt;div class="container-fluid"&gt;
        &lt;div class="row"&gt;
            &lt;div class="col-sm-3 col-md-2 sidebar"&gt;
                &lt;ul class="nav nav-sidebar"&gt;
                    &lt;li class="active"&gt;&lt;a href="#"&gt;Overview &lt;span class="sr-only"&gt;(current)&lt;/span&gt;&lt;/a&gt;&lt;/li&gt;
                    &lt;li&gt;&lt;a href="#"&gt;Reports&lt;/a&gt;&lt;/li&gt;
                    &lt;li&gt;&lt;a href="#"&gt;Analytics&lt;/a&gt;&lt;/li&gt;
                    &lt;li&gt;&lt;a href="#"&gt;Export&lt;/a&gt;&lt;/li&gt;
                &lt;/ul&gt;
                &lt;ul class="nav nav-sidebar"&gt;
                    &lt;li&gt;&lt;a href=""&gt;Users&lt;/a&gt;&lt;/li&gt;
                    &lt;li&gt;&lt;a href=""&gt;Audit Log&lt;/a&gt;&lt;/li&gt;
                &lt;/ul&gt;
                &lt;ul class="nav nav-sidebar"&gt;
                    &lt;li&gt;&lt;a href=""&gt;Sign out&lt;/a&gt;&lt;/li&gt;
                &lt;/ul&gt;
            &lt;/div&gt;
            &lt;div class="col-sm-9 col-sm-offset-3 col-md-10 col-md-offset-2 main"&gt;
                @content
            &lt;/div&gt;
        &lt;/div&gt;
    &lt;/div&gt;
     &lt;/body&gt;
    &lt;/html&gt;
    
  6. foo_scala/app/views/bootstrapped.scala.html中创建自举视图模板,内容如下:

    @mainLayout("Bootstrapped") {
    &lt;div class="hero-unit"&gt;
        &lt;h1&gt;Hello, world!&lt;/h1&gt;
        &lt;p&gt;This is a template for a simple marketing or informational website. It includes a large callout called the hero unit and three supporting pieces of content. Use it as a starting point to create something more unique.&lt;/p&gt;
        &lt;p&gt;&lt;a href="#" class="btn btn-primary btn-large"&gt;Learn more &amp;raquo;&lt;/a&gt;&lt;/p&gt;
    &lt;/div&gt;
    }
    
  7. 使用网络浏览器请求我们的新自举路由(http://localhost:9000/bootstrapped)并检查使用 Bootstrap 模板渲染的页面:图片

它是如何工作的……

在这个菜谱中,我们不是单独下载 Bootstrap 并手动管理不同版本,而是使用了 Play 模块和 WebJars,在build.sbt中将 Bootstrap 声明为前端依赖。我们创建了包含 Bootstrap 模板的新视图模板。然后我们创建了一个新的 URL 路由,将利用这些基于 Bootstrap 的新视图。

第四章. 创建和使用 Web API

在本章中,我们将介绍以下食谱:

  • 创建一个 POST API 端点

  • 创建一个 GET API 端点

  • 创建一个 PUT API 端点

  • 创建一个 DELETE API 端点

  • 使用 HTTP 基本身份验证保护 API 端点

  • 消费外部 Web API

  • 使用 OAuth 通过 Twitter API

简介

在本章中,我们将探讨如何使用 Play 2.0 创建 REST API 并与其他外部基于 Web 的 API 进行交互,在我们的例子中,是 Twitter API。

随着独立 Web 服务之间数据交换的日益流行,REST API 已经成为了一种流行的方法,不仅用于消费外部数据,还用于接收传入数据以进行进一步处理和持久化,以及向授权客户端公开数据。基于 RESTful API 规范,HTTP 方法 POST 用于插入新记录,HTTP 方法 GET 用于检索数据。HTTP 方法 PUT 用于更新现有记录,最后,HTTP 方法 DELETE 用于删除记录。

我们将看到如何利用不同的 Play 2.0 库来构建我们自己的 REST API 端点,并使用新的 Play WS 库访问其他基于 Web 的 API。

创建一个 POST API 端点

在本食谱中,我们将探索如何使用 Play 2.0 创建一个 RESTful POST 端点来向我们的 API 添加新记录。

如何做到这一点...

对于 Java,我们需要执行以下步骤:

  1. 启用热重载功能运行foo_java应用程序:

    <span class="strong"><strong>    activator "~run"</strong></span>
    
  2. foo_java/app/controllers/Products.java中创建一个新的产品控制器,内容如下:

    package controllers;
         import java.util.*;
        import play.data.Form;
        import play.mvc.*;
        import models.Product;
        import static play.libs.Json.toJson;
         public class Products extends Controller {
            public static Map&lt;String, Product&gt; products = new HashMap&lt;String, Product&gt;();
             @BodyParser.Of(BodyParser.Json.class)
            public static Result create() {
                try {
                    Form&lt;Product&gt; form = Form.form(Product.class).bindFromRequest();
                     if (form.hasErrors()) {
                        return badRequest(form.errorsAsJson());
                    } else {
                        Product product = form.get();
                        products.put(product.getSku(), product);
                        return created(toJson(product));
                    }
                 } catch (Exception e) {
                    return internalServerError(e.getMessage());
                }
            }
        }
    
  3. foo_java/app/models/Product.java中创建一个产品模型类:

    package models;
         import play.data.validation.Constraints;
         public class Product implements java.io.Serializable {
            @Constraints.Required
            private String sku;
             @Constraints.Required
            private String title;
             public String getSku() {
                return sku;
            }
            public void setSku(String sku) {
                this.sku = sku;
            }
            public String getTitle() {
                return title;
            }
            public void setTitle(String title) {
                this.title = title;
            }
        }
    
  4. foo_java/conf/routes中为新增的操作添加一个新的路由条目:

    POST    /api/products       controllers.Products.create
    
  5. 请求新的路由并检查响应体以确认:

    <span class="strong"><strong>    $ curl -v -X POST http://localhost:9000/api/products --data '{"sku":"abc",   "title":"Macbook Pro Retina"}' --header "Content-type: application/json"</strong></span>
    <span class="strong"><strong>    * Hostname was NOT found in DNS cache</strong></span>
    <span class="strong"><strong>    *   Trying ::1...</strong></span>
    <span class="strong"><strong>    * Connected to localhost (::1) port 9000 (#0)</strong></span>
    <span class="strong"><strong>    &gt; POST /api/products HTTP/1.1</strong></span>
    <span class="strong"><strong>    &gt; User-Agent: curl/7.37.1</strong></span>
    <span class="strong"><strong>    &gt; Host: localhost:9000</strong></span>
    <span class="strong"><strong>    &gt; Accept: */*</strong></span>
    <span class="strong"><strong>    &gt; Content-type: application/json</strong></span>
    <span class="strong"><strong>    &gt; Content-Length: 43</strong></span>
    <span class="strong"><strong>    &gt;</strong></span>
    <span class="strong"><strong>    * upload completely sent off: 43 out of 43 bytes</strong></span>
    <span class="strong"><strong>    &lt; HTTP/1.1 201 Created</strong></span>
    <span class="strong"><strong>    &lt; Content-Type: application/json; charset=utf-8</strong></span>
    <span class="strong"><strong>    &lt; Content-Length: 42</strong></span>
    <span class="strong"><strong>    &lt;</strong></span>
    <span class="strong"><strong>    * Connection #0 to host localhost left intact</strong></span>
    <span class="strong"><strong>    {"sku":"abc","title":"Macbook Pro Retina"}%</strong></span>
    

对于 Scala,我们需要执行以下步骤:

  1. 启用热重载功能运行foo_scala应用程序:

    <span class="strong"><strong>    activator "~run"</strong></span>
    
  2. foo_scala/app/controllers/Products.scala中创建一个新的产品控制器,内容如下:

    package controllers
         import models.Product
        import play.api.libs.json.{JsError, Json}
        import play.api.libs.json.Json._
        import play.api.mvc._
         object Products extends Controller {
          implicit private val productWrites = Json.writes[Product]
          implicit private val productReads = Json.reads[Product]
          private val products: scala.collection.mutable.ListBuffer[Product] = scala.collection.mutable.ListBuffer[Product]()
           def create = Action(BodyParsers.parse.json) { implicit request =&gt;
            val post = request.body.validate[Product]
            post.fold(
              errors =&gt; BadRequest(Json.obj("message" -&gt; JsError.toFlatJson(errors))),
              p =&gt; {
                try {
                  products += p
                  Created(Json.toJson(p))
                } catch {
                  case e: Exception =&gt; InternalServerError(e.getMessage)
                }
              }
            )
          }
        }
    
  3. foo_scala/app/models/Product.scala中创建一个产品模型类:

    package models
         case class Product(sku: String, title: String)
    
  4. foo_scala/conf/routes中为新增的操作添加一个新的路由条目:

    POST    /api/products       controllers.Products.create
    
  5. 请求新的路由并检查响应体以确认:

    <span class="strong"><strong>    $ curl -v -X POST http://localhost:9000/api/products --data '{"sku":"abc",   "title":"Macbook Pro Retina"}' --header "Content-type: application/json"</strong></span>
    <span class="strong"><strong>    * Hostname was NOT found in DNS cache</strong></span>
    <span class="strong"><strong>    *   Trying ::1...</strong></span>
    <span class="strong"><strong>    * Connected to localhost (::1) port 9000 (#0)</strong></span>
    <span class="strong"><strong>    &gt; POST /api/products HTTP/1.1</strong></span>
    <span class="strong"><strong>    &gt; User-Agent: curl/7.37.1</strong></span>
    <span class="strong"><strong>    &gt; Host: localhost:9000</strong></span>
    <span class="strong"><strong>    &gt; Accept: */*</strong></span>
    <span class="strong"><strong>    &gt; Content-type: application/json</strong></span>
    <span class="strong"><strong>    &gt; Content-Length: 43</strong></span>
    <span class="strong"><strong>    &gt;</strong></span>
    <span class="strong"><strong>    * upload completely sent off: 43 out of 43 bytes</strong></span>
    <span class="strong"><strong>    &lt; HTTP/1.1 201 Created</strong></span>
    <span class="strong"><strong>    &lt; Content-Type: application/json; charset=utf-8</strong></span>
    <span class="strong"><strong>    &lt; Content-Length: 42</strong></span>
    <span class="strong"><strong>    &lt;</strong></span>
    <span class="strong"><strong>    * Connection #0 to host localhost left intact</strong></span>
    <span class="strong"><strong>    {"sku":"abc","title":"Macbook Pro Retina"}%</strong></span>
    

它是如何工作的...

在本食谱中,我们使用 Play 2.0 实现了一个 RESTful POST 请求。第一步是创建我们的控制器和模型类。对于模型类,我们声明了两个基本的产品字段。我们将它们标注为必填字段。这允许 Play 在产品绑定到请求体时验证这两个字段。

// Java 
    @Constraints.Required
    private String sku;

对于强制执行必需请求参数的 Scala 等价物,我们使用 scala.Option 类声明可选参数。然而,在本食谱中,为了保持 Java 和 Scala 食谱的一致性,将不需要使用 scala.Option,我们将在我们的案例类中强制执行必需字段,如下所示:

case class Product(sku: String, title: String)

我们在控制器类中创建了一个动作方法,该方法将处理产品的POST请求。我们确保在数据绑定过程中play.data.Form对象没有遇到任何验证错误;然而,如果遇到问题,它将通过badRequest()辅助函数返回 HTTP 状态码 400:

// Java 
    if (form.hasErrors()) {
        return badRequest(form.errorsAsJson());
    }
     // Scala 
    errors =&gt; BadRequest(Json.obj("message" -&gt; JsError.toFlatJson(errors))),

如果没有遇到错误,我们继续持久化我们的新产品并返回由created()辅助函数包装的 HTTP 状态码 201:

// Java 
    return created(toJson(product));
     // Scala 
    Created(Json.toJson(p))

我们然后在conf/routes文件中声明了我们的新POST路由。最后,我们使用命令行工具curl模拟 HTTP POST请求来测试我们的路由。要验证我们的端点是否执行了POST表单字段的验证,请从之前的curl命令中省略标题参数,您将看到适当的错误消息:

<span class="strong"><strong>    $ curl -v -X POST http://localhost:9000/api/products --data '{"sku":"abc"}' --header "Content-type: application/json"</strong></span>
<span class="strong"><strong>    * Hostname was NOT found in DNS cache</strong></span>
<span class="strong"><strong>    *   Trying ::1...</strong></span>
<span class="strong"><strong>    * Connected to localhost (::1) port 9000 (#0)</strong></span>
<span class="strong"><strong>    &gt; POST /api/products HTTP/1.1</strong></span>
<span class="strong"><strong>    &gt; User-Agent: curl/7.37.1</strong></span>
<span class="strong"><strong>    &gt; Host: localhost:9000</strong></span>
<span class="strong"><strong>    &gt; Accept: */*</strong></span>
<span class="strong"><strong>    &gt; Content-type: application/json</strong></span>
<span class="strong"><strong>    &gt; Content-Length: 44</strong></span>
<span class="strong"><strong>    &gt;</strong></span>
<span class="strong"><strong>    * upload completely sent off: 44 out of 44 bytes</strong></span>
<span class="strong"><strong>    &lt; HTTP/1.1 400 Bad Request</strong></span>
<span class="strong"><strong>    &lt; Content-Type: application/json; charset=utf-8</strong></span>
<span class="strong"><strong>    &lt; Content-Length: 36</strong></span>
<span class="strong"><strong>    &lt;</strong></span>
<span class="strong"><strong>    * Connection #0 to host localhost left intact</strong></span>
<span class="strong"><strong>    {"title":["This field is required"]}%</strong></span>

创建一个 GET API 端点

对于这个菜谱,我们将创建一个 RESTful 的GET端点,它将返回一个 JSON 对象的集合。

如何做…

对于 Java,我们需要执行以下步骤:

  1. 启用 Hot-Reloading 运行foo_java应用程序:

    <span class="strong"><strong>    activator "~run"</strong></span>
    
  2. 通过添加以下动作方法来修改foo_java/app/controllers/Products.java中的产品控制器:

    public static Result index() {
            return ok(toJson(products));
        }
    
  3. foo_java/conf/routes中为新增的操作添加一个新的路由条目:

    GET     /api/products       controllers.Products.index
    
  4. 请求新路由并检查响应头以确认我们对 HTTP 响应头的修改:

    <span class="strong"><strong>    $ curl -v http://localhost:9000/api/products</strong></span>
    <span class="strong"><strong>    * Hostname was NOT found in DNS cache</strong></span>
    <span class="strong"><strong>    *   Trying ::1...</strong></span>
    <span class="strong"><strong>    * Connected to localhost (::1) port 9000 (#0)</strong></span>
    <span class="strong"><strong>    &gt; GET /api/products HTTP/1.1</strong></span>
    <span class="strong"><strong>    &gt; User-Agent: curl/7.37.1</strong></span>
    <span class="strong"><strong>    &gt; Host: localhost:9000</strong></span>
    <span class="strong"><strong>    &gt; Accept: */*</strong></span>
    <span class="strong"><strong>    &gt;</strong></span>
    <span class="strong"><strong>    &lt; HTTP/1.1 200 OK</strong></span>
    <span class="strong"><strong>    &lt; Content-Type: application/json; charset=utf-8</strong></span>
    <span class="strong"><strong>    &lt; Content-Length: 50</strong></span>
    <span class="strong"><strong>    &lt;</strong></span>
    <span class="strong"><strong>    * Connection #0 to host localhost left intact</strong></span>
    <span class="strong"><strong>    {"abc":{"sku":"abc","title":"Macbook Pro Retina"},"def":{"sku":"def","title":"iPad Air"}}%</strong></span>
    

对于 Scala,我们需要执行以下步骤:

  1. 启用 Hot-Reloading 运行foo_scala应用程序:

    <span class="strong"><strong>    activator "~run"</strong></span>
    
  2. 通过在foo_scala/app/controllers/Products.scala中添加以下动作方法来修改产品控制器:

    def index = Action {
          Ok(toJson(products))
        }
    
  3. foo_scala/conf/routes中为新增的操作添加一个新的路由条目:

    <span class="strong"><strong>    GET     /api/products       controllers.Products.index</strong></span>
    
  4. 请求我们的新路由并检查响应头以确认我们对 HTTP 响应头的修改:

    <span class="strong"><strong>    $ curl -v http://localhost:9000/api/products</strong></span>
    <span class="strong"><strong>    * Hostname was NOT found in DNS cache</strong></span>
    <span class="strong"><strong>    *   Trying ::1...</strong></span>
    <span class="strong"><strong>    * Connected to localhost (::1) port 9000 (#0)</strong></span>
    <span class="strong"><strong>    &gt; GET /api/products HTTP/1.1</strong></span>
    <span class="strong"><strong>    &gt; User-Agent: curl/7.37.1</strong></span>
    <span class="strong"><strong>    &gt; Host: localhost:9000</strong></span>
    <span class="strong"><strong>    &gt; Accept: */*</strong></span>
    <span class="strong"><strong>    &gt;</strong></span>
    <span class="strong"><strong>    &lt; HTTP/1.1 200 OK</strong></span>
    <span class="strong"><strong>    &lt; Content-Type: application/json; charset=utf-8</strong></span>
    <span class="strong"><strong>    &lt; Content-Length: 50</strong></span>
    <span class="strong"><strong>    &lt;</strong></span>
    <span class="strong"><strong>    * Connection #0 to host localhost left intact</strong></span>
    <span class="strong"><strong>    {"abc":{"sku":"abc","title":"Macbook Pro Retina"},"def":{"sku":"def","title":"iPad Air"}}%</strong></span>
    

如何工作…

在这个菜谱中,我们实现了一个返回产品记录列表的 API 端点。我们通过声明一个新的动作方法来实现这一点,该方法从我们的数据存储中检索记录,将对象转换为 JSON,并返回一个 JSON 集合:

// Java 
    return ok(toJson(products));
     // Scala 
    Ok(toJson(products))

我们为GET端点声明了一个新的路由条目,并使用curl验证端点的功能。

如果数据存储为空,端点将返回一个空的 JSON 数组:

<span class="strong"><strong>    # Empty product list  </strong></span>
 <span class="strong"><strong>    $ curl -v http://localhost:9000/api/products</strong></span>
<span class="strong"><strong>    * Hostname was NOT found in DNS cache</strong></span>
<span class="strong"><strong>    *   Trying ::1...</strong></span>
<span class="strong"><strong>    * Connected to localhost (::1) port 9000 (#0)</strong></span>
<span class="strong"><strong>    &gt; GET /api/products HTTP/1.1</strong></span>
<span class="strong"><strong>    &gt; User-Agent: curl/7.37.1</strong></span>
<span class="strong"><strong>    &gt; Host: localhost:9000</strong></span>
<span class="strong"><strong>    &gt; Accept: */*</strong></span>
<span class="strong"><strong>    &gt;</strong></span>
<span class="strong"><strong>    &lt; HTTP/1.1 200 OK</strong></span>
<span class="strong"><strong>    &lt; Content-Type: application/json; charset=utf-8</strong></span>
<span class="strong"><strong>    &lt; Content-Length: 2</strong></span>
<span class="strong"><strong>    &lt;</strong></span>
<span class="strong"><strong>    * Connection #0 to host localhost left intact</strong></span>
<span class="strong"><strong>    []%</strong></span>

创建一个 PUT API 端点

在这个菜谱中,我们将使用 Play 2.0 实现一个 RESTful 的PUT API 端点,以更新我们数据存储中的一个现有记录。

如何做…

对于 Java,我们需要执行以下步骤:

  1. 启用 Hot-Reloading 运行foo_java应用程序:

    <span class="strong"><strong>    activator "~run"</strong></span>
    
  2. 通过添加以下动作来修改foo_java/app/controllers/Products.java

    @BodyParser.Of(BodyParser.Json.class)
        public static Result edit(String id) {
            try {
                Product product = products.get(id);
                 if (product != null) {
                    Form&lt;Product&gt; form = Form.form(Product.class).bindFromRequest();
                    if (form.hasErrors()) {
                        return badRequest(form.errorsAsJson());
                    } else {
                        Product productForm = form.get();
                        product.setTitle(productForm.getTitle());
                        products.put(product.getSku(), product);
                         return ok(toJson(product));
                    }
                } else {
                    return notFound();
                }
            } catch (Exception e) {
                return internalServerError(e.getMessage());
            }
        }
    
  3. foo_java/conf/routes中为新增的操作添加一个新的路由:

    PUT     /api/products/:id   controllers.Products.edit(id: String)
    
  4. 使用curl,我们将更新我们数据存储中的一个现有产品:

    <span class="strong"><strong>    $ curl -v -X PUT http://localhost:9000/api/products/def --data '{"sku":"def", "title":"iPad 3 Air"}' --header "Content-type: application/json"</strong></span>
    <span class="strong"><strong>    * Hostname was NOT found in DNS cache</strong></span>
    <span class="strong"><strong>    *   Trying ::1...</strong></span>
    <span class="strong"><strong>    * Connected to localhost (::1) port 9000 (#0)</strong></span>
    <span class="strong"><strong>    &gt; PUT /api/products/def HTTP/1.1</strong></span>
    <span class="strong"><strong>    &gt; User-Agent: curl/7.37.1</strong></span>
    <span class="strong"><strong>    &gt; Host: localhost:9000</strong></span>
    <span class="strong"><strong>    &gt; Accept: */*</strong></span>
    <span class="strong"><strong>    &gt; Content-type: application/json</strong></span>
    <span class="strong"><strong>    &gt; Content-Length: 35</strong></span>
    <span class="strong"><strong>    &gt;</strong></span>
    <span class="strong"><strong>    * upload completely sent off: 35 out of 35 bytes</strong></span>
    <span class="strong"><strong>    &lt; HTTP/1.1 200 OK</strong></span>
    <span class="strong"><strong>    &lt; Content-Type: application/json; charset=utf-8</strong></span>
    <span class="strong"><strong>    &lt; Content-Length: 34</strong></span>
    <span class="strong"><strong>    &lt;</strong></span>
    <span class="strong"><strong>    * Connection #0 to host localhost left intact</strong></span>
    <span class="strong"><strong>    {"sku":"def","title":"iPad 3 Air"}%</strong></span>
    

对于 Scala,我们需要执行以下步骤:

  1. 启用 Hot-Reloading 运行foo_scala应用程序:

    <span class="strong"><strong>    activator "~run"</strong></span>
    
  2. 通过添加以下动作来修改foo_scala/app/controllers/Products.scala

    def edit(id: String) = Action(BodyParsers.parse.json) { implicit request =&gt;
          val post = request.body.validate[Product]
          post.fold(
            errors =&gt; BadRequest(Json.obj("message" -&gt; JsError.toFlatJson(errors))),
            p =&gt; {
              products.find(_.sku equals id) match {
                case Some(product) =&gt; {
                  try {
                    products -= product
                    products += p
                     Ok(Json.toJson(p))
                  } catch {
                    case e: Exception =&gt; InternalServerError(e.getMessage)
                  }
                }
                case None =&gt; NotFound
              }
            }
          )
        }
    
  3. foo_scala/conf/routes 中为新增的动作添加一个新的路由:

    PUT     /api/products/:id   controllers.Products.edit(id: String)
    
  4. 使用 curl,我们将更新我们数据存储中的现有产品:

    <span class="strong"><strong>    $ curl -v -X PUT http://localhost:9000/api/products/def --data '{"sku":"def", "title":"iPad 3 Air"}' --header "Content-type: application/json"</strong></span>
    <span class="strong"><strong>    * Hostname was NOT found in DNS cache</strong></span>
    <span class="strong"><strong>    *   Trying ::1...</strong></span>
    <span class="strong"><strong>    * Connected to localhost (::1) port 9000 (#0)</strong></span>
    <span class="strong"><strong>    &gt; PUT /api/products/def HTTP/1.1</strong></span>
    <span class="strong"><strong>    &gt; User-Agent: curl/7.37.1</strong></span>
    <span class="strong"><strong>    &gt; Host: localhost:9000</strong></span>
    <span class="strong"><strong>    &gt; Accept: */*</strong></span>
    <span class="strong"><strong>    &gt; Content-type: application/json</strong></span>
    <span class="strong"><strong>    &gt; Content-Length: 35</strong></span>
    <span class="strong"><strong>    &gt;</strong></span>
    <span class="strong"><strong>    * upload completely sent off: 35 out of 35 bytes</strong></span>
    <span class="strong"><strong>    &lt; HTTP/1.1 200 OK</strong></span>
    <span class="strong"><strong>    &lt; Content-Type: application/json; charset=utf-8</strong></span>
    <span class="strong"><strong>    &lt; Content-Length: 34</strong></span>
    <span class="strong"><strong>    &lt;</strong></span>
    <span class="strong"><strong>    * Connection #0 to host localhost left intact</strong></span>
    <span class="strong"><strong>    {"sku":"def","title":"iPad 3 Air"}%</strong></span>
    

它是如何工作的…

在本菜谱中,我们创建了一个新的 URL 路由和动作,该动作将更新我们数据存储中的现有记录。我们在产品控制器类中添加了一个新的动作,并在 conf/routes 中为它声明了一个新的路由。在我们的 edit 动作中,我们声明该动作期望请求体为 JSON 格式:

// Java
    @BodyParser.Of(BodyParser.Json.class)
     // Scala
    def edit(id: String) = Action(BodyParsers.parse.json) { implicit request =&gt;

我们通过在我们的数据存储中进行查找来检查传入的 ID 值是否有效。对于无效的 ID,我们发送 HTTP 状态码 404:

// Java 
    return notFound();
     // Scala 
    case None =&gt; NotFound

我们还检查任何表单验证错误,并在出现错误时返回适当的状态码:

// Java 
    if (form.hasErrors()) {
      return badRequest(form.errorsAsJson());
    }
     // Scala 
    errors =&gt; BadRequest(Json.obj("message" -&gt; JsError.toFlatJson(errors))),

最后,我们使用 curl 测试了新的产品 PUT 动作。我们可以进一步通过测试它如何处理无效的 ID 和无效的请求体来验证 PUT 端点:

<span class="strong"><strong># Passing an invalid Product ID:</strong></span>
 <span class="strong"><strong>$ curl -v -X PUT http://localhost:9000/api/products/XXXXXX</strong></span>
<span class="strong"><strong>* Hostname was NOT found in DNS cache</strong></span>
<span class="strong"><strong>*   Trying ::1...</strong></span>
<span class="strong"><strong>* Connected to localhost (::1) port 9000 (#0)</strong></span>
<span class="strong"><strong>&gt; PUT /api/products/XXXXXX HTTP/1.1</strong></span>
<span class="strong"><strong>&gt; User-Agent: curl/7.37.1</strong></span>
<span class="strong"><strong>&gt; Host: localhost:9000</strong></span>
<span class="strong"><strong>&gt; Accept: */*</strong></span>
<span class="strong"><strong>&gt;</strong></span>
<span class="strong"><strong>&lt; HTTP/1.1 404 Not Found</strong></span>
<span class="strong"><strong>&lt; Content-Length: 0</strong></span>
<span class="strong"><strong>&lt;</strong></span>
<span class="strong"><strong>* Connection #0 to host localhost left intact</strong></span>
 <span class="strong"><strong># PUT requests with form validation error</strong></span>
 <span class="strong"><strong>$ curl -v -X PUT http://localhost:9000/api/products/def --data '{}'  --header "Content-type: application/json"</strong></span>
<span class="strong"><strong>* Hostname was NOT found in DNS cache</strong></span>
<span class="strong"><strong>*   Trying ::1...</strong></span>
<span class="strong"><strong>* Connected to localhost (::1) port 9000 (#0)</strong></span>
<span class="strong"><strong>&gt; PUT /api/products/def HTTP/1.1</strong></span>
<span class="strong"><strong>&gt; User-Agent: curl/7.37.1</strong></span>
<span class="strong"><strong>&gt; Host: localhost:9000</strong></span>
<span class="strong"><strong>&gt; Accept: */*</strong></span>
<span class="strong"><strong>&gt; Content-type: application/json</strong></span>
<span class="strong"><strong>&gt; Content-Length: 2</strong></span>
<span class="strong"><strong>&gt;</strong></span>
<span class="strong"><strong>* upload completely sent off: 2 out of 2 bytes</strong></span>
<span class="strong"><strong>&lt; HTTP/1.1 400 Bad Request</strong></span>
<span class="strong"><strong>&lt; Content-Type: application/json; charset=utf-8</strong></span>
<span class="strong"><strong>&lt; Content-Length: 69</strong></span>
<span class="strong"><strong>&lt;</strong></span>
<span class="strong"><strong>* Connection #0 to host localhost left intact</strong></span>
<span class="strong"><strong>{"title":["This field is required"],"sku":["This field is required"]}%</strong></span>

创建 DELETE API 端点

在本菜谱中,我们将实现一个 RESTful DELETE API 端点,从我们的数据存储中删除记录。

如何操作…

对于 Java,我们需要执行以下步骤:

  1. 以启用热重载的方式运行 foo_java 应用程序:

    <span class="strong"><strong>    activator "~run"</strong></span>
    
  2. 通过添加以下动作修改 foo_java/app/controllers/Products.java

    public static Result delete(String id) {
            try {
                Product product = products.get(id);
                 if (product != null) {
                    products.remove(product);
                     return noContent();
                } else {
                    return notFound();
                }
            } catch (Exception e) {
                return internalServerError(e.getMessage());
            }
        }
    
  3. foo_java/conf/routes 中为新增的动作添加一个新的路由:

    DELETE  /api/products/:id   controllers.Products.delete(id: String)
    
  4. 使用 curl,按照以下方式删除现有记录:

    <span class="strong"><strong>    $ curl -v -X DELETE http://localhost:9000/api/products/def</strong></span>
    <span class="strong"><strong>    * Hostname was NOT found in DNS cache</strong></span>
    <span class="strong"><strong>    *   Trying ::1...</strong></span>
    <span class="strong"><strong>    * Connected to localhost (::1) port 9000 (#0)</strong></span>
    <span class="strong"><strong>    &gt; DELETE /api/products/def HTTP/1.1</strong></span>
    <span class="strong"><strong>    &gt; User-Agent: curl/7.37.1</strong></span>
    <span class="strong"><strong>    &gt; Host: localhost:9000</strong></span>
    <span class="strong"><strong>    &gt; Accept: */*</strong></span>
    <span class="strong"><strong>    &gt;</strong></span>
    <span class="strong"><strong>    &lt; HTTP/1.1 204 No Content</strong></span>
    <span class="strong"><strong>    &lt; Content-Length: 0</strong></span>
    <span class="strong"><strong>    &lt;</strong></span>
    <span class="strong"><strong>    * Connection #0 to host localhost left intact</strong></span>
    

对于 Scala,我们需要执行以下步骤:

  1. 以启用热重载的方式运行 foo_scala 应用程序:

    <span class="strong"><strong>    activator "~run"</strong></span>
    
  2. 通过添加以下动作修改 foo_scala/app/controllers/Products.scala

    def delete(id: String) = BasicAuthAction {
        products.find(_.sku equals id) match {
          case Some(product) =&gt; {
            try {
              products -= product
              NoContent
            } catch {
              case e: Exception =&gt; InternalServerError(e.getMessage)
            }
          }
          case None =&gt; NotFound
        }
      }
    
  3. foo_scala/conf/routes 中为新增的动作添加一个新的路由:

    DELETE  /api/products/:id   controllers.Products.delete(id: String)
    
  4. 使用 curl,按照以下方式删除现有记录:

    <span class="strong"><strong>    $ curl -v -X DELETE http://localhost:9000/api/products/def</strong></span>
    <span class="strong"><strong>    * Hostname was NOT found in DNS cache</strong></span>
    <span class="strong"><strong>    *   Trying ::1...</strong></span>
    <span class="strong"><strong>    * Connected to localhost (::1) port 9000 (#0)</strong></span>
    <span class="strong"><strong>    &gt; DELETE /api/products/def HTTP/1.1</strong></span>
    <span class="strong"><strong>    &gt; User-Agent: curl/7.37.1</strong></span>
    <span class="strong"><strong>    &gt; Host: localhost:9000</strong></span>
    <span class="strong"><strong>    &gt; Accept: */*</strong></span>
    <span class="strong"><strong>    &gt;</strong></span>
    <span class="strong"><strong>    &lt; HTTP/1.1 204 No Content</strong></span>
    <span class="strong"><strong>    &lt; Content-Length: 0</strong></span>
    <span class="strong"><strong>    &lt;</strong></span>
    <span class="strong"><strong>    * Connection #0 to host localhost left intact</strong></span>
    

它是如何工作的…

在本菜谱中,我们创建了一个新的 URL 路由和动作,用于删除现有的产品记录。我们声明了 delete 动作通过传入的 ID 参数来查找记录。我们确保在无效 ID 的情况下返回适当的 HTTP 状态码,在这种情况下,HTTP 状态码 404:

// Java 
    return notFound();
     // Scala 
    case None =&gt; NotFound

我们确保返回适当的 HTTP 状态码以表示记录删除成功,在这种情况下,HTTP 状态码 204:

// Java 
    return noContent();
     // Scala 
    NoContent

我们还可以测试 DELETE 端点并验证它是否正确处理无效的 ID:

<span class="strong"><strong>    $ curl -v -X DELETE http://localhost:9000/api/products/XXXXXX</strong></span>
<span class="strong"><strong>    * Hostname was NOT found in DNS cache</strong></span>
<span class="strong"><strong>    *   Trying ::1...</strong></span>
<span class="strong"><strong>    * Connected to localhost (::1) port 9000 (#0)</strong></span>
<span class="strong"><strong>    &gt; DELETE /api/products/asd HTTP/1.1</strong></span>
<span class="strong"><strong>    &gt; User-Agent: curl/7.37.1</strong></span>
<span class="strong"><strong>    &gt; Host: localhost:9000</strong></span>
<span class="strong"><strong>    &gt; Accept: */*</strong></span>
<span class="strong"><strong>    &gt;</strong></span>
<span class="strong"><strong>    &lt; HTTP/1.1 404 Not Found</strong></span>
<span class="strong"><strong>    &lt; Content-Length: 0</strong></span>
<span class="strong"><strong>    &lt;</strong></span>
<span class="strong"><strong>    * Connection #0 to host localhost left intact</strong></span>

使用 HTTP 基本认证保护 API 端点

在本菜谱中,我们将探讨如何使用 Play 2.0 的 HTTP 基本认证方案来保护 API 端点。我们将使用 Apache Commons Codec 库进行 Base64 编码和解码。这个依赖项被 Play 隐式导入,我们不需要在 build.sbt 的库依赖中显式声明它。

如何操作…

对于 Java,我们需要执行以下步骤:

  1. 以启用热重载的方式运行 foo_java 应用程序:

    <span class="strong"><strong>    activator "~run"</strong></span>
    
  2. foo_java/app/controllers/BasicAuthenticator.java 中创建一个新的 play.mvc.Security.Authenticator 实现类,内容如下:

    package controllers;
         import org.apache.commons.codec.binary.Base64;
        import play.mvc.Http;
        import play.mvc.Result;
        import play.mvc.Security;
         public class BasicAuthenticator extends Security.Authenticator {
            private static final String AUTHORIZATION = "authorization";
            private static final String WWW_AUTHENTICATE = "WWW-Authenticate";
            private static final String REALM = "Basic realm=\"API Realm\"";
             @Override
            public String getUsername(Http.Context ctx) {
                try {
                    String authHeader = ctx.request().getHeader(AUTHORIZATION);
                     if (authHeader != null) {
                        ctx.response().setHeader(WWW_AUTHENTICATE, REALM);
                        String auth = authHeader.substring(6);
                        byte[] decodedAuth = Base64.decodeBase64(auth);
                        String[] credentials = new String(decodedAuth, "UTF-8").split(":");
                         if (credentials != null &amp;&amp; credentials.length == 2) {
                            String username = credentials[0];
                            String password = credentials[1];
                            if (isAuthenticated(username, password)) {
                                return username;
                            } else {
                                return null;
                            }
                        }
                    }
                    return null;
                 } catch (Exception e) {
                    return null;
                }
            }
            private boolean isAuthenticated(String username, String password) {
                return username != null &amp;&amp; username.equals("ned") &amp;&amp;
                    password != null &amp;&amp; password.equals("flanders");
            }
             @Override
            public Result onUnauthorized(Http.Context context) {
                return unauthorized();
            }
        }
    
  3. 通过向 API 操作添加以下注解来修改foo_java/app/controllers/Products.java

    <span class="strong"><strong>    @Security.Authenticated(BasicAuthenticator.class)</strong></span>
        public static Result create() 
     <span class="strong"><strong>    @Security.Authenticated(BasicAuthenticator.class)</strong></span>
        public static Result index() 
     <span class="strong"><strong>    @Security.Authenticated(BasicAuthenticator.class)</strong></span>
        public static Result edit(String id)
     <span class="strong"><strong>    @Security.Authenticated(BasicAuthenticator.class)</strong></span>
        public static Result delete(String id)
    
  4. 使用curl,发送一个请求到我们之前所做的现有 RESTful GET端点;你现在将看到一个未授权的响应:

    <span class="strong"><strong>    $  curl -v http://localhost:9000/api/products</strong></span>
    <span class="strong"><strong>    * Hostname was NOT found in DNS cache</strong></span>
    <span class="strong"><strong>    *   Trying 127.0.0.1...</strong></span>
    <span class="strong"><strong>    * Connected to localhost (127.0.0.1) port 9000 (#0)</strong></span>
    <span class="strong"><strong>    &gt; GET /api/products HTTP/1.1</strong></span>
    <span class="strong"><strong>    &gt; User-Agent: curl/7.37.1</strong></span>
    <span class="strong"><strong>    &gt; Host: localhost:9000</strong></span>
    <span class="strong"><strong>    &gt; Accept: */*</strong></span>
    <span class="strong"><strong>    &gt;</strong></span>
    <span class="strong"><strong>    &lt; HTTP/1.1 401 Unauthorized</strong></span>
    <span class="strong"><strong>    &lt; Content-Length: 0</strong></span>
    <span class="strong"><strong>    &lt;</strong></span>
    <span class="strong"><strong>    * Connection #0 to host localhost left intact</strong></span>
    
  5. 再次使用curl,发送另一个请求到现有的 RESTful GET端点,这次带有用户凭据,ned(用户名)和flanders(密码):

    <span class="strong"><strong>    $  curl -v -u "ned:flanders" http://localhost:9000/api/products</strong></span>
    <span class="strong"><strong>    * Hostname was NOT found in DNS cache</strong></span>
    <span class="strong"><strong>    *   Trying 127.0.0.1...</strong></span>
    <span class="strong"><strong>    * Connected to localhost (127.0.0.1) port 9000 (#0)</strong></span>
    <span class="strong"><strong>    * Server auth using Basic with user 'ned'</strong></span>
    <span class="strong"><strong>    &gt; GET /api/products HTTP/1.1</strong></span>
    <span class="strong"><strong>    &gt; Authorization: Basic bmVkOmZsYW5kZXJz</strong></span>
    <span class="strong"><strong>    &gt; User-Agent: curl/7.37.1</strong></span>
    <span class="strong"><strong>    &gt; Host: localhost:9000</strong></span>
    <span class="strong"><strong>    &gt; Accept: */*</strong></span>
    <span class="strong"><strong>    &gt;</strong></span>
    <span class="strong"><strong>    &lt; HTTP/1.1 200 OK</strong></span>
    <span class="strong"><strong>    &lt; Content-Type: application/json; charset=utf-8</strong></span>
    <span class="strong"><strong>    &lt; WWW-Authenticate: Basic realm="API Realm"</strong></span>
    <span class="strong"><strong>    &lt; Content-Length: 2</strong></span>
    <span class="strong"><strong>    &lt;</strong></span>
    <span class="strong"><strong>    * Connection #0 to host localhost left intact</strong></span>
    <span class="strong"><strong>    {}%</strong></span>
    

对于 Scala,我们需要执行以下步骤:

  1. 启用热重载功能运行foo_scala应用程序:

    activator "~run"
    
  2. foo_scala/app/controllers/BasicAuthAction.scala中创建一个新的ActionBuilder类,并添加以下内容:

    package controllers
         import controllers.Products._
        import org.apache.commons.codec.binary.Base64
        import play.api.mvc._
        import scala.concurrent.Future
         object BasicAuthAction extends ActionBuilder[Request] {
          def invokeBlockA =&gt; Future[Result]) = {
            try {
              request.headers.get("authorization") match {
                case Some(headers) =&gt; {
                  val auth = headers.substring(6)
                  val decodedAuth = Base64.decodeBase64(auth)
                  val credentials = new String(decodedAuth, "UTF-8").split(":")
                   if (credentials != null &amp;&amp; credentials.length == 2 &amp;&amp;
                      isAuthenticated(credentials(0), credentials(1))) {
                    block(request)
                  } else {
                    unauthorized
                  }
                }
                case None =&gt; unauthorized
              }
            } catch {
              case e: Exception =&gt; Future.successful(InternalServerError(e.getMessage))
            }
          }
           def unauthorized = Future.successful(Unauthorized.withHeaders("WWW-Authenticate" -&gt; "Basic realm=\"API Realm\""))
           def isAuthenticated(username: String, password: String) = username != null &amp;&amp; username.equals("ned") &amp;&amp; password != null &amp;&amp; password.equals("flanders")
        }
    
  3. 通过添加新创建的ActionBuilder类和 API 操作来修改foo_scala/app/controllers/Products.scala

    def index = BasicAuthAction 
         def create = BasicAuthAction(BodyParsers.parse.json)
         def edit(id: String) = BasicAuthAction(BodyParsers.parse.json)
         def delete(id: String) = BasicAuthAction
    
  4. 使用curl,发送一个请求到我们之前所做的现有 RESTful GET端点;你现在将看到一个未授权的响应:

    <span class="strong"><strong>    $  curl -v http://localhost:9000/api/products</strong></span>
    <span class="strong"><strong>    * Hostname was NOT found in DNS cache</strong></span>
    <span class="strong"><strong>    *   Trying 127.0.0.1...</strong></span>
    <span class="strong"><strong>    * Connected to localhost (127.0.0.1) port 9000 (#0)</strong></span>
    <span class="strong"><strong>    &gt; GET /api/products HTTP/1.1</strong></span>
    <span class="strong"><strong>    &gt; User-Agent: curl/7.37.1</strong></span>
    <span class="strong"><strong>    &gt; Host: localhost:9000</strong></span>
    <span class="strong"><strong>    &gt; Accept: */*</strong></span>
    <span class="strong"><strong>    &gt;  </strong></span>
    <span class="strong"><strong>    &lt; HTTP/1.1 401 Unauthorized</strong></span>
    <span class="strong"><strong>    &lt; Content-Length: 0</strong></span>
    <span class="strong"><strong>    &lt;</strong></span>
    <span class="strong"><strong>    * Connection #0 to host localhost left intact</strong></span>
    
  5. 再次使用curl,发送另一个请求到现有的 RESTful GET端点,这次带有用户凭据,ned(用户名)和flanders(密码):

    <span class="strong"><strong>    $  curl -v -u "ned:flanders" http://localhost:9000/api/products</strong></span>
    <span class="strong"><strong>    * Hostname was NOT found in DNS cache</strong></span>
    <span class="strong"><strong>    *   Trying 127.0.0.1...</strong></span>
    <span class="strong"><strong>    * Connected to localhost (127.0.0.1) port 9000 (#0)</strong></span>
    <span class="strong"><strong>    * Server auth using Basic with user 'ned'</strong></span>
    <span class="strong"><strong>    &gt; GET /api/products HTTP/1.1</strong></span>
    <span class="strong"><strong>    &gt; Authorization: Basic bmVkOmZsYW5kZXJz</strong></span>
    <span class="strong"><strong>    &gt; User-Agent: curl/7.37.1</strong></span>
    <span class="strong"><strong>    &gt; Host: localhost:9000</strong></span>
    <span class="strong"><strong>    &gt; Accept: */*</strong></span>
    <span class="strong"><strong>    &gt;</strong></span>
    <span class="strong"><strong>    &lt; HTTP/1.1 200 OK</strong></span>
    <span class="strong"><strong>    &lt; Content-Type: application/json; charset=utf-8</strong></span>
     <span class="strong"><strong>    &lt; Content-Length: 2</strong></span>
    <span class="strong"><strong>    &lt;</strong></span>
    <span class="strong"><strong>    * Connection #0 to host localhost left intact</strong></span>
    <span class="strong"><strong>    {}%</strong></span>
    

它是如何工作的…

在这个菜谱中,我们使用 Play 2.0 的 HTTP 基本认证方案对 RESTful API 端点进行了安全保护。我们为 Java 和 Scala 创建了相应的安全实现类。对于每个安全实现类,BasicAuthenticator.javaBasicAuthAction.scala,我们检索了授权头并解码了值字符串以解密我们传递的用户凭据:

// Java
    String authHeader = ctx.request().getHeader(AUTHORIZATION);
    if (authHeader != null) {
        ctx.response().setHeader(WWW_AUTHENTICATE, REALM);
        String auth = authHeader.substring(6);
        byte[] decodedAuth = Base64.decodeBase64(auth);
        String[] credentials = new String(decodedAuth, "UTF-8").split(":");
    }
     // Scala 
    request.headers.get("authorization") match {
        case Some(headers) =&gt; {
          val auth = headers.substring(6)
          val decodedAuth = Base64.decodeBase64(auth)
          val credentials = new String(decodedAuth, "UTF-8").split(":")
        }
    }

一旦我们获得了用户名和密码,我们就调用了isAuthenticated函数来检查用户凭据的有效性:

// Java
    if (credentials != null &amp;&amp; credentials.length == 2) {
     String username = credentials[0];
        String password = credentials[1];
        if (isAuthenticated(username, password)) {
            return username;
        } else {
            return null;
        }
    }
     // Scala
    if (credentials != null &amp;&amp; credentials.length == 2 &amp;&amp;
          isAuthenticated(credentials(0), credentials(1))) {
        block(request)
    } else {
        unauthorized
    }

我们通过注解 Java API 操作并声明为 API 操作类来利用安全实现类:

// Java
    @Security.Authenticated(BasicAuthenticator.class)
    public static Result index() {
        return ok(toJson(products));
    }
     // Scala 
    def index = BasicAuthAction {
        Ok(toJson(products))
    }

使用curl,我们还可以检查我们的安全 API 操作是否处理未认证的请求:

<span class="strong"><strong>    $ curl -v http://localhost:9000/api/products</strong></span>
<span class="strong"><strong>    * Hostname was NOT found in DNS cache</strong></span>
<span class="strong"><strong>    *   Trying ::1...</strong></span>
<span class="strong"><strong>    * Connected to localhost (::1) port 9000 (#0)</strong></span>
<span class="strong"><strong>    &gt; GET /api/products HTTP/1.1</strong></span>
<span class="strong"><strong>    &gt; User-Agent: curl/7.37.1</strong></span>
<span class="strong"><strong>    &gt; Host: localhost:9000</strong></span>
<span class="strong"><strong>    &gt; Accept: */*</strong></span>
<span class="strong"><strong>    &gt;</strong></span>
<span class="strong"><strong>    &lt; HTTP/1.1 401 Unauthorized</strong></span>
<span class="strong"><strong>    &lt; WWW-Authenticate: Basic realm="API Realm"</strong></span>
<span class="strong"><strong>    &lt; Content-Length: 0</strong></span>
<span class="strong"><strong>    &lt;</strong></span>
<span class="strong"><strong>    * Connection #0 to host localhost left intact</strong></span>

消费外部 Web API

在这个菜谱中,我们将探索 Play WS API,从 Play 2 Web 应用程序中消费外部 Web 服务。随着 Web 应用程序需求的发展,我们对外部数据服务的依赖性越来越大,例如外汇汇率、实时天气数据等。Play WS 库为我们提供了与外部 Web 服务接口的 API。

如何做到这一点…

对于 Java,我们需要执行以下步骤:

  1. 启用热重载功能运行foo_java应用程序:

    <span class="strong"><strong>    activator "~run"</strong></span>
    
  2. build.sbt中将playWs声明为项目依赖项:

    libraryDependencies ++= Seq(
            javaWs
        )
    
  3. foo_java/app/controllers/WebClient.java中创建一个新的控制器并添加以下内容:

    package controllers;
         import com.fasterxml.jackson.databind.JsonNode;
        import play.libs.F;
        import play.libs.F.Promise;
        import play.libs.ws.WS;
        import play.mvc.Controller;
        import play.mvc.Result;
         public class WebClient extends Controller {
            public static Promise&lt;Result&gt; getTodos() {
                Promise&lt;play.libs.ws.WSResponse&gt; todos = WS.url("http://jsonplaceholder.typicode.com/todos").get();
                return todos.map(
                    new F.Function&lt;play.libs.ws.WSResponse, Result&gt;() {
                        public Result apply(play.libs.ws.WSResponse res) {
                            JsonNode json = res.asJson();
                            return ok("Todo Title: " + json.findValuesAsText("title"));
                        }
                    }
                );
            }
        }
    
  4. foo_java/conf/routes中为新增的操作添加一个新的路由条目:

    GET     /client/get_todos   controllers.WebClient.getTodos
    
  5. 使用curl,我们将能够测试我们的新操作如何消费外部 Web API:

    <span class="strong"><strong>    $ curl -v http://localhost:9000/client/get_todos</strong></span>
    <span class="strong"><strong>    * Hostname was NOT found in DNS cache</strong></span>
    <span class="strong"><strong>    *   Trying ::1...</strong></span>
    <span class="strong"><strong>    * Connected to localhost (::1) port 9000 (#0)</strong></span>
    <span class="strong"><strong>    &gt; GET /client/get_todos HTTP/1.1</strong></span>
    <span class="strong"><strong>    &gt; User-Agent: curl/7.37.1</strong></span>
    <span class="strong"><strong>    &gt; Host: localhost:9000</strong></span>
    <span class="strong"><strong>    &gt; Accept: */*</strong></span>
    <span class="strong"><strong>    &gt;</strong></span>
    <span class="strong"><strong>    &lt; HTTP/1.1 200 OK</strong></span>
    <span class="strong"><strong>    &lt; Content-Type: text/plain; charset=utf-8</strong></span>
    <span class="strong"><strong>    &lt; Content-Length: 8699</strong></span>
    <span class="strong"><strong>    &lt;</strong></span>
    <span class="strong"><strong>    Todo Title: [delectus aut autem, quis ut nam facilis et officia qui, fugiat veniam minus, et porro tempora, laboriosam mollitia et enim quasi adipisci quia provident illum, qui ullam ratione quibusdam voluptatem quia omnis, illo expedita</strong></span>
    

对于 Scala,我们需要执行以下步骤:

  1. 启用热重载功能运行foo_scala应用程序:

    <span class="strong"><strong>    activator "~run"</strong></span>
    
  2. build.sbt中将playWs声明为项目依赖项:

    libraryDependencies ++= Seq(
            ws
        )
    
  3. foo_scala/app/controllers/WebClient.scala中创建一个新的控制器并添加以下内容:

    package controllers
         import play.api.libs.concurrent.Execution.Implicits.defaultContext
        import play.api.Play.current
        import play.api.libs.ws.WS
        import play.api.mvc.{Action, Controller}
         object WebClient extends Controller {
          def getTodos = Action.async {
            WS.url("http://jsonplaceholder.typicode.com/todos").get().map { res =&gt;
              Ok("Todo Title: " + (res.json \\ "title").map(_.as[String]))
            }
          }
        }
    
  4. foo_scala/conf/routes中为新增的操作添加一个新的路由条目:

    GET     /client/get_todos   controllers.WebClient.getTodos
    
  5. 使用curl,我们将能够测试我们的新操作如何消费外部 Web API:

    <span class="strong"><strong>    $ curl -v http://localhost:9000/client/get_todos</strong></span>
    <span class="strong"><strong>    * Hostname was NOT found in DNS cache</strong></span>
    <span class="strong"><strong>    *   Trying ::1...</strong></span>
    <span class="strong"><strong>    * Connected to localhost (::1) port 9000 (#0)</strong></span>
    <span class="strong"><strong>    &gt; GET /client/get_todos HTTP/1.1</strong></span>
    <span class="strong"><strong>    &gt; User-Agent: curl/7.37.1</strong></span>
    <span class="strong"><strong>    &gt; Host: localhost:9000</strong></span>
    <span class="strong"><strong>    &gt; Accept: */*</strong></span>
    <span class="strong"><strong>    &gt;</strong></span>
    <span class="strong"><strong>    &lt; HTTP/1.1 200 OK</strong></span>
    <span class="strong"><strong>    &lt; Content-Type: text/plain; charset=utf-8</strong></span>
    <span class="strong"><strong>    &lt; Content-Length: 8699</strong></span>
    <span class="strong"><strong>    &lt;</strong></span>
    <span class="strong"><strong>    Todo Title: [delectus aut autem, quis ut nam facilis et officia qui, fugiat veniam minus, et porro tempora, laboriosam mollitia et enim quasi adipisci quia provident illum, qui ullam ratione quibusdam voluptatem quia omnis, illo expedita</strong></span>
    

它是如何工作的…

在本菜谱中,我们利用了 Play 2.0 插件 WS 来消费外部 Web API。我们创建了一个新的路由和AsynchronousAction方法。在操作中,我们将外部 API 的 URL 传递给 WS api,并指定它将是一个GET操作:

// Java  
    Promise&lt;play.libs.ws.WSResponse&gt; todos = WS.url("http://jsonplaceholder.typicode.com/todos").get();
    // Scala 
WS.url("http://jsonplaceholder.typicode.com/todos").get()

我们然后解析了 JSON 响应,并将其管道输入到新创建的路由/client/get_todos的结果响应中:

// Java 
    return todos.map(
      new F.Function&lt;play.libs.ws.WSResponse, Result&gt;() {
        public Result apply(play.libs.ws.WSResponse res) {
          JsonNode json = res.asJson();
          return ok("Todo Title: " + json.findValuesAsText("title"));
        }
      }
    );
     // Scala 
    Ok("Todo Title: " + (res.json \\ "title").map(_.as[String]))

使用 Twitter API 和 OAuth

在本菜谱中,我们将探讨如何使用 Play 2.0 的内置 OAuth 支持从 Twitter API 检索推文。

如何做到这一点…

对于 Java,我们需要执行以下步骤:

  1. 启用 Hot-Reloading 运行foo_java应用程序:

    <span class="strong"><strong>    activator "~run"</strong></span>
    
  2. foo_java/conf/application.conf中指定您的 Twitter API 信息:

    tw.consumerKey="YOUR TWITTER DEV CONSUMER KEY HERE"
        tw.consumerSecret="YOUR TWITTER DEV CONSUMER SECRET HERE"
        tw.accessToken="YOUR TWITTER DEV ACCESS TOKEN HERE"
        tw.accessTokenSecret="YOUR TWITTER DEV ACCESS TOKEN SECRET HERE"
    
  3. foo_java/app/controllers/WebClient.java中修改WebClient控制器,按照以下操作:

    // Add additional imports at the top section of the class file 
        import play.Play;
        import play.libs.oauth.OAuth;
        import play.libs.oauth.OAuth.OAuthCalculator;
        import play.libs.ws.WSResponse;
        import java.util.HashMap;
        import java.util.Iterator;
        import java.util.Map;
         // Add the Action method
        public static Promise&lt;Result&gt; getTweets(String hashtag) {
            final String url = "https://api.twitter.com/1.1/search/tweets.json?q=%40" + hashtag;
            final OAuth.ConsumerKey consumerInfo = new OAuth.ConsumerKey(
                Play.application().configuration().getString("tw.consumerKey"),
                Play.application().configuration().getString("tw.consumerSecret")
            );
            final OAuth.RequestToken tokens = new OAuth.RequestToken(
                Play.application().configuration().getString("tw.accessToken"),
                Play.application().configuration().getString("tw.accessTokenSecret")
            );
             Promise&lt;play.libs.ws.WSResponse&gt; twRequest = WS.url(url).sign(new OAuthCalculator(consumerInfo, tokens)).get();
            return twRequest.map(
                new F.Function&lt;WSResponse, Result&gt;(){
                    @Override
                    public Result apply(WSResponse res) throws Throwable {
                        Map&lt;String, String&gt; map = new HashMap&lt;String, String&gt;();
                        JsonNode root = res.asJson();
                         for (JsonNode json : root.get("statuses")) {
                            map.put(
             json.findValue("user").findValue("screen_name").asText(),
                                json.findValue("text").asText()
                            );
                        }
                         return ok(views.html.tweets.render(map));
                    }
                }
            );
        }
    
  4. foo_java/conf/routes中为getTweets(hashtag: String)操作添加新的路由:

    GET /client/get_tweets/:hashtag controllers.WebClient.getTweets(hashtag)
    
  5. foo_java/app/views/tweets.scala.html中添加一个新的视图模板,内容如下:

    @(tweets: Map[String, String])
         &lt;ul&gt;
          @tweets.map { tw =&gt;
            &lt;li&gt;&lt;strong&gt;@@@tw._1&lt;/strong&gt; says &lt;i&gt;"@tw._2"&lt;/i&gt;&lt;/li&gt;
          }
        &lt;/ul&gt;
    
  6. 使用网络浏览器访问/client/get_tweets/:hashtag路由以查看从 Twitter API 检索的推文:

对于 Scala,我们需要执行以下步骤:

  1. 启用 Hot-Reloading 运行foo_scala应用程序:

    <span class="strong"><strong>    activator "~run"</strong></span>
    
  2. foo_scala/conf/application.conf中指定您的 Twitter API 信息:

    tw.consumerKey="YOUR TWITTER DEV CONSUMER KEY HERE"
        tw.consumerSecret="YOUR TWITTER DEV CONSUMER SECRET HERE"
        tw.accessToken="YOUR TWITTER DEV ACCESS TOKEN HERE"
        tw.accessTokenSecret="YOUR TWITTER DEV ACCESS TOKEN SECRET HERE"
    
  3. foo_scala/app/controllers/WebClient.scala中修改WebClient控制器,按照以下操作:

    def getTweets(hashtag: String) = Action.async {
          import play.api.Play
           val consumerInfo = ConsumerKey(
            Play.application.configuration.getString("tw.consumerKey").get,
            Play.application.configuration.getString("tw.consumerSecret").get
          )
          val tokens = RequestToken(
            Play.application.configuration.getString("tw.accessToken").get,
            Play.application.configuration.getString("tw.accessTokenSecret").get
          )
          val url = "https://api.twitter.com/1.1/search/tweets.json?q=%40" + hashtag
           WS.url(url).sign(OAuthCalculator(consumerInfo, tokens)).get().map { res =&gt;
            val tweets = ListBuffer[(String, String)]()
            (res.json \ "statuses").as[List[JsObject]].map { tweet =&gt;
              tweets += ((
                (tweet \ "user" \ "screen_name").as[String],
                (tweet \ "text").as[String]
              ))
            }
             Ok(views.html.tweets(tweets.toList))
          }
        }
    
  4. foo_scala/conf/routes中为getTweets(hashtag: String)操作添加新的路由:

    GET /client/get_tweets/:hashtag controllers.WebClient.getTweets(hashtag)
    
  5. foo_scala/app/views/tweets.scala.html中添加一个新的视图模板,内容如下:

    @(tweets: List[(String, String)])
         &lt;ul&gt;
          @tweets.map { tw =&gt;
            &lt;li&gt;&lt;strong&gt;@@@tw._1&lt;/strong&gt; says &lt;i&gt;"@tw._2"&lt;/i&gt;&lt;/li&gt;
          }
        &lt;/ul&gt;
    
  6. 使用网络浏览器访问/client/get_tweets/:hashtag路由以查看从 Twitter API 检索的推文,如下截图所示:

它是如何工作的…

在本菜谱中,我们创建了一个新的 URL 路由和操作来检索和显示由请求路由/client/get_tweets/:hashtag中指定的 hashtag 标记的推文。我们通过从conf/application.conf(记得在dev.twitter.com注册 Twitter 开发者账户并为本菜谱生成消费者和访问令牌)检索所需的 Twitter API 消费者和访问令牌密钥来实现操作方法:

// Java
    final OAuth.ConsumerKey consumerInfo = new OAuth.ConsumerKey(
        Play.application().configuration().getString("tw.consumerKey"),
        Play.application().configuration().getString("tw.consumerSecret")
    );
    final OAuth.RequestToken tokens = new OAuth.RequestToken(
        Play.application().configuration().getString("tw.accessToken"),
        Play.application().configuration()
    .getString("tw.accessTokenSecret")
    );
     // Scala 
    val consumerInfo = ConsumerKey(
      Play.application.configuration.getString("tw.consumerKey").get,
      Play.application.configuration.getString("tw.consumerSecret").get
    )
    val tokens = RequestToken(
      Play.application.configuration.getString("tw.accessToken").get,
      Play.application.configuration.getString("tw.accessTokenSecret").get
    )

我们将这些凭证传递给 Play 类OAuthCalculator,当我们访问 Twitter 搜索 API 端点时:

// Java
    Promise&lt;play.libs.ws.WSResponse&gt; twRequest = 
WS.url(url).sign(new OAuthCalculator(consumerInfo, tokens)).get();
     // Scala 
    WS.url(url).sign(OAuthCalculator(consumerInfo, tokens)).get()

一旦 Twitter API 响应返回,我们解析响应的 JSON 并将其推送到一个中间集合对象,然后将其传递给我们的视图模板:

// Java 
    Map&lt;String, String&gt; map = new HashMap&lt;String, String&gt;();
    JsonNode root = res.asJson();
     for (JsonNode json : root.get("statuses")) {
      map.put(
        json.findValue("user")
          .findValue("screen_name").asText(),
        json.findValue("text").asText()
      );
    }
     return ok(views.html.tweets.render(map));
     // Scala 
    val tweets = ListBuffer[(String, String)]()
    (res.json \ "statuses").as[List[JsObject]].map { tweet =&gt;
      tweets += ((
        (tweet \ "user" \ "screen_name").as[String],
        (tweet \ "text").as[String]
      ))
    }
     Ok(views.html.tweets(tweets.toList))

第五章。创建插件和模块:

在本章中,我们将介绍以下内容:

  • 创建并使用您自己的插件:

  • 构建灵活的注册模块

  • 使用相同的模型为不同的应用程序服务

  • 管理模块依赖关系:

  • 使用 Amazon S3 添加私有模块仓库:

简介

在本章中,我们将探讨如何将我们的 Play 2.0 网络应用程序分解为模块化、可重用的组件。我们将探讨如何将插件和模块作为 Play 2.0 子项目以及作为独立模块发布到内部模块仓库:

在创建独立服务并初始化共享资源(如数据库连接和 Akka actor 引用)时,Play 2.0 插件非常有用。其他有用的 Play 插件示例包括 play.i18n.MessagesPlugin,它管理文本的国际化和 play.api.db.DBPlugin,它抽象了 Play 网络应用程序如何连接和与数据库接口:

Play 2.0 模块对于创建较大应用程序的较小、逻辑子组件非常有用;这促进了更好的代码维护和测试的隔离:

创建并使用您自己的插件:

在本食谱中,我们将探讨如何使用用于监视指定文件的 Play 2.0 插件。我们将初始化我们的插件作为 Play 网络应用程序生命周期的一部分,并且主要的插件逻辑将在应用程序启动时触发。

如何做到这一点……

对于 Java,我们需要执行以下步骤:

  1. 启用 Hot-Reloading 功能运行 foo_java 应用程序:

    <span class="strong"><strong>    activator "~run"</strong></span>
    
  2. foo_java 内创建 modules 目录:

    <span class="strong"><strong>    mkdir modules</strong></span>
    
  3. foo_java/modules 内为我们的第一个插件生成项目目录:

    <span class="strong"><strong>    activator new filemon play-java</strong></span>
    
  4. 移除 modules/filemon/conf/application.conf 文件的内容,因为这些设置将与我们在项目根目录中定义的主要配置文件冲突:

    echo "" &gt; modules/filemon/conf/application.conf
    
  5. 移除 modules/filemon/conf/routes 文件的内容,并将其重命名为 filemon.routes

    echo "" &gt; modules/filemon/conf/routes &amp;&amp; mv modules/filemon/conf/routes modules/filemon/conf/filemon.routes
    
  6. modules/filemon/app 中移除 views 目录:

    <span class="strong"><strong>    rm -rf modules/filemon/app/views</strong></span>
    
  7. 使用以下命令移除 modules/filemon/app/controller/Application.java 文件:

    <span class="strong"><strong>    rm modules/filemon/app/controllers/Application.java</strong></span>
    
  8. modules/filemon/app/ 内创建一个新的包:

    <span class="strong"><strong>    mkdir modules/filemon/app/filemon</strong></span>
    
  9. modules/filemon/app/FileMonitor.java 中创建 FileMonitor 插件,内容如下:

    package filemon;
         import java.io.*;
        import java.util.concurrent.TimeUnit;
        import akka.actor.ActorSystem;
        import play.Plugin;
        import play.Application;
        import play.libs.Akka;
        import scala.concurrent.duration.Duration;
         public class FileMonitor extends Plugin {
            private Application app;
            private ActorSystem actorSystem = ActorSystem.create("filemon");
            private File file = new File("/var/tmp/foo");
             public FileMonitor(Application app) {
                this.app = app;
            }
           @Override
            public void onStart() {
                actorSystem.scheduler().schedule(
                    Duration.create(0, TimeUnit.SECONDS),
                    Duration.create(1, TimeUnit.SECONDS),
                    new Runnable() {
                        public void run() {
                            if (file.exists()) {
                                System.out.println(file.toString() + " exists..");
                            } else {
                                System.out.println(file.toString() + " does not exist..");    
                            }
                        }
                    },
                    Akka.system().dispatcher()
                );
            }
           @Override
            public void onStop() {
                actorSystem.shutdown();
            }
           @Override
            public boolean enabled() {
                return true;
            }
        }
    
  10. 通过创建插件配置文件 foo_java/conf/play.plugins 并在其中声明我们的插件来启用 foo_java 应用程序中的插件:

    echo "1001:filemon.FileMonitor" &gt; conf/play.plugins
    
  11. build.sbt 中添加根项目 (foo_java) 和模块 (filemon) 之间的依赖关系,并添加 aggregate() 设置以确保从项目根目录 foo_java 调用的 activator 任务也会在子模块 filemon 上执行:

    lazy val root = (project in file("."))
          .enablePlugins(PlayJava)
          .aggregate(filemon)
          .dependsOn(filemon)
         lazy val filemon = (project in file("modules/filemon"))
          .enablePlugins(PlayJava)
    
  12. 启动 foo_java 应用程序:

    <span class="strong"><strong>    activator clean "~run"</strong></span>
    
  13. 请求默认路由并初始化我们的应用程序:

    <span class="strong"><strong>    $ curl -v http://localhost:9000</strong></span>
    
  14. 通过查看 foo_java 应用程序的控制台日志来确认文件监视器正在运行:

    <span class="strong"><strong>(Server started, use Ctrl+D to stop and go back to the console...)</strong></span>
     <span class="strong"><strong>[info] Compiling 5 Scala sources and 1 Java source to ...</strong></span>
    <span class="strong"><strong>[success] Compiled in 4s</strong></span>
    <span class="strong"><strong>Starting file mon</strong></span>
    <span class="strong"><strong>/var/tmp/foo not found..</strong></span>
    <span class="strong"><strong>[info] play - Application started (Dev)</strong></span>
    <span class="strong"><strong>/var/tmp/foo not found..</strong></span>
    <span class="strong"><strong>/var/tmp/foo not found..</strong></span>
    <span class="strong"><strong>/var/tmp/foo not found..</strong></span>
    <span class="strong"><strong>/var/tmp/foo not found.. </strong></span>
    

对于 Scala,我们需要执行以下步骤:

  1. 启用 Hot-Reloading 功能运行 foo_scala 应用程序:

    <span class="strong"><strong>    activator "~run"</strong></span>
    
  2. foo_scala内部创建模块目录:

    <span class="strong"><strong>    mkdir modules</strong></span>
    
  3. foo_scala/modules内部为我们的第一个插件生成项目目录:

    <span class="strong"><strong>    activator new filemon play-scala</strong></span>
    
  4. 删除modules/filemon/conf/application.conf文件的内容:

    <span class="strong"><strong>    echo "" &gt; modules/filemon/conf/application.conf</strong></span>
    
  5. 删除modules/filemon/conf/routes文件的内容,并将其重命名为filemon.routes

    echo "" &gt; modules/filemon/conf/routes &amp;&amp; mv modules/filemon/conf/routes  modules/filemon/conf/filemon.routes
    
  6. modules/filemon/app中删除视图目录:

    <span class="strong"><strong>    rm -rf modules/filemon/app/views</strong></span>
    
  7. 使用以下命令删除文件modules/filemon/app/controller/Application.scala

    <span class="strong"><strong>    rm modules/filemon/app/controllers/Application.scala</strong></span>
    
  8. modules/filemon/app/内部创建一个新的包:

    <span class="strong"><strong>    mkdir modules/filemon/app/filemon</strong></span>
    
  9. modules/filemon/app/FileMonitor.scala内部创建FileMonitor插件,内容如下:

    package filemon
         import java.io.File
        import scala.concurrent.duration._
        import akka.actor.ActorSystem
        import play.api.{Plugin, Application}
        import play.api.libs.concurrent.Execution.Implicits._
         class FileMonitor(app: Application) extends Plugin {
          val system = ActorSystem("filemon")
          val file = new File("/var/tmp/foo")
           override def onStart() = {
            println("Starting file mon")
            system.scheduler.schedule(0 second, 1 second) {
              if (file.exists()) {
                println("%s exists..".format(file))
              } else {
                println("%s not found..".format(file))
              }
            }
          }
           override def onStop() = {
            println("Stopping file mon")
            system.shutdown()
          }
           override def enabled = true
        }
    
  10. 通过创建插件配置文件foo_scala/conf/play.plugins并在其中声明我们的插件来从foo_scala应用程序中启用插件:

    <span class="strong"><strong>    echo "1001:filemon.FileMonitor" &gt; conf/play.plugins</strong></span>
    
  11. build.sbt中添加根项目(foo_scala)和模块(filemon)之间的依赖关系,并添加aggregate()设置以确保从项目根foo_java调用的 activator 任务也会在子模块filemon上执行:

    lazy val root = (project in file("."))
          .enablePlugins(PlayScala)
          .aggregate(filemon)
          .dependsOn(filemon)
         lazy val filemon = (project in file("modules/filemon"))
          .enablePlugins(PlayScala)
    
  12. 启动foo_scala应用程序:

    <span class="strong"><strong>    activator clean "~run"</strong></span>
    
  13. 请求我们的默认路由并初始化我们的应用程序:

    <span class="strong"><strong>    $ curl -v http://localhost:9000</strong></span>
    
  14. 通过查看foo_scala应用程序的控制台日志来确认文件监控器正在运行:

    <span class="strong"><strong>(Server started, use Ctrl+D to stop and go back to the console...)</strong></span>
     <span class="strong"><strong>[info] Compiling 5 Scala sources and 1 Java source to ...</strong></span>
    <span class="strong"><strong>[success] Compiled in 4s</strong></span>
    <span class="strong"><strong>Starting file mon</strong></span>
    <span class="strong"><strong>/var/tmp/foo not found..</strong></span>
    <span class="strong"><strong>[info] play - Application started (Dev)</strong></span>
    <span class="strong"><strong>/var/tmp/foo not found..</strong></span>
    <span class="strong"><strong>/var/tmp/foo not found..</strong></span>
    <span class="strong"><strong>/var/tmp/foo not found..</strong></span>
    <span class="strong"><strong>/var/tmp/foo not found.. </strong></span>
    

它是如何工作的...

在这个菜谱中,我们设置了我们的第一个 Play 2.0 插件。该插件简单地检查本地文件系统中是否存在文件或目录,并在控制台日志中记录文件是否被找到。我们通过在项目根模块foo_java/modulesfoo_scala/modules中创建插件项目来设置我们的插件:

<span class="strong"><strong>    $ ls</strong></span>
<span class="strong"><strong>    LICENSE                     conf</strong></span>
<span class="strong"><strong>    README                      logs</strong></span>
<span class="strong"><strong>    activator                   modules</strong></span>
<span class="strong"><strong>    activator-launch-1.2.10.jar project</strong></span>
<span class="strong"><strong>    activator.bat               public</strong></span>
<span class="strong"><strong>    app                         target</strong></span>
<span class="strong"><strong>    build.sbt                   test</strong></span>
 <span class="strong"><strong>    $ ls modules/filemon</strong></span>
<span class="strong"><strong>    LICENSE                     build.sbt</strong></span>
<span class="strong"><strong>    README                      conf</strong></span>
<span class="strong"><strong>    activator                   project</strong></span>
<span class="strong"><strong>    activator-launch-1.2.10.jar public</strong></span>
<span class="strong"><strong>    activator.bat               target</strong></span>
<span class="strong"><strong>    app                         test</strong></span>

一旦创建了插件项目,我们需要删除一些样板文件和配置,以确保插件不会与根项目foo_javafoo_scala冲突:

然后,我们在modules/filemon/app/filemon/FileMonitor.scala中创建了FileMonitor插件,它扩展了play.api.Plugin特质,在启动时创建一个计划任务,该任务每隔一秒检查本地文件是否存在:

override def onStart() = {
      println("Starting file mon")
      system.scheduler.schedule(0 second, 1 second) {
        if (file.exists()) {
          println("%s exists..".format(file))
        } else {
          println("%s not found..".format(file))
        }
      }
    }

一旦我们设置了插件,我们通过在根项目foo_javafoo_scalaconf/play.plugins文件中声明它来激活它,该文件遵循<优先级级别>:<插件>的表示法:

1001:filemon.FileMonitor

在我们的案例中,我们使用了1001作为优先级级别,以确保 Akka Play 2.0 插件首先加载。请参考官方 Play 文档以获取在play.plugins配置文件中声明您的插件的额外指南:

www.playframework.com/documentation/2.3.x/JavaPlugins

www.playframework.com/documentation/2.3.x/ScalaPlugins

最后,我们运行了 Web 应用程序,并通过查看控制台日志来确认我们的插件正在运行:

<span class="strong"><strong>    Starting file mon</strong></span>
<span class="strong"><strong>    /var/tmp/foo not found..</strong></span>

您可以通过创建或删除监控文件来确认您插件的运行行为,在这个例子中,是/var/tmp/foo

<span class="strong"><strong>    # Create foo file then delete after 3 seconds</strong></span>
<span class="strong"><strong>    touch /var/tmp/foo &amp;&amp; sleep 3 &amp;&amp; rm /var/tmp/foo</strong></span>

您将看到日志输出相应地改变:

<span class="strong"><strong>    /var/tmp/foo not found..</strong></span>
<span class="strong"><strong>    /var/tmp/foo not found..</strong></span>
<span class="strong"><strong>    /var/tmp/foo exists..</strong></span>
<span class="strong"><strong>    /var/tmp/foo exists..</strong></span>
<span class="strong"><strong>    /var/tmp/foo exists..</strong></span>
<span class="strong"><strong>    /var/tmp/foo not found..</strong></span>
<span class="strong"><strong>    /var/tmp/foo not found..</strong></span>

构建一个灵活的注册模块

在这个菜谱中,我们将创建一个新的注册模块,该模块将管理用户注册和身份验证请求。创建这样一个模块允许我们在现代 Web 应用程序中重用一个非常常见的流程。

如何做到这一点…

对于 Java,我们需要执行以下步骤:

  1. 使用 Hot-Reloading 启用运行foo_java应用程序:

    <span class="strong"><strong>    activator "~run"</strong></span>
    
  2. 在模块目录内,foo_java/modules中,使用activator生成注册模块项目:

    <span class="strong"><strong>    activator new registration play-java</strong></span>
    
  3. foo_java/build.sbt中添加根项目foo_java和模块registration之间的依赖关系:

    lazy val root = (project in file("."))
          .enablePlugins(PlayJava)
          .aggregate(filemon)
          .dependsOn(filemon)
    <span class="strong"><strong>      .aggregate(registration)</strong></span>
    <span class="strong"><strong>      .dependsOn(registration)</strong></span>
         lazy val filemon = (project in file("modules/filemon"))
          .enablePlugins(PlayJava)
     <span class="strong"><strong>    lazy val registration = (project in file("modules/registration"))</strong></span>
    <span class="strong"><strong>      .enablePlugins(PlayJava)</strong></span>
    
  4. 从注册模块中移除所有不必要的样板文件和配置:

    <span class="strong"><strong>    rm -rf app/views app/controllers</strong></span>
    <span class="strong"><strong>    rm conf/routes</strong></span>
    <span class="strong"><strong>    mkdir app/registration</strong></span>
    <span class="strong"><strong>    echo "" &gt; conf/application.conf</strong></span>
    
  5. module/registration/app/registration/RegistrationPlugin.java中创建以下内容的注册插件:

    package registration;
         import play.Application;
        import play.Plugin;
         public class RegistrationPlugin extends Plugin {
            private Application app;
            private RegistrationService registrationService;
             public RegistrationPlugin(Application app) {
                this.app = app;
            }
           @Override
            public void onStart() {
                registrationService = new RegistrationServiceImpl();
                registrationService.init();
            }
            @Override
            public void onStop() {
                registrationService.shutdown();
            }
           @Override
            public boolean enabled() {
                return true;
            }
             public RegistrationService getRegistrationService() {
                return registrationService;
            }
        }
    
  6. 接下来,创建由Registration插件引用的RegistrationService接口和实现类:

    // In modules/registration/app/registration/RegistrationService.java
         package registration;
         public interface RegistrationService {
            void init();
            void shutdown();
            void create(User user);
            Boolean auth(String username, String password);
        }
         // In modules/registration/app/registration/RegistrationServiceImpl.java    
        package registration;
         import java.util.LinkedHashMap;
        import java.util.Map;
        import java.util.UUID;
         public class RegistrationServiceImpl implements RegistrationService {
            private Map&lt;String, User&gt; registrations;
             @Override
            public void create(User user) {
                final String id = UUID.randomUUID().toString();
                registrations.put(id, new User(id, user.getUsername(), user.getPassword()));
            }
             @Override
            public Boolean auth(String username, String password) {
                for(Map.Entry&lt;String, User&gt; entry : registrations.entrySet()) {
                    if (entry.getValue().getUsername().equals(username) &amp;&amp;
                        entry.getValue().getPassword().equals(password)) {
                        return true;
                    }
                }
                return false;
            }
             @Override
            public void init() {
                registrations = new LinkedHashMap&lt;String, User&gt;();
            }
             @Override
            public void shutdown() {
                registrations.clear();
            }
        }
    
  7. modules/registration/app/registration/User.java中创建User模型实体:

    package registration;
         public class User {
            private String id;
            private String username;
            private String password;
             public User() {}
            public User(String id, String username, String password) {
                this.id = id;
                this.username = username;
                this.password = password;
            }
             public String getId() {
                return id;
            }
            public void setId(String id) {
                this.id = id;
            }
            public String getUsername() {
                return username;
            }
            public void setUsername(String username) {
                this.username = username;
            }
            public String getPassword() {
                return password;
            }
            public void setPassword(String password) {
                this.password = password;
            }
        }
    
  8. 在项目根目录,foo/java/app/controllers/Registrations.java中创建处理注册和登录请求的Registration控制器和路由:

    package controllers;
         import play.Play;
        import play.data.Form;
        import play.mvc.BodyParser;
        import play.mvc.Controller;
        import play.mvc.Result;
        import registration.RegistrationPlugin;
        import registration.RegistrationService;
        import registration.User;
        import static play.libs.Json.toJson;
         public class Registrations extends Controller {
            private static RegistrationService registrationService =          Play.application().plugin(RegistrationPlugin.class).getRegistrationService();
             @BodyParser.Of(BodyParser.Json.class)
            public static Result register() {
                try {
                    Form&lt;User&gt; form = Form.form(User.class).bindFromRequest();
                    User user = form.get();
                    registrationService.create(user);
                    return created(toJson(user));
                } catch (Exception e) {
                    return internalServerError(e.getMessage());
                }
            }
             @BodyParser.Of(BodyParser.Json.class)
            public static Result login() {
                try {
                    Form&lt;User&gt; form = Form.form(User.class).bindFromRequest();
                    User user = form.get();
                    if (registrationService.auth(user.getUsername(), user.getPassword())) {
                        return ok();
                    } else {
                        return forbidden();
                    }
                } catch (Exception e) {
                    return internalServerError(e.getMessage());
                }
            }
        }
    
  9. 在项目根目录,foo_java/conf/routes中添加新添加的Registration动作的路由:

    POST    /register   controllers.Registrations.register
        POST    /auth       controllers.Registrations.login
    
  10. 最后,在项目根目录下的foo_java/conf/play.plugins文件中声明注册插件:

    599:registration.RegistrationPlugin
    
  11. 使用curl,提交新的注册和登录请求,并使用指定的注册详情;通过检查响应头来验证我们的端点对于成功操作返回 HTTP 状态码 200:

    <span class="strong"><strong>    $ curl -v -X POST --header "Content-type: application/json"  http://localhost:9000/register -d '{"username":"ned@flanders.com", "password":"password"}'</strong></span>
    <span class="strong"><strong>    * Hostname was NOT found in DNS cache</strong></span>
    <span class="strong"><strong>    *   Trying ::1...</strong></span>
    <span class="strong"><strong>    * Connected to localhost (::1) port 9000 (#0)</strong></span>
    <span class="strong"><strong>    &gt; POST /register HTTP/1.1</strong></span>
    <span class="strong"><strong>    &gt; User-Agent: curl/7.37.1</strong></span>
    <span class="strong"><strong>    &gt; Host: localhost:9000</strong></span>
    <span class="strong"><strong>    &gt; Accept: */*</strong></span>
    <span class="strong"><strong>    &gt; Content-type: application/json</strong></span>
    <span class="strong"><strong>    &gt; Content-Length: 54</strong></span>
    <span class="strong"><strong>    &gt;</strong></span>
    <span class="strong"><strong>    * upload completely sent off: 54 out of 54 bytes</strong></span>
    <span class="strong"><strong>    &lt; HTTP/1.1 201 Created</strong></span>
    <span class="strong"><strong>    &lt; Content-Type: application/json; charset=utf-8</strong></span>
    <span class="strong"><strong>    &lt; Content-Length: 63</strong></span>
    <span class="strong"><strong>    &lt;</strong></span>
    <span class="strong"><strong>    * Connection #0 to host localhost left intact</strong></span>
    <span class="strong"><strong>    {"id":null,"username":"ned@flanders.com","password":"password"}%</strong></span>
     <span class="strong"><strong>    $ curl -v -X POST --header "Content-type: application/json"  http://localhost:9000/auth -d '{"username":"ned@flanders.com", "password":"password"}'</strong></span>
    <span class="strong"><strong>    * Hostname was NOT found in DNS cache</strong></span>
    <span class="strong"><strong>    *   Trying ::1...</strong></span>
    <span class="strong"><strong>    * Connected to localhost (::1) port 9000 (#0)</strong></span>
    <span class="strong"><strong>    &gt; POST /auth HTTP/1.1</strong></span>
    <span class="strong"><strong>    &gt; User-Agent: curl/7.37.1</strong></span>
    <span class="strong"><strong>    &gt; Host: localhost:9000</strong></span>
    <span class="strong"><strong>    &gt; Accept: */*</strong></span>
    <span class="strong"><strong>    &gt; Content-type: application/json</strong></span>
    <span class="strong"><strong>    &gt; Content-Length: 54</strong></span>
    <span class="strong"><strong>    &gt;</strong></span>
    <span class="strong"><strong>    * upload completely sent off: 54 out of 54 bytes</strong></span>
    <span class="strong"><strong>    &lt; HTTP/1.1 200 OK</strong></span>
    <span class="strong"><strong>    &lt; Content-Length: 0</strong></span>
    <span class="strong"><strong>    &lt;</strong></span>
    <span class="strong"><strong>    * Connection #0 to host localhost left intact</strong></span>
    

对于 Scala,我们需要执行以下步骤:

  1. 使用 Hot-Reloading 启用运行foo_scala应用程序:

    <span class="strong"><strong>    activator "~run"</strong></span>
    
  2. 在模块目录(foo_scala/modules)内,使用activator生成我们的注册模块项目:

    <span class="strong"><strong>    activator new registration play-scala</strong></span>
    
  3. build.sbt中添加根项目foo_scala和模块registration之间的依赖关系:

    lazy val root = (project in file("."))
          .enablePlugins(PlayScala)
          .aggregate(filemon)
          .dependsOn(filemon)
    <span class="strong"><strong>      .aggregate(registration)</strong></span>
    <span class="strong"><strong>      .dependsOn(registration)</strong></span>
         lazy val filemon = (project in file("modules/filemon"))
          .enablePlugins(PlayScala)
     <span class="strong"><strong>    lazy val registration = (project in file("modules/registration"))</strong></span>
    <span class="strong"><strong>      .enablePlugins(PlayScala)</strong></span>
    
  4. 从注册模块中移除所有不必要的样板文件和配置:

    <span class="strong"><strong>    rm -rf app/views app/controllers</strong></span>
    <span class="strong"><strong>    rm conf/routes</strong></span>
    <span class="strong"><strong>    mkdir app/registration</strong></span>
    <span class="strong"><strong>    echo "" &gt; conf/application.conf</strong></span>
    
  5. module/registration/app/registration/RegistrationPlugin.scala中创建以下内容的注册插件:

    package registration
         import play.api.{Application, Plugin}
         class RegistrationPlugin(app: Application) extends Plugin {
          val registrationService = new RegistrationService
           override def onStart() = {
            registrationService.init
          }
           override def onStop() = {
            registrationService.shutdown
          }
           override def enabled = true
        }
    
  6. 接下来,创建由Registration插件引用的RegistrationService类:

    // In modules/registration/app/registration/RegistrationService.scala
         package registration
         import java.util.UUID
         class RegistrationService {
          type ID = String
          private val registrations = scala.collection.mutable.Map[ID, User]()
           def init = {
            registrations.clear()
          }
           def create(user: User) = {
            val id: ID = UUID.randomUUID().toString
            registrations += (id -&gt; user.copy(Some(id), user.username, user.password))
          }
           def auth(username: String, password: String) = registrations.find(_._2.username equals username) match {
            case Some(reg) =&gt; {
              if (reg._2.password equals password) {
                Some(reg._2)
              } else {
                None
              }
            }
            case None =&gt; None
          }
           def shutdown = {
            registrations.clear()
          }
        }
    
  7. modules/registration/app/registration/User.scala中创建User模型实体:

    package registration
         case class User(id: Option[String], username: String, password: String)
    
  8. 在项目根目录,foo_scala/app/controllers/Registrations.scala中创建处理注册和登录请求的Registration控制器和路由:

    package controllers
         import play.api.Play.current
        import play.api.Play
        import play.api.libs.json.{JsError, Json}
        import play.api.mvc.{BodyParsers, Action, Controller}
        import registration.{RegistrationPlugin, User, RegistrationService}
         object Registrations extends Controller {
          implicit private val writes = Json.writes[User]
          implicit private val reads = Json.reads[User]
          private val registrationService: RegistrationService = Play.application.plugin[RegistrationPlugin]
        .getOrElse(throw new IllegalStateException("RegistrationService is required!"))
        .registrationService
           def register = Action(BodyParsers.parse.json) { implicit request =&gt;
            val post = request.body.validate[User]
             post.fold(
              errors =&gt; BadRequest(JsError.toFlatJson(errors)),
              u =&gt; {
                registrationService.create(u)
                Created(Json.toJson(u))
              }
            )
          }
           def login = Action(BodyParsers.parse.json) { implicit request =&gt;
            val login = request.body.validate[User]
             login.fold(
              errors =&gt; BadRequest(JsError.toFlatJson(errors)),
              u =&gt; {
                registrationService.auth(u.username, u.password) match {
                  case Some(user) =&gt; Ok
                  case None =&gt; Forbidden
                }
              }
            )
          }
        }
    
  9. 添加新添加的Registration动作的路由:

    POST    /register   controllers.Registrations.register
        POST    /auth       controllers.Registrations.login
    
  10. 最后,在项目根目录中,foo_scala/conf/play.plugins文件中声明注册插件:

    599:registration.RegistrationPlugin
    
  11. 使用curl,提交新的注册和登录请求,并使用指定的注册详情;通过检查响应头来验证我们的端点对于成功操作返回 HTTP 状态码 200:

    <span class="strong"><strong>    $ curl -v -X POST --header "Content-type: application/json"  http://localhost:9000/register -d '{"username":"ned@flanders.com", "password":"password"}'</strong></span>
    <span class="strong"><strong>    * Hostname was NOT found in DNS cache</strong></span>
    <span class="strong"><strong>    *   Trying ::1...</strong></span>
    <span class="strong"><strong>    * Connected to localhost (::1) port 9000 (#0)</strong></span>
    <span class="strong"><strong>    &gt; POST /register HTTP/1.1</strong></span>
    <span class="strong"><strong>    &gt; User-Agent: curl/7.37.1</strong></span>
    <span class="strong"><strong>    &gt; Host: localhost:9000</strong></span>
    <span class="strong"><strong>    &gt; Accept: */*</strong></span>
    <span class="strong"><strong>    &gt; Content-type: application/json</strong></span>
    <span class="strong"><strong>    &gt; Content-Length: 54</strong></span>
    <span class="strong"><strong>    &gt;</strong></span>
    <span class="strong"><strong>    * upload completely sent off: 54 out of 54 bytes</strong></span>
    <span class="strong"><strong>    &lt; HTTP/1.1 201 Created</strong></span>
    <span class="strong"><strong>    &lt; Content-Type: application/json; charset=utf-8</strong></span>
    <span class="strong"><strong>    &lt; Content-Length: 63</strong></span>
    <span class="strong"><strong>    &lt;</strong></span>
    <span class="strong"><strong>    * Connection #0 to host localhost left intact</strong></span>
    <span class="strong"><strong>    {"id":null,"username":"ned@flanders.com","password":"password"}%</strong></span>
     <span class="strong"><strong>    $ curl -v -X POST --header "Content-type: application/json"  http://localhost:9000/auth -d '{"username":"ned@flanders.com", "password":"password"}'</strong></span>
    <span class="strong"><strong>    * Hostname was NOT found in DNS cache</strong></span>
    <span class="strong"><strong>    *   Trying ::1...</strong></span>
    <span class="strong"><strong>    * Connected to localhost (::1) port 9000 (#0)</strong></span>
    <span class="strong"><strong>    &gt; POST /auth HTTP/1.1</strong></span>
    <span class="strong"><strong>    &gt; User-Agent: curl/7.37.1</strong></span>
    <span class="strong"><strong>    &gt; Host: localhost:9000</strong></span>
    <span class="strong"><strong>    &gt; Accept: */*</strong></span>
    <span class="strong"><strong>    &gt; Content-type: application/json</strong></span>
    <span class="strong"><strong>    &gt; Content-Length: 54</strong></span>
    <span class="strong"><strong>    &gt;</strong></span>
    <span class="strong"><strong>    * upload completely sent off: 54 out of 54 bytes</strong></span>
    <span class="strong"><strong>    &lt; HTTP/1.1 200 OK</strong></span>
    <span class="strong"><strong>    &lt; Content-Length: 0</strong></span>
    <span class="strong"><strong>    &lt;</strong></span>
    <span class="strong"><strong>    * Connection #0 to host localhost left intact</strong></span>
    

它是如何工作的…

在这个配方中,我们创建了一个模块来处理注册函数,例如注册和登录。我们将其创建为一个 Play 插件,以便它不仅易于维护,而且可以在其他应用程序中重用。使用模块的另一个优点是,在编写单元测试时,我们可以在其封闭的子项目中隔离其执行,而不是整个项目。

我们在项目根目录的模块目录内创建了registration插件。我们在build.sbt中声明了模块与主项目之间的依赖关系:

lazy val root = (project in file("."))
      .enablePlugins(PlayScala)
      .aggregate(filemon)
      .dependsOn(filemon)
      .aggregate(registration)
      .dependsOn(registration)

然后,我们在conf/play.plugins中使用优先级599(在 500-600 范围内用于数据相关插件)启用了插件:

599:registration.RegistrationPlugin

然后,我们从控制器中的Registration插件中获取了RegistrationService接口的引用:

// Java
    private static RegistrationService registrationService =
            Play.application().plugin(RegistrationPlugin.class).getRegistrationService();
     // Scala
    private val registrationService: RegistrationService = Play.application.plugin[RegistrationPlugin]
    .getOrElse(throw new IllegalStateException("RegistrationService is required!"))
    .registrationService

一旦建立了引用,所有注册函数都简单地从控制器委托给了RegistrationService接口:

// Java
    registrationService.create(user);
     // Scala
    registrationService.create(u)

使用curl,我们还可以验证我们的注册控制器是否正确响应了错误的认证:

<span class="strong"><strong>    $ curl -v -X POST --header "Content-type: application/json"  http://localhost:9000/auth -d '{"username":"ned@flanders.com", "password":"passwordz"}'</strong></span>
<span class="strong"><strong>    * Hostname was NOT found in DNS cache</strong></span>
<span class="strong"><strong>    *   Trying ::1...</strong></span>
<span class="strong"><strong>    * Connected to localhost (::1) port 9000 (#0)</strong></span>
<span class="strong"><strong>    &gt; POST /auth HTTP/1.1</strong></span>
<span class="strong"><strong>    &gt; User-Agent: curl/7.37.1</strong></span>
<span class="strong"><strong>    &gt; Host: localhost:9000</strong></span>
<span class="strong"><strong>    &gt; Accept: */*</strong></span>
<span class="strong"><strong>    &gt; Content-type: application/json</strong></span>
<span class="strong"><strong>    &gt; Content-Length: 55</strong></span>
<span class="strong"><strong>    &gt;</strong></span>
<span class="strong"><strong>    * upload completely sent off: 55 out of 55 bytes</strong></span>
<span class="strong"><strong>    &lt; HTTP/1.1 403 Forbidden</strong></span>
<span class="strong"><strong>    &lt; Content-Length: 0</strong></span>
<span class="strong"><strong>    &lt;</strong></span>
<span class="strong"><strong>    * Connection #0 to host localhost left intact</strong></span>

使用相同的模型为不同的应用程序

对于这个配方,我们将创建一个新的独立模块,该模块将包含与产品相关的函数和数据模型类,并且我们将它发布到本地仓库中:

如何操作...

对于 Java,我们需要执行以下步骤:

  1. 在与foo_java相同的目录级别中创建一个新的 Play 2 项目:

    <span class="strong"><strong>    activator new product-contrib play-java</strong></span>
    
  2. app/product-contrib/app目录中创建我们的默认模块包:

    <span class="strong"><strong>    mkdir app/productcontrib</strong></span>
    
  3. 创建一个包含所有数据模型类的模型包:

    <span class="strong"><strong>    mkdir app/productcontrib/models</strong></span>
    
  4. 删除conf/application.conf文件的内容:

    echo "" &gt; conf/application.conf
    
  5. 删除conf/routes文件的内容,并将其重命名为productcontrib.routes

    echo "" &gt; conf/routes &amp;&amp; mv conf/routes  modules/filemon/conf/productcontrib.routes
    
  6. modules/filemon/app中删除视图目录:

    <span class="strong"><strong>    rm -rf app/views</strong></span>
    
  7. 删除app/controller/Application.java文件:

    <span class="strong"><strong>    rm app/controllers/Application.java</strong></span>
    
  8. app/productcontrib/models/Product.java中创建产品模型,内容如下:

    package productcontrib.models;
         import java.io.Serializable;
         public class Product implements Serializable {
            private String sku;
            private String title;
            private Double price;
             public String getSku() {
                return sku;
            }
            public void setSku(String sku) {
                this.sku = sku;
            }
            public String getTitle() {
                return title;
            }
            public void setTitle(String title) {
                this.title = title;
            }
            public Double getPrice() {
                return price;
            }
            public void setPrice(Double price) {
                this.price = price;
            }
        }
    
  9. app/productcontrib/services包中创建ProductService接口(ProductService.java)和实现类(ProductServiceImpl.java):

    // ProductService.java
        package productcontrib.services;
         public interface ProductService {
            String generateProductId();
        }
         // ProductServiceImpl.java 
       package productcontrib.services;
         import java.util.UUID;
         public class ProductServiceImpl implements ProductService {
            @Override
            public String generateProductId() {
                return UUID.randomUUID().toString();
            }
        }
    
  10. build.sbt文件中插入额外的模块包设置:

    name := """product-contrib"""
      version := "1.0-SNAPSHOT"
      organization := "foojava"
    
  11. 使用 activator 构建contrib.jar并将其发布到远程内部仓库:

    <span class="strong"><strong>    activator clean publish-local</strong></span>
    
  12. 你应该在控制台日志中能够确认上传是否成功:

    <span class="strong"><strong>    [info]   published product-contrib_2.11 to /.ivy2/local/foojava/product-contrib_2.11/1.0-SNAPSHOT/poms/product-contrib_2.11.pom</strong></span>
    <span class="strong"><strong>    [info]   published product-contrib_2.11 to /.ivy2/local/foojava/product-contrib_2.11/1.0-SNAPSHOT/jars/product-contrib_2.11.jar</strong></span>
    <span class="strong"><strong>    [info]   published product-contrib_2.11 to /.ivy2/local/foojava/product-contrib_2.11/1.0-SNAPSHOT/srcs/product-contrib_2.11-sources.jar</strong></span>
    <span class="strong"><strong>    [info]   published product-contrib_2.11 to /.ivy2/local/foojava/product-contrib_2.11/1.0-SNAPSHOT/docs/product-contrib_2.11-javadoc.jar</strong></span>
    <span class="strong"><strong>    [info]   published ivy to /.ivy2/local/foojava/product-contrib_2.11/1.0-SNAPSHOT/ivys/ivy.xml</strong></span>
    

对于 Scala,我们需要执行以下步骤:

  1. 在与foo_scala相同的目录级别中创建一个新的 Play 2 项目:

    <span class="strong"><strong>    activator new user-contrib play-scala</strong></span>
    
  2. app/user-contrib/app目录中创建默认模块包:

    <span class="strong"><strong>    mkdir app/usercontrib</strong></span>
    
  3. 创建一个包含所有数据模型类的模型包:

    <span class="strong"><strong>    mkdir app/usercontrib/models</strong></span>
    
  4. 删除conf/application.conf文件的内容:

    echo "" &gt; conf/application.conf
    
  5. 删除conf/routes文件的内容,并将其重命名为usercontrib.routes

    echo "" &gt; conf/routes &amp;&amp; mv conf/routes  modules/filemon/conf/usercontrib.routes
    
  6. modules/filemon/app中删除视图目录:

    <span class="strong"><strong>    rm -rf app/views</strong></span>
    
  7. 删除app/controller/Application.scala文件:

    <span class="strong"><strong>    rm app/controllers/Application.scala</strong></span>
    
  8. app/usercontrib/models/User.scala中创建User模型,内容如下:

    package usercontrib.models
         import java.util.UUID
         case class User(id: Option[String], username: String, password: String)
         object User {
          def generateId = UUID.randomUUID().toString
        }
    
  9. build.sbt文件中插入额外的模块包设置:

    name := """product-contrib"""
      version := "1.0-SNAPSHOT"
      organization := "foojava"
    
  10. 使用 activator 构建contrib.jar并将其发布到远程内部仓库:

    <span class="strong"><strong>    activator clean publish-local</strong></span>
    
  11. 您应该能够在控制台日志中确认上传是否成功:

    <span class="strong"><strong>    [info]   published user-contrib_2.11 to ivy/fooscala/user-contrib_2.11/1.0-SNAPSHOT/user-contrib_2.11-1.0-SNAPSHOT.pom</strong></span>
    <span class="strong"><strong>    [info]   published user-contrib_2.11 to ivy/fooscala/user-contrib_2.11/1.0-SNAPSHOT/user-contrib_2.11-1.0-SNAPSHOT.jar</strong></span>
    <span class="strong"><strong>    [info]   published user-contrib_2.11 to ivy/fooscala/user-contrib_2.11/1.0-SNAPSHOT/user-contrib_2.11-1.0-SNAPSHOT-sources.jar</strong></span>
    <span class="strong"><strong>    [info]   published user-contrib_2.11 to ivy/fooscala/user-contrib_2.11/1.0-SNAPSHOT/user-contrib_2.11-1.0-SNAPSHOT-javadoc.jar</strong></span>
    

它是如何工作的...

在这个菜谱中,我们创建了一个新的 Play 2.0 模块,目的是将模块打包并发布到我们的本地仓库。这使得 Play 模块可供我们正在工作的其他 Play web 应用程序使用。我们为将成为我们模块一部分的产品创建了模型和服务类:

# Java 
     $ find app/productcontrib
    app/productcontrib
    app/productcontrib/models
    app/productcontrib/models/Product.java
    app/productcontrib/services
    app/productcontrib/services/ProductService.java
    app/productcontrib/services/ProductServiceImpl.java
     # Scala
  $ find app/usercontrib
    app/usercontrib
    app/usercontrib/models
    app/usercontrib/models/User.scala

我们使用activator的发布命令构建并发布了这两个模块到内部仓库:

<span class="strong"><strong>    activator clean publish-local</strong></span>

一旦这些模块在内部仓库中发布,我们就将它们声明为基于 Maven 的 Java 项目的依赖项,不仅限于 Play 2.0 应用程序,在我们的例子中是build.sbt

// Java 
    "foojava" %% "product-contrib" % "1.0-SNAPSHOT"
     // Scala
    "fooscala" %% "user-contrib" % "1.0-SNAPSHOT"

管理模块依赖项

在这个菜谱中,我们将解决将 Play 模块添加到您的 Play 2.0 应用程序中的问题,这进一步展示了 Play 2.0 生态系统的强大。这个菜谱需要运行之前的菜谱,并假设您已经继续执行。

如何做到这一点...

对于 Java,我们需要执行以下步骤:

  1. 启用热重载功能运行foo_java应用程序:

    activator "~run"
    
  2. build.sbt中将fooscala user-contrib模块添加为项目依赖:

    "fooscala" %% "user-contrib" % "1.0-SNAPSHOT",
        "foojava" %% "product-contrib" % "1.0-SNAPSHOT"
    
  3. 通过添加以下操作修改foo_java/app/controllers/Application.java

    // Add the required imports  
        import productcontrib.services.ProductService;
        import productcontrib.services.ProductServiceImpl;
        import usercontrib.models.User;
         // Add the necessary Action methods and helper
        private static ProductService productService = new ProductServiceImpl();
         public static Result generateProductId() {
            return ok("Your generated product id: " + productService.generateProductId());
        }
         public static Result generateUserId() {
            return ok("Your generated product id: " + User.generateId());
        }
    
  4. foo_java/conf/routes中为新增的操作添加一个新的路由:

    GET     /generate-product-id       controllers.Application.generateProductId()
        GET     /generate-user-id          controllers.Application.generateUserId()
    
  5. 使用curl,我们将能够显示由产品和用户贡献模块生成的产品和用户 Ids:

    <span class="strong"><strong>    $ curl http://localhost:9000/generate-product-id</strong></span>
    <span class="strong"><strong>Your generated product id: 3acd3f36-6ee6-45ce-af07-faa257724b1e%                </strong></span>
    <span class="strong"><strong>    $ curl http://localhost:9000/generate-user-id</strong></span>
    <span class="strong"><strong>Your generated product id: ffca654e-35d8-48cd-9acd-9ea9fe567ba7%</strong></span>
    

对于 Scala,我们需要执行以下步骤:

  1. 启用热重载功能运行foo_scala应用程序:

    <span class="strong"><strong>    activator "~run"</strong></span>
    
  2. build.sbt中将foojava productcontrib模块添加为项目依赖:

    "foojava" %% "product-contrib" % "1.0-SNAPSHOT",
      "fooscala" %% "user-contrib" % "1.0-SNAPSHOT"
    
  3. 通过添加以下操作修改foo_scala/app/controllers/Application.scala

    import productcontrib.services.{ProductServiceImpl, ProductService}
        import usercontrib.models.User
         def productService: ProductService = new ProductServiceImpl
         def generateProductId = Action {
          Ok("Your generated product id: " + productService.generateProductId())
        }
         def generateUserId = Action {
          Ok("Your generated product id: " + User.generateId);
        }
    
  4. foo_scala/conf/routes中为新增的操作添加一个新的路由:

    GET     /generate-product-id       controllers.Application.generateProductId()
        GET     /generate-user-id          controllers.Application.generateUserId()
    
  5. 使用curl,我们将能够显示由产品和用户贡献模块生成的产品和用户 Ids:

    <span class="strong"><strong>    $ curl http://localhost:9000/generate-product-id</strong></span>
    <span class="strong"><strong>Your generated product id: 3acd3f36-6ee6-45ce-af07-faa257724b1e%                </strong></span>
    <span class="strong"><strong>    $ curl http://localhost:9000/generate-user-id</strong></span>
    <span class="strong"><strong>Your generated product id: ffca654e-35d8-48cd-9acd-9ea9fe567ba7%</strong></span>
    

它是如何工作的...

在这个菜谱中,我们探讨了如何将其他模块包含到我们的 Play 2.0 web 应用程序中。通过这个菜谱,我们还展示了 Play Scala 应用程序如何与 Play Java 模块协同工作,反之亦然。

我们首先在build.sbt中声明我们的根项目将使用用户和产品贡献模块:

libraryDependencies ++= Seq(
      "foojava" %% "product-contrib" % "1.0-SNAPSHOT",
      "fooscala" %% "user-contrib" % "1.0-SNAPSHOT"
    )

然后,我们在控制器中添加了导入语句,以便我们可以调用它们的 ID 生成函数:

// Java
    import productcontrib.services.ProductService;
    import productcontrib.services.ProductServiceImpl;
    import usercontrib.models.User;
     // Invoke the contrib functions in foo_java
    return ok("Your generated product id: " + productService.generateProductId());
    return ok("Your generated product id: " + User.generateId());

     // Scala
    import productcontrib.services.{ProductServiceImpl, ProductService}
    import usercontrib.models.User
     // Invoke the contrib functions in foo_scala:
    Ok("Your generated product id: " + productService.generateProductId())
    Ok("Your generated product id: " + User.generateId);

最后,我们使用curl请求我们的新路由,以查看生成的 Ids 在实际中的应用。

使用 Amazon S3 添加私有模块仓库

在这个菜谱中,我们将探讨如何使用外部模块存储库来发布和解析内部模块,以利于我们模块的分布。在这个菜谱中,我们将使用 Amazon S3,这是一个流行的云存储服务,来存储我们的 ivy 风格存储库资产。您需要有效的 AWS 账户来遵循这个菜谱,确保您在aws.amazon.com/上注册了一个账户。

请参阅 S3 的在线文档以获取更多信息:

aws.amazon.com/s3/

如何操作…

我们需要执行以下步骤:

  1. 打开product-contrib项目,并启用热重载功能运行应用程序:

    <span class="strong"><strong>    activator "~run"</strong></span>
    
  2. project/plugins.sbt文件中编辑插件配置文件,并添加以下插件和解析器:

    resolvers += "Era7 maven releases" at "http://releases.era7.com.s3.amazonaws.com"
         addSbtPlugin("ohnosequences" % "sbt-s3-resolver" % "0.12.0")
    
  3. build.sbt文件中编辑构建配置文件,以指定我们将用于 S3 解析器插件的设置:

    S3Resolver.defaults
         s3credentials := file(System.getProperty("user.home")) / ".sbt" / ".s3credentials"
         publishMavenStyle := false
         publishTo := {
          val prefix = if (isSnapshot.value) "snapshots" else "releases"
          Some(s3resolver.value(prefix+" S3 bucket",     s3(prefix+".YOUR-S3-BUCKET-NAME-HERE.amazonaws.com")) withIvyPatterns)
        }
    
  4. 在文件~/.sbt/.s3credentials中指定您的 Amazon S3 API 密钥:

    accessKey = &lt;YOUR S3 API ACCESS KEY&gt;
        secretKey = &lt;YOUR S3 API SECRET KEY&gt;
    
  5. 接下来,使用 activator 发布product-contrib快照:

    <span class="strong"><strong>    activator clean publish</strong></span>
    
  6. 您将在控制台日志中看到上传的成功状态消息:

    <span class="strong"><strong>    [info]   published ivy to s3://snapshots.XXX.XXX.amazonaws.com/foojava/product-contrib_2.11/1.0-SNAPSHOT/ivys/ivy.xml</strong></span>
    <span class="strong"><strong>    [success] Total time: 58 s, completed 02 3, 15 9:39:44 PM</strong></span>
    
  7. 我们现在将在新的 Play 2.0 应用程序中使用此存储库:

    <span class="strong"><strong>    activator new s3deps play-scala</strong></span>
    
  8. s3deps/project/plugins.sbt文件中编辑插件配置文件,并添加以下插件和解析器:

    resolvers += "Era7 maven releases" at "http://releases.era7.com.s3.amazonaws.com"
        addSbtPlugin("ohnosequences" % "sbt-s3-resolver" % "0.12.0")
    
  9. build.sbt文件中编辑构建配置文件,以指定我们将用于 S3 解析器插件的设置:

    S3Resolver.defaults
         resolvers ++= SeqResolver) withIvyPatterns
        )
         libraryDependencies ++= Seq(
          "foojava" %% "product-contrib" % "1.0-SNAPSHOT"
        )
    
  10. 使用 activator 检索 product-contrib 模块:

    activator clean dependencies
       ...
        com.typesafe.play:play-java-ws_2.11:2.3.7
        com.typesafe.play:play-java-jdbc_2.11:2.3.7
        foojava:product-contrib_2.11:1.0-SNAPSHOT
    

它是如何工作的…

在这个菜谱中,我们使用了 sbt-s3-resolver 插件,通过 Amazon S3 发布和解析依赖项。我们在project/plugins.sbt文件中包含了sbt插件:

resolvers += "Era7 maven releases" at "http://releases.era7.com.s3.amazonaws.com"
     addSbtPlugin("ohnosequences" % "sbt-s3-resolver" % "0.12.0")

我们在~/.sbt 目录中的.s3credentials文件中指定我们的 Amazon S3 API 密钥:

accessKey = &lt;ACCESS KEY&gt;
    secretKey = &lt;SECRET KEY&gt;

对于发布,我们在发布项目的build.sbt中指定解析器存储库:

S3Resolver.defaults
     s3credentials := file(System.getProperty("user.home")) / ".sbt" / ".s3credentials"
     publishMavenStyle := false
     publishTo := {
      val prefix = if (isSnapshot.value) "snapshots" else "releases"
      Some(s3resolver.value(prefix+" S3 bucket",     s3(prefix+".achiiva.devint.amazonaws.com")) withIvyPatterns)
    }

要解析依赖项,我们在消费项目的build.sbt中指定以下内容(s3deps):

S3Resolver.defaults
     resolvers ++= SeqResolver) withIvyPatterns
    )
     libraryDependencies ++= Seq(
      "foojava" %% "product-contrib" % "1.0-SNAPSHOT"
    )

第六章. 实际模块示例

在本章中,我们将涵盖以下食谱:

  • 将 Play 应用程序与消息队列集成

  • 将 Play 应用程序与 ElasticSearch 集成

  • 使用 JWT 实现令牌认证

简介

在本章中,我们将进一步探讨将 Play 应用程序与现代网络应用程序的必要服务和工具集成。具体来说,我们将探讨如何使用 Play 插件集成外部消息队列服务。我们将使用流行的云服务 IronMQ

我们还将探讨如何使用 ElasticSearchDocker 将全文搜索引擎服务与 Play 应用程序集成。

最后,我们将实现自己的 Play 包装器,用于使用 JSON Web TokensJWT)进行令牌认证的集成。

将 Play 应用程序与消息队列集成

在这个食谱中,我们将探讨如何使用 Play 2.0 集成 IronMQ,这是一个流行的云消息队列服务。我们将使用 IronMQ 的 Java 库,可以在以下位置找到:

github.com/iron-io/iron_mq_java

我们将使用一个 Play 插件来初始化我们的 IronMQ 客户端和队列对象,并公开发送和检索消息的辅助方法。然后,我们将在这个 Play 控制器中使用此插件,允许客户端使用 HTTP 方法 GET 发布和检索消息。

如何做到这一点...

对于 Java,我们需要执行以下步骤:

  1. 以启用热重载的方式运行 foo_java 应用程序:

    <span class="strong"><strong>    activator "~run"</strong></span>
    
  2. www.iron.io/ 上创建 IronMQ 账户并创建 IronMQ 项目;记下您的项目 ID 和令牌

  3. build.sbt 中将官方 IronMQ Java 库作为应用程序依赖项导入:

    libraryDependencies ++= Seq(
          "io.iron.ironmq" % "ironmq" % "0.0.19"
        )
    
  4. foo_java/app 内创建插件包:

    <span class="strong"><strong>    mkdir plugins</strong></span>
    
  5. foo_java/app/plugins 目录中创建我们的插件类,MQPlugin:

    package plugins;
         import io.iron.ironmq.Client;
        import io.iron.ironmq.Message;
        import io.iron.ironmq.Messages;
        import io.iron.ironmq.Queue;
        import play.Application;
        import play.Logger;
        import play.Plugin;
        import java.util.UUID;
         public class MQPlugin extends Plugin {
            final private Integer messageSize = 10;
            private Client client;
            private Queue queue;
             public MQPlugin(Application app) {
                super();
                client = new Client(
                    app.configuration().getString("mq.projectId"),
                    app.configuration().getString("mq.token")
                );
            }
             public void onStart() {
                queue = client.queue(UUID.randomUUID().toString());
            }
             public void onStop() {
                try {
                    queue.clear();
                    queue.destroy();
                    client = null;
                } catch(Exception e) {
                    Logger.error(e.toString());
                }
            }
             public void send(String msg) throws Exception {
                queue.push(msg);
            }
             public Message[] retrieve() throws Exception {
                Messages messages = queue.get(messageSize);
                if (messages.getSize() &gt; 0) {
                    Message[] msgArray = messages.getMessages();
                     for(Message m : msgArray) {
                        queue.deleteMessage(m);
                    }
                     return msgArray;
                }
                 return new Message[] {};
            }
             public boolean enabled() {
                return true;
            }
        }
    
  6. 修改 conf/application.conf 并添加您的 IronMQ 项目 ID 和令牌:

    mq.projectId="YOUR PROJECT ID"
        mq.token="YOUR TOKEN"
    
  7. 通过在 conf/play.plugins 文件中声明来初始化 MQPlugin:

    599:plugins.MQPlugin
    
  8. app/controllers 中创建 Messenger 控制器类:

    package controllers;
         import play.Logger;
        import play.Play;
        import play.data.Form;
        import play.mvc.BodyParser;
        import play.mvc.Controller;
        import play.mvc.Result;
        import plugins.MQPlugin;
        import java.util.HashMap;
        import java.util.Map;
         import static play.libs.Json.toJson;
         public class Messenger extends Controller {
            private static MQPlugin mqPlugin = Play.application().plugin(MQPlugin.class);
             @BodyParser.Of(BodyParser.Json.class)
            public static Result sendMessage() {
                try {
                    Form&lt;Message&gt; form = Form.form(Message.class).bindFromRequest();
                    Message m = form.get();
                    mqPlugin.send(m.getBody());
                     Map&lt;String, String&gt; map = new HashMap&lt;&gt;();
                    map.put("status", "Message sent.");
                    return created(toJson(map));
                 } catch (Exception e) {
                    return internalServerError(e.getMessage());
                }
            }
             public static Result getMessages() {
                try {
                    return ok(toJson(mqPlugin.retrieve()));
                } catch (Exception e) {
                    Logger.error(e.toString());
                    return internalServerError();
                }
            }
        }
    
  9. Messenger 控制器操作添加新路由:

    POST    /messages/send       controllers.Messenger.sendMessage
        GET     /messages            controllers.Messenger.getMessages
    
  10. 使用 curl 发送和检索消息:

    <span class="strong"><strong>    $ curl -v -X POST http://localhost:9000/messages/send --header "Content-type: application/json" -d '{"body":"Her mouth the mischief he doth seek"}'</strong></span>
    <span class="strong"><strong>    * Hostname was NOT found in DNS cache</strong></span>
    <span class="strong"><strong>    *   Trying ::1...</strong></span>
    <span class="strong"><strong>    * Connected to localhost (::1) port 9000 (#0)</strong></span>
    <span class="strong"><strong>    &gt; POST /messages/send HTTP/1.1</strong></span>
    <span class="strong"><strong>    &gt; User-Agent: curl/7.37.1</strong></span>
    <span class="strong"><strong>    &gt; Host: localhost:9000</strong></span>
    <span class="strong"><strong>    &gt; Accept: */*</strong></span>
    <span class="strong"><strong>    &gt; Content-type: application/json</strong></span>
    <span class="strong"><strong>    &gt; Content-Length: 46</strong></span>
    <span class="strong"><strong>    &gt;</strong></span>
    <span class="strong"><strong>    * upload completely sent off: 46 out of 46 bytes</strong></span>
    <span class="strong"><strong>    &lt; HTTP/1.1 201 Created</strong></span>
    <span class="strong"><strong>    &lt; Content-Type: application/json; charset=utf-8</strong></span>
    <span class="strong"><strong>    &lt; Content-Length: 26</strong></span>
    <span class="strong"><strong>    &lt;</strong></span>
    <span class="strong"><strong>    * Connection #0 to host localhost left intact</strong></span>
    <span class="strong"><strong>    {"status":"Message sent."}%</strong></span>
     <span class="strong"><strong>    $ curl -v http://localhost:9000/messages</strong></span>
    <span class="strong"><strong>    * Hostname was NOT found in DNS cache</strong></span>
    <span class="strong"><strong>    *   Trying ::1...</strong></span>
    <span class="strong"><strong>    * Connected to localhost (::1) port 9000 (#0)</strong></span>
    <span class="strong"><strong>    &gt; GET /messages HTTP/1.1</strong></span>
    <span class="strong"><strong>    &gt; User-Agent: curl/7.37.1</strong></span>
    <span class="strong"><strong>    &gt; Host: localhost:9000</strong></span>
    <span class="strong"><strong>    &gt; Accept: */*</strong></span>
    <span class="strong"><strong>    &gt;</strong></span>
    <span class="strong"><strong>    &lt; HTTP/1.1 200 OK</strong></span>
    <span class="strong"><strong>    &lt; Content-Type: application/json; charset=utf-8</strong></span>
    <span class="strong"><strong>    &lt; Content-Length: 95</strong></span>
    <span class="strong"><strong>    &lt;</strong></span>
    <span class="strong"><strong>    * Connection #0 to host localhost left intact</strong></span>
    <span class="strong"><strong>    {"messages":["Her mouth the mischief he doth seek","Her heart the captive of which he speaks"]}%</strong></span>
    
  11. 在 IronMQ 网络控制台中,您还可以确认队列消息大小,以确认我们能够发布消息!图片

对于 Scala,我们需要执行以下步骤:

  1. 以启用热重载的方式运行 foo_scala 应用程序:

    <span class="strong"><strong>    activator "~run"</strong></span>
    
  2. www.iron.io/ 上创建 IronMQ 账户并创建 IronMQ 项目;记下您的项目 ID 和令牌。

  3. build.sbt 中将官方 IronMQ Java 库作为应用程序依赖项导入:

    libraryDependencies ++= Seq(
          "io.iron.ironmq" % "ironmq" % "0.0.19"
        )
    
  4. foo_scala/app 中创建插件包:

    <span class="strong"><strong>    mkdir plugins</strong></span>
    
  5. foo_scala/app/plugins 目录中创建我们的插件类,MQPlugin:

    package plugins
         import java.util.UUID
        import io.iron.ironmq.{Client, Queue}
        import play.api.Play.current
        import play.api.{Application, Play, Plugin}
        import play.api.libs.concurrent.Execution.Implicits._
        import scala.concurrent.Future
         class MQPlugin(app: Application) extends Plugin {
          private val messageSize = 10
          private var client: Client = null
          private var queue: Queue = null
           override def onStart() = {
            client = new Client(
              Play.configuration.getString("mq.projectId").get,
              Play.configuration.getString("mq.token").get
            )
            queue = client.queue(UUID.randomUUID().toString)
          }
           override def onStop() = {
            queue.clear()
            queue.destroy()
            client = null
          }
           def send(msg: String) = queue.push(msg)
           def retrieve = {
            val list = queue.get(messageSize)
            Future {
              list.getMessages.map(queue.deleteMessage(_))
            }
            list.getMessages.map(_.getBody)
          }
        override def enabled = true
        }
    
  6. 修改 conf/application.conf 并添加您的 IronMQ 项目 ID 和令牌:

    mq.projectId="YOUR PROJECT ID"
        mq.token="YOUR TOKEN"
    
  7. 通过在 conf/play.plugins 文件中声明来初始化 MQPlugin:

    599:plugins.MQPlugin
    
  8. app/controllers 中创建 Messenger 控制器类:

    package controllers
         import play.api.Play.current
        import play.api.Play
        import play.api.libs.json.{JsError, Json}
        import play.api.mvc.{BodyParsers, Action, Controller}
        import plugins.MQPlugin
         case class Message(body: String)
         object Messenger extends Controller {
          implicit private val writes = Json.writes[Message]
          implicit private val reads = Json.reads[Message]
          private val mqPlugin = Play.application.plugin[MQPlugin].get
           def sendMessage = Action(BodyParsers.parse.json) { implicit request =&gt;
            val post = request.body.validate[Message]
             post.fold(
              errors =&gt; BadRequest(JsError.toFlatJson(errors)),
              p =&gt; {
                mqPlugin.send(p.body)
                Created(Json.obj("status" -&gt; "Message sent."))
              }
            )
          }
           def getMessages = Action {
            Ok(Json.obj("messages" -&gt; mqPlugin.retrieve))
          }
        }
    
  9. Messenger控制器动作添加新路由:

    POST    /messages/send       controllers.Messenger.sendMessage
        GET     /messages            controllers.Messenger.getMessages
    
  10. 使用curl发送和检索消息:

    <span class="strong"><strong>    $ curl -v -X POST http://localhost:9000/messages/send --header "Content-type: application/json" -d '{"body":"Her mouth the mischief he doth seek"}'</strong></span>
    <span class="strong"><strong>    * Hostname was NOT found in DNS cache</strong></span>
    <span class="strong"><strong>    *   Trying ::1...</strong></span>
    <span class="strong"><strong>    * Connected to localhost (::1) port 9000 (#0)</strong></span>
    <span class="strong"><strong>    &gt; POST /messages/send HTTP/1.1</strong></span>
    <span class="strong"><strong>    &gt; User-Agent: curl/7.37.1</strong></span>
    <span class="strong"><strong>    &gt; Host: localhost:9000</strong></span>
    <span class="strong"><strong>    &gt; Accept: */*</strong></span>
    <span class="strong"><strong>    &gt; Content-type: application/json</strong></span>
    <span class="strong"><strong>    &gt; Content-Length: 46</strong></span>
    <span class="strong"><strong>    &gt;</strong></span>
    <span class="strong"><strong>    * upload completely sent off: 46 out of 46 bytes</strong></span>
    <span class="strong"><strong>    &lt; HTTP/1.1 201 Created</strong></span>
    <span class="strong"><strong>    &lt; Content-Type: application/json; charset=utf-8</strong></span>
    <span class="strong"><strong>    &lt; Content-Length: 26</strong></span>
    <span class="strong"><strong>    &lt;</strong></span>
    <span class="strong"><strong>    * Connection #0 to host localhost left intact</strong></span>
    <span class="strong"><strong>    {"status":"Message sent."}%</strong></span>
     <span class="strong"><strong>    $ curl -v http://localhost:9000/messages</strong></span>
    <span class="strong"><strong>    * Hostname was NOT found in DNS cache</strong></span>
    <span class="strong"><strong>    *   Trying ::1...</strong></span>
    <span class="strong"><strong>    * Connected to localhost (::1) port 9000 (#0)</strong></span>
    <span class="strong"><strong>    &gt; GET /messages HTTP/1.1</strong></span>
    <span class="strong"><strong>    &gt; User-Agent: curl/7.37.1</strong></span>
    <span class="strong"><strong>    &gt; Host: localhost:9000</strong></span>
    <span class="strong"><strong>    &gt; Accept: */*</strong></span>
    <span class="strong"><strong>    &gt;</strong></span>
    <span class="strong"><strong>    &lt; HTTP/1.1 200 OK</strong></span>
    <span class="strong"><strong>    &lt; Content-Type: application/json; charset=utf-8</strong></span>
    <span class="strong"><strong>    &lt; Content-Length: 95</strong></span>
    <span class="strong"><strong>    &lt;</strong></span>
    <span class="strong"><strong>    * Connection #0 to host localhost left intact</strong></span>
    <span class="strong"><strong>    {"messages":["Her mouth the mischief he doth seek","Her heart the captive of which he speaks"]}%</strong></span>
    
  11. 在 IronMQ Web 控制台中,您还可以检查队列消息大小以确认我们能够发布消息:图片

它是如何工作的…

在此配方中,我们首先通过将官方 IronMQ Java 库导入到build.sbt中来设置消息队列插件。我们还需要登录 IronMQ 来创建我们的 IronMQ 项目和检索我们的项目令牌:

图片

我们通过将 IronMQ 凭据添加到conf/application.conf来配置 Play 应用程序:

mq.projectId="YOUR PROJECT ID"
    mq.token="YOUR TOKEN"

然后,我们通过从配置文件中检索项目 ID 和令牌并将它们传递给io.iron.ironmq.Client实例来实现MQPlugin类:

// Java 
    public MQPlugin(Application app) {
        super();
        client = new Client(
            app.configuration().getString("mq.projectId"),
            app.configuration().getString("mq.token")
        );
    }
     // Scala
    override def onStart() = {
      client = new Client(
        Play.configuration.getString("mq.projectId").get,
        Play.configuration.getString("mq.token").get
      )
    }

我们在onStart方法中创建了我们的动态消息队列,并将UUID参数作为默认队列名称传递:

// Java 
    public void onStart() {
        queue = client.queue(UUID.randomUUID().toString());
    }
     // Scala
    queue = client.queue(UUID.randomUUID().toString)

然后,我们声明了两个将促进消息发送和检索的方法:

// Java
    public void send(String msg) throws Exception {
        queue.push(msg);
    }
     public Message[] retrieve() throws Exception {
        Messages messages = queue.get(messageSize);
        if (messages.getSize() &gt; 0) {
            Message[] msgArray = messages.getMessages();
             for(Message m : msgArray) {
                queue.deleteMessage(m);
            }
             return msgArray;
        }
         return new Message[] {};
    }    
     // Scala
    def send(msg: String) = queue.push(msg)
     def retrieve = {
      val list = queue.get(messageSize)
      Future {
        list.getMessages.map(queue.deleteMessage(_))
      }
      list.getMessages.map(_.getBody)
    }

我们希望能够从队列中删除已读消息并在此处运行一个异步函数来删除它们:

// Scala
    import play.api.libs.concurrent.Execution.Implicits._
    import scala.concurrent.Future
     Future {
      list.getMessages.map(queue.deleteMessage(_))
    }

对于 Java,删除消息将同步发生:

for(Message m : msgArray) {
      queue.deleteMessage(m);
    }

最后,我们通过创建控制器类Messenger来实现端点,该类公开两个动作;一个用于检索消息,另一个用于发布消息:

// Java 
    private static MQPlugin mqPlugin = Play.application().plugin(MQPlugin.class);
     @BodyParser.Of(BodyParser.Json.class)
    public static Result sendMessage() {
        try {
            Form&lt;Message&gt; form = Form.form(Message.class).bindFromRequest();
            Message m = form.get();
            mqPlugin.send(m.getBody());
             Map&lt;String, String&gt; map = new HashMap&lt;&gt;();
            map.put("status", "Message sent.");
            return created(toJson(map));
         } catch (Exception e) {
            return internalServerError(e.getMessage());
        }
    }
     public static Result getMessages() {
        try {
            return ok(toJson(mqPlugin.retrieve()));
        } catch (Exception e) {
            Logger.error(e.toString());
            return internalServerError();
        }
    }    
    // Scala
    implicit private val writes = Json.writes[Message]
    implicit private val reads = Json.reads[Message]
    private val mqPlugin = Play.application.plugin[MQPlugin].get
     def sendMessage = Action(BodyParsers.parse.json) { implicit request =&gt;
      val post = request.body.validate[Message]
       post.fold(
        errors =&gt; BadRequest(JsError.toFlatJson(errors)),
        p =&gt; {
          mqPlugin.send(p.body)
          Created(Json.obj("status" -&gt; "Message sent."))
        }
      )
    }
     def getMessages = Action {
      Ok(Json.obj("messages" -&gt; mqPlugin.retrieve))
    }

最后,将相应的路由添加到conf/routes

POST    /messages/send       controllers.Messenger.sendMessage
    GET     /messages            controllers.Messenger.getMessages

将 Play 应用程序与 ElasticSearch 集成

在此配方中,我们将创建一个非常常见的 Web 应用程序功能,用于创建、索引和搜索,在我们的案例中是产品。我们将使用 ElasticSearch 作为我们的搜索服务。我们将使用 Docker 创建我们的本地 ElasticSearch 容器并运行所有搜索操作。

此配方的先决条件是能够访问 ElasticSearch 实例,无论是本地还是远程,在我们的配方中,以及在本地的开发机器上安装 Docker:

<span class="strong"><strong>    $ docker -v</strong></span>
<span class="strong"><strong>    Docker version 1.3.3, build d344625</strong></span>

我们使用 Docker 使用以下命令部署我们的本地 ElasticSearch 实例:

<span class="strong"><strong>    docker run -d -p 9200:9200 -p 9300:9300 dockerfile/elasticsearch</strong></span>

此前命令指示docker以分离服务运行elasticsearch容器,并且容器中的端口92009300应可以从主机中的相应端口访问。

我们将使用一个开源的 Play 模块,play2-elasticsearch,来包装我们对 ElasticSearch 实例的调用。此配方假设对 Docker 和全文搜索服务有一定的了解。有关 play2-elasticsearch 的更多信息,请参阅github.com/cleverage/play2-elasticsearch

注意

有关 Docker 及其安装的更多信息,请参阅他们的在线文档docs.docker.com/installation/

如何做到这一点…

对于 Java,我们需要执行以下步骤:

  1. 首先,让我们使用 Docker 启动一个本地的 ElasticSearch 容器:

    <span class="strong"><strong>    $ docker run -d -p 9200:9200 -p 9300:9300 dockerfile/elasticsearch</strong></span>
    
  2. 使用启用热重载的foo_java应用程序:

    <span class="strong"><strong>    activator "~run"</strong></span>
    
  3. build.sbt中添加 play2-elasticsearch 依赖。需要注意的是,截至编写本文时,play2-elasticsearch 尚未发布对 Play 2.3.x 的支持,因此需要排除旧版 Play 库的依赖:

    resolvers += "Sonatype OSS Snapshots" at "https://oss.sonatype.org/content/repositories/snapshots"
         libraryDependencies ++= Seq(
          ("com.clever-age" % "play2-elasticsearch" % "1.4-SNAPSHOT")
            .exclude("com.typesafe.play", "play-functional_2.10")
            .exclude("com.typesafe.akka", "akka-actor_2.10")
            .exclude("com.typesafe.play", "play-json_2.10")
            .exclude("com.typesafe.play", "play_2.10")
            .exclude("com.typesafe.play", "play-iteratees_2.10")
            .exclude("com.typesafe.akka", "akka-slf4j_2.10")
            .exclude("org.scala-stm", "scala-stm_2.10")
            .exclude("com.typesafe.play", "play-datacommons_2.10")
         .exclude("com.typesafe.play", "play-java_2.10")
        )
    
  4. conf/play.plugins中声明 play2-elasticsearch 插件:

    9000:com.github.cleverage.elasticsearch.plugin.IndexPlugin
    
  5. 将 play2-elasticsearch 配置参数添加到conf/application.conf

    elasticsearch.local=false
        elasticsearch.client="&lt;YOUR ELASTIC SEARCH HOST HERE&gt;:9300"
        elasticsearch.sniff=false
        elasticsearch.index.name="test"
        elasticsearch.index.settings="{ analysis: { analyzer: { my_analyzer: { type: \"custom\", tokenizer: \"standard\" } } } }"
        elasticsearch.index.clazzs="models.*"
        elasticsearch.index.show_request=true
        elasticsearch.cluster.name=elasticsearch
    
  6. app/models/Product.java中创建产品模型:

    package models;
         import com.github.cleverage.elasticsearch.Index;
        import com.github.cleverage.elasticsearch.IndexQuery;
        import com.github.cleverage.elasticsearch.IndexResults;
        import com.github.cleverage.elasticsearch.Indexable;
        import com.github.cleverage.elasticsearch.annotations.IndexType;
        import org.elasticsearch.index.query.QueryBuilders;
        import java.util.HashMap;
        import java.util.Map;
         @IndexType(name = "product")
        public class Product extends Index {
            private String id;
            private String title;
            private String shortDesc;
             public Product() {}
             public Product(String id, String title, String shortDesc) {
                this.id = id;
                this.title = title;
                this.shortDesc = shortDesc;
            }
             public String getId() {
                return id;
            }
             public void setId(String id) {
                this.id = id;
            }
             public String getTitle() {
                return title;
            }
             public void setTitle(String title) {
                this.title = title;
            }
             public String getShortDesc() {
                return shortDesc;
            }
             public void setShortDesc(String shortDesc) {
                this.shortDesc = shortDesc;
            }
             public static Finder&lt;Product&gt; find = new Finder&lt;&gt;(Product.class);
             @Override
            public Map toIndex() {
                Map&lt;String, Object&gt; map = new HashMap&lt;&gt;();
                map.put("id", this.id);
                map.put("title", this.title);
                map.put("description", this.getShortDesc());
                return map;
            }
             @Override
            public Indexable fromIndex(Map map) {
                Product p = new Product();
                p.setId((String) map.get("id"));
                p.setTitle((String) map.get("title"));
                p.setShortDesc((String) map.get("description"));
                return p;
            }
             public static IndexResults&lt;Product&gt; doSearch(String keyword) {
                IndexQuery&lt;Product&gt; indexQuery = Product.find.query();
                indexQuery.setBuilder(QueryBuilders.multiMatchQuery(keyword, "title", "description"));
                return Product.find.search(indexQuery);
            }
        }
    
  7. 接下来,在app/controllers/Products.java中创建将提供产品创建和搜索的产品端点:

    package controllers;
         import com.github.cleverage.elasticsearch.IndexResults;
        import models.Product;
        import play.data.Form;
        import play.mvc.BodyParser;
        import play.mvc.Controller;
        import play.mvc.Result;
        import java.util.HashMap;
        import java.util.Map;
         import static play.libs.Json.toJson;
         public class Products extends Controller {
            @BodyParser.Of(BodyParser.Json.class)
            public static Result create() {
                try {
                    Form&lt;Product&gt; form = Form.form(Product.class).bindFromRequest();
                    Product product = form.get();
                    product.index();
                     return created(toJson(product));
                 } catch (Exception e) {
                    return internalServerError(e.getMessage());
                }
            }
             @BodyParser.Of(BodyParser.Json.class)
            public static Result search() {
                try {
                    Form&lt;Search&gt; form = Form.form(Search.class).bindFromRequest();
                    Search search = form.get();
                     IndexResults&lt;Product&gt; results = Product.doSearch(search.getKeyword());
                    Map&lt;String, Object&gt; map = new HashMap&lt;&gt;();
                    map.put("total", results.getTotalCount());
                    map.put("products", results.getResults());
                     return ok(toJson(map));
                 } catch (Exception e) {
                    return internalServerError(e.getMessage());
                }
            }
        }
    
  8. 让我们也将辅助类中的Search类添加到app/controllers/Search.java中:

    package controllers;
         public class Search {
            private String keyword;
             public String getKeyword() {
                return keyword;
            }
            public void setKeyword(String keyword) {
                this.keyword = keyword;
            }
        }
    
  9. 最后,让我们将产品控制器操作的路由添加到conf/routes中:

    POST    /products            controllers.Products.create
        GET     /products/search     controllers.Products.search
    
  10. 使用curl,我们可以如下测试产品创建和索引:

    <span class="strong"><strong>    # Let's insert 2 products:</strong></span>
     <span class="strong"><strong>    curl -v -X POST http://localhost:9000/products --header "Content-type: application/json" -d '{"id":"1001", "title":"Intel Core i7-4790K Processor", "shortDesc": "New Unlocked 4th Gen Intel Core Processors deliver 4 cores of up to 4 GHz base frequency providing blazing-fast computing performance for the most demanding users"}'</strong></span>
     <span class="strong"><strong>    curl -v -X POST http://localhost:9000/products --header "Content-type: application/json" -d '{"id":"1002", "title": "AMD FD6300WMHKBOX FX-6300 6-Core Processor", "shortDesc": "AMD FX 6-Core Processor Unlocked Black Edition. AMDs next-generation architecture takes 8-core processing to a new level. Get up to 24% better frame rates in some of the most demanding games at stunning resolutions. Get faster audio encoding so you can enjoy your music sooner. Go up to 5.0 GHz with aggressive cooling solutions from AMD."}'</strong></span>
    
  11. 我们也可以使用curl来执行产品搜索:

    <span class="strong"><strong>    $ curl -X GET http://localhost:9000/products/search --header "Content-type: application/json" -d '{"keyword":"processor"}'</strong></span>
    <span class="strong"><strong>{"total":2,"products":[{"id":"1001","title":"Intel Core i7-4790K Processor","shortDesc":"New Unlocked 4th Gen Intel Core Processors deliver 4 cores of up to 4 GHz base frequency providing blazing-fast computing performance for the most demanding users"},{"id":"1002","title":"AMD FD6300WMHKBOX FX-6300 6-Core Processor","shortDesc":"AMD FX 6-Core Processor Unlocked Black Edition. AMDs next-generation architecture takes 8-core processing to a new level. Get up to 24% better frame rates in some of the most demanding games at stunning resolutions. Get faster audio encoding so you can enjoy your music sooner. Go up to 5.0 GHz with aggressive cooling solutions from AMD."}]}%</strong></span>
     <span class="strong"><strong>    $ curl -X GET http://localhost:9000/products/search --header "Content-type: application/json" -d '{"keyword":"amd"}'</strong></span>
    <span class="strong"><strong>{"total":1,"products":[{"id":"1002","title":"AMD FD6300WMHKBOX FX-6300 6-Core Processor","shortDesc":"AMD FX 6-Core Processor Unlocked Black Edition. AMDs next-generation architecture takes 8-core processing to a new level. Get up to 24% better frame rates in some of the most demanding games at stunning resolutions. Get faster audio encoding so you can enjoy your music sooner. Go up to 5.0 GHz with aggressive cooling solutions from AMD."}]}%</strong></span>
    

对于 Scala,我们需要执行以下步骤:

  1. 首先,让我们使用 Docker 启动一个本地的 ElasticSearch 容器:

    <span class="strong"><strong>    $ docker run -d -p 9200:9200 -p 9300:9300 dockerfile/elasticsearch</strong></span>
    
  2. 以启用热重载的方式运行foo_scala应用程序:

    <span class="strong"><strong>    activator "~run"</strong></span>
    
  3. build.sbt中添加 play2-elasticsearch 依赖。需要注意的是,截至编写本文时,play2-elasticsearch 尚未发布对 Play 2.3.x 的支持,因此需要排除旧版 Play 库的依赖:

    resolvers += "Sonatype OSS Snapshots" at "https://oss.sonatype.org/content/repositories/snapshots"
         libraryDependencies ++= Seq(
          ("com.clever-age" % "play2-elasticsearch" % "1.4-SNAPSHOT")
            .exclude("com.typesafe.play", "play-functional_2.10")
            .exclude("com.typesafe.akka", "akka-actor_2.10")
            .exclude("com.typesafe.play", "play-json_2.10")
            .exclude("com.typesafe.play", "play_2.10")
            .exclude("com.typesafe.play", "play-iteratees_2.10")
            .exclude("com.typesafe.akka", "akka-slf4j_2.10")
            .exclude("org.scala-stm", "scala-stm_2.10")
            .exclude("com.typesafe.play", "play-datacommons_2.10")
        )
    
  4. conf/play.plugins中声明 play2-elasticsearch 插件:

    9000:com.github.cleverage.elasticsearch.plugin.IndexPlugin
    
  5. 将 play2-elasticsearch 配置参数添加到conf/application.conf

    elasticsearch.local=false
        elasticsearch.client="&lt;YOUR_ELASTICSEARCH_HOST_HERE&gt;:9300"
        elasticsearch.sniff=false
        elasticsearch.index.name="test"
        elasticsearch.index.settings="{ analysis: { analyzer: { my_analyzer: { type: \"custom\", tokenizer: \"standard\" } } } }"
        elasticsearch.index.clazzs="models.*"
        elasticsearch.index.show_request=true
        elasticsearch.cluster.name=elasticsearch
    
  6. app/models/Product.scala中创建我们的产品模型和ProductManager类:

    package models
         import com.github.cleverage.elasticsearch.ScalaHelpers.{IndexQuery,     IndexableManager, Indexable}
        import org.elasticsearch.index.query.QueryBuilders
        import play.api.libs.json.{Writes, Json, Reads}
         case class Product(id: String, title: String, shortDesc: String) extends Indexable
         object ProductManager extends IndexableManager[Product] {
          override val indexType: String = "string"
          override val reads: Reads[Product] = Json.reads[Product]
          override val writes: Writes[Product] = Json.writes[Product]
           def doSearch(keyword: String) = {
            val indexQuery = new IndexQuery[Product]()
              .withBuilder(QueryBuilders.multiMatchQuery(keyword, "title", "description"))
             search(indexQuery)
          }
        }
    
  7. 接下来,在app/controllers/Products.scala中创建将提供产品创建和搜索的产品端点:

    package controllers
         import models.{Product, ProductManager}
        import play.api.libs.json.{JsError, Json}
        import play.api.mvc.{BodyParsers, Action, Controller}
         case class Search(keyword: String)
         object Products extends Controller {
          implicit private val productWrites = Json.writes[Product]
          implicit private val productReads = Json.reads[Product]
          implicit private val searchWrites = Json.writes[Search]
          implicit private val searchReads = Json.reads[Search]
           def create = Action(BodyParsers.parse.json) { implicit request =&gt;
            val post = request.body.validate[Product]
             post.fold(
              errors =&gt; BadRequest(JsError.toFlatJson(errors)),
              p =&gt; {
                ProductManager.index(p)
                Created(Json.toJson(p))
              }
            )
          }
           def search = Action(BodyParsers.parse.json) { implicit request =&gt;
            request.body.validate[Search].fold(
              errors =&gt; BadRequest(JsError.toFlatJson(errors)),
              search =&gt; {
                val results = ProductManager.doSearch(search.keyword)
                Ok(Json.obj(
                  "total" -&gt; results.totalCount,
                  "products" -&gt; results.results
                ))
              }
            )
          }
        }
    
  8. 最后,让我们将产品控制器操作的路由添加到conf/routes中:

    POST    /products            controllers.Products.create
        GET     /products/search     controllers.Products.search
    
  9. 使用curl,我们可以如下测试产品创建和索引:

    <span class="strong"><strong>    # Let's insert 2 products:</strong></span>
     <span class="strong"><strong>    curl -v -X POST http://localhost:9000/products --header "Content-type: application/json" -d '{"id":"1001", "title":"Intel Core i7-4790K Processor", "shortDesc": "New Unlocked 4th Gen Intel Core Processors deliver 4 cores of up to 4 GHz base frequency providing blazing-fast computing performance for the most demanding users"}'</strong></span>
     <span class="strong"><strong>    curl -v -X POST http://localhost:9000/products --header "Content-type: application/json" -d '{"id":"1002", "title": "AMD FD6300WMHKBOX FX-6300 6-Core Processor", "shortDesc": "AMD FX 6-Core Processor Unlocked Black Edition. AMDs next-generation architecture takes 8-core processing to a new level. Get up to 24% better frame rates in some of the most demanding games at stunning resolutions. Get faster audio encoding so you can enjoy your music sooner. Go up to 5.0 GHz with aggressive cooling solutions from AMD."}'</strong></span>
    
  10. 我们也可以使用curl来执行产品搜索:

    <span class="strong"><strong>    $ curl -X GET http://localhost:9000/products/search --header "Content-type: application/json" -d '{"keyword":"processor"}'</strong></span>
    <span class="strong"><strong>{"total":2,"products":[{"id":"1001","title":"Intel Core i7-4790K Processor","shortDesc":"New Unlocked 4th Gen Intel Core Processors deliver 4 cores of up to 4 GHz base frequency providing blazing-fast computing performance for the most demanding users"},{"id":"1002","title":"AMD FD6300WMHKBOX FX-6300 6-Core Processor","shortDesc":"AMD FX 6-Core Processor Unlocked Black Edition. AMDs next-generation architecture takes 8-core processing to a new level. Get up to 24% better frame rates in some of the most demanding games at stunning resolutions. Get faster audio encoding so you can enjoy your music sooner. Go up to 5.0 GHz with aggressive cooling solutions from AMD."}]}%</strong></span>
     <span class="strong"><strong>    $ curl -X GET http://localhost:9000/products/search --header "Content-type: application/json" -d '{"keyword":"amd"}'</strong></span>
    <span class="strong"><strong>{"total":1,"products":[{"id":"1002","title":"AMD FD6300WMHKBOX FX-6300 6-Core Processor","shortDesc":"AMD FX 6-Core Processor Unlocked Black Edition. AMDs next-generation architecture takes 8-core processing to a new level. Get up to 24% better frame rates in some of the most demanding games at stunning resolutions. Get faster audio encoding so you can enjoy your music sooner. Go up to 5.0 GHz with aggressive cooling solutions from AMD."}]}%</strong></span>
    

如何工作……

在这个菜谱中,我们利用 ElasticSearch 作为底层搜索服务,创建了产品创建和产品搜索的端点。这个菜谱的前提条件是能够访问到 ElasticSearch 实例,无论是本地还是远程,在我们的菜谱中,以及在本地的开发机器上安装了 Docker:

<span class="strong"><strong>    $ docker -v</strong></span>
<span class="strong"><strong>    Docker version 1.3.3, build d344625</strong></span>

我们使用以下命令使用 Docker 部署我们的本地 ElasticSearch 实例:

<span class="strong"><strong>    docker run -d -p 9200:9200 -p 9300:9300 dockerfile/elasticsearch</strong></span>

上述命令指示docker以分离服务的方式运行elasticsearch容器,并且容器中的端口92009300应该可以从主机的相应端口访问。

我们首先通过在build.sbt文件中声明它来导入开源 play 模块 play2-elasticsearch:

resolvers +=   "Sonatype OSS Snapshots" at "https://oss.sonatype.org/content/repositories/snapshots"
     libraryDependencies ++= Seq(
      "io.iron.ironmq" % "ironmq" % "0.0.19",
      ("com.clever-age" % "play2-elasticsearch" % "1.4-SNAPSHOT")
        .exclude("com.typesafe.play", "play-functional_2.10")
        .exclude("com.typesafe.akka", "akka-actor_2.10")
        .exclude("com.typesafe.play", "play-json_2.10")
        .exclude("com.typesafe.play", "play_2.10")
        .exclude("com.typesafe.play", "play-iteratees_2.10")
        .exclude("com.typesafe.akka", "akka-slf4j_2.10")
        .exclude("org.scala-stm", "scala-stm_2.10")
        .exclude("com.typesafe.play", "play-datacommons_2.10")
    )

我们在conf/play.plugins中激活插件,并在conf/application.conf中指定配置参数:

// conf/play.plugins
    9000:com.github.cleverage.elasticsearch.plugin.IndexPlugin
    // conf/application.conf
    elasticsearch.local=false
    elasticsearch.client="192.168.59.103:9300"
    elasticsearch.sniff=false
    elasticsearch.index.name="test"
    elasticsearch.index.settings="{ analysis: { analyzer: { my_analyzer: { type: \"custom\", tokenizer: \"standard\" } } } }"
    elasticsearch.index.clazzs="models.*"
    elasticsearch.index.show_request=true
    elasticsearch.cluster.name=elasticsearch

然后,我们创建了我们的产品模型,它扩展了 play2-elasticsearch 类、Indexable,以及一个搜索管理类,它扩展了 play2-elasticsearch 的 IndexableManager 类。我们编写了一个辅助方法来执行多字段查询,通过关键词搜索标题和描述字段:

// Java 
    IndexQuery&lt;Product&gt; indexQuery = Product.find.query();
    indexQuery.setBuilder(QueryBuilders.multiMatchQuery(keyword, "title", "description"));    
     // Scala
    val indexQuery = new IndexQuery[Product]()
      .withBuilder(QueryBuilders.multiMatchQuery(keyword, "title", "description"))

最后,在我们的控制器类Products中,我们调用了创建和搜索操作的相关产品管理方法:

// Java
    @BodyParser.Of(BodyParser.Json.class)
    public static Result create() {
     ..
     product.index();
     ..
    }
     @BodyParser.Of(BodyParser.Json.class)
    public static Result search() {
        .. 
        Product.doSearch(search.getKeyword());
        ..
    }    
     // Scala
    def create = Action(BodyParsers.parse.json) { implicit request =&gt;
   ..
      ProductManager.index(p)
      ..
    }
     def search = Action(BodyParsers.parse.json) { implicit request =&gt;
      ..
      val results = ProductManager.doSearch(search.keyword)
      ..
    }

当 Web 应用程序启动或重新加载时,您将能够看到有关 play2-elasticsearch 插件初始化的日志信息:

<span class="strong"><strong>[info] application - Elasticsearch : Settings  {client.transport.sniff=false, cluster.name=elasticsearch}</strong></span>
<span class="strong"><strong>[info] application - ElasticSearch : Starting in Client Mode</strong></span>
<span class="strong"><strong>[info] application - ElasticSearch : Client - Host: 192.168.59.103 Port: 9300</strong></span>
<span class="strong"><strong>[info] application - ElasticSearch : Started in Client Mode</strong></span>
<span class="strong"><strong>[info] application - ElasticSearch : Plugin has started</strong></span>
<span class="strong"><strong>[info] play - Application started (Dev)</strong></span>

实现使用 JWT 的令牌认证

在这个菜谱中,我们将广泛使用的用户认证策略,令牌认证,与认证请求到受保护的 Play 操作和端点集成。我们将使用 Connect2Id 的开源库nimbus-jose-jwt来进行登录和验证 JWT 以成功登录用户。

随后的请求到其他受保护的端点和操作现在将仅需要将 JWT 添加到请求头中的授权头。然而,已签名的 JWT 将有一个规定的过期日期,我们将确保为每个签名的 JWT 请求验证这一点。

注意

更多关于 Connect2id 和 nimbus-jose-jwt 的信息请在此处查看:

connect2id.com/products/nimbus-jose-jwt

关于 JWT 的更多信息请在此处查看:

self-issued.info/docs/draft-ietf-oauth-json-web-token.html

如何做到这一点…

对于 Java,我们需要执行以下步骤:

  1. 以启用热重载的方式运行foo_java应用程序:

    <span class="strong"><strong>    activator "~run"</strong></span>
    
  2. build.sbt中添加nimbus-jose-jwt依赖项:

    libraryDependencies ++= Seq(
          "com.nimbusds" % "nimbus-jose-jwt" % "3.8.2"
        )
    
  3. 使用activator生成一个新的应用程序密钥,如下所示:

    <span class="strong"><strong>    $ activator play-generate-secret</strong></span>
    <span class="strong"><strong>    [info] Loading project definition from /Users/ginduc/Developer/workspace/bitbucket/Play2.0CookbookRecipes/Ch06/foo_java/project</strong></span>
    <span class="strong"><strong>    [info] Set current project to foo_java (in build file:/Users/ginduc/Developer/workspace/bitbucket/Play2.0CookbookRecipes/Ch06/foo_java/)</strong></span>
    <span class="strong"><strong>    [info] Generated new secret: DDqEUkPssmdHOL=U`XMANZAPYG4fUYA5QwGtK49[PmUh2kAH/IpHuHuLIuNgv_o_</strong></span>
    <span class="strong"><strong>    [success] Total time: 0 s, completed 02 24, 15 11:44:42 AM</strong></span>
    
  4. 使用上一步生成的密钥作为jwt.sharedSecret的值,将所需的配置参数添加到conf/application.conf中。至于发行者和受众,根据 JWT 规范,发行者是签发令牌的实体,而受众是令牌的预期接收者:

    jwt.sharedSecret = "DDqEUkPssmdHOL=U`XMANZAPYG4fUYA5QwGtK49[PmUh2kAH/IpHuHuLIuNgv_o_"
        jwt.issuer=&lt;YOUR_ISSUER&gt;
        jwt.expiryInSecs=600
        jwt.audience=&lt;YOUR_AUDIENCE&gt;
    
  5. app/plugins/JWTPlugin.java中创建 JWT 插件类:

    package plugins;
         import com.nimbusds.jose.JWSAlgorithm;
        import com.nimbusds.jose.JWSHeader;
        import com.nimbusds.jose.crypto.MACSigner;
        import com.nimbusds.jose.crypto.MACVerifier;
        import com.nimbusds.jwt.JWTClaimsSet;
        import com.nimbusds.jwt.SignedJWT;
        import play.Application;
        import play.Logger;
        import play.Plugin;
        import java.util.Date;
         public class JWTPlugin extends Plugin {
            final private String tokenPrefix = "Bearer ";
             private String issuer;
            private String sharedSecret;
            private Integer expiryTime;
            private String audience;
            private JWSHeader algorithm;
            private MACSigner signer;
            private MACVerifier verifier;
             public JWTPlugin(Application app) {
                super();
                 issuer = app.configuration().getString("jwt.issuer");
                sharedSecret = app.configuration().getString("jwt.sharedSecret");
                expiryTime = app.configuration().getInt("jwt.expiryInSecs");
                audience = app.configuration().getString("jwt.audience");
            }
             public void onStart() {
                algorithm = new JWSHeader(JWSAlgorithm.HS256);
                signer = new MACSigner(sharedSecret);
                verifier = new MACVerifier(sharedSecret);
            }
             public void onStop() {
                algorithm = null;
                signer = null;
                verifier = null;
            }
             public boolean verify(String token) {
                try {
                    final JWTClaimsSet payload = decode(token);
                     // Check expiration date
                    if (!new Date().before(payload.getExpirationTime())) {
                        Logger.error("Token expired: " + payload.getExpirationTime());
                        return false;
                    }
                     // Match Issuer
                    if (!payload.getIssuer().equals(issuer)) {
                        Logger.error("Issuer mismatch: " + payload.getIssuer());
                        return false;
                    }
                     // Match Audience
                    if (payload.getAudience() != null &amp;&amp; payload.getAudience().size() &gt; 0) {
                        if (!payload.getAudience().get(0).equals(audience)) {
                            Logger.error("Audience mismatch: " + payload.getAudience().get(0));
                            return false;
                        }
                    } else {
                        Logger.error("Audience is required");
                        return false;
                    }
                     return true;
                } catch(Exception e) {
                    return false;
                }
            }
             public JWTClaimsSet decode(String token) throws Exception {
                Logger.debug("Verifying: " + token.substring(tokenPrefix.length()));
                SignedJWT signedJWT = SignedJWT.parse(token.substring(tokenPrefix.length()));
                 if (!signedJWT.verify(verifier)) {
                    throw new IllegalArgumentException("Json Web Token cannot be verified!");
                }
                 return (JWTClaimsSet) signedJWT.getJWTClaimsSet();
            }
             public String sign(String userInfo) throws Exception {
                final JWTClaimsSet claimsSet = new JWTClaimsSet();
                claimsSet.setSubject(userInfo);
                claimsSet.setIssueTime(new Date());
                claimsSet.setIssuer(issuer);
                claimsSet.setAudience(audience);
                claimsSet.setExpirationTime(
                    new Date(claimsSet.getIssueTime().getTime() + (expiryTime * 1000))
                );
                 SignedJWT signedJWT = new SignedJWT(algorithm, claimsSet);
                signedJWT.sign(signer);
                return signedJWT.serialize();
            }
             public boolean enabled() {
                return true;
            }
        }
    
  6. conf/play.plugins中初始化 JWT 插件:

    10099:plugins.JWTPlugin
    
  7. app/controllers/JWTSigned.java中创建一个继承自Simple Action类的操作类,我们将使用它来使用 JWT 保护操作:

    package controllers;
         import play.*;
        import play.mvc.*;
        import play.libs.*;
        import play.libs.F.*;
        import plugins.JWTPlugin;
         public class JWTSigned extends play.mvc.Action.Simple {
            private static final String AUTHORIZATION = "Authorization";
            private static final String WWW_AUTHENTICATE = "WWW-Authenticate";
            private static final String APP_REALM = "Protected Realm";
            private static final String AUTH_HEADER_PREFIX = "Bearer ";
            private static JWTPlugin jwt = Play.application().plugin(JWTPlugin.class);
             public F.Promise&lt;Result&gt; call(Http.Context ctx) throws Throwable {
                try {
                    final String authHeader = ctx.request().getHeader(AUTHORIZATION);
                     if (authHeader != null &amp;&amp; authHeader.startsWith(AUTH_HEADER_PREFIX)) {
                        if (jwt.verify(authHeader)) {
                            return delegate.call(ctx);
                        }
                    } else {
                        return Promise.pure((Result) unauthorized());
                    }
                } catch (Exception e) {
                    Logger.error("Error during session authentication: " + e);
                }
                 ctx.response().setHeader(WWW_AUTHENTICATE, APP_REALM);
                return Promise.pure((Result) forbidden());
            }
        }
    
  8. app/controllers/Application.java中创建用于登录和令牌签名的测试操作,以及另一个使用JWTSigned进行安全保护的操作:

    package controllers;
         import play.*;
        import play.data.Form;
        import play.mvc.*;
         import plugins.JWTPlugin;
        import views.html.*;
         import java.util.HashMap;
        import java.util.Map;
         import static play.libs.Json.toJson;
         public class Application extends Controller {
            private static JWTPlugin jwt = Play.application().plugin(JWTPlugin.class);
             public static Result index() {
                return ok(index.render("Your new application is ready."));
            }
             @With(JWTSigned.class)
            public static Result adminOnly() {
                return ok("");
            }
             @BodyParser.Of(BodyParser.Json.class)
            public static Result auth() {
                try {
                    Form&lt;Login&gt; form = Form.form(Login.class).bindFromRequest();
                    Login login = form.get();
                    if (login.getUsername().equals("ned") &amp;&amp; login.getPassword().equals("flanders")) {
                        final String token = jwt.sign(login.getUsername());
                        final Map&lt;String, String&gt; map = new HashMap&lt;&gt;();
                        map.put("token", token);
                        return ok(toJson(map));
                    } else {
                        return forbidden();
                    }
                 } catch (Exception e) {
                    return internalServerError(e.getMessage());
                }
            }
        }
    
  9. 我们还需要在app/controllers/Login.java中创建用于用户认证的Login模型:

    package controllers;
         public class Login {
            private String username;
            private String password;
             public Login() {}
            public Login(String username, String password) {
                this.username = username;
                this.password = password;
            }
             public String getUsername() {
                return username;
            }
             public void setUsername(String username) {
                this.username = username;
            }
             public String getPassword() {
                return password;
            }
             public void setPassword(String password) {
                this.password = password;
            }
        }
    
  10. 最后,我们将必要的条目添加到conf/routes中,以支持我们的新操作:

    POST    /user/auth           controllers.Application.auth
        GET     /admin               controllers.Application.adminOnly
    
  11. 使用curl验证/admin路由是否由JWTSigned保护:

    <span class="strong"><strong>    $ curl -v http://localhost:9000/admin</strong></span>
    <span class="strong"><strong>    * Hostname was NOT found in DNS cache</strong></span>
    <span class="strong"><strong>    *   Trying ::1...</strong></span>
    <span class="strong"><strong>    * Connected to localhost (::1) port 9000 (#0)</strong></span>
    <span class="strong"><strong>    &gt; GET /admin HTTP/1.1</strong></span>
    <span class="strong"><strong>    &gt; User-Agent: curl/7.37.1</strong></span>
    <span class="strong"><strong>    &gt; Host: localhost:9000</strong></span>
    <span class="strong"><strong>    &gt; Accept: */*</strong></span>
    <span class="strong"><strong>    &gt;</strong></span>
    <span class="strong"><strong>    &lt; HTTP/1.1 401 Unauthorized</strong></span>
    <span class="strong"><strong>    &lt; WWW-Authenticate: Basic realm="Protected Realm"</strong></span>
    <span class="strong"><strong>    &lt; Content-Length: 0</strong></span>
    <span class="strong"><strong>    &lt;</strong></span>
    <span class="strong"><strong>    * Connection #0 to host localhost left intact</strong></span>
    
  12. 接下来,让我们登录并注意响应体中返回的令牌:

    <span class="strong"><strong>    $ curl -v http://localhost:9000/user/auth --header "Content-type: application/json" -d '{"username":"ned", "password":"flanders"}'</strong></span>
    <span class="strong"><strong>    * Hostname was NOT found in DNS cache</strong></span>
    <span class="strong"><strong>    *   Trying ::1...</strong></span>
    <span class="strong"><strong>    * Connected to localhost (::1) port 9000 (#0)</strong></span>
    <span class="strong"><strong>    &gt; POST /user/auth HTTP/1.1</strong></span>
    <span class="strong"><strong>    &gt; User-Agent: curl/7.37.1</strong></span>
    <span class="strong"><strong>    &gt; Host: localhost:9000</strong></span>
    <span class="strong"><strong>    &gt; Accept: */*</strong></span>
    <span class="strong"><strong>    &gt; Content-type: application/json</strong></span>
    <span class="strong"><strong>    &gt; Content-Length: 41</strong></span>
    <span class="strong"><strong>    &gt;</strong></span>
    <span class="strong"><strong>    * upload completely sent off: 41 out of 41 bytes</strong></span>
    <span class="strong"><strong>    &lt; HTTP/1.1 200 OK</strong></span>
    <span class="strong"><strong>    &lt; Content-Type: application/json; charset=utf-8</strong></span>
    <span class="strong"><strong>    &lt; Content-Length: 181</strong></span>
    <span class="strong"><strong>    &lt;</strong></span>
    <span class="strong"><strong>    * Connection #0 to host localhost left intact</strong></span>
    <span class="strong"><strong>{"token":"eyJhbGciOiJIUzI1NiJ9.eyJleHAiOjE0MjQ3MDM0NjAsInN1YiI6Im5lZCIsImF1ZCI6ImFwaWNsaWVudHMiLCJpc3MiOiJwMmMiLCJpYXQiOjE0MjQ3MDM0MDB9.No2skaVfGeERDY6yEMJV8KiRddZsZEcW5BAH2vw99Xc"}%</strong></span>
    
  13. 最后,让我们再次请求/admin路由,但这次,通过在授权头中添加已签名的令牌,前面加上Bearer

    <span class="strong"><strong>    $ curl -v http://localhost:9000/admin --header "Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJleHAiOjE0MjQ3MDM0NjAsInN1YiI6Im5lZCIsImF1ZCI6ImFwaWNsaWVudHMiLCJpc3MiOiJwMmMiLCJpYXQiOjE0MjQ3MDM0MDB9.No2skaVfGeERDY6yEMJV8KiRddZsZEcW5BAH2vw99Xc"</strong></span>
    <span class="strong"><strong>    * Hostname was NOT found in DNS cache</strong></span>
    <span class="strong"><strong>    *   Trying ::1...</strong></span>
    <span class="strong"><strong>    * Connected to localhost (::1) port 9000 (#0)</strong></span>
    <span class="strong"><strong>    &gt; GET /admin HTTP/1.1</strong></span>
    <span class="strong"><strong>    &gt; User-Agent: curl/7.37.1</strong></span>
    <span class="strong"><strong>    &gt; Host: localhost:9000</strong></span>
    <span class="strong"><strong>    &gt; Accept: */*</strong></span>
    <span class="strong"><strong>    &gt; Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJleHAiOjE0MjQ3MDM0NjAsInN1YiI6Im5lZCIsImF1ZCI6ImFwaWNsaWVudHMiLCJpc3MiOiJwMmMiLCJpYXQiOjE0MjQ3MDM0MDB9.No2skaVfGeERDY6yEMJV8KiRddZsZEcW5BAH2vw99Xc</strong></span>
    <span class="strong"><strong>    &gt;</strong></span>
    <span class="strong"><strong>    &lt; HTTP/1.1 200 OK</strong></span>
    <span class="strong"><strong>    &lt; Content-Type: text/plain; charset=utf-8</strong></span>
    <span class="strong"><strong>    &lt; Content-Length: 0</strong></span>
    <span class="strong"><strong>    &lt;</strong></span>
    <span class="strong"><strong>    * Connection #0 to host localhost left intact</strong></span>
    
  14. 我们还可以通过在令牌中设置过期时间后再次运行之前的请求来验证 JWTPlugin 是否正确处理了令牌过期,它应该导致类似以下的结果:

    <span class="strong"><strong>    $ curl -v http://localhost:9000/admin --header "Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJleHAiOjE0MjQ3MDM0NjAsInN1YiI6Im5lZCIsImF1ZCI6ImFwaWNsaWVudHMiLCJpc3MiOiJwMmMiLCJpYXQiOjE0MjQ3MDM0MDB9.No2skaVfGeERDY6yEMJV8KiRddZsZEcW5BAH2vw99Xc"</strong></span>
    <span class="strong"><strong>    * Hostname was NOT found in DNS cache</strong></span>
    <span class="strong"><strong>    *   Trying ::1...</strong></span>
    <span class="strong"><strong>    * Connected to localhost (::1) port 9000 (#0)</strong></span>
    <span class="strong"><strong>    &gt; GET /admin HTTP/1.1</strong></span>
    <span class="strong"><strong>    &gt; User-Agent: curl/7.37.1</strong></span>
    <span class="strong"><strong>    &gt; Host: localhost:9000</strong></span>
    <span class="strong"><strong>    &gt; Accept: */*</strong></span>
    <span class="strong"><strong>    &gt; Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJleHAiOjE0MjQ3MDM0NjAsInN1YiI6Im5lZCIsImF1ZCI6ImFwaWNsaWVudHMiLCJpc3MiOiJwMmMiLCJpYXQiOjE0MjQ3MDM0MDB9.No2skaVfGeERDY6yEMJV8KiRddZsZEcW5BAH2vw99Xc</strong></span>
    <span class="strong"><strong>    &gt;</strong></span>
    <span class="strong"><strong>    &lt; HTTP/1.1 403 Forbidden</strong></span>
    <span class="strong"><strong>    &lt; Content-Length: 0</strong></span>
    <span class="strong"><strong>    &lt;</strong></span>
    <span class="strong"><strong>    * Connection #0 to host localhost left intact</strong></span>
    

对于 Scala,我们需要执行以下步骤:

  1. 启用 Hot-Reloading 运行 foo_scala 应用程序:

    <span class="strong"><strong>    activator "~run"</strong></span>
    
  2. nimbus-jose-jwt 依赖项添加到 build.sbt 文件中:

    libraryDependencies ++= Seq(
          "com.nimbusds" % "nimbus-jose-jwt" % "3.8.2"
         )
    
  3. 使用 activator 生成新的应用程序密钥,如下所示:

    <span class="strong"><strong>    $ activator play-generate-secret</strong></span>
    <span class="strong"><strong>    [info] Loading project definition from /Users/ginduc/Developer/workspace/bitbucket/Play2.0CookbookRecipes/Ch06/foo_scala/project</strong></span>
    <span class="strong"><strong>    [info] Set current project to foo_scala (in build file:/Users/ginduc/Developer/workspace/bitbucket/Play2.0CookbookRecipes/Ch06/foo_scala/)</strong></span>
    <span class="strong"><strong>    [info] Generated new secret: LKQhArck[KCAFcEplWDeBSV^e@a1o6X&gt;2SI_D3Q^X0h`eigla5ywm^k6E9z7Nx=p</strong></span>
    <span class="strong"><strong>    [success] Total time: 0 s, completed 02 23, 15 10:32:56 PM</strong></span>
    
  4. 使用上一步生成的密钥作为 jwt.sharedSecret 的值,将所需的配置参数添加到 conf/application.conf 文件中。至于发行者和受众,根据 JWT 规范,发行者是发行令牌的实体,受众是令牌的预期接收者:

    jwt.sharedSecret = "LKQhArckKCAFcEplWDeBSV^e@a1o6X&gt;2SI_D3Q^X0h`eigla5ywm^k6E9z7Nx=p"
        jwt.issuer=&lt;YOUR_ISSUER&gt;
        jwt.expiryInSecs=600
        jwt.audience=&lt;YOUR_AUDIENCE&gt;
    
  5. app/plugins/JWTPlugin.scala 中创建 JWT 插件类:

    package plugins
         import java.util.Date
        import com.nimbusds.jose.crypto.{MACVerifier, MACSigner}
        import com.nimbusds.jose.{JWSAlgorithm, JWSHeader}
        import com.nimbusds.jwt.{JWTClaimsSet, SignedJWT}
        import play.api.{Logger, Play, Application, Plugin}
        import play.api.Play.current
         class JWTPlugin(app: Application) extends Plugin {
          val tokenPrefix = "Bearer "
           private val issuer = Play.application.configuration.getString("jwt.issuer").getOrElse("jwt")
          private val sharedSecret = Play.application.configuration.getString("jwt.sharedSecret")
        .getOrElse(throw new IllegalStateException("JWT Shared Secret is required!"))
          private val expiryTime = Play.application.configuration.getInt("jwt.expiryInSecs").getOrElse(60 * 60 * 24)
          private val audience = Play.application.configuration.getString("jwt.audience").getOrElse("jwt")
          private val algorithm = new JWSHeader(JWSAlgorithm.HS256)
    
          private lazy val signer: MACSigner = new MACSigner(sharedSecret)
          private lazy val verifier: MACVerifier = new MACVerifier(sharedSecret)
           override def onStart() = {
    
           signer
            verifier
          }
           override def onStop() = {
            Logger.info("Shutting down plugin")
          }
           def verify(token: String): Boolean = {
            val payload = decode(token)
             // Check expiration date
            if (!new Date().before(payload.getExpirationTime)) {
              Logger.error("Token expired: " + payload.getExpirationTime)
              return false
            }
             // Match Issuer
            if (!payload.getIssuer.equals(issuer)) {
              Logger.error("Issuer mismatch: " + payload.getIssuer)
              return false
            }
             // Match Audience
            if (payload.getAudience != null &amp;&amp; payload.getAudience.size() &gt; 0) {
              if (!payload.getAudience.get(0).equals(audience)) {
                Logger.error("Audience mismatch: " + payload.getAudience.get(0))
                return false
              }
            } else {
              Logger.error("Audience is required")
              return false
            }
            return true
          }
           def decode(token: String) = {
            val signedJWT = SignedJWT.parse(token.substring(tokenPrefix.length))
             if (!signedJWT.verify(verifier)) {
              throw new IllegalArgumentException("Json Web Token cannot be verified!")
            }
             signedJWT.getJWTClaimsSet
          }
           def sign(userInfo: String): String = {
            val claimsSet = new JWTClaimsSet()
            claimsSet.setSubject(userInfo)
            claimsSet.setIssueTime(new Date)
            claimsSet.setIssuer(issuer)
            claimsSet.setAudience(audience)
            claimsSet.setExpirationTime(
              new Date(claimsSet.getIssueTime.getTime + (expiryTime * 1000))
            )
             val signedJWT = new SignedJWT(algorithm, claimsSet)
            signedJWT.sign(signer)
            signedJWT.serialize()
          }
        override def enabled = true
        }
    
  6. conf/play.plugins 中初始化 JWTPlugin:

    10099:plugins.JWTPlugin
    
  7. app/controllers/JWTSigned.scala 中创建一个 ActionBuilder 类,我们将使用它来使用 JWT 保护操作:

    package controllers
         import play.api.Play
        import play.api.mvc.{Result, WrappedRequest, Request, ActionBuilder}
        import play.api.http.HeaderNames._
        import play.api.mvc.Results._
        import play.api.Play.current
        import plugins.JWTPlugin
        import scala.concurrent.Future
         class JWTSignedRequest[A extends WrappedRequestA
         object JWTSigned extends ActionBuilder[JWTSignedRequest] {
          private val jwt = Play.application.plugin[JWTPlugin].get
           def invokeBlockA =&gt; Future[Result]) = {
            req.headers.get(AUTHORIZATION) map { token =&gt;
              if (jwt.verify(token)) {
                block(new JWTSignedRequest(token, req))
              } else {
                Future.successful(Forbidden)
              }
            } getOrElse {
              Future.successful(Unauthorized.withHeaders(WWW_AUTHENTICATE -&gt; """Basic realm="Protected Realm""""))
            }
          }
        }
    
  8. app/controllers/Application.scala 中创建用于登录和令牌签名的测试操作,以及另一个需要用 JWTSigned 保护的操作:

    case class Login(username: String, password: String)
    
        private val jwt = Play.application.plugin[JWTPlugin].get
        implicit private val productWrites = Json.writes[Login]
        implicit private val productReads = Json.reads[Login]
         def adminOnly = JWTSigned {
          Ok("")
        }
         def auth = Action(BodyParsers.parse.json) { implicit request =&gt;
          val post = request.body.validate[Login]
           post.fold(
            errors =&gt; Unauthorized,
            u =&gt; {
              if (u.username.equals("ned") &amp;&amp; u.password.equals("flanders")) {
                 Ok(Json.obj("token" -&gt; jwt.sign(u.username)))
              } else {
                Forbidden
              }
            }
          )
        }
    
  9. 最后,我们将必要的条目添加到 conf/routes 文件中,以支持我们的新操作:

    POST    /user/auth           controllers.Application.auth
        GET     /admin               controllers.Application.adminOnly
    
  10. 使用 curl 验证 /admin 路由是否由 JWTSigned 保护:

    <span class="strong"><strong>    $ curl -v http://localhost:9000/admin</strong></span>
    <span class="strong"><strong>    * Hostname was NOT found in DNS cache</strong></span>
    <span class="strong"><strong>    *   Trying ::1...</strong></span>
    <span class="strong"><strong>    * Connected to localhost (::1) port 9000 (#0)</strong></span>
    <span class="strong"><strong>    &gt; GET /admin HTTP/1.1</strong></span>
    <span class="strong"><strong>    &gt; User-Agent: curl/7.37.1</strong></span>
    <span class="strong"><strong>    &gt; Host: localhost:9000</strong></span>
    <span class="strong"><strong>    &gt; Accept: */*</strong></span>
    <span class="strong"><strong>    &gt;</strong></span>
    <span class="strong"><strong>    &lt; HTTP/1.1 401 Unauthorized</strong></span>
    <span class="strong"><strong>    &lt; WWW-Authenticate: Basic realm="Protected Realm"</strong></span>
    <span class="strong"><strong>    &lt; Content-Length: 0</strong></span>
    <span class="strong"><strong>    &lt;</strong></span>
    <span class="strong"><strong>    * Connection #0 to host localhost left intact</strong></span>
    
  11. 接下来,让我们登录并记录响应体中返回的令牌:

    <span class="strong"><strong>    $ curl -v http://localhost:9000/user/auth --header "Content-type: application/json" -d '{"username":"ned", "password":"flanders"}'</strong></span>
    <span class="strong"><strong>    * Hostname was NOT found in DNS cache</strong></span>
    <span class="strong"><strong>    *   Trying ::1...</strong></span>
    <span class="strong"><strong>    * Connected to localhost (::1) port 9000 (#0)</strong></span>
    <span class="strong"><strong>    &gt; POST /user/auth HTTP/1.1</strong></span>
    <span class="strong"><strong>    &gt; User-Agent: curl/7.37.1</strong></span>
    <span class="strong"><strong>    &gt; Host: localhost:9000</strong></span>
    <span class="strong"><strong>    &gt; Accept: */*</strong></span>
    <span class="strong"><strong>    &gt; Content-type: application/json</strong></span>
    <span class="strong"><strong>    &gt; Content-Length: 41</strong></span>
    <span class="strong"><strong>    &gt;</strong></span>
    <span class="strong"><strong>    * upload completely sent off: 41 out of 41 bytes</strong></span>
    <span class="strong"><strong>    &lt; HTTP/1.1 200 OK</strong></span>
    <span class="strong"><strong>    &lt; Content-Type: application/json; charset=utf-8</strong></span>
    <span class="strong"><strong>    &lt; Content-Length: 181</strong></span>
    <span class="strong"><strong>    &lt;</strong></span>
    <span class="strong"><strong>    * Connection #0 to host localhost left intact</strong></span>
    <span class="strong"><strong>{"token":"eyJhbGciOiJIUzI1NiJ9.eyJleHAiOjE0MjQ3MDM0NjAsInN1YiI6Im5lZCIsImF1ZCI6ImFwaWNsaWVudHMiLCJpc3MiOiJwMmMiLCJpYXQiOjE0MjQ3MDM0MDB9.No2skaVfGeERDY6yEMJV8KiRddZsZEcW5BAH2vw99Xc"}%</strong></span>
    
  12. 最后,让我们再次请求 /admin 路由,但这次,在授权头中添加已签名的令牌,前缀为 Bearer

    <span class="strong"><strong>    $ curl -v http://localhost:9000/admin --header "Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJleHAiOjE0MjQ3MDM0NjAsInN1YiI6Im5lZCIsImF1ZCI6ImFwaWNsaWVudHMiLCJpc3MiOiJwMmMiLCJpYXQiOjE0MjQ3MDM0MDB9.No2skaVfGeERDY6yEMJV8KiRddZsZEcW5BAH2vw99Xc"</strong></span>
    <span class="strong"><strong>    * Hostname was NOT found in DNS cache</strong></span>
    <span class="strong"><strong>    *   Trying ::1...</strong></span>
    <span class="strong"><strong>    * Connected to localhost (::1) port 9000 (#0)</strong></span>
    <span class="strong"><strong>    &gt; GET /admin HTTP/1.1</strong></span>
    <span class="strong"><strong>    &gt; User-Agent: curl/7.37.1</strong></span>
    <span class="strong"><strong>    &gt; Host: localhost:9000</strong></span>
    <span class="strong"><strong>    &gt; Accept: */*</strong></span>
    <span class="strong"><strong>    &gt; Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJleHAiOjE0MjQ3MDM0NjAsInN1YiI6Im5lZCIsImF1ZCI6ImFwaWNsaWVudHMiLCJpc3MiOiJwMmMiLCJpYXQiOjE0MjQ3MDM0MDB9.No2skaVfGeERDY6yEMJV8KiRddZsZEcW5BAH2vw99Xc</strong></span>
    <span class="strong"><strong>    &gt;</strong></span>
    <span class="strong"><strong>    &lt; HTTP/1.1 200 OK</strong></span>
    <span class="strong"><strong>    &lt; Content-Type: text/plain; charset=utf-8</strong></span>
    <span class="strong"><strong>    &lt; Content-Length: 0</strong></span>
    <span class="strong"><strong>    &lt;</strong></span>
    <span class="strong"><strong>    * Connection #0 to host localhost left intact</strong></span>
    
  13. 我们也可以通过在令牌过期后再次运行之前的请求来验证 JWTPlugin 是否正确处理令牌过期,应该得到类似以下的结果:

    <span class="strong"><strong>    $ curl -v http://localhost:9000/admin --header "Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJleHAiOjE0MjQ3MDM0NjAsInN1YiI6Im5lZCIsImF1ZCI6ImFwaWNsaWVudHMiLCJpc3MiOiJwMmMiLCJpYXQiOjE0MjQ3MDM0MDB9.No2skaVfGeERDY6yEMJV8KiRddZsZEcW5BAH2vw99Xc"</strong></span>
    <span class="strong"><strong>    * Hostname was NOT found in DNS cache</strong></span>
    <span class="strong"><strong>    *   Trying ::1...</strong></span>
    <span class="strong"><strong>    * Connected to localhost (::1) port 9000 (#0)</strong></span>
    <span class="strong"><strong>    &gt; GET /admin HTTP/1.1</strong></span>
    <span class="strong"><strong>    &gt; User-Agent: curl/7.37.1</strong></span>
    <span class="strong"><strong>    &gt; Host: localhost:9000</strong></span>
    <span class="strong"><strong>    &gt; Accept: */*</strong></span>
    <span class="strong"><strong>    &gt; Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJleHAiOjE0MjQ3MDM0NjAsInN1YiI6Im5lZCIsImF1ZCI6ImFwaWNsaWVudHMiLCJpc3MiOiJwMmMiLCJpYXQiOjE0MjQ3MDM0MDB9.No2skaVfGeERDY6yEMJV8KiRddZsZEcW5BAH2vw99Xc</strong></span>
    <span class="strong"><strong>    &gt;</strong></span>
    <span class="strong"><strong>    &lt; HTTP/1.1 403 Forbidden</strong></span>
    <span class="strong"><strong>    &lt; Content-Length: 0</strong></span>
    <span class="strong"><strong>    &lt;</strong></span>
    <span class="strong"><strong>    * Connection #0 to host localhost left intact</strong></span>
    

它是如何工作的…

在这个菜谱中,我们实现了 JWT 的签名和验证,目的是使用令牌身份验证来保护 Play 操作。

我们创建了一个 Play 插件,JWTPlugin,它将从 conf/application.conf 文件中加载配置,并包含使用 Connect2id 的 nimbus-jose-jwt 库进行签名、解码和验证 JWT 的方法定义:

// Java 
    public JWTPlugin(Application app) {
        super();
        issuer = app.configuration().getString("jwt.issuer");
        sharedSecret = app.configuration().getString("jwt.sharedSecret");
        expiryTime = app.configuration().getInt("jwt.expiryInSecs");
        audience = app.configuration().getString("jwt.audience");
    }
     public void onStart() {
        algorithm = new JWSHeader(JWSAlgorithm.HS256);
        signer = new MACSigner(sharedSecret);
        verifier = new MACVerifier(sharedSecret);
    }    
     // Scala
    private val issuer = Play.application.configuration.getString("jwt.issuer").getOrElse("jwt")
    private val sharedSecret = Play.application.configuration.getString("jwt.sharedSecret")
    .getOrElse(throw new IllegalStateException("JWT Shared Secret is required!"))
    private val expiryTime = Play.application.configuration.getInt("jwt.expiryInSecs").getOrElse(60 * 60 * 24)
    private val audience = Play.application.configuration.getString("jwt.audience").getOrElse("jwt")
    private val algorithm = new JWSHeader(JWSAlgorithm.HS256)
    private var signer: MACSigner = null
    private var verifier: MACVerifier = null
     override def onStart() = {
      signer = new MACSigner(sharedSecret)
      verifier = new MACVerifier(sharedSecret)
    }

在前面的代码中,你会注意到我们正在使用 SHA-256 作为默认的哈希算法来利用 HMAC。

对于 Java,添加以下代码:

// Java
    algorithm = new JWSHeader(JWSAlgorithm.HS256);

对于 Scala,添加以下代码:

// Scala
    private val algorithm = new JWSHeader(JWSAlgorithm.HS256)

对于签名,我们构建了声明集,这是根据 JWT 规范的标准令牌元数据集:你可以参考以下链接:

self-issued.info/docs/draft-ietf-oauth-json-web-token.html#Claims

使用适当的参数值,并将用户信息作为声明主题添加,然后我们对其进行签名并将其序列化为 String

// Java 
    public String sign(String userInfo) throws Exception {
        final JWTClaimsSet claimsSet = new JWTClaimsSet();
        claimsSet.setSubject(userInfo);
        claimsSet.setIssueTime(new Date());
        claimsSet.setIssuer(issuer);
        claimsSet.setAudience(audience);
        claimsSet.setExpirationTime(
            new Date(claimsSet.getIssueTime().getTime() + (expiryTime * 1000))
        );
         SignedJWT signedJWT = new SignedJWT(algorithm, claimsSet);
        signedJWT.sign(signer);
        return signedJWT.serialize();
    }    
     // Scala
    def sign(userInfo: String): String = {
      val claimsSet = new JWTClaimsSet()
      claimsSet.setSubject(userInfo)
      claimsSet.setIssueTime(new Date)
      claimsSet.setIssuer(issuer)
      claimsSet.setAudience(audience)
      claimsSet.setExpirationTime(
        new Date(claimsSet.getIssueTime.getTime + (expiryTime * 1000))
      )
       val signedJWT = new SignedJWT(algorithm, claimsSet)
      signedJWT.sign(signer)
      signedJWT.serialize()
    }

为了验证,我们获取传递的已解码和验证的令牌,然后继续验证声明集的部分。只有当令牌通过所有验证测试时,它才返回布尔值 true

// Java
    public boolean verify(String token) {
        try {
            final JWTClaimsSet payload = decode(token);
             // Check expiration date
            if (!new Date().before(payload.getExpirationTime())) {
                Logger.error("Token expired: " + payload.getExpirationTime());
                return false;
            }
             // Match Issuer
            if (!payload.getIssuer().equals(issuer)) {
                Logger.error("Issuer mismatch: " + payload.getIssuer());
                return false;
            }
             // Match Audience
            if (payload.getAudience() != null &amp;&amp; payload.getAudience().size() &gt; 0) {
                if (!payload.getAudience().get(0).equals(audience)) {
                    Logger.error("Audience mismatch: " + payload.getAudience().get(0));
                    return false;
                }
            } else {
                Logger.error("Audience is required");
                return false;
            }
             return true;
        } catch(Exception e) {
            return false;
        }
    }
     // Scala
    def verify(token: String): Boolean = {
      val payload = decode(token)
       // Check expiration date
      if (!new Date().before(payload.getExpirationTime)) {
        Logger.error("Token expired: " + payload.getExpirationTime)
        return false
      }
       // Match Issuer
      if (!payload.getIssuer.equals(issuer)) {
        Logger.error("Issuer mismatch: " + payload.getIssuer)
        return false
      }
       // Match Audience
      if (payload.getAudience != null &amp;&amp; payload.getAudience.size() &gt; 0) {
        if (!payload.getAudience.get(0).equals(audience)) {
          Logger.error("Audience mismatch: " + payload.getAudience.get(0))
          return false
        }
      } else {
        Logger.error("Audience is required")
        return false
      }
       return true
    }

然后,我们创建了一个 Simple Action / Action Builder 类,它将执行实际的 JWT 验证:

// Java 
    public F.Promise&lt;Result&gt; call(Http.Context ctx) throws Throwable {
        try {
            final String authHeader = ctx.request().getHeader(AUTHORIZATION);
             if (authHeader != null &amp;&amp; authHeader.startsWith(AUTH_HEADER_PREFIX)) {
                if (jwt.verify(authHeader)) {
                    return delegate.call(ctx);
                }
            }
        } catch (Exception e) {
            Logger.error("Error during session authentication: " + e);
        }
         ctx.response().setHeader(WWW_AUTHENTICATE, APP_REALM);
        return Promise.pure((Result) forbidden());
    }
     // Scala
    def invokeBlockA =&gt; Future[Result]) = {
      req.headers.get(AUTHORIZATION) map { token =&gt;
        if (jwt.verify(token)) {
          block(new JWTSignedRequest(token, req))
        } else {
          Future.successful(Forbidden)
        }
      } getOrElse {
        Future.successful(Unauthorized.withHeaders(WWW_AUTHENTICATE -&gt; """Basic realm="Protected Realm""""))
      }
    }

前面的代码只有在令牌通过验证测试的情况下才会调用下一个请求块。对于验证失败的情况,它将返回一个Http 状态禁止错误,对于没有设置授权头的请求,则返回一个Http 状态未授权错误。

我们现在可以使用 JWTSigned ActionBuilder 类来保护 Play 控制器操作:

// Java 
    @With(JWTSigned.class)
    public static Result adminOnly() {
        return ok("");
    }
     // Scala
    def adminOnly = JWTSigned {
      Ok("")
    }

最后,我们得到的是一个返回序列化、签名 JWT 的 Play 动作,以及一个使用 JWTSigned ActionBuilder 类来保护操作免受未经认证和未经授权请求的 Play 动作:

<span class="strong"><strong>    # Signing    </strong></span>
 <span class="strong"><strong>    $ curl http://localhost:9000/user/auth --header "Content-type: application/json" -d '{"username":"ned", "password":"flanders"}'</strong></span>
<span class="strong"><strong>{"token":"eyJhbGciOiJIUzI1NiJ9.eyJleHAiOjE0MjQ3MDUzOTgsInN1YiI6Im5lZCIsImF1ZCI6ImFwaWNsaWVudHMiLCJpc3MiOiJwMmMiLCJpYXQiOjE0MjQ3MDUzMzh9.uE5GNQv2uXQh29sHhy_Jbg9omDhQMrnW1tjqFBrUwSs"}%</strong></span>
 <span class="strong"><strong>    # Verifying</strong></span>
<span class="strong"><strong>    $ curl -v http://localhost:9000/admin</strong></span>
<span class="strong"><strong>    * Hostname was NOT found in DNS cache</strong></span>
<span class="strong"><strong>    *   Trying ::1...</strong></span>
<span class="strong"><strong>    * Connected to localhost (::1) port 9000 (#0)</strong></span>
<span class="strong"><strong>    &gt; GET /admin HTTP/1.1</strong></span>
<span class="strong"><strong>    &gt; User-Agent: curl/7.37.1</strong></span>
<span class="strong"><strong>    &gt; Host: localhost:9000</strong></span>
<span class="strong"><strong>    &gt; Accept: */*</strong></span>
<span class="strong"><strong>    &gt;</strong></span>
<span class="strong"><strong>    &lt; HTTP/1.1 401 Unauthorized</strong></span>
<span class="strong"><strong>    &lt; WWW-Authenticate: Basic realm="Protected Realm"</strong></span>
<span class="strong"><strong>    &lt; Content-Length: 0</strong></span>
<span class="strong"><strong>    &lt;</strong></span>
<span class="strong"><strong>    * Connection #0 to host localhost left intact</strong></span>

第七章:部署 Play 2 Web 应用

在本章中,我们将涵盖以下菜谱:

  • 在 Heroku 上部署 Play 应用

  • 在 AWS Elastic Beanstalk 上部署 Play 应用

  • 使用 CoreOS 和 Docker 部署 Play 应用

  • 使用 Dokku 部署 Play 应用

  • 使用 Nginx 部署 Play 应用

简介

在本章中,我们将探讨 Play 2.0 Web 应用的多种部署选项。在众多基于云的服务,如基础设施即服务IaaS)和平台即服务PaaS)服务提供的情况下,我们有多种选择来部署我们的 Play 2.0 到不同的环境,无论是开发、测试、集成还是生产。

我们通过流行的开发者服务,如 Heroku 和亚马逊网络服务,来探索这一点。我们还将探讨使用流行的开发者工具,如DockerDokku来部署 Play 2 Web 应用。

我们还将使用两种不同的操作系统作为我们的基础操作系统,CentOSCoreOS

在本章中,我们的目标是展示使用云服务部署 Web 应用与手动使用原始虚拟机部署 Web 应用所需的努力之间的对比。我们还将探讨现代工具如何通过更多地专注于开发而不是基础设施来帮助开发者提高效率。

在 Heroku 上部署 Play 应用

在这个菜谱中,我们将部署一个 Play Web 应用到Heroku,特别是支持 Play 框架的 Heroku Cedar 堆栈。在 Heroku 上部署需要一些 Git,即流行的源代码管理软件的知识。

如何做到这一点...

  1. 首先,通过以下链接注册 Heroku 账户:

    signup.heroku.com/

  2. 安装 Heroku CLI 工具(更多信息请参阅devcenter.heroku.com/articles/heroku-command)。安装完成后,你现在应该可以使用 CLI 工具登录 Heroku,如下所示:

    <span class="strong"><strong>    $ heroku login</strong></span>
    <span class="strong"><strong>    Enter your Heroku credentials.</strong></span>
    <span class="strong"><strong>    Email: &lt;YOUR_HEROKU_USER&gt;</strong></span>
    <span class="strong"><strong>    Password (typing will be hidden):</strong></span>
    <span class="strong"><strong>    Authentication successful.</strong></span>
    
  3. 使用 activator 模板computer-database-scala创建我们的 Play Web 应用:

    <span class="strong"><strong>    activator new play2-heroku computer-database-scala</strong></span>
    
  4. 一旦我们的 Web 应用生成后,切换到play2-heroku的项目根目录,并在项目上初始化 Git:

    <span class="strong"><strong>     cd play2-heroku</strong></span>
    <span class="strong"><strong>     git init</strong></span>
    
  5. 创建一个新的heroku应用。Heroku 将分配默认的应用名称,对于这个菜谱,shielded-dusk-8715

    <span class="strong"><strong>    $ heroku create --stack cedar-14</strong></span>
    <span class="strong"><strong>    Creating shielded-dusk-8715... done, stack is cedar-14</strong></span>
    <span class="strong"><strong>    https://shielded-dusk-8715.herokuapp.com/ | https://git.heroku.com/shielded-dusk-8715.git</strong></span>
    <span class="strong"><strong>    Git remote heroku added</strong></span>
    
  6. 在项目根目录中添加 Heroku 特定的配置。可选地,你可以创建一个名为system.properties的文件,包含以下内容,以指定我们需要 JDK 版本 8:

    <span class="strong"><strong>    java.runtime.version=1.8</strong></span>
    
  7. 在项目根目录中创建 Heroku 文件Procfile,这是 Heroku 的标准基于文本的文件,用于声明你的应用以及运行命令和其他环境变量,内容如下:

    <span class="strong"><strong>    web: target/universal/stage/bin/play2-heroku -Dhttp.port=${PORT} -DapplyEvolutions.default=true</strong></span>
    
  8. 使用以下命令验证我们新的 Heroku git 远程位置:

    <span class="strong"><strong>    $ git remote show heroku</strong></span>
    <span class="strong"><strong>    * remote heroku</strong></span>
    <span class="strong"><strong>      Fetch URL: https://git.heroku.com/shielded-dusk-8715.git</strong></span>
    <span class="strong"><strong>      Push  URL: https://git.heroku.com/shielded-dusk-8715.git</strong></span>
    <span class="strong"><strong>      HEAD branch: unknown</strong></span>
    <span class="strong"><strong>      Remote branch:</strong></span>
    <span class="strong"><strong>        master tracked</strong></span>
    <span class="strong"><strong>      Local branch configured for 'git pull':</strong></span>
    <span class="strong"><strong>        master merges with remote master</strong></span>
    <span class="strong"><strong>      Local ref configured for 'git push':</strong></span>
    <span class="strong"><strong>        master pushes to master (up to date)</strong></span>
    
  9. 将我们的 Web 应用样板代码添加到 git 并提交:

    <span class="strong"><strong>    git add &#x2013;-all &amp;&amp; git commit -am "Initial commit"</strong></span>
    
  10. 最后,让我们通过将本地仓库推送到 Heroku 的 git origin 来部署我们的 Web 应用:

    <span class="strong"><strong>    $ git push heroku master</strong></span>
    <span class="strong"><strong>    Counting objects: 40, done.</strong></span>
    <span class="strong"><strong>    Delta compression using up to 8 threads.</strong></span>
    <span class="strong"><strong>    Compressing objects: 100% (35/35), done.</strong></span>
    <span class="strong"><strong>    Writing objects: 100% (40/40), 1.01 MiB | 0 bytes/s, done.</strong></span>
    <span class="strong"><strong>    Total 40 (delta 1), reused 0 (delta 0)</strong></span>
    <span class="strong"><strong>    remote: Compressing source files... done.</strong></span>
    <span class="strong"><strong>    remote: Building source:</strong></span>
    <span class="strong"><strong>    remote:</strong></span>
    <span class="strong"><strong>    remote: -----&gt; Play 2.x - Scala app detected</strong></span>
    <span class="strong"><strong>    remote: -----&gt; Installing OpenJDK 1.7... done</strong></span>
    <span class="strong"><strong>    remote: -----&gt; Priming Ivy cache (Scala-2.11, Play-2.3)... done</strong></span>
    <span class="strong"><strong>    remote: -----&gt; Running: sbt compile stage</strong></span>
    <span class="strong"><strong>    ..</strong></span>
    <span class="strong"><strong>    ..</strong></span>
    <span class="strong"><strong>    remote:        Default types for Play 2.x - Scala -&gt; web</strong></span>
    <span class="strong"><strong>    remote:</strong></span>
    <span class="strong"><strong>    remote: -----&gt; Compressing... done, 95.0MB</strong></span>
    <span class="strong"><strong>    remote: -----&gt; Warning: This app's git repository is large.</strong></span>
    <span class="strong"><strong>    remote:        Large repositories can cause problems.</strong></span>
    <span class="strong"><strong>    remote:        See: https://devcenter.heroku.com/articles/git#repo-size</strong></span>
    <span class="strong"><strong>    remote: -----&gt; Launching... done, v6</strong></span>
    <span class="strong"><strong>    remote:        https://shielded-dusk-8715.herokuapp.com/ deployed to Heroku</strong></span>
    <span class="strong"><strong>    remote:</strong></span>
    <span class="strong"><strong>    remote: Verifying deploy.... done.</strong></span>
    <span class="strong"><strong>    To https://git.heroku.com/shielded-dusk-8715.git</strong></span>
    
  11. 使用网络浏览器,我们现在可以访问部署在 Heroku 上的computer-database-scala网络应用:图片

  12. 实际的 Heroku URL 可能会有所不同;请确保您在 URL 中替换了 Heroku 生成的应用名称,但在这个菜谱中,它应该部署在以下位置:

    shielded-dusk-8715.herokuapp.com

它是如何工作的...

在这个菜谱中,我们在流行的 PaaS 平台 Heroku 上部署了 Play 2.0 网络应用。部署到 Heroku 需要非常少的定制和配置,这使得开发者能够轻松地将 Play 2.0 网络应用快速部署到像 Heroku 这样的稳固平台上。

在这个菜谱中,我们使用了 activator 模板computer-database-scala,并将其用作我们的示例网络应用,然后我们将其部署到 Heroku 的 Cedar 堆栈。在设置好网络应用模板的样板代码后,我们添加了两个文件,作为 Heroku 的配置文件:

Procfile

Procfile中,我们指定了网络应用的入口点,Heroku 使用它来初始化和运行网络应用,在这个菜谱中是bin/play2-heroku。我们还在这里指定了网络应用将使用的端口号,这由 Heroku 运行时决定,指定在环境变量${PORT}中:

<span class="strong"><strong>    web: target/universal/stage/bin/play2-heroku -Dhttp.port=${PORT} -DapplyEvolutions.default=true</strong></span>

最后,我们设置了一个由 Play 使用的 jvm 属性,用于确定是否执行数据库演变脚本。

<span class="strong"><strong>    -DapplyEvolutions.default=true</strong></span>

system.properties

<span class="strong"><strong>    java.runtime.version=1.8</strong></span>

作为可选步骤,在system.properties中,我们简单地指定了用于我们的 Play 网络应用的 JDK 版本。

还有更多...

有关 Git 和 post-commit 钩子的更多信息,请参阅官方 Git 文档:

在 AWS Elastic Beanstalk 上部署 Play 应用

在这个菜谱中,我们将部署一个 Play 2 网络应用到亚马逊网络服务的 Elastic Beanstalk。Elastic Beanstalk是亚马逊的 PaaS 服务,允许开发者以与其他 PAAS 服务(如 Heroku)相同的简便性部署网络应用。

我们将我们的 Play 2 网络应用打包成 Docker 镜像,并将其作为 ZIP 包上传到 Elastic Beanstalk。我们将主要通过网络浏览器与 AWS 管理控制台进行交互。

确保您已在以下位置注册了 AWS 账户:

aws.amazon.com

如何操作...

  1. 使用 activator 模板创建我们的 Play 2 网络应用computer-database-scala

    <span class="strong"><strong>    activator new play2-deploy-72 computer-database-scala</strong></span>
    
  2. 编辑conf/application.conf以启用自动数据库演变:

    <span class="strong"><strong>    applyEvolutions.default=true</strong></span>
    
  3. 编辑build.sbt以指定网络应用的 Docker 设置,注意维护者和dockerExposedPorts设置,分别使用您的 Docker Hub 用户名和网络应用端口号:

    import NativePackagerKeys._
        import com.typesafe.sbt.SbtNativePackager._
         name := """play2-deploy-72"""
         version := "0.0.1-SNAPSHOT"
         scalaVersion := "2.11.4"
         maintainer := "ginduc"
         dockerExposedPorts in Docker := Seq(9000)
         libraryDependencies ++= Seq(
          jdbc,
          anorm,
          "org.webjars" % "jquery" % "2.1.1",
          "org.webjars" % "bootstrap" % "3.3.1"
        )     
         lazy val root = (project in file(".")).enablePlugins(PlayScala)
    
  4. 在项目根目录中创建一个新的 JSON 配置文件,专门针对 Elastic Beanstalk 和 Docker 应用,文件名为Dockerrun.aws.json.template,内容如下:

    {
          "AWSEBDockerrunVersion": "1",
          "Ports": [
            {
              "ContainerPort": "9000"
            }
          ]
        }
    
  5. 使用activator生成 Docker 镜像:

    <span class="strong"><strong>    $ activator clean docker:stage</strong></span>
    
  6. Dockerrun.aws.json.template文件复制到target/docker中的 Docker 根目录:

    <span class="strong"><strong>    $ cp Dockerrun.aws.json.template target/docker/Dockerrun.aws.json</strong></span>
    
  7. target/docker目录打包,以便上传到 Elastic Beanstalk。注意输出 ZIP 文件的位置:

    <span class="strong"><strong>    $ cd target/docker &amp;&amp;  zip -r ../play2-deploy-72.zip .</strong></span>
    
  8. 一旦docker包准备就绪,前往 AWS 管理控制台创建一个 Elastic Beanstalk (EB)应用程序,确保我们选择 Docker 作为平台!Qin8fjTn

  9. 一旦初始化了初始 EB 应用程序,我们需要导航到配置标签,点击实例旁边的齿轮图标来编辑环境并选择不同的 EC2 实例类型,选择t2.medium而不是t2.micro(选择除t2.micro之外的实例类型意味着您将使用非免费 EC2 实例):csOFqb5O

  10. 一旦应用了此环境更改,导航回仪表板页面,以便我们可以上传我们之前构建的docker包:

  11. 点击部署

  12. 指定版本标签和部署限制;在我们的菜谱中,我们将保持默认设置。上传和初始化可能需要几分钟,但一旦 Play 2 网络应用程序已部署,我们就可以通过仪表板标签下默认环境旁边的链接使用网络浏览器访问它!dU0SP7IB

工作原理...

在这个菜谱中,我们将 Play 2 网络应用程序部署到了 AWS Elastic Beanstalk。我们将 Play 2 网络应用程序打包成 Docker 镜像,并打包后通过网络浏览器上传到 AWS 管理控制台。有关 sbt 和 Docker 集成的更多信息,请在此处查看:

github.com/sbt/sbt-native-packager

我们对build.sbt文件进行了非常小的修改,以指定在构建阶段所需的特定 Docker 设置:

import NativePackagerKeys._
    import com.typesafe.sbt.SbtNativePackager._
    /* ... */
     maintainer := "ginduc"
     dockerExposedPorts in Docker := Seq(9000)

我们导入了包含 sbt 和 Docker 集成的必要包,并将它们放在文件顶部,build.sbt。然后我们指定了 docker 容器将使用的维护者和端口号,在这个菜谱中,端口号为 9000。

我们在项目根目录中添加了一个 Elastic Beanstalk-Docker 配置文件,Dockerrun.aws.json,该文件指定了运行版本和我们将要使用的 docker 容器端口,格式为 JSON:

{
      "AWSEBDockerrunVersion": "1",
      "Ports": [
        {
          "ContainerPort": "9000"
        }
      ]
    }

值得注意的是,最后的部署配置步骤是修改 Elastic Beanstalk 默认设置的 EC2 实例类型,从t2.micro改为t2.medium实例类型。t2.medium实例是一个非免费 EC2 实例,这意味着您将使用此 EC2 实例类型时产生费用。这是必要的,因为我们可能会在使用小于 2GB RAM 的实例类型时遇到 JVM 问题。应用此环境更改将需要 Elastic Beanstalk 重新初始化环境,并可能需要几分钟才能完成。然后我们可以继续使用 AWS 管理控制台上传和部署我们预先打包的 Docker 镜像。

图片

一旦在 AWS 管理控制台中完成部署,我们就可以通过 Web 浏览器访问computer-database-scala web 应用程序的 Elastic Beanstalk URL。

更多内容...

请参考在线文档以获取有关 AWS Elastic Beanstalk 的更多信息:

AWS Elastic Beanstalk 开发者资源

在 CoreOS 和 Docker 上部署 Play 应用程序

在这个食谱中,我们将使用 CoreOS 和 Docker 部署一个 Play 2 web 应用程序。CoreOS是一个新的、轻量级的操作系统,非常适合现代应用程序堆栈。与 Docker 软件容器管理系统结合使用,这形成了一个强大的部署环境,为 Play 2 web 应用程序提供了简化的部署、进程隔离、易于扩展等功能。

对于这个食谱,我们将使用流行的云 IaaS,Digital Ocean。确保您在此处注册账户:

Digital Ocean 注册

此食谱还需要在开发者的机器上安装 Docker。有关安装的官方 Docker 文档请参阅:

Docker 安装指南

如何做到这一点...

  1. 使用 CoreOS 作为基础操作系统创建一个新的 Digital Ocean droplet。确保您使用至少有 1 GB RAM 的 droplet,以便食谱可以正常工作。请注意,Digital Ocean 没有免费层,所有都是付费实例图片

  2. 确保您选择了适当的 droplet 区域图片

  3. 选择CoreOS 607.0.0并指定要使用的SSH 密钥。如果您需要有关 SSH 密钥生成的更多信息,请访问以下链接:

    如何设置 SSH 密钥:

  4. 一旦创建 Droplet,请特别记录 Droplet 的 IP 地址,我们将使用它登录到 Droplet图片

  5. 接下来,在Docker.com上创建一个新的账户,hub.docker.com/account/signup/

  6. 创建一个新的存储库来存放我们将用于部署的play2-deploy-73 docker 镜像图片

  7. 使用 activator 模板computer-database-scala创建一个新的 Play 2 webapp,并切换到项目根目录:

    <span class="strong"><strong>    activator new play2-deploy-73 computer-database-scala &amp;&amp; cd  play2-deploy-73</strong></span>
    
  8. 编辑conf/application.conf以启用自动数据库演变:

    applyEvolutions.default=true
    
  9. 编辑build.sbt以指定 web 应用的 Docker 设置:

    import NativePackagerKeys._
        import com.typesafe.sbt.SbtNativePackager._
         name := """play2-deploy-73"""
         version := "0.0.1-SNAPSHOT"
         scalaVersion := "2.11.4"
         maintainer := "&lt;YOUR_DOCKERHUB_USERNAME HERE&gt;"
         dockerExposedPorts in Docker := Seq(9000)
         dockerRepository := Some("YOUR_DOCKERHUB_USERNAME HERE ")
         libraryDependencies ++= Seq(
          jdbc,
          anorm,
          "org.webjars" % "jquery" % "2.1.1",
          "org.webjars" % "bootstrap" % "3.3.1"
        )     
         lazy val root = (project in file(".")).enablePlugins(PlayScala)
    
  10. 接下来,我们构建 Docker 镜像并将其发布到 Docker Hub:

    <span class="strong"><strong>    $ activator clean docker:stage docker:publish</strong></span>
    <span class="strong"><strong>    ..</strong></span>
    <span class="strong"><strong>    [info] Step 0 : FROM dockerfile/java</strong></span>
    <span class="strong"><strong>    [info]  ---&gt; 68987d7b6df0</strong></span>
    <span class="strong"><strong>    [info] Step 1 : MAINTAINER ginduc</strong></span>
    <span class="strong"><strong>    [info]  ---&gt; Using cache</strong></span>
    <span class="strong"><strong>    [info]  ---&gt; 9f856752af9e</strong></span>
    <span class="strong"><strong>    [info] Step 2 : EXPOSE 9000</strong></span>
    <span class="strong"><strong>    [info]  ---&gt; Using cache</strong></span>
    <span class="strong"><strong>    [info]  ---&gt; 834eb5a7daec</strong></span>
    <span class="strong"><strong>    [info] Step 3 : ADD files /</strong></span>
    <span class="strong"><strong>    [info]  ---&gt; c3c67f0db512</strong></span>
    <span class="strong"><strong>    [info] Removing intermediate container 3b8d9c18545e</strong></span>
    <span class="strong"><strong>    [info] Step 4 : WORKDIR /opt/docker</strong></span>
    <span class="strong"><strong>    [info]  ---&gt; Running in 1b150e98f4db</strong></span>
    <span class="strong"><strong>    [info]  ---&gt; ae6716cd4643</strong></span>
    <span class="strong"><strong>    [info] Removing intermediate container 1b150e98f4db</strong></span>
    <span class="strong"><strong>    [info] Step 5 : RUN chown -R daemon .</strong></span>
    <span class="strong"><strong>    [info]  ---&gt; Running in 9299421b321e</strong></span>
    <span class="strong"><strong>    [info]  ---&gt; 8e15664b6012</strong></span>
    <span class="strong"><strong>    [info] Removing intermediate container 9299421b321e</strong></span>
    <span class="strong"><strong>    [info] Step 6 : USER daemon</strong></span>
    <span class="strong"><strong>    [info]  ---&gt; Running in ea44f3cc8e11</strong></span>
    <span class="strong"><strong>    [info]  ---&gt; 5fd0c8a22cc7</strong></span>
    <span class="strong"><strong>    [info] Removing intermediate container ea44f3cc8e11</strong></span>
    <span class="strong"><strong>    [info] Step 7 : ENTRYPOINT bin/play2-deploy-73</strong></span>
    <span class="strong"><strong>    [info]  ---&gt; Running in 7905c6e2d155</strong></span>
    <span class="strong"><strong>    [info]  ---&gt; 47fded583dd7</strong></span>
    <span class="strong"><strong>    [info] Removing intermediate container 7905c6e2d155</strong></span>
    <span class="strong"><strong>    [info] Step 8 : CMD</strong></span>
    <span class="strong"><strong>    [info]  ---&gt; Running in b807e6360631</strong></span>
    <span class="strong"><strong>    [info]  ---&gt; c3e1999cfbfd</strong></span>
    <span class="strong"><strong>    [info] Removing intermediate container b807e6360631</strong></span>
    <span class="strong"><strong>    [info] Successfully built c3e1999cfbfd</strong></span>
    <span class="strong"><strong>    [info] Built image ginduc/play2-deploy-73:0.0.2-SNAPSHOT</strong></span>
    <span class="strong"><strong>    [info] The push refers to a repository [ginduc/play2-deploy-73] (len: 1)</strong></span>
    <span class="strong"><strong>    [info] Sending image list</strong></span>
    <span class="strong"><strong>    [info] Pushing repository ginduc/play2-deploy-73 (1 tags)</strong></span>
    <span class="strong"><strong>    [info] Pushing tag for rev [c3e1999cfbfd] on {https://cdn-registry-1.docker.io/v1/repositories/ginduc/play2-deploy-73/tags/0.0.2-SNAPSHOT}</strong></span>
    <span class="strong"><strong>    [info] Published image ginduc/play2-deploy-73:0.0.2-SNAPSHOT</strong></span>
    
  11. 一旦 Docker 镜像发布,使用 SSH 登录到 Digital Ocean droplet 以拉取上传的 docker 镜像。您需要使用core用户登录您的 CoreOS Droplet:

    <span class="strong"><strong>    ssh core@&lt;DROPLET_IP_ADDRESS HERE&gt;</strong></span>
    <span class="strong"><strong>    core@play2-deploy-73 ~ $ docker pull &lt;YOUR_DOCKERHUB_USERNAME HERE&gt;/play2-deploy-73:0.0.1-SNAPSHOT</strong></span>
    <span class="strong"><strong>    Pulling repository ginduc/play2-deploy-73</strong></span>
    <span class="strong"><strong>    6045dfea237d: Download complete</strong></span>
    <span class="strong"><strong>    511136ea3c5a: Download complete</strong></span>
    <span class="strong"><strong>    f3c84ac3a053: Download complete</strong></span>
    <span class="strong"><strong>    a1a958a24818: Download complete</strong></span>
    <span class="strong"><strong>    709d157e1738: Download complete</strong></span>
    <span class="strong"><strong>    d68e2305f8ed: Download complete</strong></span>
    <span class="strong"><strong>    b87155bee962: Download complete</strong></span>
    <span class="strong"><strong>    2097f889870b: Download complete</strong></span>
    <span class="strong"><strong>    5d2fb9a140e9: Download complete</strong></span>
    <span class="strong"><strong>    c5bdb4623fac: Download complete</strong></span>
    <span class="strong"><strong>    68987d7b6df0: Download complete</strong></span>
    <span class="strong"><strong>    9f856752af9e: Download complete</strong></span>
    <span class="strong"><strong>    834eb5a7daec: Download complete</strong></span>
    <span class="strong"><strong>    fae5f7dab7bb: Download complete</strong></span>
    <span class="strong"><strong>    ee5ccc9a9477: Download complete</strong></span>
    <span class="strong"><strong>    74b51b6dcfe7: Download complete</strong></span>
    <span class="strong"><strong>    41791a2546ab: Download complete</strong></span>
    <span class="strong"><strong>    8096c6beaae7: Download complete</strong></span>
    <span class="strong"><strong>    Status: Downloaded newer image for &lt;YOUR_DOCKERHUB_USERNAME HERE&gt;/play2-deploy-73:0.0.2-SNAPSHOT</strong></span>
    
  12. 现在我们已经准备好使用以下docker命令运行我们的 Docker 镜像:

    <span class="strong"><strong>    core@play2-deploy-73 ~ $ docker run -p 9000:9000 &lt;YOUR_DOCKERHUB_USERNAME_HERE&gt;/play2-deploy-73:0.0.1-SNAPSHOT</strong></span>
    
  13. 使用网页浏览器,通过我们在本菜谱早期步骤中记录的 IP 地址(http://192.241.239.43:9000/computers)访问 computer-database 网页应用:

它是如何工作的...

在这个菜谱中,我们通过打包成 Docker 镜像并在 Digital Ocean Droplet 中安装和运行相同的 Docker 镜像来部署 Play 2 网页应用。首先,我们需要在 DigitalOcean.comDocker.com 上有一个账户。

一旦我们的账户准备就绪并经过验证,我们创建一个基于 CoreOS 的 droplet。CoreOS 默认安装了 Docker,所以我们只需要在 droplet 中安装 Play 2 网页应用 Docker 镜像。

Play 2 网页应用 Docker 镜像基于 activator 模板,computer-database-scala,我们将其命名为 play2-deploy-73

我们对样板代码进行了两项修改。第一项修改在 conf/application.conf 中:

applyEvolutions.default=true

此设置默认启用数据库演变。其他修改需要在 build.sbt 中进行。我们导入包含 Docker 特定设置的所需包:

import NativePackagerKeys._
    import com.typesafe.sbt.SbtNativePackager._

下一个设置是指定存储库维护者、暴露的 Docker 端口和 Docker.com 上的 Docker 存储库;在这种情况下,将您自己的 Docker Hub 用户名作为维护者和 Docker 存储库值:

maintainer := "&lt;YOUR DOCKERHUB_USERNAME&gt;"
     dockerExposedPorts in Docker := Seq(9000)
     dockerRepository := Some("&lt;YOUR_DOCKERHUB_USERNAME&gt;")

我们现在可以使用 activator 命令构建 Docker 镜像,这将生成构建 Docker 镜像所需的所有文件:

<span class="strong"><strong>    activator clean docker:stage</strong></span>

现在,我们将使用 activator docker 命令上传并发布到您指定的 Docker.com 存储库:

<span class="strong"><strong>    activator clean docker:publish</strong></span>

要在我们的 Digital Ocean Droplet 中安装 Docker 镜像,我们首先使用 core 用户登录到 droplet:

<span class="strong"><strong>    ssh core@&lt;DROPLET_IP_ADDRESS&gt;</strong></span>

然后,我们使用 docker pull 命令从 Docker.com 下载 play2-deploy-73 镜像,指定标签:

<span class="strong"><strong>    docker pull &lt;YOUR_DOCKERHUB_USERNAME&gt;/play2-deploy-73:0.0.1-SNAPSHOT</strong></span>

最后,我们可以使用 docker run 命令运行 Docker 镜像,暴露容器端口 9000

<span class="strong"><strong>    docker run -p 9000:9000 &lt;YOUR_DOCKERHUB_USERNAME&gt;/play2-deploy-73:0.0.1-SNAPSHOT</strong></span>

更多内容...

参考以下链接获取有关 Docker 和 Digital Ocean 的更多信息:

使用 Dokku 部署 Play 应用程序

在这个菜谱中,我们将使用基于 Docker 的工具 Dokku 来管理我们的 Play 2 网页应用部署。Dokku 提供了一个非常直接的部署界面,与 Heroku 的部署界面非常相似,并允许开发者快速部署 Play 2 网页应用。

我们将运行 Dokku 并在 Digital Ocean Droplet 中部署我们的 Play 2 网页应用。我们需要一个至少有 2GB RAM 的 Droplet 来运行我们的示例网页应用。确保您注册了 Digital Ocean 账户以遵循此菜谱:

cloud.digitalocean.com/registrations/new

如何操作...

  1. 通过创建一个新的 2GB RAM Droplet 并预安装 Dokku v0.3.15 on 14.04 或更高版本的应用选项来准备你的部署 Dropletimg/SD7Arw3g.jpg

  2. 一旦 Droplet 创建完成,你可以在仪表板上验证 Droplet 设置,确保它配置了 2GB 的 RAM,并记录分配给 Droplet 的 IP 地址img/e6asnGU0.jpg

  3. 接下来,我们需要通过访问新创建的 Droplet 的网络端口来完成 Dokku 安装;这可以通过使用网络浏览器访问 Droplet 分配的 IP 地址来完成,以确认 Dokku 设置,如你的公钥、主机名等img/xAfGBbSl.jpg

  4. 一旦我们的 Droplet 设置完成,我们需要使用 activator 模板 computer-database-scala 在本地开发机器上准备我们的 Play 2 网络应用,并切换到项目根目录:

    <span class="strong"><strong>    $ activator new play2-deploy-74 computer-database-scala &amp;&amp; cd  play2-deploy-74</strong></span>
    
  5. 编辑 conf/application.conf 以启用自动数据库演变:

    applyEvolutions.default=true
    
  6. 我们需要在项目根目录上初始化 git:

    <span class="strong"><strong>    git init &amp;&amp; git add &#x2013;-all &amp;&amp; git commit -am "initial"</strong></span>
    
  7. 现在我们将向代码库添加一个新的 git 远程,指向我们的 Dokku Droplet:

    <span class="strong"><strong>    git remote add dokku dokku@&lt;YOUR_DOKKU_IP_ADDRESS&gt;::play2-deploy-74</strong></span>
    
  8. 作为最后的部署步骤,我们将我们的提交推送到我们的 Dokku 远程。Dokku 将然后自动使用 git post-commit hooks 来部署网络应用:

    <span class="strong"><strong>    $ git push dokku master</strong></span>
    <span class="strong"><strong>    Counting objects: 5, done.</strong></span>
    <span class="strong"><strong>    Delta compression using up to 8 threads.</strong></span>
    <span class="strong"><strong>    Compressing objects: 100% (3/3), done.</strong></span>
    <span class="strong"><strong>    Writing objects: 100% (3/3), 293 bytes | 0 bytes/s, done.</strong></span>
    <span class="strong"><strong>    Total 3 (delta 2), reused 0 (delta 0)</strong></span>
    <span class="strong"><strong>    -----&gt; Cleaning up...</strong></span>
    <span class="strong"><strong>    -----&gt; Building play2-deploy-74 from buildstep...</strong></span>
    <span class="strong"><strong>    -----&gt; Adding BUILD_ENV to build environment...</strong></span>
    <span class="strong"><strong>    -----&gt; Play 2.x - Scala app detected</strong></span>
    <span class="strong"><strong>    -----&gt; Installing OpenJDK 1.6...done</strong></span>
    <span class="strong"><strong>    -----&gt; Running: sbt compile stage</strong></span>
    <span class="strong"><strong>    ..</strong></span>
    <span class="strong"><strong>    -----&gt; Dropping ivy cache from the slug</strong></span>
    <span class="strong"><strong>    -----&gt; Dropping compilation artifacts from the slug</strong></span>
    <span class="strong"><strong>    -----&gt; Discovering process types</strong></span>
    <span class="strong"><strong>       Default process types for Play 2.x - Scala -&gt; web</strong></span>
    <span class="strong"><strong>    -----&gt; Releasing play2-deploy-74...</strong></span>
    <span class="strong"><strong>    -----&gt; Deploying play2-deploy-74...</strong></span>
    <span class="strong"><strong>    -----&gt; Running pre-flight checks</strong></span>
    <span class="strong"><strong>       check-deploy: /home/dokku/play2-deploy-74/CHECKS not found. attempting to retrieve it from container ...</strong></span>
    <span class="strong"><strong>       CHECKS file not found in container. skipping checks.</strong></span>
    <span class="strong"><strong>    -----&gt; Running post-deploy</strong></span>
    <span class="strong"><strong>    -----&gt; NO_VHOST config detected</strong></span>
    <span class="strong"><strong>    -----&gt; Shutting down old container in 60 seconds</strong></span>
    <span class="strong"><strong>    =====&gt; Application deployed:</strong></span>
    <span class="strong"><strong>       http://&lt;YOUR_DOKKU_IP&gt;:&lt;YOUR_DOKKU_ASSIGNED PORT</strong></span>
     <span class="strong"><strong>    To dokku@192.241.239.43:play2-deploy-74</strong></span>
    <span class="strong"><strong>       77a951d..a007b6b  master -&gt; master</strong></span>
    

我们现在可以使用网络浏览器访问我们的 Dokku 部署的 Play 2 网络应用:http://<YOUR_DOKKU_IP_ADDRESS>:<YOUR_DOKKU_PORT>

img/bu8g5ACy.jpg

它是如何工作的...

在这个菜谱中,我们探讨了使用 Dokku 在 Digital Ocean Droplet 上部署我们的 Play 2 网络应用。经过一些非常直接的初始化和配置后,我们能够以最小的摩擦和轻松的方式部署我们的 Play 2 网络应用。

这个菜谱的关键是预先在虚拟机上安装 Dokku。基于 Docker 的部署工具 Dokku 为开发者提供了一个非常简单的部署过程,最终归结为一个 git push 命令:

<span class="strong"><strong>    $ git push dokku master</strong></span>

Dokku 配置了 git post-commit hooks 来检测对代码库的提交,随后对每个检测到的代码提交运行部署脚本。

<span class="strong"><strong>    -----&gt; Discovering process types</strong></span>
<span class="strong"><strong>           Default process types for Play 2.x - Scala -&gt; web</strong></span>
<span class="strong"><strong>    -----&gt; Releasing play2-deploy-74...</strong></span>
<span class="strong"><strong>    -----&gt; Deploying play2-deploy-74...</strong></span>
<span class="strong"><strong>    -----&gt; Running pre-flight checks</strong></span>
<span class="strong"><strong>           check-deploy: /home/dokku/play2-deploy-74/CHECKS not found. attempting to retrieve it from container ...</strong></span>
<span class="strong"><strong>           CHECKS file not found in container. skipping checks.</strong></span>
<span class="strong"><strong>    -----&gt; Running post-deploy</strong></span>
<span class="strong"><strong>    -----&gt; NO_VHOST config detected</strong></span>
<span class="strong"><strong>    -----&gt; Shutting down old container in 60 seconds</strong></span>
<span class="strong"><strong>    =====&gt; Application deployed:</strong></span>
<span class="strong"><strong>           http://192.241.239.43:49154</strong></span>

这种易用性使得开发者和非开发者都能快速启动开发和测试实例。

然而,要到达这个开发生命周期中的这个点,我们需要遵循一些步骤来初始化和配置我们的 Dokku 设置。对于这个菜谱,我们依赖于流行的云基础虚拟机提供商 Digital Ocean 和其预定义的 Dokku 应用程序实例来启动一个可用的 Dokku VM 实例。

下一步必要的步骤是配置我们的 Play 2 网络应用以启用 git,通过初始化代码库,将所有代码库文件添加到 git 仓库,并提交网络应用的初始状态:

<span class="strong"><strong>    $ git init &amp;&amp; git add &#x2013;all &amp;&amp; git commit -am "initial"</strong></span>

此外,为我们的示例应用添加一些必要的应用程序配置,特别是 conf/application.conf 文件,包含以下设置:

applyEvolutions.default=true

我们还需要将此更改提交到本地仓库:

<span class="strong"><strong>    $ git commit -am "enabled automatic db evolutions"</strong></span>

现在的最终步骤是将我们的提交推送到我们刚刚创建的 Dokku 实例,这将触发部署:

<span class="strong"><strong>    $ git push dokku master</strong></span>

更多内容...

有关 Dokku 的更多信息,请参阅以下链接:

部署使用 Nginx 的 Play 应用程序

在这个菜谱中,我们将手动使用基于 CentOS 6.5 的虚拟机部署 Play 2 网络应用程序,其中Nginx作为我们的 Play 2 网络应用程序的前端服务器。我们将使用 Digital Ocean;请确保您在此处注册账户:

cloud.digitalocean.com/registrations/new

如何做到这一点...

  1. 登录 Digital Ocean 并创建一个新的 Droplet,选择 CentOS 作为基础操作系统,并至少有 2GB 的 RAM:

  2. 选择CentOS 6.5 x64作为 Droplet 镜像,并指定您的SSH 密钥

  3. 一旦创建了水滴,请特别记下虚拟机的 IP 地址:

  4. 使用 SSH 登录我们新创建的基于 Centos 的 Droplet:

    <span class="strong"><strong>    ssh root@&lt;YOUR_DROPLET_IP_ADDRESS&gt;</strong></span>
    
  5. 使用adduser命令创建非 root 用户,使用passwd命令为其分配密码,最后将我们的新用户添加到sudoers组,该组是具有特殊系统权限的用户列表:

    <span class="strong"><strong>    $ adduser deploy</strong></span>
    <span class="strong"><strong>    $ passwd deploy</strong></span>
    <span class="strong"><strong>    $ echo "deploy ALL=(ALL) ALL" &gt;&gt; /etc/sudoers</strong></span>
    
  6. 使用非 root 用户重新登录我们的 Droplet:

    <span class="strong"><strong>    ssh deploy@&lt;YOUR_DROPLET_IP_ADDRESS&gt;</strong></span>
    
  7. 使用Yum,CentOS 包管理器安装必要的工具,以继续我们的虚拟机设置:

    <span class="strong"><strong>    sudo yum install -y yum-plugin-fastestmirror</strong></span>
    <span class="strong"><strong>    sudo yum install -y git unzip wget</strong></span>
    
  8. 使用以下命令安装 JDK:

    <span class="strong"><strong>    curl -LO 'http://download.oracle.com/otn-pub/java/jdk/7u51-b13/jdk-7u51-linux-x64.rpm' -H 'Cookie: oraclelicense=accept-securebackup-cookie'</strong></span>
    <span class="strong"><strong>    sudo rpm -i jdk-7u51-linux-x64.rpm</strong></span>
    <span class="strong"><strong>    sudo /usr/sbin/alternatives --install /usr/bin/java java /usr/java/default/bin/java 200000</strong></span>
    
  9. 验证 Oracle JDK 是否已安装且可访问:

    <span class="strong"><strong>    # Verify installed jdk</strong></span>
    <span class="strong"><strong>    $ java -version</strong></span>
    <span class="strong"><strong>    java version "1.7.0_51"</strong></span>
    <span class="strong"><strong>    Java(TM) SE Runtime Environment (build 1.7.0_51-b13)</strong></span>
    <span class="strong"><strong>    Java HotSpot(TM) 64-Bit Server VM (build 24.51-b03, mixed mode)</strong></span>
    
  10. 使用以下命令安装activator

    <span class="strong"><strong>    cd ~</strong></span>
    <span class="strong"><strong>    wget http://downloads.typesafe.com/typesafe-activator/1.3.2/typesafe-activator-1.3.2-minimal.zip</strong></span>
    <span class="strong"><strong>    unzip typesafe-activator-1.3.2-minimal.zip</strong></span>
    <span class="strong"><strong>    chmod u+x ~/activator-1.3.2-minimal/activator</strong></span>
    <span class="strong"><strong>    export PATH=$PATH:~/activator-1.3.2-minimal/</strong></span>
    

使用以下命令添加官方 Nginx yum 仓库:

<span class="strong"><strong>    echo -e "[nginx]\nname=nginx repo\nbaseurl=http://nginx.org/packages/centos/\$releasever/\$basearch/\ngpgcheck=0\nenabled=1" &gt; /tmp/nginx.repo &amp;&amp; sudo mv /tmp/nginx.repo /etc/yum.repos.d/nginx.repo</strong></span>
  1. 使用yum安装 Nginx:

    <span class="strong"><strong>    sudo yum install -y nginx</strong></span>
    
  2. 使用以下内容将自定义配置文件添加到/etc/nginx/conf.d/computer-database-scala.conf

    upstream playapp1 {
          server 127.0.0.1:9000;
        }
         server {
          listen       80;
          server_name computer-database-scala.com;
          location / {
            proxy_pass http://playapp1;
          }
        }
    
  3. 重启 Nginx 以加载我们的新配置:

    <span class="strong"><strong>    sudo service nginx restart</strong></span>
    
  4. 配置iptables仅允许访问最小的一组端口(端口 22 和 80):

    <span class="strong"><strong>    sudo /sbin/iptables -P INPUT ACCEPT</strong></span>
    <span class="strong"><strong>    sudo /sbin/iptables -F</strong></span>
    <span class="strong"><strong>    sudo /sbin/iptables -A INPUT -i lo -j ACCEPT</strong></span>
    <span class="strong"><strong>    sudo /sbin/iptables -A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT</strong></span>
    <span class="strong"><strong>    sudo /sbin/iptables -A INPUT -p tcp --dport 22 -j ACCEPT</strong></span>
    <span class="strong"><strong>    sudo /sbin/iptables -A INPUT -p tcp --dport 80 -j ACCEPT</strong></span>
    <span class="strong"><strong>    sudo /sbin/iptables -P INPUT DROP</strong></span>
    <span class="strong"><strong>    sudo /sbin/iptables -P FORWARD DROP</strong></span>
    <span class="strong"><strong>    sudo /sbin/iptables -P OUTPUT ACCEPT</strong></span>
    <span class="strong"><strong>    sudo /sbin/iptables -L -v</strong></span>
    <span class="strong"><strong>    sudo /sbin/service iptables save</strong></span>
    <span class="strong"><strong>    sudo /sbin/service iptables restart</strong></span>
    <span class="strong"><strong>    sudo /sbin/service iptables status</strong></span>
    
  5. ~/apps目录中克隆computer-database-scala Play2 网络应用程序:

    <span class="strong"><strong>    $ mkdir ~/apps &amp;&amp; cd $_ &amp;&amp; git clone https://github.com/typesafehub/activator-computer-database-scala.git</strong></span>
    
  6. 编辑conf/application.conf以启用自动数据库演变:

    applyEvolutions.default=true
    
  7. 我们现在可以使用activator启动 web 应用程序:

    <span class="strong"><strong>    $ activator start</strong></span>
    <span class="strong"><strong>    [info] Loading project definition from /home/deploy/apps/activator-computer-database-scala/project</strong></span>
    <span class="strong"><strong>    [info] Set current project to computer-database-scala (in build file:/home/deploy/apps/activator-computer-database-scala/)</strong></span>
    <span class="strong"><strong>    [info] Packaging /home/deploy/apps/activator-computer-database-scala/target/scala-2.11/computer-database-scala_2.11-0.0.1-SNAPSHOT-sources.jar ...</strong></span>
    <span class="strong"><strong>    [info] Done packaging.</strong></span>
    <span class="strong"><strong>    [warn] There may be incompatibilities among your library dependencies.</strong></span>
    <span class="strong"><strong>    [warn] Here are some of the libraries that were evicted:</strong></span>
    <span class="strong"><strong>    [warn]   * org.webjars:jquery:1.11.1 -&gt; 2.1.1</strong></span>
    <span class="strong"><strong>    [warn] Run 'evicted' to see detailed eviction warnings</strong></span>
    <span class="strong"><strong>    [info] Wrote /home/deploy/apps/activator-computer-database-scala/target/scala-2.11/computer-database-scala_2.11-0.0.1-SNAPSHOT.pom</strong></span>
    <span class="strong"><strong>    [info] Packaging /home/deploy/apps/activator-computer-database-scala/target/scala-2.11/computer-database-scala_2.11-0.0.1-SNAPSHOT.jar ...</strong></span>
    <span class="strong"><strong>    [info] Done packaging.</strong></span>
     <span class="strong"><strong>    (Starting server. Type Ctrl+D to exit logs, the server will remain in background)</strong></span>
     <span class="strong"><strong>    Play server process ID is 3094</strong></span>
    <span class="strong"><strong>    [info] play - database [default] connected at jdbc:h2:mem:play</strong></span>
    <span class="strong"><strong>    [info] play - Application started (Prod)</strong></span>
    <span class="strong"><strong>    [info] play - Listening for HTTP on /0:0:0:0:0:0:0:0:9000</strong></span>
    
  8. 使用网络浏览器,我们现在可以通过 Droplet 的 IP 地址访问 Play 2 网络应用程序,在我们的例子中,http://&lt;YOUR_DROPLET_IP ADDRESS&gt;/computers

我们可以使用curl和响应头验证 Nginx 确实在为我们提供 HTTP 请求服务:

<span class="strong"><strong>    $ curl -v http://192.241.239.43/computers</strong></span>
<span class="strong"><strong>    * Hostname was NOT found in DNS cache</strong></span>
<span class="strong"><strong>    *   Trying 192.241.239.43...</strong></span>
<span class="strong"><strong>    * Connected to 192.241.239.43 (192.241.239.43) port 80 (#0)</strong></span>
<span class="strong"><strong>    &gt; GET /computers HTTP/1.1</strong></span>
<span class="strong"><strong>    &gt; User-Agent: curl/7.37.1</strong></span>
<span class="strong"><strong>    &gt; Host: 192.241.239.43</strong></span>
<span class="strong"><strong>    &gt; Accept: */*</strong></span>
<span class="strong"><strong>    &gt;</strong></span>
<span class="strong"><strong>    &lt; HTTP/1.1 200 OK</strong></span>
<span class="strong"><strong>    * Server nginx/1.6.2 is not blacklisted</strong></span>
<span class="strong"><strong>    &lt; Server: nginx/1.6.2</strong></span>
<span class="strong"><strong>    &lt; Date: Tue, 24 Mar 2015 08:23:04 GMT</strong></span>
<span class="strong"><strong>    &lt; Content-Type: text/html; charset=utf-8</strong></span>
<span class="strong"><strong>    &lt; Content-Length: 7371</strong></span>
<span class="strong"><strong>    &lt; Connection: keep-alive</strong></span>

它是如何工作的...

在这个菜谱中,我们手动在远程基于 CentOS 的虚拟机上部署了 Play 2 网络应用程序,我们使用 Digital Ocean 初始化了它。然后我们继续安装和配置各种软件组件,如 Java 开发工具包、Nginx、IPTables 等:

<span class="strong"><strong>    $ java -version</strong></span>
<span class="strong"><strong>    java version "1.7.0_51"</strong></span>
 <span class="strong"><strong>    $ activator --version</strong></span>
<span class="strong"><strong>    sbt launcher version 0.13.8-M5</strong></span>
 <span class="strong"><strong>    $ nginx -v</strong></span>
<span class="strong"><strong>    nginx version: nginx/1.6.2</strong></span>
 <span class="strong"><strong>    $ iptables --version</strong></span>
<span class="strong"><strong>    iptables v1.4.7</strong></span>
<span class="strong"><strong>    $ curl --version</strong></span>
<span class="strong"><strong>    curl 7.19.7</strong></span>
 <span class="strong"><strong>    $ git --version</strong></span>
<span class="strong"><strong>    git version 1.9.5</strong></span>

一旦我们安装并配置了必要的服务,我们需要启动我们的 Play 2 网络应用程序:

<span class="strong"><strong>    $ cd apps/activator-computer-database-scala/ &amp;&amp; activator clean start</strong></span>
<span class="strong"><strong>    Play server process ID is 3917</strong></span>
<span class="strong"><strong>    [info] play - database [default] connected at jdbc:h2:mem:play</strong></span>
<span class="strong"><strong>    [info] play - Application started (Prod)</strong></span>
<span class="strong"><strong>    [info] play - Listening for HTTP on /0:0:0:0:0:0:0:0:9000</strong></span>

我们随后成功地在适当的端口,即端口 80,使用网页浏览器和curl访问了我们部署的 Play 2 网络应用程序。我们还可以通过尝试使用curl直接访问端口 9000 来验证,我们只能通过 Nginx 访问 Play 2 网络应用程序:

<span class="strong"><strong>    $ curl -v http://192.241.239.43:9000/computers</strong></span>
<span class="strong"><strong>    * Hostname was NOT found in DNS cache</strong></span>
<span class="strong"><strong>    *   Trying 192.241.239.43...</strong></span>
<span class="strong"><strong>    * connect to 192.241.239.43 port 9000 failed: Operation timed out</strong></span>
<span class="strong"><strong>    * Failed to connect to 192.241.239.43 port 9000: Operation timed out</strong></span>
<span class="strong"><strong>    * Closing connection 0</strong></span>
<span class="strong"><strong>    curl: (7) Failed to connect to 192.241.239.43 port 9000: Operation timed out</strong></span>

在本章中,我们展示了手动部署 Play 2 应用程序是多么复杂,而与之相比,使用云服务如 Heroku、AWS Beanstalk 以及 Docker 和 Dokku 等工具在云中部署 Play 2 应用程序是多么方便和简单。虽然手动部署 Play 网络应用程序可能有其优点,但毫无疑问,云 PAAS 服务极大地提高了开发者的生产力和效率,并允许开发者专注于实际的软件开发。

第八章。额外的 Play 信息

本章将涵盖以下食谱:

  • 使用 Travis CI 进行测试

  • 使用 New Relic 进行监控

  • 将 Play 应用程序与 AngularJS 集成

  • 将 Play 应用程序与 Parse 集成

  • 使用 Vagrant 创建 Play 开发环境

  • 使用 IntelliJ IDEA 14 编写 Play 2 网络应用

简介

在本章中,我们将探讨 Play 的额外食谱,开发者会发现这些食谱在他们的工具箱中非常实用和有用。我们将涉及 Play 2.0 网络应用的自动化测试和监控工具,这些是现代网络应用必不可少的辅助工具。我们还将探讨将 AngularJS 前端集成以及集成Parse.com,一个后端即服务BaaS),以在 Play 网络应用中管理我们的数据。

最后,我们将探讨使用流行的工具Vagrant自动化创建 Play 开发环境,允许开发者创建可共享和更便携的开发环境。

使用 Travis CI 进行测试

对于这个食谱,我们将探讨如何使用Travis CI为 Play 2.0 网络应用构建和运行自动化测试。我们需要在 Travis CI 上注册一个账户,并与 GitHub 账户一起注册。我们还将配置我们的 Travis 账户,使其连接到一个 GitHub 仓库,以便在代码提交上进行自动测试。

如何做到这一点…

对于这个食谱,您需要执行以下步骤:

  1. github.com/join创建一个 GitHub 账户:

  2. 在以下位置创建一个新的公共 GitHub 仓库,命名为 play2-travis:

    github.com/new

  3. 在您的开发机器上,使用activator模板play-slick-angular-test-example创建一个新的 Play 2.0 网络应用:

    <span class="strong"><strong>    activator new play2-travis play-slick-angular-test-example</strong></span>
    
  4. 编辑.travis.yml以触发我们的测试脚本:

    <span class="strong"><strong>    language: scala</strong></span>
    <span class="strong"><strong>    scala:</strong></span>
    <span class="strong"><strong>    - 2.11.2</strong></span>
    <span class="strong"><strong>    script:</strong></span>
    <span class="strong"><strong>    - sbt test</strong></span>
    
  5. 提交并推送到 GitHub 远程仓库(请特别注意您的 GitHub 用户名,并在以下命令中指定它):

    <span class="strong"><strong>    git add --all &amp;&amp; git commit -am "Initial" &amp;&amp; git remote add origin https://github.com/&lt;YOUR_GITHUB_USER&gt;/play2-travis.git &amp;&amp; git push origin master</strong></span>
    
  6. 使用您的 GitHub 账户注册 Travis 账户:

    travis-ci.org

  7. 在 Travis 同步您的 GitHub 仓库后,在以下位置启用 play2-travis 仓库的 Travis 构建:

    https://travis-ci.org/profile/<YOUR_GITHUB_USER>:

  8. 接下来,通过添加一个示例测试失败来修改test/controllers/ReportSpec.scala

    "testing a failure" in {
          true must equalTo(false)
        }
    
  9. 提交并推送以触发具有预期测试失败的 Travis 构建:

    <span class="strong"><strong>    git commit -am "With expected test failure" &amp;&amp; git push origin master</strong></span>
    
  10. 这应该在几秒钟后触发我们的构建。我们预计我们的第一次构建将失败,并且应该看到以下类似的结果:

    <span class="strong"><strong>    [info] x testing a failure</strong></span>
    <span class="strong"><strong>    [error]  the value is not equal to 'false' (ReportSpec.scala:58)</strong></span>
    

  11. 现在,在test/controllers/ReportSpec.scala中取消注释示例测试失败:

    /*"testing a failure" in {
          true must equalTo(false)
        }*/
    
  12. 让我们提交并推送这些最新的更改,这次我们期望 Travis 构建通过:

    <span class="strong"><strong>    git commit -am "disabling failing test" &amp;&amp; git push origin master</strong></span>
    
  13. 这个提交应该再次触发 Travis 的构建,这次我们应该在我们的 Travis 仪表板上看到所有测试都通过。

它是如何工作的…

在这个配方中,我们使用了 Travis CI 在我们的链接 GitHub 存储库中构建和执行测试。这种设置使我们能够建立 开发-提交-测试 流程。此设置需要 Travis CI 和 GitHub 的用户账户。

一旦我们确定了 GitHub 存储库,我们希望与 Travis 集成。我们需要更新项目根目录中的 Travis 配置(.travis.yml),指定一个运行脚本以执行我们的网络应用程序测试:

<span class="strong"><strong>    script:</strong></span>
<span class="strong"><strong>    - sbt test</strong></span>

这是 Travis 在运行我们的测试时执行的命令。Travis 将根据 .travis.yml 配置文件(在我们的配方中,运行 sbt 任务测试)中的设置来配置构建,以执行我们的网络应用程序测试。构建结果在 Travis 的存储库仪表板上显示:

一旦我们将 GitHub 存储库链接并启用在 Travis 中,我们会观察到每次代码提交并推送到 GitHub 存储库后都会触发构建。这对于开发者来说是一个很好的开发工具和流程,可以帮助他们意识到最近提交的代码中的回归问题,并支持其他构建工具,如工件发布和通知。

使用 New Relic 进行监控

对于这个配方,我们将使用 Docker 和 Digital Ocean 部署 Play 2.0 网络应用程序,并使用 New Relic 监控该网络应用程序。我们将以 Docker 容器的形式部署网络应用程序,并详细说明如何使用与我们的 activator 构建脚本集成的 New Relic JAR 文件来对 computer-database-scala 示例网络应用程序进行配置。

如何做到这一点...

对于这个配方,您需要执行以下步骤:

  1. 首先,在 newrelic.com/signup 注册 New Relic 账户

  2. 在 New Relic 仪表板中创建一个新的 Java 应用程序:

  3. 在创建 Java 应用程序期间,请注意您的 New Relic 许可证密钥:

  4. 接下来,下载 New Relic Java 代理 ZIP 文件,并记下下载位置。此 ZIP 文件应包含 Java 代理库、许可证密钥文件、API 文档和其他有用的 New Relic 资源。

  5. 解压 Java 代理 ZIP 文件,并注意两个重要的文件,我们将需要的 newrelic.ymlnewrelic.jar

    <span class="strong"><strong>    $ ls ~/Downloads/newrelic/</strong></span>
    <span class="strong"><strong>    CHANGELOG </strong></span>
    <span class="strong"><strong>    extension-example.xml </strong></span>
    <span class="strong"><strong>    newrelic-api-sources.jar </strong></span>
    <span class="strong"><strong>    newrelic.yml</strong></span>
    <span class="strong"><strong>    LICENSE </strong></span>
    <span class="strong"><strong>    extension.xsd </strong></span>
    <span class="strong"><strong>    newrelic-api.jar </strong></span>
    <span class="strong"><strong>    nrcerts</strong></span>
    <span class="strong"><strong>    README.txt </strong></span>
    <span class="strong"><strong>    newrelic-api-javadoc.jar </strong></span>
    <span class="strong"><strong>    newrelic.jar</strong></span>
    
  6. 通过添加相关名称到设置参数 app_name 编辑 newrelic.yml 文件,对于这个配方,我们将 app_name 命名为 computer-database-scala

    <span class="strong"><strong>    common: &amp;default_settings</strong></span>
    <span class="strong"><strong>  </strong></span>
    <span class="strong"><strong>      license_key: '111112222223333344444455555556666666'</strong></span>
    <span class="strong"><strong>      agent_enabled: true</strong></span>
    <span class="strong"><strong>      app_name: computer-database-scala</strong></span>
    <span class="strong"><strong>      # ..</strong></span>
    
  7. 使用 activator 模板 computer-database-scala 创建一个新的 Play 网络应用程序,并将其更改到项目根目录:

    <span class="strong"><strong>    activator new play2-deploy-81 computer-database-scala</strong></span>
    <span class="strong"><strong>    cd play2-deploy-81</strong></span>
    
  8. conf 目录中创建一个 instrument 目录:

    <span class="strong"><strong>    mkdir conf/instrument</strong></span>
    
  9. 将我们的两个 New Relic 配置文件复制到 conf/instrument

    <span class="strong"><strong>    cp ~/Downloads/newrelic/newrelic.yml conf/instrument</strong></span>
    <span class="strong"><strong>    cp ~/Downloads/newrelic/newrelic.jar conf/instrument</strong></span>
    
  10. 编辑 conf/application.conf 以启用自动数据库演变:

    <span class="strong"><strong>    applyEvolutions.default=true</strong></span>
    
  11. project/plugins.sbt 中添加较新版本的本地 Docker 打包器 sbt 插件,它具有对 Docker 的额外原生支持:

    <span class="strong"><strong>    addSbtPlugin("com.typesafe.sbt" % "sbt-native-packager" % "1.0.0-M3")</strong></span>
    
  12. 编辑 build.sbt 以指定网络应用程序的特定 Docker 设置:

    <span class="strong"><strong>    $ vi build.sbt</strong></span>
    <span class="strong"><strong>    import com.typesafe.sbt.SbtNativePackager._</strong></span>
     <span class="strong"><strong>    name := """play2-deploy-81"""</strong></span>
     <span class="strong"><strong>    version := "0.0.1-SNAPSHOT"</strong></span>
     <span class="strong"><strong>    scalaVersion := "2.11.4"</strong></span>
     <span class="strong"><strong>    dockerRepository := Some("ginduc")</strong></span>
     <span class="strong"><strong>    dockerExposedPorts := Seq(9000)</strong></span>
     <span class="strong"><strong>     dockerEntrypoint := Seq("bin/play2-deploy-81", "-J-javaagent:conf/instrument/newrelic.jar")</strong></span>
     <span class="strong"><strong>    libraryDependencies ++= Seq(</strong></span>
    <span class="strong"><strong>      jdbc,</strong></span>
    <span class="strong"><strong>      anorm,</strong></span>
    <span class="strong"><strong>      "org.webjars" % "jquery" % "2.1.1",</strong></span>
    <span class="strong"><strong>      "org.webjars" % "bootstrap" % "3.3.1"</strong></span>
    <span class="strong"><strong>    )</strong></span>
     <span class="strong"><strong>    lazy val root = (project in file(".")).enablePlugins(PlayScala)</strong></span>
    
  13. 使用 activator 创建 Docker 镜像:

    <span class="strong"><strong>     activator clean docker:stage</strong></span>
    
  14. 使用您的 Docker Hub 凭据从您的本地开发机器登录到 Docker:

    <span class="strong"><strong>    $ docker login</strong></span>
    
  15. 使用activator构建镜像并上传到hub.docker.com

    <span class="strong"><strong>    activator docker:publish</strong></span>
    
  16. hub.docker.com拉取play2-deploy-81 Docker 镜像到我们将部署网络应用程序的虚拟机中:

    <span class="strong"><strong>    docker pull &lt;YOUR_DOCKERHUB_USERNAME&gt;/play2-deploy-81:0.0.1-SNAPSHOT</strong></span>
    
  17. 在同一远程虚拟机中运行play2-deploy-81 Docker 容器:

    <span class="strong"><strong>    docker run -d -p 9000:9000 ginduc/play2-deploy-81:0.0.1-SNAPSHOT</strong></span>
    
  18. 使用网络浏览器,您现在应该能够访问我们刚刚部署的计算机数据库网络应用程序:

  19. 现在,登录到您的 New Relic 账户并导航到您的应用程序仪表板。您应该能够以图表和图形的形式看到一些相关的应用程序统计数据:

它是如何工作的...

在本食谱中,我们在远程虚拟机vm)中部署了 Play 2.0 网络应用程序。对于虚拟机,我们使用了 CoreOS 版本 607.0.0 作为基础操作系统,它应该会自动安装 Docker:

<span class="strong"><strong>    $ docker -v</strong></span>
<span class="strong"><strong>    Docker version 1.5.0, build a8a31ef-dirty</strong></span>

一旦部署虚拟机设置完成,我们就将注意力转向设置我们的 New Relic 账户。创建账户后,我们下载了 New Relic Java 代理,并特别记录了我们的账户许可证密钥。我们将两者都使用,因为我们将在我们的 Play 网络应用程序中集成 New Relic Java 代理:

<span class="strong"><strong>  $ unzip newrelic-java-3.15.0.zip</strong></span>
<span class="strong"><strong>  Archive:  newrelic-java-3.15.0.zip</strong></span>
<span class="strong"><strong>     creating: newrelic/</strong></span>
<span class="strong"><strong>    inflating: newrelic/newrelic.jar</strong></span>
<span class="strong"><strong>    inflating: newrelic/LICENSE</strong></span>
<span class="strong"><strong>    inflating: newrelic/README.txt</strong></span>
<span class="strong"><strong>    inflating: newrelic/extension.xsd</strong></span>
<span class="strong"><strong>   inflating: newrelic/nrcerts</strong></span>
<span class="strong"><strong>    inflating: newrelic/extension-example.xml</strong></span>
<span class="strong"><strong>   inflating: newrelic/CHANGELOG</strong></span>
<span class="strong"><strong>   inflating: newrelic/newrelic.yml</strong></span>
<span class="strong"><strong>    inflating: newrelic/newrelic-api.jar</strong></span>
<span class="strong"><strong>   inflating: newrelic/newrelic-api-sources.jar</strong></span>
<span class="strong"><strong>    inflating: newrelic/newrelic-api-javadoc.jar</strong></span>

我们使用 activator 模板computer-database-scala作为本食谱中的示例 Play 网络应用程序。

一旦我们生成了我们的 Web 项目,我们将把两个 New Relic 配置文件放在项目根目录下的conf/instrument目录中:

<span class="strong"><strong>    $ ls conf/instrument</strong></span>
<span class="strong"><strong>    newrelic.jar </strong></span>
<span class="strong"><strong>    newrelic.yml</strong></span>

要加载本机 Docker 打包器,我们需要将sbt-native-packager插件添加到我们的project/plugins.sbt构建插件文件中:

addSbtPlugin("com.typesafe.sbt" % "sbt-native-packager" % "1.0.0-M3")

最后一步是配置我们的主要构建文件build.sbt,以指定创建镜像的 docker 特定设置:

maintainer := "ginduc &lt;ginduc@dynamicobjx.com&gt;"
     dockerRepository := Some("dynobjx")
     dockerExposedPorts := Seq(9000)
     dockerEntrypoint := Seq("bin/imapi", "-J-javaagent:conf/instrument/newrelic.jar")

在前面的设置中,我们指定了hub.docker.com中的默认仓库和我们将公开应用程序的主要端口号(在本食谱中为端口号 9000)。最后的设置是指定entrypoint命令的位置。我们不得不修改它,以便传递必要的设置来指定 New Relic Java 代理:

dockerEntrypoint := Seq("bin/imapi", "-J-javaagent:conf/instrument/newrelic.jar")

前面的设置生成的Dockerfile应该看起来像这样:

<span class="strong"><strong>    $ cat target/docker/Dockerfile</strong></span>
<span class="strong"><strong>    FROM dockerfile/java:latest</strong></span>
<span class="strong"><strong>    MAINTAINER ginduc &lt;ginduc@dynamicobjx.com&gt;</strong></span>
<span class="strong"><strong>    EXPOSE 9000</strong></span>
<span class="strong"><strong>    ADD files /</strong></span>
<span class="strong"><strong>    WORKDIR /opt/docker</strong></span>
<span class="strong"><strong>    RUN ["chown", "-R", "daemon", "."]</strong></span>
<span class="strong"><strong>    USER daemon</strong></span>
<span class="strong"><strong>    ENTRYPOINT ["bin/imapi", "-J-javaagent:conf/instrument/newrelic.jar"]</strong></span>
<span class="strong"><strong>    CMD []</strong></span>

我们可以通过运行我们的 Docker 容器时查看 Docker 日志来验证当前是否已加载 New Relic Java 代理:

<span class="strong"><strong>    $ docker logs     9790caf8046c7da1d561dcc6e221169d64fa125cdd0a689222fe31637c7bc234</strong></span>
<span class="strong"><strong>    Mar 30, 2015 13:54:11 +0000 [1 1] com.newrelic INFO: New Relic Agent:      Loading configuration file "/opt/docker/conf/instrument/./newrelic.yml"</strong></span>
<span class="strong"><strong>    Mar 30, 2015 13:54:11 +0000 [1 1] com.newrelic INFO: New Relic Agent: Writing to log file: /opt/docker/lib/logs/newrelic_agent.log</strong></span>
<span class="strong"><strong>    Play server process ID is 1</strong></span>
<span class="strong"><strong>    [info] play - database [default] connected at jdbc:h2:mem:play</strong></span>
<span class="strong"><strong>    [info] play - Application started (Prod)</strong></span>
<span class="strong"><strong>    [info] play - Listening for HTTP on /0:0:0:0:0:0:0:0:9000</strong></span>

一旦我们在部署虚拟机中安装和部署了 Docker 容器,我们就可以使用网络浏览器访问computer-database-scala网络应用程序。然后我们可以使用 New Relic 仪表板查看所有相关的仪表数据点,例如 JVM 和数据库指标,以图表和图形的形式:

在这里,我们可以查看 New Relic 的应用指标有多深入,包括 JVM、数据库连接等的报告视图:

将 Play 应用程序与 AngularJS 集成

对于这个食谱,我们将集成一个基于 AngularJS 的前端 Play web 应用程序。AngularJS 是一个流行的 JavaScript 框架,为开发者提供了构建强大交互式 UI 的便捷工具。本食谱假定开发者对 AngularJS 有一定的了解。

更多关于 AngularJS 的信息可以在angularjs.org/找到。

在这个食谱中,我们还将使用 WebJars,一个 Play 友好的依赖管理仓库,来管理我们的 AngularJS 库。

更多关于 WebJars 的信息可以在www.webjars.org/找到。

我们还将使用 RequireJS,这是一个 JavaScript 模块脚本加载器,用于管理 AngularJS 模块和public/javascripts/main.js,我们的主要应用程序 JavaScript 模块。有关 RequireJS 的更多信息,请参阅他们的在线文档requirejs.org/

如何做到这一点…

对于这个食谱,你需要执行以下步骤:

  1. 通过使用activator模板play-scala创建一个新的 Play 2 web 应用程序项目:

    <span class="strong"><strong>    activator new play2-angular-83 play-scala &amp;&amp; cd play2-angular-83</strong></span>
    
  2. 编辑build.sbt构建文件以导入 RequireJS、AngularJS 和 Bootstrap:

    libraryDependencies ++= Seq(
          "org.webjars" %% "webjars-play" % "2.3.0",
          "org.webjars" % "angularjs" % "1.3.4",
          "org.webjars" % "bootstrap" % "3.3.1" exclude("org.webjars", "jquery"),
          "org.webjars" % "requirejs" % "2.1.15" exclude("org.webjars", "jquery")
        )
    
  3. 编辑默认应用程序控制器文件app/controllers/Application.scala的内容,并用以下片段替换:

    package controllers
         import play.api._
        import play.api.libs.json.Json
        import play.api.mvc._
         case class Product(sku: String, title: String)
         object Application extends Controller {
          implicit val productWrites = Json.writes[Product]
           def index = Action {
            Ok(views.html.index())
          }
           val products = Seq(
            Product("733733-421", "HP ProLiant DL360p Gen8"),
            Product("7147H2G", "IBM System x x3690 X5"),
            Product("R630-3552", "DELL PowerEdge R630"),
            Product("RX-2280I", "Supermicro RTG RX-2280I"),
            Product("MB449D/A", "Apple Xserve")
          )
           def listProducts = Action {
            Ok(Json.toJson(products))
          }
        }
    
  4. 编辑路由文件conf/routes的内容,并用以下片段替换:

    # Routes
        GET     /                           controllers.Application.index
        GET     /assets/*file               controllers.Assets.versioned(path="/public", file: Asset)
        GET     /api/products               controllers.Application.listProducts
        GET     /webjars/*file              controllers.WebJarAssets.at(file)
    
  5. 编辑默认索引模板 HTML 文件app/views/index.scala.html的内容,并用以下片段替换:

    @main("Product Catalogue") {
          &lt;nav class="navbar navbar-inverse navbar-fixed-top" role="navigation"&gt;
            &lt;div class="container-fluid"&gt;
              &lt;div class="navbar-header"&gt;
                &lt;button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#navbar" aria-expanded="false" aria-controls="navbar"&gt;
                    &lt;span class="sr-only"&gt;Toggle navigation&lt;/span&gt;
                    &lt;span class="icon-bar"&gt;&lt;/span&gt;
                    &lt;span class="icon-bar"&gt;&lt;/span&gt;
                    &lt;span class="icon-bar"&gt;&lt;/span&gt;
                &lt;/button&gt;
                &lt;a class="navbar-brand" href="#"&gt;Product Catalogue&lt;/a&gt;
              &lt;/div&gt;
            &lt;/div&gt;
          &lt;/nav&gt;
           &lt;div class="container-fluid"&gt;
            &lt;div class="row"&gt;
              &lt;ng-view /&gt;
            &lt;/div&gt;
          &lt;/div&gt;
        }
    
  6. 编辑默认布局模板文件app/views/main.scala.html的内容,并用以下片段替换:

    @(title: String)(content: Html)&lt;!DOCTYPE html&gt;
       &lt;html&gt;
      &lt;head&gt;
        &lt;meta charset="utf-8"&gt;
        &lt;meta http-equiv="X-UA-Compatible" content="IE=edge"&gt;
        &lt;meta name="viewport" content="width=device-width, initial-scale=1"&gt;
        &lt;meta name="description" content=""&gt;
        &lt;meta name="author" content=""&gt;
        &lt;title&gt;@title&lt;/title&gt;
        &lt;link rel="shortcut icon" type="image/png" href='@routes.Assets.versioned("images/favicon.png")'&gt;
        &lt;link rel="stylesheet" media="screen" href='@routes.WebJarAssets.at(WebJarAssets.locate("css/bootstrap.min.css"))' /&gt;
        &lt;style&gt;
            body {
                padding-top: 50px;
            }
        &lt;/style&gt;
      &lt;/head&gt;
      &lt;body&gt;
        @content
        &lt;script data-main='@routes.Assets.versioned("javascripts/main.js").url' src='@routes.WebJarAssets.at(WebJarAssets.locate("require.min.js")).url'&gt;&lt;/script&gt;
      &lt;/body&gt;
      &lt;/html&gt;
    
  7. public/javascripts/main.js中添加我们 web 应用程序的主要 JavaScript 文件,并添加以下片段:

    'use strict';
       requirejs.config({
        paths: {
          'angular': ['../lib/angularjs/angular'],
          'angular-route': ['../lib/angularjs/angular-route'],
          'angular-resource': ['../lib/angularjs/angular-resource.min']
        },
        shim: {
          'angular': {
            exports : 'angular'
          },
          'angular-route': {
            deps: ['angular'],
            exports : 'angular'
          },
          'angular-resource': {
            deps: ['angular'],
            exports : 'angular'
          }
        }
      });
       require([
        'angular',
        'angular-route',
        'angular-resource',
        './services',
        './controllers'
        ],
        function(angular) {
          angular.module('azApp', [
            'ngRoute',
            'ngResource',
            'azApp.services',
            'azApp.controllers'
          ])
           .config(['$routeProvider', '$locationProvider', '$httpProvider', function($routeProvider, $locationProvider, $httpProvider) {
            $routeProvider
              .when('/', {
                templateUrl: 'assets/javascripts/partials/products.html',
                controller: 'ProductsCtrl'
              })
          }]);
           angular.bootstrap(document, ['azApp']);
      });
    
  8. 接下来,我们在public/javascripts/controllers.js中添加了 Angular 控制器 JavaScript 文件,内容如下:

    'use strict';
       define(['angular'], function(angular) {
        angular.module('azApp.controllers', [])
           .controller('ProductsCtrl', ['$scope', 'Products', function ($scope, Products) {
            $scope.products = Products.list().query();
          }])
         ;
      });
    
  9. 在添加 angular 控制器文件后,我们在public/javascript/services.js中添加了 Angular 工厂 JavaScript 文件,内容如下:

    'use strict';
       define(['angular'], function(angular) {
        angular.module('azApp.services', [])
          .factory('Products', ['$resource', '$http', function Contacts($resource, $http) {
              var endpointURI = '/api/products';
               return {
                list: function(options) {
                    return $resource(endpointURI);
                }
              }
          }])
        ;
      });
    
  10. 最后,我们在public/javascripts/partials/products.html中添加了产品部分的 HTML 文件,内容如下:

    &lt;div class="table-responsive"&gt;
          &lt;table class="table table-striped table-hover"&gt;
            &lt;thead&gt;
              &lt;th&gt;Product Title&lt;/th&gt;
              &lt;th&gt;SKU&lt;/th&gt;
            &lt;/thead&gt;
          &lt;tbody&gt;
            &lt;tr ng-repeat="p in products | orderBy:'title'"&gt;
              &lt;td ng-bind="p.title"&gt;&lt;/td&gt;
              &lt;td ng-bind="p.sku"&gt;&lt;/td&gt;
            &lt;/tr&gt;
          &lt;/tbody&gt;
        &lt;/table&gt;
      &lt;/div&gt;
    
  11. 我们现在可以执行activator命令run来启动 Play 2 web 应用程序:

    <span class="strong"><strong>    $ activator "~run"</strong></span>
    <span class="strong"><strong>    [info] Loading project definition</strong></span>
    <span class="strong"><strong>    [info] Set current project to play2-angular-83</strong></span>
    <span class="strong"><strong>    --- (Running the application, auto-reloading is enabled) ---</strong></span>
    <span class="strong"><strong>    [info] play - Listening for HTTP on /0:0:0:0:0:0:0:0:9000</strong></span>
    <span class="strong"><strong>    (Server started, use Ctrl+D to stop and go back to the console...)</strong></span>
     <span class="strong"><strong>    [success] Compiled in 354ms</strong></span>
    
  12. 使用curl,我们可以验证我们的产品 API 端点是否正常工作:

    <span class="strong"><strong>    $ curl http://localhost:9000/api/products</strong></span>
    <span class="strong"><strong>    [{"sku":"733733-421","title":"HP ProLiant DL360p Gen8"}, {"sku":"7147H2G","title":"IBM System x x3690 X5"},{"sku":"R630- 3552","title":"DELL PowerEdge R630"},{"sku":"RX-2280I","title":"Supermicro RTG  RX-2280I"},{"sku":"MB449D/A","title":"Apple Xserve"}]%</strong></span>
    
  13. 我们现在可以通过在浏览器中加载 URLhttp://localhost:9000来访问由 Play 2 支持的 API 端点驱动的产品列表页面:图片

它是如何工作的…

在这个食谱中,我们创建了一个使用 AngularJS 和 Bootstrap 来显示产品列表的 Play 2 web 应用程序。产品列表由基于 Play 2 的 Rest API 端点提供,该端点返回包含产品标题和 SKU 的产品集。

为了将所有东西连接起来,我们不得不修改一些基于 play-scala 的activator模板的配置设置,并添加包含我们主要 AngularJS 脚本的 JavaScript 文件。

  1. 首先,我们必须通过修改build.sbt文件中的库依赖项来声明我们的 Web 应用程序需要 AngularJS、RequireJS 和 Bootstrap:

    libraryDependencies ++= Seq(
          "org.webjars" %% "webjars-play" % "2.3.0",
          "org.webjars" % "angularjs" % "1.3.4",
          "org.webjars" % "bootstrap" % "3.3.1" exclude("org.webjars", "jquery"),
          "org.webjars" % "requirejs" % "2.1.15" exclude("org.webjars", "jquery")
        )
    
  2. 接下来,我们修改了应用程序控制器,添加了一个产品案例类和listProducts操作,这将服务于我们的产品 API 端点:

    case class Product(sku: String, title: String)
    
        def listProducts = Action {
          Ok(Json.toJson(products))
        }
    
  3. 接下来,我们需要修改我们的routes文件以声明新路由和重新配置现有路由:

    GET     /assets/*file               controllers.Assets.versioned(path="/public", file: Asset)
        GET     /api/products               controllers.Application.listProducts
        GET     /webjars/*file              controllers.WebJarAssets.at(file)
    
  4. 在前面的代码片段中,我们通过使用versioned操作来重新配置现有的/assets/*file路由。然后我们添加了产品 API 端点路由和 WebJars 资产的路由条目。

  5. 接下来,我们需要修改现有的app/views/index.scala.html模板以插入 Angular 视图标签来渲染部分 HTML:

    @main("Product Catalogue") {
    
          &lt;div class="container-fluid"&gt;
            &lt;div class="row"&gt;
              &lt;ng-view /&gt;
            &lt;/div&gt;
          &lt;/div&gt;
        }
    
  6. 下一步是修改布局模板文件app/views/main.scala.html以加载我们的主要 JavaScript 文件及其依赖项:

    @(title: String)(content: Html)&lt;!DOCTYPE html&gt;
       &lt;html&gt;
      &lt;head&gt;
        &lt;meta charset="utf-8"&gt;
        &lt;meta http-equiv="X-UA-Compatible" content="IE=edge"&gt;
        &lt;meta name="viewport" content="width=device-width, initial-scale=1"&gt;
        &lt;meta name="description" content=""&gt;
        &lt;meta name="author" content=""&gt;
        &lt;title&gt;@title&lt;/title&gt;
        &lt;link rel="shortcut icon" type="image/png" href='@routes.Assets.versioned("images/favicon.png")'&gt;
        &lt;link rel="stylesheet" media="screen" href='@routes.WebJarAssets.at(WebJarAssets.locate("css/bootstrap.min.css"))' /&gt;
        &lt;style&gt;
            body {
                padding-top: 50px;
            }
        &lt;/style&gt;
      &lt;/head&gt;
      &lt;body&gt;
        @content
        &lt;script data-main='@routes.Assets.versioned("javascripts/main.js").url' src='@routes.WebJarAssets.at(WebJarAssets.locate("require.min.js")).url'&gt;&lt;/script&gt;
      &lt;/body&gt;
      &lt;/html&gt;
    
  7. 然后,我们需要在public/javascripts/main.js中添加我们的主要 JavaScript 文件以配置我们的主要 Angular 应用程序:

    'use strict';
       requirejs.config({
        paths: {
          'angular': ['../lib/angularjs/angular'],
          'angular-route': ['../lib/angularjs/angular-route'],
          'angular-resource': ['../lib/angularjs/angular-resource.min']
        },
        shim: {
          'angular': {
            exports : 'angular'
          },
          'angular-route': {
            deps: ['angular'],
            exports : 'angular'
          },
          'angular-resource': {
            deps: ['angular'],
            exports : 'angular'
          }
        }
      });
       require([
        'angular',
        'angular-route',
        'angular-resource',
        './services',
        './controllers'
        ],
        function(angular) {
          angular.module('azApp', [
            'ngRoute',
            'ngResource',
            'azApp.services',
            'azApp.controllers'
          ])
           .config(['$routeProvider', '$locationProvider', '$httpProvider', function($routeProvider, $locationProvider, $httpProvider) {
            $routeProvider
              .when('/', {
                templateUrl: 'assets/javascripts/partials/products.html',
                controller: 'ProductsCtrl'
              })
          }]);
           angular.bootstrap(document, ['azApp']);
      });
    
  8. 在前面的代码片段中,我们初始化了 Angular 和另外两个 Angular 插件angular-routesangular-resources,它们将处理请求路由并管理 API 调用。我们还加载并初始化了我们的 Angular 控制器和服务脚本文件:

    require([
        'angular',
        'angular-route',
        'angular-resource',
        './services',
        './controllers'
        ])
    
  9. 最后,我们使用$routeProvider指令配置了我们的 Angular 应用程序路由。对于这个菜谱,默认情况下,基本 URL 通过模板部分public/javascripts/partials/products.html加载产品控制器:

    $routeProvider
        .when('/', {
          templateUrl: 'assets/javascripts/partials/products.html',
          controller: 'ProductsCtrl'
        })
      }])
    

对于这个菜谱,我们成功地将 AngularJS 与我们的 Play 2 Web 应用程序集成,使用 WebJars 仓库来管理所有前端库(在这个菜谱中,Angular、RequireJS 和 Bootstrap)。我们能够访问产品 API 端点并显示其内容在 Angular 模板中。

将 Play 应用程序与 Parse.com 集成

对于这个菜谱,我们将集成一个 Play 2 Rest API 与一个 BaaS,如Parse.com的 Parse Core,这是一个允许开发者将数据存储在云中的云服务。在这个菜谱中,我们想要看看我们如何使用 Play 将其他外部 Web 服务集成到我们的 Web 应用程序中。对于现代 Web 应用程序来说,拥有多个数据源并不罕见。我们将使用一个 Parse Core 应用程序来模拟这一点。我们将使用 Play WS 库来连接到 Parse API,特别是使用 HTTP 头发送应用程序凭据和 JSON 数据到 Parse API Web 服务。我们还将能够使用Parse.com自己的核心数据浏览器来查看我们在 Parse Core 应用程序中存储的数据。

对于这个菜谱,我们需要一个Parse.com账户。你可以在parse.com/#signup注册一个账户。

如何做到这一点...

对于这个菜谱,你需要执行以下步骤:

  1. 登录到你的Parse.com账户并创建一个新的 Parse Core 应用程序!图片

  2. 创建一个新的 Parse 应用后,导航到设置部分以检索您的应用程序 IDRest API 密钥图片

  3. 接下来,在 Parse Core 部分创建一个新的 Parse 类(类似于数据库表)。对于这个菜谱,我们将存储Item记录:图片

  4. 创建 Parse 类后,为我们的项目添加必要的列。对于这个菜谱,我们将添加标题和 SKU 列:图片

  5. 接下来,我们将着手于我们的 Play 2 Web 应用,该应用将与 Parse Core 进行交互。基于 activator 模板play-scala生成一个新的 Play 2 Web 应用:

    <span class="strong"><strong>    activator new play2-parse-84 play-scala &amp;&amp; cd play2-parse-84</strong></span>
    
  6. 接下来,在app/plugins/ParsePlugin.scala中创建一个新的插件,以下内容:

    package plugins
         import play.api.{Application, Plugin}
         class ParsePlugin(app: Application) extends Plugin {
          lazy val parseAPI: ParseAPI = {
            new ParseAPI(
              app.configuration.getString("parse.core.appId").get,
              app.configuration.getString("parse.core.restKey").get
            )
          }
           override def onStart() = {
            parseAPI
          }
        }
    
  7. 接下来,在conf/application.conf文件中添加 Parse Core 密钥,我们在上一步中做了笔记。确保用您实际的 Parse 应用 ID 和 Rest 密钥替换占位符:

    parse.core.appId=&lt;YOUR_PARSE_APP_ID&gt;
        parse.core.restKey=&lt;YOUR_PARSE_REST_KEY&gt;
    
  8. 接下来,在app/plugins/ParseAPI.scala中创建我们的 Parse 辅助类,以下内容:

    package plugins
         import play.api.Play.current
        import play.api.libs.json.JsValue
        import play.api.libs.ws.WS
         class ParseAPI(appId: String, restKey: String) {
          private val PARSE_API_HEADER_APP_ID = "X-Parse-Application-Id"
          private val PARSE_API_HEADER_REST_API_KEY = "X-Parse-REST-API-Key"
          private val PARSE_API_URL = "https://api.parse.com"
          private val PARSE_API_URL_CLASSES = "/1/classes/"
          private val PARSE_API_HEADER_CONTENT_TYPE = "Content-Type"
          private val CONTENT_TYPE_JSON = "application/json; charset=utf-8"
           private val parseBaseUrl = "%s%s".format(PARSE_API_URL, PARSE_API_URL_CLASSES)
           def list(className: String) = parseWS(className).get()
           def create(className: String, json: JsValue) = {
            parseWS(className)
              .withHeaders(PARSE_API_HEADER_CONTENT_TYPE -&gt; CONTENT_TYPE_JSON)
          .post(json)
          }
           private def parseWS(className: String) = WS.url("%s%s".format(parseBaseUrl, className))
            .withHeaders(PARSE_API_HEADER_APP_ID -&gt; appId)
            .withHeaders(PARSE_API_HEADER_REST_API_KEY -&gt; restKey)
        }
    
  9. 接下来,在应用启动时初始化插件,在conf/play.plugins中创建 Play 插件配置文件,以下内容:

    799:plugins.ParsePlugin
    
  10. 最后,让我们在app/controllers/Items.scala文件中添加我们的Items控制器,以下内容将添加两个操作方法,index()用于从 Parse Core 返回项目,以及create(),它将在 Parse Core 上持久化项目:

    package controllers
         import play.api.Play
        import play.api.Play.current
        import play.api.libs.json.{JsError, Json, JsObject}
        import play.api.mvc.{BodyParsers, Action, Controller}
        import play.api.libs.concurrent.Execution.Implicits._
        import plugins.ParsePlugin
        import scala.collection.mutable.ListBuffer
        import scala.concurrent.Future
         case class Item(objectId: Option[String], title: String, sku: String)
         object Items extends Controller {
          private val parseAPI = Play.application.plugin[ParsePlugin].get.parseAPI
          implicit val itemWrites = Json.writes[Item]
          implicit val itemReads = Json.reads[Item]
           val `Items` = "Items"
           def index = Action.async { implicit request =&gt;
            parseAPI.list(`Items`).map { res =&gt;
              val list = ListBuffer[Item]()
              (res.json \ "results").as[List[JsObject]].map { itemJson =&gt;
                list += itemJson.as[Item]
              }
              Ok(Json.toJson(list))
            }
          }
           def create = Action.async(BodyParsers.parse.json) { implicit request =&gt;
            val post = request.body.validate[Item]
            post.fold(
              errors =&gt; Future.successful {
                BadRequest(Json.obj("error" -&gt; JsError.toFlatJson(errors)))
              },
              item =&gt; {
                parseAPI.create(`Items`, Json.toJson(item)).map { res =&gt;
                  if (res.status == CREATED) {
                    Created(Json.toJson(res.json))
                  } else {
                    BadGateway("Please try again later")
                  }
                }
              }
            )
          }
        }
    
  11. conf/routes文件中添加必要的路由条目以供我们的Items操作使用:

    GET     /api/items                  controllers.Items.index
        POST    /api/items                  controllers.Items.create
    
  12. 要运行我们的 Web 应用,我们将使用 activator 命令run,使用波浪字符(~)表示我们希望为此 Web 应用启用热重载:

    <span class="strong"><strong>    $ activator "~run"</strong></span>
    <span class="strong"><strong>    [info] Loading project definition</strong></span>
    <span class="strong"><strong>    [info] Set current project to play2-parse-84</strong></span>
     <span class="strong"><strong>    --- (Running the application, auto-reloading is enabled) ---</strong></span>
     <span class="strong"><strong>    [info] play - Listening for HTTP on /0:0:0:0:0:0:0:0:9000</strong></span>
     <span class="strong"><strong>    (Server started, use Ctrl+D to stop and go back to the console...)</strong></span>
     <span class="strong"><strong>    [success] Compiled in 400ms</strong></span>
    <span class="strong"><strong>    [info] play - Application started (Dev)</strong></span>
    
  13. 使用curl,我们现在可以向我们的 Parse Core 应用中插入新的记录:

    <span class="strong"><strong>    $ curl -v -X POST http://localhost:9000/api/items --header "Content-type: application/json" -d '{"title":"Supermicro RTG RX-2280I", "sku":"RX-2280I"}'</strong></span>
    <span class="strong"><strong>    * Hostname was NOT found in DNS cache</strong></span>
    <span class="strong"><strong>    *   Trying ::1...</strong></span>
    <span class="strong"><strong>    * Connected to localhost (::1) port 9000 (#0)</strong></span>
    <span class="strong"><strong>    &gt; POST /api/items HTTP/1.1</strong></span>
    <span class="strong"><strong>    &gt; User-Agent: curl/7.37.1</strong></span>
    <span class="strong"><strong>    &gt; Host: localhost:9000</strong></span>
    <span class="strong"><strong>    &gt; Accept: */*</strong></span>
    <span class="strong"><strong>    &gt; Content-type: application/json</strong></span>
    <span class="strong"><strong>    &gt; Content-Length: 53</strong></span>
    <span class="strong"><strong>    &gt;</strong></span>
    <span class="strong"><strong>    * upload completely sent off: 53 out of 53 bytes</strong></span>
    <span class="strong"><strong>    &lt; HTTP/1.1 201 Created</strong></span>
    <span class="strong"><strong>    &lt; Content-Type: application/json; charset=utf-8</strong></span>
    <span class="strong"><strong>    &lt; Content-Length: 64</strong></span>
    <span class="strong"><strong>    &lt;</strong></span>
    <span class="strong"><strong>    * Connection #0 to host localhost left intact</strong></span>
    <span class="strong"><strong>    {"createdAt":"2015-04-08T06:13:52.103Z","objectId":"K7Q2JEXxmI"}%</strong></span>
    
  14. 现在,我们也可以通过使用curl检索存储在 Parse Core 上的items

    <span class="strong"><strong>    $ curl -v http://localhost:9000/api/items</strong></span>
    <span class="strong"><strong>    * Hostname was NOT found in DNS cache</strong></span>
    <span class="strong"><strong>    *   Trying ::1...</strong></span>
    <span class="strong"><strong>    * Connected to localhost (::1) port 9000 (#0)</strong></span>
    <span class="strong"><strong>    &gt; GET /api/items HTTP/1.1</strong></span>
    <span class="strong"><strong>    &gt; User-Agent: curl/7.37.1</strong></span>
    <span class="strong"><strong>    &gt; Host: localhost:9000</strong></span>
    <span class="strong"><strong>    &gt; Accept: */*</strong></span>
    <span class="strong"><strong>    &gt;</strong></span>
    <span class="strong"><strong>    &lt; HTTP/1.1 200 OK</strong></span>
    <span class="strong"><strong>    &lt; Content-Type: application/json; charset=utf-8</strong></span>
    <span class="strong"><strong>    &lt; Content-Length: 231</strong></span>
    <span class="strong"><strong>    &lt;</strong></span>
    <span class="strong"><strong>    * Connection #0 to host localhost left intact</strong></span>
    <span class="strong"><strong>    [{"objectId":"0aTdEVAwaF","title":"DELL PowerEdge R630","sku":"R630-3552"},    {"objectId":"T3TqVdi9a2","title":"HP ProLiant DL360p Gen8","sku":"733733-421"},    {"objectId":"K7Q2JEXxmI","title":"Supermicro RTG RX-2280I","sku":"RX-2280I"}]%</strong></span>
    

它是如何工作的…

在这个菜谱中,我们查看了一个更实际的示例,即使用 Play WS 库将外部 Web 服务与 Play 2 Web 应用集成。Play WS 库在设置和连接到远程 HTTP 主机时节省了开发者大量的样板代码,它还提供了方便的方法来设置头信息、请求参数等。

Parse Core 是一个非常受欢迎且稳定的后端即服务提供商,它为开发者提供其他服务,如移动推送通知和移动分析,这些都是任何开发者工具链的重要补充。

对于这个菜谱来说,注册一个Parse.com账户并创建一个 Parse Core 应用是至关重要的。一旦我们设置好,我们就可以继续创建一个 Play 插件,该插件将负责初始化和设置我们与 Parse API 的连接:

// app/plugins/ParsePlugin.scala
    class ParsePlugin(app: Application) extends Plugin {
      lazy val parseAPI: ParseAPI = {
        new ParseAPI(
          app.configuration.getString("parse.core.appId").get,
          app.configuration.getString("parse.core.restKey").get
        )
      }
       override def onStart() = {
        parseAPI
      }
    }
     // conf/application.conf
    parse.core.appId=&lt;YOUR_PARSE_APP_ID&gt;
    parse.core.restKey=&lt;YOUR_PARSE_REST_KEY&gt;

接下来,我们创建我们的 Parse Core 代理类conf/plugins/ParseAPI.scala,它将封装所有与 Parse API 的交互:

package plugins
     import play.api.Play.current
    import play.api.libs.json.JsValue
    import play.api.libs.ws.WS
     class ParseAPI(appId: String, restKey: String) {
      private val PARSE_API_HEADER_APP_ID = "X-Parse-Application-Id"
      private val PARSE_API_HEADER_REST_API_KEY = "X-Parse-REST-API-Key"
      private val PARSE_API_URL = "https://api.parse.com"
      private val PARSE_API_URL_CLASSES = "/1/classes/"
      private val PARSE_API_HEADER_CONTENT_TYPE = "Content-Type"
      private val CONTENT_TYPE_JSON = "application/json; charset=utf-8"
       private val parseBaseUrl = "%s%s".format(PARSE_API_URL, PARSE_API_URL_CLASSES)
       def list(className: String) = parseWS(className).get()
       def create(className: String, json: JsValue) = {
        parseWS(className)
          .withHeaders(PARSE_API_HEADER_CONTENT_TYPE -&gt; CONTENT_TYPE_JSON)
      .post(json)
      }
       private def parseWS(className: String) = WS.url("%s%s".format(parseBaseUrl, className))
        .withHeaders(PARSE_API_HEADER_APP_ID -&gt; appId)
        .withHeaders(PARSE_API_HEADER_REST_API_KEY -&gt; restKey)
    }

在前面的类中,我们创建了两个公共方法,应该具有数据检索和记录创建的功能。我们在进行 GETPOST 请求时包括所需的 Parse API 身份验证头:

private def parseWS(className: String) = WS.url("%s%s".format(parseBaseUrl, className))
        .withHeaders(PARSE_API_HEADER_APP_ID -&gt; appId)
        .withHeaders(PARSE_API_HEADER_REST_API_KEY -&gt; restKey)

对于 POST 请求,我们添加所需的附加头,将内容类型设置为 application/json

def create(className: String, json: JsValue) = {
      parseWS(className)
        .withHeaders(PARSE_API_HEADER_CONTENT_TYPE -&gt; CONTENT_TYPE_JSON)
        .post(json)
    }

一旦 Parse 插件全部设置完成,我们创建 Items 控制器,它将接收项目请求和项目创建请求,并将负责将这些请求委派给 Parse API 辅助工具:

def index = Action.async { implicit request =&gt;
      parseAPI.list(`Items`).map { res =&gt;
        val list = ListBuffer[Item]()
        (res.json \ "results").as[List[JsObject]].map { itemJson =&gt;
          list += itemJson.as[Item]
        }
        Ok(Json.toJson(list))
      }
    }
     def create = Action.async(BodyParsers.parse.json) { implicit request =&gt;
      val post = request.body.validate[Item]
      post.fold(
        errors =&gt; Future.successful {
          BadRequest(Json.obj("error" -&gt; JsError.toFlatJson(errors)))
        },
        item =&gt; {
          parseAPI.create(`Items`, Json.toJson(item)).map { res =&gt;
            if (res.status == CREATED) {
              Created(Json.toJson(res.json))
            } else {
              BadGateway("Please try again later")
            }
          }
        }
      )
    }

不要忘记在 conf/routes 配置文件中添加后续的路由:

GET     /api/items                  controllers.Items.index
    POST    /api/items                  controllers.Items.create

我们可以使用 Parse Core 仪表板查看通过 Parse API 创建的所有数据:

图片

还有更多...

有关 Parse Core 的更多信息,请参阅他们位于 parse.com/docs 的在线文档。

使用 Vagrant 创建 Play 开发环境

我们将探讨如何创建一个用于 Play 2 开发的便携式开发环境,使用 Vagrant,这是任何开发者工具链的有力补充。Vagrant 允许开发者自动化创建开发环境,从安装用于 Read-Eval-Print Loop (REPL) 工具所需的开发套件,到安装其他服务如 MySQL 和 Redis。这种设置对于多成员开发团队或需要在多个工作站上工作的开发者来说非常有用,其中需要一个一致、相同的开发环境,这是必要且理想的。

对于这个配方,我们将从头开始创建我们的 Vagrant 实例,安装运行我们的示例 Play 网络应用程序所需的库,并使用 Docker 运行 MySQL 服务,以及使用 activator 模板 play-slick-angular-test-example 运行实际的示例 Play 网络应用程序。

如何做到这一点...

对于这个配方,你需要执行以下步骤:

  1. 按照以下链接中的安装说明安装 Vagrant:

    Vagrant 安装文档

  2. 现在你应该已经安装了本地的 Vagrant 版本:

    <span class="strong"><strong>    $ vagrant -v</strong></span>
    <span class="strong"><strong>    Vagrant 1.6.3</strong></span>
    
  3. 创建一个工作空间目录并切换到新创建的目录:

    <span class="strong"><strong>    mkdir play2-vagrant-85 &amp;&amp; cd $_</strong></span>
    
  4. 在项目根目录中创建一个 Vagrantfile,内容如下:

    # -*- mode: ruby -*-
        # vi: set ft=ruby :
         VAGRANTFILE_API_VERSION = "2"
         Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|
          config.vm.box = "Centos-6.5"
          config.vm.box_url = "https://github.com/2creatives/vagrant-centos/releases/download/v6.5.1/centos65-x86_64-20131205.box"
          config.vm.hostname = "VG-play2"
           config.vm.provision :shell, :path =&gt; "bootstrap.sh"
           config.vm.network "forwarded_port", guest: 9000, host: 9000
          config.vm.network "forwarded_port", guest: 3306, host: 3306
           config.vm.provider :virtualbox do |vb|
            vb.name = "VG-play2"
            vb.gui = false
            vb.customize ["modifyvm", :id, "--memory", "4096", "--cpus", "2", "--    ioapic", "on"]
          end
        end
    
  5. 创建一个 Bootstrap bash 脚本文件 bootstrap.sh,内容如下:

    #!/usr/bin/env bash
         set -x
         SCALA_VER=2.11.2
        ACTIVATOR_VER=1.3.2
        MYSQL_ROOT_PW="cookbook"
         if [ ! -e "/home/vagrant/.firstboot" ]; then
           # tools etc
          yum -y install yum-plugin-fastestmirror
          yum -y install git wget curl rpm-build
           # Pre-docker install: http://stackoverflow.com/a/27216873
          sudo yum-config-manager --enable public_ol6_latest
          sudo yum install -y device-mapper-event-libs
           # Docker
          yum -y install docker-io
          service docker start
          chkconfig docker on
          usermod -a -G docker vagrant
           # Install the JDK
          curl -LO 'http://download.oracle.com/otn-pub/java/jdk/7u51-b13/jdk-7u51-linux-x64.rpm' -H 'Cookie: oraclelicense=accept-securebackup-cookie'
          rpm -i jdk-7u51-linux-x64.rpm
          /usr/sbin/alternatives --install /usr/bin/java java /usr/java/default/bin/java 200000
          rm jdk-7u51-linux-x64.rpm
           # Install scala repl
          rpm -ivh http://scala-lang.org/files/archive/scala-$SCALA_VER.rpm
           # Install Activator
          cd /opt
          wget http://downloads.typesafe.com/typesafe-activator/$ACTIVATOR_VER/typesafe-activator-$ACTIVATOR_VER.zip
          unzip typesafe-activator-$ACTIVATOR_VER.zip
          chown -R vagrant:vagrant /opt/activator-$ACTIVATOR_VER
           # Set Path
          echo "export JAVA_HOME=/usr/java/default/" &gt;&gt; /home/vagrant/.bash_profile
          echo "export PATH=$PATH:$JAVA_HOME/bin:/home/vagrant/bin:/opt/activator-$ACTIVATOR_VER" &gt;&gt; /home/vagrant/.bash_profile
           touch /home/vagrant/.firstboot
        fi
         # Start MySQL
        docker run -e MYSQL_PASS=$MYSQL_ROOT_PW -i -d -p 3306:3306 --name mysqld -t tutum/mysql
    
  6. 创建一个 git 忽略文件 .gitignore,以排除 Vagrant 目录工作空间从 git:

    <span class="strong"><strong>    $ cat .gitignore</strong></span>
    <span class="strong"><strong>    .vagrant</strong></span>
    
  7. 一旦 Vagrant 安装完成并且我们的 Vagrantfile 已经正确配置,我们就可以初始化我们的 Vagrant 实例:

    <span class="strong"><strong>    vagrant up</strong></span>
    
  8. 我们可以使用以下命令登录到 Vagrant 实例:

    <span class="strong"><strong>    vagrant ssh</strong></span>
    
  9. 登录到 Vagrant 实例后,使用以下命令切换到 Vagrant 工作空间目录:

    <span class="strong"><strong>    cd /vagrant</strong></span>
    
  10. 列出 /vagrant 目录的内容以验证你处于正确的目录:

    <span class="strong"><strong>    $ ls -ltra</strong></span>
    <span class="strong"><strong>    -rw-r--r--   1 vagrant vagrant    9 Mar 31 12:48 .gitignore</strong></span>
    <span class="strong"><strong>    -rw-r--r--   1 vagrant vagrant  683 Mar 31 12:50 Vagrantfile</strong></span>
    <span class="strong"><strong>    drwxr-xr-x   1 vagrant vagrant  102 Mar 31 12:57 .vagrant</strong></span>
    <span class="strong"><strong>    -rwxr-xr-x   1 vagrant vagrant 1632 Apr  1 12:44 bootstrap.sh</strong></span>
    
  11. 对于这个配方,我们将使用 activator 模板 play-slick-angular-test-example 并基于此生成一个新的 Play 2 项目:

    <span class="strong"><strong>    $ activator new play-slick-angular-test-example play-slick-angular-test-example</strong></span>
    
  12. 通过修改以下代码行编辑 conf/application.conf 配置文件:

    db.default.driver=com.mysql.jdbc.Driver
        db.default.url="jdbc:mysql://localhost/report?createDatabaseIfNotExist=true"
        db.default.user=admin
        db.default.password="cookbook"
         applyEvolutions.default=true
    
  13. 使用 activator 运行 Play 网络应用:

    <span class="strong"><strong>    $ activator run</strong></span>
    
  14. 现在,你应该能够通过网页浏览器访问 Play 网络应用:

如何工作…

在这个菜谱中,我们按照以下步骤安装了 Vagrant,这是一个流行的开发者工具,它自动化了开发者环境的初始化和设置:

  1. Vagrantfile 配置文件中,我们声明我们将使用 Centos 6.5 Vagrant box 作为我们的基础操作系统:

    config.vm.box_url = "https://github.com/2creatives/vagrant-centos/releases/download/v6.5.1/centos65-x86_64-20131205.box"
    
  2. 我们声明在 Vagrant 实例配置过程中运行我们的 bootstrap.sh 脚本文件:

    config.vm.provision :shell, :path =&gt; "bootstrap.sh"
    
  3. 接下来,我们声明了从我们的 Vagrant 实例转发到主机机的端口,Play 使用端口 9000,MySQL 使用端口 3306:

    config.vm.network "forwarded_port", guest: 9000, host: 9000
        config.vm.network "forwarded_port", guest: 3306, host: 3306
    
  4. 最后,我们可选地配置了我们的 Vagrant 实例具有 4-GB RAM 并使用两个 CPU:

    vb.customize ["modifyvm", :id, "--memory", "4096", "--cpus", "2", "--ioapic", "on"]
    
  5. 我们安装了用于 Play 开发的相关工具,我们在 bootstrap.sh 脚本文件中指定了这些工具。我们在 bootstrap.sh 文件的顶部声明了 Scala 和 Activator 的版本:

    SCALA_VER=2.11.2
        ACTIVATOR_VER=1.3.2
    
  6. 我们还声明了用于我们的 MySQL 实例的默认密码:

    MYSQL_ROOT_PW="cookbook"
    
  7. 接下来,我们安装了所需的 CentOS 软件包:

    <span class="strong"><strong># tools etc</strong></span>
    <span class="strong"><strong>  yum -y install yum-plugin-fastestmirror</strong></span>
    <span class="strong"><strong>  yum -y install git wget curl rpm-build</strong></span>
    
  8. 下一个要安装的软件包是 Docker 及其所需的库:

    <span class="strong"><strong># Pre-docker install: http://stackoverflow.com/a/27216873</strong></span>
    <span class="strong"><strong>  sudo yum-config-manager --enable public_ol6_latest</strong></span>
    <span class="strong"><strong>  sudo yum install -y device-mapper-event-libs</strong></span>
     <span class="strong"><strong>  # Docker</strong></span>
    <span class="strong"><strong>  yum -y install docker-io</strong></span>
    <span class="strong"><strong>  service docker start</strong></span>
    <span class="strong"><strong>  chkconfig docker on</strong></span>
    <span class="strong"><strong>  usermod -a -G docker vagrant</strong></span>
    
  9. 接下来,我们安装了 JDK、Scala 二进制文件和 Activator:

    <span class="strong"><strong>    # Install the JDK</strong></span>
    <span class="strong"><strong>    curl -LO 'http://download.oracle.com/otn-pub/java/jdk/7u51-b13/jdk-7u51-linux-x64.rpm' -H 'Cookie: oraclelicense=accept-securebackup-cookie'</strong></span>
    <span class="strong"><strong>    rpm -i jdk-7u51-linux-x64.rpm</strong></span>
    <span class="strong"><strong>    /usr/sbin/alternatives --install /usr/bin/java java /usr/java/default/bin/java 200000</strong></span>
    <span class="strong"><strong>    rm jdk-7u51-linux-x64.rpm</strong></span>
     <span class="strong"><strong>    # Install scala repl</strong></span>
    <span class="strong"><strong>    rpm -ivh http://scala-lang.org/files/archive/scala-$SCALA_VER.rpm</strong></span>
     <span class="strong"><strong>    # Install Activator</strong></span>
    <span class="strong"><strong>    cd /opt</strong></span>
    <span class="strong"><strong>    wget http://downloads.typesafe.com/typesafe-activator/$ACTIVATOR_VER/typesafe-activator-$ACTIVATOR_VER.zip</strong></span>
    <span class="strong"><strong>    unzip typesafe-activator-$ACTIVATOR_VER.zip</strong></span>
    <span class="strong"><strong>    chown -R vagrant:vagrant /opt/activator-$ACTIVATOR_VER</strong></span>
     <span class="strong"><strong>    # Set Path</strong></span>
    <span class="strong"><strong>    echo "export JAVA_HOME=/usr/java/default/" &gt;&gt; /home/vagrant/.bash_profile</strong></span>
    <span class="strong"><strong>    echo "export PATH=$PATH:$JAVA_HOME/bin:/home/vagrant/bin:/opt/activator-$ACTIVATOR_VER" &gt;&gt; /home/vagrant/.bash_profile</strong></span>
    
  10. 最后,我们在实例启动时运行了一个 MySQL Docker 容器:

    <span class="strong"><strong>    # Start MySQL</strong></span>
    <span class="strong"><strong>    docker run -e MYSQL_PASS=$MYSQL_ROOT_PW -i -d -p 3306:3306 --name mysqld -t tutum/mysql</strong></span>
    
  11. 我们运行 Vagrant 命令 vagrant up,从头开始初始化 Vagrant 实例。经过一段时间后,我们的 Play 2 开发环境应该准备好了。使用命令 vagrant ssh 登录到 Vagrant 实例。你应该能够验证是否已安装所有必需的二进制文件:

    <span class="strong"><strong>    [vagrant@VG-play2 ~]$ java -version</strong></span>
    <span class="strong"><strong>    java version "1.7.0_51"</strong></span>
    <span class="strong"><strong>    Java(TM) SE Runtime Environment (build 1.7.0_51-b13)</strong></span>
    <span class="strong"><strong>    Java HotSpot(TM) 64-Bit Server VM (build 24.51-b03, mixed mode)</strong></span>
    <span class="strong"><strong>    [vagrant@VG-play2 ~]$ activator --version</strong></span>
    <span class="strong"><strong>    sbt launcher version 0.13.8-M5</strong></span>
    <span class="strong"><strong>    [vagrant@VG-play2 ~]$ scala -version</strong></span>
    <span class="strong"><strong>    Scala code runner version 2.11.2 -- Copyright 2002-2013, LAMP/EPFL</strong></span>
    <span class="strong"><strong>    [vagrant@VG-play2 ~]$ docker ps</strong></span>
    <span class="strong"><strong>    CONTAINER ID        IMAGE                COMMAND             CREATED                STATUS              PORTS                    NAMES</strong></span>
    <span class="strong"><strong>    08ecd4a3d98c        tutum/mysql:latest   "/run.sh"           21 minutes ago          Up 21 minutes       0.0.0.0:3306-&gt;3306/tcp   mysqld</strong></span>
    
  12. 一旦 Vagrant 实例启动并运行,我们就可以构建和运行一个 Play 网络应用;在这个菜谱中,我们在 /vagrant 目录中安装的 play-slick-angular-test-example activator 模板:

    <span class="strong"><strong>    cd /vagrant/play-slick-angular-test-example</strong></span>
    <span class="strong"><strong>    activator run</strong></span>
    <span class="strong"><strong>    [info] Loading project definition from /vagrant/play-slick-angular-test-example/project</strong></span>
    <span class="strong"><strong>    [info] Set current project to PlaySlickAngularBootstrapH2TestsExample (in build file:/vagrant/play-slick-angular-test-example/)</strong></span>
    <span class="strong"><strong>    [success] Total time: 4 s, completed Apr 6, 2015 2:55:54 PM</strong></span>
    <span class="strong"><strong>    [info] Updating {file:/vagrant/play-slick-angular-test-example/}root...</strong></span>
    <span class="strong"><strong>    [info] Resolving jline#jline;2.12 ...</strong></span>
    <span class="strong"><strong>    [info] Done updating.</strong></span>
     <span class="strong"><strong>    --- (Running the application from SBT, auto-reloading is enabled) ---</strong></span>
     <span class="strong"><strong>    [info] play - Listening for HTTP on /0:0:0:0:0:0:0:0:9000</strong></span>
     <span class="strong"><strong>    (Server started, use Ctrl+D to stop and go back to the console...)</strong></span>
    

还有更多…

记得在你想暂时关闭实例并稍后返回时暂停你的 Vagrant 实例:

<span class="strong"><strong>    vagrant halt</strong></span>

这允许 Vagrant 实例在不重新初始化 Vagrant 实例的情况下保留其当前状态。有关 Vagrant 的更多信息,请参阅docs.vagrantup.com/v2/的文档。

使用 IntelliJ IDEA 14 编码 Play 2 网络应用

对于这个菜谱,我们将探讨如何使用流行的 IDE,IntelliJ IDEA 14 来编码 Play 2 网络应用。我们将使用社区版:

如何做…

对于这个菜谱,你需要执行以下步骤:

  1. 从 JetBrains 网站下载并安装 IntelliJ IDEA 14:

    www.jetbrains.com/idea/download/

  2. 导航到你想使用 IDEA 14 的 Play 2 网络应用;在这个菜谱中,play2-parse-84

    <span class="strong"><strong>    cd play2-parse-84</strong></span>
    
  3. 使用 activator 生成基础 IDEA 14 项目文件:

    <span class="strong"><strong>    $ activator idea</strong></span>
    <span class="strong"><strong>    [info] Creating IDEA module for project 'play2-parse-84' ...</strong></span>
    <span class="strong"><strong>    [info] Running compile:managedSources ...</strong></span>
    <span class="strong"><strong>    [info] Running test:managedSources ...</strong></span>
    <span class="strong"><strong>    [info] Created /Users/cookbook/play2-parse-84/.idea/IdeaProject.iml</strong></span>
    <span class="strong"><strong>    [info] Created /Users/cookbook/play2-parse-84/.idea</strong></span>
    <span class="strong"><strong>    [info] Created /Users/cookbook/play2-parse-84.iml</strong></span>
    <span class="strong"><strong>    [info] Created /Users/cookbook/play2-parse-84/.idea_modules/play2-parse-84- build.iml</strong></span>
    
  4. 启动 IntelliJ IDEA 14 并点击 导入项目

  5. 使用 IDEA 14,导航到您想要工作的项目目录;在这个菜谱中,play2-parse-84:

  6. 在下一屏幕上,选择SBT作为项目的外部模型:

  7. 接下来,选择额外的项目设置,例如要使用的已安装 JDK 版本:

  8. 点击完成后,您应该在 IntelliJ IDEA 14 上正确加载了您的 Play 2 网络应用:

它是如何工作的…

在这个菜谱中,我们简单地使用了 Activator 内置对 IntelliJ IDEA 的支持,通过命令activator idea生成我们的 Play 2 网络应用 IDEA 项目文件。一旦我们从当前代码库中生成了 IDEA 项目文件,我们所需做的就是将其导入到 IntelliJ IDEA 中,并按照项目设置屏幕进行操作。现在我们应该能够使用 IntelliJ IDEA 来处理我们的 Play 2 网络应用。

posted @ 2025-09-11 09:46  绝不原创的飞龙  阅读(22)  评论(0)    收藏  举报