Scala-应用构建指南-全-

Scala 应用构建指南(全)

原文:zh.annas-archive.org/md5/01331a76a3ec40ee1513488a952f28cd

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

函数式编程始于学术界,最终在 IT 行业中得到了应用。Scala 语言是一种多范式语言,被大型企业和大型组织使用,可以帮助您获得正确的(在纯函数式编程的意义上)软件,同时,软件既实用又可扩展。

Scala 拥有一个非常丰富的生态系统,包括 Play 框架、Akka、Slick、Gatling、Finable 等。在这本书中,我们将从函数式和 ReactiveX 编程的基本原则和思想开始,并通过实际示例,您将学习如何使用 Scala 生态系统中最重要的框架,如 Play、Akka 和 Slick 进行编码。

您将学习如何使用 SBT 和 Activator 引导 Scala 应用程序,如何逐步构建 Play 和 Akka 应用程序,以及我们如何使用云和 NetflixOSS 堆栈的理论来扩展大规模 Scala 应用程序。这本书将帮助您从基础知识到最先进的知识,以使您成为 Scala 专家。

本书涵盖的内容

第一章, FP、反应式和 Scala 简介,探讨了如何设置 Scala 开发环境,函数式编程与面向对象编程之间的区别,以及函数式编程的概念。

第二章, 创建您的应用程序架构和使用 SBT 引导,讨论了整体架构,SBT 基础知识,以及如何创建您自己的应用程序。

第三章, 使用 Play 框架开发 UI,涵盖了 Scala 中网络开发的原则,创建我们的模型,创建我们的视图,以及添加验证。

第四章, 开发反应式后端服务,介绍了反应式编程原则,重构我们的控制器,以及将 Rx Scala 添加到我们的服务中。

第五章, 测试您的应用程序,探讨了 Scala 和 JUnit 中的测试原则,行为驱动开发原则,在我们的测试中使用 ScalaTest 规范和 DSL,以及使用 SBT 运行我们的测试。

第六章, 使用 Slick 进行持久化,涵盖了使用 Slick 的数据库持久化原则,在您的应用程序中处理函数式关系映射,使用 SQL 支持创建所需的查询,以及通过异步数据库操作改进代码。

第七章, 创建报告,帮助您了解 Jasper 报告并添加数据库报告到您的应用程序中。

第八章,使用 Akka 开发聊天程序,讨论了 actor 模型、actor 系统、actor 路由和调度器。

第九章,设计您的 REST API,探讨了 REST 和 API 设计,使用 REST 和 JSON 创建我们的 API,添加验证,添加背压,并创建 Scala 客户端。

第十章,扩展,涉及架构原则和扩展 UI、响应式驱动程序和可发现性。它还涵盖了中间层负载均衡器、超时、背压和缓存,并指导你使用 Akka 集群扩展微服务,以及使用 Docker 和 AWS 云扩展基础设施。

你需要为这本书准备什么

为这本书,你需要以下内容:

  • Ubuntu Linux 14 或更高版本

  • Java 8 更新 48 或更高版本

  • Scala 2.11.7

  • Typesafe Activator 1.3.9

  • Jasper Reports 设计器

  • Linux 的 Windows 字体

  • Eclipse IDE

这本书面向的对象

这本书是为想要学习 Scala、以及函数式和响应式技术的专业人士而写的。本书主要面向软件开发者、工程师和架构师。这是一本实用的书,包含实用的代码;然而,我们也提供了关于函数式和响应式编程的理论。

规范

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

文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 昵称将显示如下:“下一步是创建名为SCALA_HOME的环境变量,并将 Scala 二进制文件放入PATH变量中。”

代码块设置如下:

    package scalabook.javacode.chap1; 

    public class HelloWorld { 
      public static void main(String args[]){ 
        System.out.println("Hellow World"); 
      } 
    }

任何命令行输入或输出都写作如下:

export JAVA_HOME=~/jdk1.8.0_77
export PATH=$PATH:$JAVA_HOME/bin

新术语和重要词汇以粗体显示。屏幕上看到的单词,例如在菜单或对话框中,在文本中显示如下:“Actor行为是你将在你的Actor内部拥有的代码。”

注意

警告或重要提示会以这样的框中出现。

提示

技巧和窍门会像这样显示。

读者反馈

我们欢迎读者的反馈。告诉我们你对这本书的看法——你喜欢什么或不喜欢什么。读者反馈对我们很重要,因为它帮助我们开发出你真正能从中获得最大收益的标题。

要向我们发送一般反馈,只需发送电子邮件至 feedback@packtpub.com,并在邮件主题中提及本书的标题。

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

客户支持

现在,你已经是 Packt 图书的骄傲拥有者,我们有一些东西可以帮助你从购买中获得最大收益。

下载示例代码

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

您可以通过以下步骤下载代码文件:

  1. 使用您的电子邮件地址和密码登录或注册我们的网站。

  2. 将鼠标指针悬停在顶部的“支持”标签上。

  3. 点击“代码下载与勘误表”。

  4. 在搜索框中输入书的名称。

  5. 选择您想要下载代码文件的书籍。

  6. 从下拉菜单中选择您购买本书的地方。

  7. 点击“代码下载”。

文件下载完成后,请确保使用最新版本解压缩或提取文件夹:

  • Windows 版的 WinRAR / 7-Zip

  • Mac 版的 Zipeg / iZip / UnRarX

  • Linux 版的 7-Zip / PeaZip

本书代码包也托管在 GitHub 上,网址为github.com/PacktPublishing/Building-Applications-with-Scala。我们还有其他来自我们丰富图书和视频目录的代码包,可在github.com/PacktPublishing/找到。查看它们吧!

下载本书的颜色图像

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

勘误

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

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

盗版

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

请通过版权@packtpub.com 与我们联系,并提供涉嫌盗版材料的链接。

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

问题

如果您对本书的任何方面有问题,您可以通过 questions@packtpub.com 与我们联系,我们将尽力解决问题。

第一章。FP、响应式和 Scala 简介

在我们的第一章中,我们将学习函数式编程(FP)、响应式编程和 Scala 语言的基本概念。这些概念如下所示:

  • 使用 Eclipse Scala IDE 设置 Scala 开发环境。

  • 语言的基本结构,如 var、val、for、if、switch 和操作符重载。

  • 函数式编程与面向对象编程之间的区别。

  • 纯函数式编程(FP)的原则:不可变性、无副作用、状态纪律、组合和高级函数。

  • FP 的概念,如 lambda、递归、for 推导、部分函数、Monads、柯里化和函数。

  • 模式匹配、递归、反射、包对象和并发。

让我们开始吧!

函数式编程

FP 根本不新。FP 的第一个实现是 Lisp,始于 20 世纪 50 年代。目前,我们正处于一个后函数式编程时代,那时我们拥有 50 年代强大的数学原理和思想,与现代最美丽和先进的工程技术相结合,也称为Java 虚拟机JVM)。Scala 是一种基于 JVM 的后函数式编程语言。位于 JVM 之上为我们带来了许多好处,如下所示:

Scala 是一种基于 JVM 的函数式编程语言。位于 JVM 之上为我们带来了许多好处,如下所示:

  • 可靠性和性能:Java 被我们目前排名前 10 的网站中的 10 个使用,如 Netflix、Apple、Uber、Twitter、Yahoo、eBay、Yelp、LinkedIn、Google、Amazon 等。JVM 是规模化的最佳解决方案,并且已经由这些网站规模公司进行了实战测试。

  • 原生 JVM 生态系统:完全访问包括框架、库、服务器和工具在内的所有 Java 生态系统。

  • 操作利用:您的操作团队可以像运行 Java 一样运行 Scala。

  • 遗留代码利用:Scala 允许您轻松地将 Scala 代码与 Java 代码集成。这个特性非常棒,因为它使得 Java 遗留系统集成变得容易。

  • Java 互操作性:用 Scala 编写的代码可以在 Java 中访问。

Scala 是由 Martin Odersky 于 2001 年在 EPFL 创建的。Scala 是一种强静态类型语言,并受到了另一种名为Haskell的函数式语言的启发。Scala 解决了 Java 语言的几个批评,并通过更少的代码和更简洁的程序,在不损失性能的情况下提供了更好的开发者体验。

Scala 和 Java 与 JVM 具有相同的底层结构,但在设计上,Scala 与 Java 相比是不同的语言。Java 是一种命令式面向对象的语言,而 Scala 是一种后函数式、多范式的编程语言。FP 与 面向对象编程OOP)的工作原理不同。OOP 由于 Java、C#、Ruby 和 Python 等语言而变得非常流行和稳固。然而,Scala、Clojure、F# 和 Swift 等语言正在获得巨大的动力,FP 在过去 10 年中得到了很大的发展。大多数新的语言都是纯函数式、后函数式或混合(如 Java 8)。在这本书中,你将看到 Scala 代码与 Java 代码的比较,这样你可以亲自看到 Scala 相比 Java 和命令式 OOP 语言要紧凑、客观和直接得多。

FP 从学术界开始,传播到全世界;FP 到处都是。像 Hadoop 和 Spark 这样的大数据和流处理解决方案(建立在 Scala 和 Akka 之上)都是建立在 FP 思想和原则之上的。FP 传播到 UI,有了 RxJavaScript - 你甚至可以在 Datomic(Clojure)数据库中找到 FP。像 Clojure 和 Scala 这样的语言使 FP 对企业和专业开发者更加实用和有吸引力。在这本书中,我们将探讨 Scala 语言的原理和实践方面。

函数式编程原理

FP 是一种思考方式,一种构建和构建程序的具体风格。拥有 FP 语言在语法方面有很大帮助,但最终,一切都关于思想和开发者心态。FP 倾向于以声明式编程方式管理纪律状态和不可变性,而不是像 Java、Python 和 Ruby 这样的 OOP 语言主要使用的命令式编程。

FP 的根源可以追溯到数学中的 Lambda 演算 - 20 世纪 30 年代开发的一种形式系统。Lambda 演算是一种数学抽象,不是编程语言,但如今在编程语言中很容易看到其概念。

命令式编程使用语句来改变程序状态。换句话说,这意味着你向程序下达命令以执行操作。这种方式描述了程序需要操作的步骤序列。你需要记住的是,FP 的工作方式关注于程序应该完成什么,而不是告诉程序如何去做。当你用 FP 编码时,你倾向于使用更少的变量、循环和条件语句,并编写更多的函数和函数组合。

以下 FP 的核心原理:

  • 不可变性

  • 纪律状态

  • 纯函数和无副作用/纪律状态

  • 首类函数和高阶函数

  • 类型系统

  • 引用透明性

让我们详细理解这些原理。

不可变性

不变性概念是 FP 的核心,这意味着一旦你给某个东西赋值,该值就不会改变。这非常重要,因为它消除了副作用(任何在局部函数作用域之外的东西),例如,在函数外部更改其他变量。不可变性使得代码更容易阅读,因为你知道你正在使用的函数是一个纯函数。由于你的函数具有规范的状态且不更改函数外部的其他变量,因此你不需要查看函数定义外的代码。这听起来像你根本不处理状态,那么你怎么可能以这种方式编写专业应用程序呢?好吧,你会改变状态,但以一种非常规范的方式。你会创建另一个实例或指向该实例的另一个指针,但你不会改变该变量的值。拥有不可变性是拥有更好、更快、更正确程序的关键,因为你不需要使用锁,而且你的代码天生就是并行的。

规范的状态

共享可变状态是邪恶的,因为它很难扩展和并发运行。什么是共享可变状态?一个简单的方式来理解它就是将其视为一个所有函数都可以访问的全局变量。为什么这很糟糕?首先,因为很难保持这个状态正确,因为有许多函数可以直接访问这个状态。其次,如果你正在进行重构,这种代码通常也是最难重构的。阅读这种代码也很困难。这是因为你永远不能信任局部方法,因为你的局部方法只是程序的一部分。并且带有可变状态,你需要查找所有使用该变量的函数,以便理解逻辑。调试也是如此困难。当你带着 FP 原则进行编码时,你应尽可能避免共享可变状态。当然,你可以有状态,但你应该将其保持为局部状态,这意味着在函数内部。这就是状态纪律:你使用状态,但以一种非常规范的方式。这很简单,但如果你是一名专业开发者,这可能很难,因为这种方面现在在 Java、.NET、Ruby 和 Python 等企业语言中很常见。

纯函数和无副作用

纯函数是没有副作用的函数。副作用很糟糕,因为它们是不可预测的,并使你的软件难以测试。假设你有一个没有参数接收且不返回任何内容的方法——这是我们可能遇到的最糟糕的事情之一,因为你怎么测试它?你怎么重用这段代码?这并不是我们所说的纯函数。可能有哪些副作用?数据库调用、全局变量、IO 调用等等。这很有道理,但你不能只使用纯函数,因为这样不实用。

一等函数和高级函数

首类意味着语言将函数视为一等公民。换句话说,这意味着语言支持将函数作为参数传递给其他函数,并将值作为函数返回。首类函数还意味着语言允许你将函数存储为变量或任何其他数据结构。

高阶函数与首类函数相关,但它们不是同一回事。高阶函数通常意味着语言支持部分函数应用和柯里化。高阶函数是一个数学概念,其中函数与其他函数一起操作。

部分函数是指你可以将一个值(参数)固定到特定的函数上,你以后可能改变也可能不改变。这对于函数组合来说很棒。

柯里化是一种将具有多个参数的函数转换为一系列函数的技术,每个函数只有一个参数。Scala 语言并不强制使用柯里化,然而,像 ML 和 Haskell 这样的语言几乎总是使用这种技术。

类型系统

类型系统完全关乎编译器。其理念很简单:你创建一个类型系统,通过这样做,你可以利用编译器来避免各种错误和错误。这是因为编译器有助于确保你只有正确的类型作为参数、转换语句、函数组合等等。编译器不会允许你犯任何基本错误。Scala 和 Haskell 是强类型语言的例子。与此同时,Common Lisp、Scheme 和 Clojure 是动态语言,它们可能在编译时接受错误值。强类型系统最大的好处之一是你必须编写的测试更少,因为编译器会为你处理几个问题。例如,如果你有一个接收字符串的函数,这可能很危险,因为你可以几乎在字符串中传递任何东西。然而,如果你有一个接收名为 salesman 类型的函数,那么你不需要编写验证来检查它是否是 salesman。所有这些可能听起来很愚蠢,但在实际应用中,这可以节省大量的代码并使你的程序编写得更好。强类型的另一个巨大好处是,你将拥有更好的文档,因为你的代码成为你的文档,而且可以更清楚地了解你可以或不能做什么。

指称透明性

指称透明性是一个与纯函数和不可变性紧密相关的概念,因为你的程序有较少的赋值语句,并且当你有它时,你往往永远不会改变那个值。这很好,因为你可以用这种技术消除副作用。在程序执行期间,由于没有副作用,任何变量都可以被替换,程序变得指称透明。Scala 语言在声明变量时使这个概念非常清晰。

安装 Java 8 和 Scala 2.11

Scala 需要 JVM 才能运行,因此我们在安装 Scala 之前需要获取 JDK 8。访问 Oracle 网站,并从 www.oracle.com/technetwork/pt/java/javase/downloads/index.html 下载并安装 JDK 8。

下载 Java 后,我们需要将 Java 添加到 PATH 变量中;否则,你可以使用终端。我们这样做如下:

$ cd ~/
$ wget --no-cookies --no-check-certificate --header "Cookie: gpw_e24=http%3A%2F%2Fwww.oracle.com%2F; oraclelicense=accept-securebackup-cookie" "
http://download.oracle.com/otn-pub/java/jdk/8u77-b03/jdk-8u77-linux-i586.tar.gz"
$ tar -xzvf $ jdk-8u77-linux-x64.tar.gz 
$ rm -f jdk-8u77-linux-x64.tar.gz

下一步是创建一个名为 JAVA_HOME 的环境变量,并将 Java 8 二进制文件放入 PATH 变量中。在 Linux 中,我们需要编辑 ~/.bashrc 文件,并导出所需的变量,如下所示:

export JAVA_HOME=~/jdk1.8.0_77
export PATH=$PATH:$JAVA_HOME/bin

保存文件后,在同一终端中,我们需要通过 $ source ~/.bashrc 命令来源文件。

现在我们可以测试我们的 Java 8 安装。只需输入 $ java -version。你应该会看到如下类似的内容:

$ java -version
java version "1.8.0_77"
Java(TM) SE Runtime Environment (build 1.8.0_77-b03)
Java HotSpot(TM) Server VM (build 25.77-b03, mixed mode)

让我们开始吧。我们将使用最新的 Scala 版本 2.11.8。然而,这本书中的代码应该与任何 Scala 2.11.x 版本兼容。首先,让我们从 www.scala-lang.org/ 下载 Scala。

Scala 在 Windows、Mac 和 Linux 上都能运行。对于这本书,我将展示如何在基于 Debian 的 Ubuntu Linux 上使用 Scala。打开你的浏览器并访问 www.scala-lang.org/download/

下载 scala 2.11.8:它将是一个 TGZ 文件。解压它并将其添加到你的路径中;否则,你可以使用终端。这样做如下:

$ cd ~/
$ wget http://downloads.lightbend.com/scala/2.11.8/scala-2.11.8.tgz
$ tar -xzvf scala-2.11.8.tgz
$ rm -rf scala-2.11.8.tgz

下一步是创建一个名为 SCALA_HOME 的环境变量,并将 Scala 二进制文件放入 PATH 变量中。在 Linux 中,我们需要编辑 ~/.bashrc 文件并导出所需的变量,如下所示:

export SCALA_HOME=~/scala-2.11.8/
export PATH=$PATH:$SCALA_HOME/bin

保存文件后,在同一终端中,我们需要通过 $ source ~/.bashrc 命令来源文件。

现在我们可以测试我们的 Scala 安装。只需输入 $ scala -version。你应该会看到如下类似的内容:

$ scala -version
Scala code runner version 2.11.8 -- Copyright 2002-2016, LAMP/EPFL

你已经成功安装了 Java 8 和 Scala 2.11。现在我们准备好开始学习 Scala 中的 FP 原则了。为此,我们将在开始时使用 Scala REPL。Scala REPL 是默认 Scala 安装的一部分,你只需在终端中输入 $ scala,如下所示:

$ scala
Welcome to Scala 2.11.8 (Java HotSpot(TM) Server VM, Java 1.8.0_77).
Type in expressions for evaluation. Or try :help.
scala>
Scala REPL

恭喜!你已经成功安装了 Java 8 和 Scala 2.11。

读取、评估、打印和循环 - REPL

读取评估打印循环REPL)也被称为语言外壳。许多其他语言都有外壳,例如 Lisp、Python 和 Ruby 等。REPL 是一个简单的环境,可以用来实验语言。使用 REPL 可以编写非常复杂的程序,但这不是 REPL 的目标。使用 REPL 并不影响像 Eclipse 或 IntelliJ IDEA 这样的 IDE 的使用。REPL 是测试简单命令和程序的理想选择,无需像使用 IDE 那样花费大量时间配置项目。Scala REPL 允许你创建变量、函数、类以及复杂的函数。每个命令都有一个历史记录;也有一定程度的自动完成。作为一个 REPL 用户,你可以打印变量值并调用函数。

使用 REPL 的 Scala Hello World

让我们开始吧。打开你的终端,输入 $ scala 以打开 Scala REPL。一旦 REPL 打开,你只需输入 "Hello World"。通过这样做,你执行了两个操作:评估和打印。Scala REPL 将创建一个名为 res0 的变量,并将你的字符串存储在那里。然后它将打印 res0 变量的内容。

Scala REPL Hello World 程序

我们将看到如何在 Scala REPL 中创建 Hello World 程序,如下所示:

$ scala
Welcome to Scala 2.11.8 (Java HotSpot(TM) 64-Bit Server VM, Java 1.8.0_77).
Type in expressions for evaluation. Or try :help.
scala> "Hello World"
res0: String = Hello World
scala> 

Scala 是一种混合语言,这意味着它既是面向对象的,也是函数式的。你可以在 Scala 中创建类和对象。接下来我们将使用类创建一个完整的 Hello World 应用程序。

Scala 面向对象的 Hello World 程序

我们将看到如何在 Scala REPL 中创建面向对象的 Hello World 程序,如下所示:

$ scala
Welcome to Scala 2.11.8 (Java HotSpot(TM) 64-Bit Server VM, Java 1.8.0_77).
Type in expressions for evaluation. Or try :help.
scala> object HelloWorld {
 |   def main(args:Array[String]) = println("Hello World")
 | }
defined object HelloWorld
scala> HelloWorld.main(null)
Hello World
scala>

首先你需要意识到,我们使用的是 object 而不是 class。与 Java 相比,Scala 语言有不同的结构。在 Scala 中,Object 是一个单例。这与在 Java 中编写单例模式相同。

接下来我们看到 Scala 中用于创建函数的 def 关键字。在前面的程序中,我们创建了一个类似于 Java 中创建的方式的 main 函数,并调用内置函数 println 来打印字符串 Hello World。Scala 默认导入了一些 Java 对象和包。在 Scala 中编码不需要你输入,例如,System.out.println("Hello World"),但如果你愿意,你也可以这样做。让我们在下面的代码中看看它:

$ scala
Welcome to Scala 2.11.8 (Java HotSpot(TM) 64-Bit Server VM, Java 1.8.0_77).
Type in expressions for evaluation. Or try :help.
scala> System.out.println("Hello World") 
Hello World
scala>

我们可以,我们也将做得更好。Scala 为控制台应用程序提供了一些抽象,因此我们可以用更少的代码行数来编写这个代码。为了实现这个目标,我们需要扩展 Scala 类 App。当我们从 App 继承时,我们执行了继承,我们不需要定义 main 函数。我们只需将所有代码放入类的主体中,这非常方便,使得代码整洁且易于阅读。

Scala REPL 中的 Scala HelloWorld App

我们将看到如何在 Scala REPL 中创建 Scala HelloWorld App,如下所示:

$ scala
Welcome to Scala 2.11.8 (Java HotSpot(TM) 64-Bit Server VM, Java 1.8.0_77).
Type in expressions for evaluation. Or try :help.
scala> object HelloWorld extends App {
 |  println("Hello World")
 | }
defined object HelloWorld
scala> HelloWorld
object HelloWorld
scala> HelloWorld.main(null)
Hello World
scala>

在 Scala REPL 中编码了 HelloWorld 对象之后,我们可以询问 REPL HelloWorld 是什么,正如你可能意识到的,REPL 会回答 HelloWorld 是一个对象。这是 Scala 编写控制台应用程序的一种非常方便的方法,因为我们只需三行代码就可以有一个 Hello World 应用程序。遗憾的是,要在 Java 中实现相同的程序,需要更多的代码。Java 是一种性能出色的语言,但与 Scala 相比,它是一种冗长的语言。

Java HelloWorld 应用程序

我们将看到如何创建 Java HelloWorld 应用程序如下:

    package scalabook.javacode.chap1; 

    public class HelloWorld { 
      public static void main(String args[]){ 
        System.out.println("Hellow World"); 
      } 
    } 

Java 应用程序需要六行代码,而在 Scala 中,我们能够用 50%的代码(三行代码)完成同样的工作。这是一个非常简单的应用程序。当我们编写复杂的应用程序时,这种差异会更大,因为 Scala 应用程序的代码量远少于 Java。

记住,我们在 Scala 中使用对象是为了有一个单例(确保只有一个类实例的设计模式),如果我们想在 Java 中实现相同的功能,代码可能如下所示:

    package scalabook.javacode.chap1; 

    public class HelloWorldSingleton { 

      private HelloWorldSingleton(){} 

      private static class SingletonHelper{ 
        private static final HelloWorldSingleton INSTANCE =  
        new HelloWorldSingleton(); 
      } 

      public static HelloWorldSingleton getInstance(){ 
        return SingletonHelper.INSTANCE; 
      } 

      public void sayHello(){ 
        System.out.println("Hello World"); 
      } 

      public static void main(String[] args) { 
        getInstance().sayHello(); 
      } 
    } 

这不仅仅关乎代码的大小,还关乎一致性和语言为你提供更多的抽象。如果你编写的代码更少,最终你的软件中出现的错误也会更少。

Scala 语言 - 基础知识

Scala 是一种静态类型语言,具有非常表达性的类型系统,以安全且连贯的方式强制执行抽象。Scala 中的所有值都是 Java 对象(在运行时未装箱的原生类型),因为最终,Scala 是在 Java JVM 上运行的。Scala 强制执行不可变性作为核心的 FP 原则。这种强制执行在 Scala 语言的多个方面发生,例如,当你创建一个变量时,你以不可变的方式创建它,当你使用集合时,你会使用不可变集合。Scala 还允许你使用可变变量和可变结构,但按照设计,它更倾向于不可变结构。

Scala 变量 - var 和 val

当你在 Scala 中编码时,你使用运算符var创建变量,或者你可以使用运算符valvar运算符允许你创建可变状态,只要你将其本地化,遵循 CORE-FP 原则,并避免可变共享状态,这是可以的。

Scala REPL 中的 var 使用

我们将看到如何在 Scala REPL 中使用 var 如下:

$ scala
Welcome to Scala 2.11.8 (Java HotSpot(TM) 64-Bit Server VM, Java 1.8.0_77).
Type in expressions for evaluation. Or try :help.

scala> var x = 10
x: Int = 10

scala> x
res0: Int = 10

scala> x = 11
x: Int = 11

scala> x 
res1: Int = 11

scala> 

然而,Scala 有一个更有趣的结构叫做val。使用val运算符使你的变量不可变,这意味着一旦设置了值,就不能更改它。如果你尝试更改 Scala 中val变量的值,编译器会给你一个错误。作为一个 Scala 开发者,你应该尽可能多地使用val变量,因为这是一种良好的 FP 思维模式,并且会使你的程序更好。在 Scala 中,一切都是对象;没有原生类型 -- varval规则适用于所有可能的对象,除了IntString甚至是一个类。

Scala 在 Scala REPL 中的 val 使用

我们将看到如何在 Scala REPL 中使用val,如下所示:

$ scala
Welcome to Scala 2.11.8 (Java HotSpot(TM) 64-Bit Server VM, Java 1.8.0_77).
Type in expressions for evaluation. Or try :help.
scala> val x = 10
x: Int = 10
scala> x
res0: Int = 10
scala> x = 11
<console>:12: error: reassignment to val
 x = 11
 ^
scala> x
res1: Int = 10
scala>

创建不可变变量

现在,让我们看看如何在 Scala 中定义最常见的数据类型,如IntDoubleBooleanString。记住,你可以根据需要使用valvar创建这些变量。

Scala REPL 中的 Scala 变量类型

我们将看到如何在 Scala REPL 中查看 Scala 变量类型,如下所示:

$ scala
Welcome to Scala 2.11.8 (Java HotSpot(TM) 64-Bit Server VM, Java 1.8.0_77).
Type in expressions for evaluation. Or try :help.
scala> val x = 10
x: Int = 10
scala> val y = 11.1
y: Double = 11.1
scala> val b = true
b: Boolean = true
scala> val f = false
f: Boolean = false
scala> val s = "A Simple String"
s: String = A Simple String
scala>

在前面的代码中,我们没有定义变量的类型。Scala 语言会为我们解决这个问题。然而,如果你想指定类型,也是可能的。在 Scala 中,类型位于变量名称之后。

Scala REPL 中的具有显式类型的 Scala 变量

我们将在 Scala REPL 中看到具有显式类型的 Scala 变量,如下所示:

$ scala
Welcome to Scala 2.11.8 (Java HotSpot(TM) 64-Bit Server VM, Java 1.8.0_77).
Type in expressions for evaluation. Or try :help.
scala> val x:Int = 10
x: Int = 10
scala> val y:Double = 11.1
y: Double = 11.1
scala> val s:String = "My String "
s: String = "My String "
scala> val b:Boolean = true
b: Boolean = true
scala>

Scala 的条件和循环语句

与任何其他语言一样,Scala 支持ifelse这样的条件语句。虽然 Java 有switch语句,但 Scala 有一个更强大且功能性的结构,称为模式匹配器(Pattern Matcher),我们将在本章后面介绍。Scala 允许你在变量赋值时使用if语句,这既实用又有用。

在 Scala REPL 中的 if 语句

我们将看到如何在 Scala REPL 中使用if语句,如下所示:

$ scala
Welcome to Scala 2.11.8 (Java HotSpot(TM) 64-Bit Server VM, Java 1.8.0_77).
Type in expressions for evaluation. Or try :help.
scala> val x = 10
x: Int = 10
scala> if (x == 10) 
 | println ("X is 10")
X is 10
scala> val y = if (x == 10 ) 11
y: AnyVal = 11
scala> y
res1: AnyVal = 11
scala>

在前面的代码中,你可以看到我们根据if条件设置了变量y。Scala 的if条件非常强大,并且也可以用于返回语句。

在 Scala REPL 中的返回语句中的 if 语句

我们将看到如何在 Scala REPL 中的返回语句中使用if语句,如下所示:

$ scala
Welcome to Scala 2.11.8 (Java HotSpot(TM) 64-Bit Server VM, Java 1.8.0_77).
Type in expressions for evaluation. Or try :help.
scala> val x = 10
x: Int = 10
scala> def someFunction = if (x == 10) "X is 10"
someFunction: Any
scala> someFunction
res0: Any = X is 10
scala>

Scala 也支持else语句,你还可以在变量和返回语句中使用它们,如下所示:

~$ scala
Welcome to Scala 2.11.8 (Java HotSpot(TM) 64-Bit Server VM, Java 1.8.0_77).
Type in expressions for evaluation. Or try :help.
scala> val x = 10
x: Int = 10
scala> if (x==10){
 |   println("X is 10")
 | } else {
 |   println ("X is something else")
 | }
X is 10
scala>

现在你将学习如何在 Scala 中使用 for 循环。在 Scala 中,for 循环非常强大。我们将从基础知识开始,稍后我们将继续介绍用于列表推导的函数式循环,也称为List推导。

在 Scala 中,for 循环与范围(range)一起工作,范围是 Scala 的另一种数据结构,表示从起点到终点的数字。范围是通过左箭头运算符(<-)创建的。只要你在同一个 for 循环中使用分号(;),Scala 就允许你在同一个 for 循环中有多个范围。

你还可以在 for 循环中使用if语句来过滤数据,并顺畅地与List结构一起工作。Scala 允许你在 for 循环中创建变量。现在,让我们看看一些代码,这些代码展示了 Scala 语言中各种 for 循环的使用。

Scala REPL 中的基本 for 循环

我们将看到如何在 Scala REPL 中使用基本 for 循环,如下所示:

$ scala
Welcome to Scala 2.11.8 (Java HotSpot(TM) 64-Bit Server VM, Java 1.8.0_77).
Type in expressions for evaluation. Or try :help.
scala> for ( i <- 1 to 10)
 | println("i * " + i + " = " +  i * 10)
i * 1 = 10
i * 2 = 20
i * 3 = 30
i * 4 = 40
i * 5 = 50
i * 6 = 60
i * 7 = 70
i * 8 = 80
i * 9 = 90
i * 10 = 100
scala> 

现在,我们将使用名为List的 Scala 数据结构创建一个 for 循环。这非常有用,因为在代码的第一行,你可以定义一个List以及设置其值。由于我们使用的是List结构,因此除了List本身之外,你不需要传递任何其他参数。

在 Scala REPL 中使用带有 List 的 for 循环

我们将看到如何在 Scala REPL 中使用forList,如下所示:

$ scala
Welcome to Scala 2.11.8 (Java HotSpot(TM) 64-Bit Server VM, Java 1.8.0_77).
Type in expressions for evaluation. Or try :help.
scala> val listOfValues = List(1,2,3,4,5,6,7,8,9,10)
listOfValues: List[Int] = List(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
scala> for ( i<- listOfValues ) println(i)
1
2
3
4
5
6
7
8
9
10
scala>

接下来,我们可以使用带有 if 语句的 for 循环来实现一些过滤操作。在本书的后续部分,我们将探讨使用函数的更函数式的方法来处理过滤。对于这段代码,假设我们只想获取列表中的偶数并打印它们。

Scala REPL 中的带有 if 语句的 for 推导式 - Scala REPL

我们将展示如何在 Scala REPL 中使用 forif 语句,如下所示:

$ scala
Welcome to Scala 2.11.8 (Java HotSpot(TM) 64-Bit Server VM, Java 1.8.0_77).
Type in expressions for evaluation. Or try :help.
scala> val listOfValues = List(1,2,3,4,5,6,7,8,9,10)
listOfValues: List[Int] = List(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
scala> for ( i<- listOfValues ) if  (i % 2== 0) println(i)
2
4
6
8
10
scala>

Java 代码过滤偶数

在 Scala 语言中,我们只需要两行代码就可以完成这个过滤操作,而在 Java 中,这至少需要十一行代码,如下面的代码所示:

    package scalabook.javacode.chap1; 

    import java.util.Arrays; 
    import java.util.List; 

    public class ForLoopsEvenNumberFiltering { 
      public static void main(String[] args) { 
        List<Integer> listOfValues = Arrays.asList( 
          new Integer[]{1,2,3,4,5,6,7,8,9,10}); 
        for(Integer i : listOfValues){ 
          if (i%2==0) System.out.println(i); 
        } 
      } 
    } 

对于推导式

也称为列表或序列推导式,for 推导式是函数式编程中执行循环的一种方式。这是语言支持,用于根据其他集合创建 List 结构或集合。这项任务在 SetBuilder 语法中执行。另一种实现相同目标的方法是使用 Mapfilter 函数,我们将在本章后面介绍。for 推导式可以用生成器形式使用,这将引入新的变量和值,或者以归约方式使用,这将过滤值,生成一个新的集合或序列。语法是:for (expt) yield e,其中 yield 操作符将新值添加到从原始序列创建的新集合/序列中。

Scala REPL 中的推导式

我们将展示如何在 Scala REPL 中使用 for 推导式,如下所示:

$ scala
Welcome to Scala 2.11.8 (Java HotSpot(TM) 64-Bit Server VM, Java 1.8.0_77).
Type in expressions for evaluation. Or try :help.
scala> val names = Set("Diego", "James", "John", "Sam", "Christophe")
names: scala.collection.immutable.Set[String] = Set(John, Sam, Diego, James, Christophe)
scala> 
scala> val brazilians = for {
 |   name <- names 
 |   initial <- name.substring(0, 1)
 |     } yield if (name.contains("Die")) name
brazillians: scala.collection.immutable.Set[Any] = Set((), Diego)
scala>

在前面的代码中,我们创建了一个名字的集合。正如你所见,Scala 默认偏好不可变数据结构,并使用 immutable.Set。当我们应用 for 循环时,我们只是在过滤包含特定子字符串的 names,然后,使用 yield 操作符,我们创建了一个新的 Set 结构。yield 操作符将保持你正在使用的结构。例如,如果我们使用 List 结构,它将创建一个 List 而不是 Set 结构,yield 操作符将始终保持变量上的相同数据集合。前面代码的另一个有趣之处在于,我们将 for 推导式的结果保存在一个名为 Brazilians 的变量中。Java 没有 for 推导式,但我们可以使用类似的代码,尽管这将需要更多的行数。

使用集合进行过滤的 Java 代码

我们将展示如何使用 Java 代码进行集合过滤,如下所示:

    package scalabook.javacode.chap1; 

    import java.util.LinkedHashSet; 
    import java.util.Set; 

    public class JavaNoForComprehension { 
      public static void main(String[] args) { 

        Set<String> names = new LinkedHashSet<>(); 
        Set<String> brazillians = new LinkedHashSet<>(); 

        names.add("Diego"); 
        names.add("James"); 
        names.add("John"); 
        names.add("Sam"); 
        names.add("Christophe"); 

        for (String name: names){ 
          if (name.contains("Die")) brazillians.add(name);  
        } 

        System.out.println(brazillians); 
      } 
    } 

Scala 集合

在上一节中,我们看到了如何在 Scala 中以不可变的方式创建 ListSet 结构。现在我们将学习如何以可变的方式处理 ListSet 结构,以及其他集合,如序列、元组和 Maps。让我们看看 Scala 集合层次结构的树状图,如下面的图所示:

Scala 集合

现在我们来看看 Scala 的 Seq 类层次结构。正如你所见,Seq 也是可遍历的。

Scala 集合

Scala 集合扩展自可遍历,这是所有集合派生的主要特质。例如,List 结构扩展自 Seq 类层次结构,这意味着序列 - List 是一种序列。所有这些树都是不可变或可变的,具体取决于您最终使用的 Scala 包。

让我们看看如何在 Scala 中使用 List 结构执行基本可变操作。为了进行过滤和删除操作,我们需要使用以下 Buffer 序列:

$ scala
Welcome to Scala 2.11.8 (Java HotSpot(TM) 64-Bit Server VM, Java 1.8.0_77).
Type in expressions for evaluation. Or try :help.
scala> var ms = scala.collection.mutable.ListBuffer(1,2,3)
ms: scala.collection.mutable.ListBuffer[Int] = ListBuffer(1, 2, 3)
scala> ms += 4
res0: scala.collection.mutable.ListBuffer[Int] = ListBuffer(1, 2, 3, 4)
scala> ms += 5
res1: scala.collection.mutable.ListBuffer[Int] = ListBuffer(1, 2, 3, 4, 5)
scala> ms += 6
res2: scala.collection.mutable.ListBuffer[Int] = ListBuffer(1, 2, 3, 4, 5, 6)
scala> ms(1)
res3: Int = 2
scala> ms(5)
res4: Int = 6
scala> ms -= 5
res5: scala.collection.mutable.ListBuffer[Int] = ListBuffer(1, 2, 3, 4, 6)
scala> ms -= 6
res6: scala.collection.mutable.ListBuffer[Int] = ListBuffer(1, 2, 3, 4)
scala>

让我们看看下一组代码。

在 Scala REPL 中创建、删除和获取可变列表中的项

我们将看到如何在 Scala REPL 中创建、删除和获取可变列表中的项如下:

$ scala
Welcome to Scala 2.11.8 (Java HotSpot(TM) 64-Bit Server VM, Java 1.8.0_77).
Type in expressions for evaluation. Or try :help.
scala> var names = scala.collection.mutable.SortedSetString
names: scala.collection.mutable.SortedSet[String] = TreeSet(Diego, Jackson, Poletto)
scala> names += "Sam"
res2: scala.collection.mutable.SortedSet[String] = TreeSet(Diego, Jackson, Poletto, Sam)
scala> names("Diego")
res4: Boolean = true
scala> names -= "Jackson"
res5: scala.collection.mutable.SortedSet[String] = TreeSet(Diego, Poletto, Sam)
scala>

您是否曾想在方法中返回多个值?嗯,在 Java 中您必须创建一个类,但在 Scala 中,有一个更方便的方式来执行此任务,而且您不需要每次都创建新类。元组允许您在方法中返回或简单地持有多个值,而无需创建特定类型。

Scala 元组

我们将看到 Scala 元组如下:

$ scala
Welcome to Scala 2.11.8 (Java HotSpot(TM) 64-Bit Server VM, Java 1.8.0_77).
Type in expressions for evaluation. Or try :help.
scala> val config = ("localhost", 8080)
config: (String, Int) = (localhost,8080)
scala> config._1
res0: String = localhost
scala> config._2
res1: Int = 8080
scala>

Scala 有特殊的方法称为 _1_2,您可以使用它们来检索元组的值。您需要记住的唯一一点是值在元组中按照插入顺序存储。

Scala 拥有一个非常实用且有用的集合库。例如,Map 是一个键/值对,可以根据键检索,键是唯一的。然而,Map 的值不需要是唯一的。像其他 Scala 集合一样,您有可变和不可变 Map 集合。请记住,Scala 更倾向于不可变集合而不是可变集合。

Scala REPL 中的 Scala 不变 Map

我们将在 Scala REPL 中看到 Scala 不变 Map 的用法如下:

$ scala
Welcome to Scala 2.11.8 (Java HotSpot(TM) 64-Bit Server VM, Java 1.8.0_77).
Type in expressions for evaluation. Or try :help.
scala> val numbers = Map("one"   -> 1, 
 |                   "two"   -> 2,
 |                   "three" -> 3,
 |                   "four"  -> 4,
 |                   "five"  -> 5,
 |                   "six"   -> 6,
 |                   "seven" -> 7,
 |                   "eight" -> 8,
 |                   "nine"  -> 9,
 |                   "ten"   -> 10)
numbers: scala.collection.immutable.Map[String,Int] = Map(four -> 4, three -> 3, two -> 2, six -> 6, seven -> 7, ten -> 10, five -> 5, nine -> 9, one -> 1, eight -> 8)
scala> 
scala> numbers.keys
res0: Iterable[String] = Set(four, three, two, six, seven, ten, five, nine, one, eight)
scala> 
scala> numbers.values
res1: Iterable[Int] = MapLike(4, 3, 2, 6, 7, 10, 5, 9, 1, 8)
scala> 
scala> numbers("one")
res2: Int = 1
scala>

如您所见,当您使用 Map() 创建 Map 时,Scala 使用 scala.collection.immutable.Map。键和值都是可迭代的,您可以使用 keys 方法访问所有键,或使用 values 方法访问所有值。

Scala REPL 中的 Scala 可变 Maps

我们将在 Scala REPL 中看到 Scala 可变 Map 的用法如下:

$ scala
Welcome to Scala 2.11.8 (Java HotSpot(TM) 64-Bit Server VM, Java 1.8.0_77).
Type in expressions for evaluation. Or try :help.
scala> val map = scala.collection.mutable.HashMap.empty[Int,String]
map: scala.collection.mutable.HashMap[Int,String] = Map()
scala> map += (1 -> "one")
res0: map.type = Map(1 -> one)
scala> map += (2 -> "two")
res1: map.type = Map(2 -> two, 1 -> one)
scala> map += (3 -> "three")
res2: map.type = Map(2 -> two, 1 -> one, 3 -> three)
scala> map += (4 -> "mutable")
res3: map.type = Map(2 -> two, 4 -> mutable, 1 -> one, 3 -> three)
scala> 

如果您正在处理可变状态,您必须明确指定,这在 Scala 中是很好的,因为它增加了开发者的意识,并默认避免了可变共享状态。因此,为了有一个可变 Map,我们需要明确使用 scala.collection.mutable.HashMap 创建 Map。

摩纳哥

摩纳哥是可组合的参数化容器类型,它支持高阶函数。记住,高阶函数是接收函数作为参数并返回函数作为结果的函数。在函数式编程中最常用的函数之一是 Map。Map 接收一个函数,将其应用于容器中的每个元素,并返回一个新的容器。

Scala REPL 中的 Scala Map 函数

我们将在 Scala REPL 中看到 Map 函数的用法如下:

$ scala
Welcome to Scala 2.11.8 (Java HotSpot(TM) 64-Bit Server VM, Java 1.8.0_77).
Type in expressions for evaluation. Or try :help.
scala> 
scala> val numbers = List(1,2,3,4,5,6)
numbers: List[Int] = List(1, 2, 3, 4, 5, 6)
scala> def doubleIt(i:Int):Double = i * 2
doubleIt: (i: Int)Double
scala> val doubled = numbers.map( doubleIt _ )
doubled: List[Double] = List(2.0, 4.0, 6.0, 8.0, 10.0, 12.0)
scala> val doubled = numbers.map( 2.0 * _ )
doubled: List[Int] = List(2.0, 4.0, 6.0, 8.0, 10.0, 12.0)
scala>

在前面的代码中,我们创建了一个包含数字 1,2,3,4,5 和 6 的数字列表。我们还定义了一个名为doubleIt的 Scala 函数,它接收一个整数并将其乘以2.0,得到一个双精度浮点数。map函数对List (1,2,3,4,5,6)中的每个元素调用doubleIt,结果是新的容器,一个新的List实例,包含新的值。

Scala 有一些语法糖可以帮助我们更高效。例如,你可能会意识到在之前的代码中,我们也做了2.0 * _。下划线是一个特殊操作符,用于这个特定情况——它表示当前值正在被迭代到集合中。Scala 会为我们从这个表达式创建一个函数。

正如你可能已经意识到的,map函数有很多用途:一个原因是你可以不显式使用for循环来做复杂的计算,这使得你的代码更函数式。其次,我们可以使用map函数将元素类型从一种类型转换为另一种类型。这就是我们在之前的代码中所做的:我们将整数列表转换为双精度浮点数列表。看看下面的:

scala> val one = Some(1) 
one: Some[Int] = Some(1)
scala> val oneString = one.map(_.toString)
oneString: Option[String] = Some(1)

map函数在多个数据结构上操作,而不仅仅是集合,正如你在之前的代码中看到的。你几乎可以在 Scala 语言中的任何地方使用map函数。

map函数很棒,但你可能会得到嵌套的结构。这就是为什么,当我们与 Monads 一起工作时,我们使用一个稍微不同的map函数版本,称为flatMap,它的工作方式与map函数非常相似,但以扁平形式返回值而不是嵌套值。

为了有一个 monad,你需要有一个名为flatMap的方法。其他函数语言,如 Haskell,将flatMap称为bind,并使用操作符>>=。语法随着语言的变化而变化,但概念是相同的。

Monads 可以以不同的方式构建。在 Scala 中,我们需要一个单参数构造函数,它将作为 monad 工厂工作。基本上,构造函数接收一个类型A,并返回Monad[A]或简称为M[A]。例如,对于Listunit(A)将是== List[A]unit(A),其中a是一个 Option == Option[A]。在 Scala 中,你不需要有 unit;这是可选的。要在 Scala 中有一个 monad,你需要实现 map 和flatMap

与 Monads 一起工作会使你写的代码比以前稍微多一点。然而,你将获得一个更好的 API,这将更容易重用,并且你的潜在复杂性将被管理,因为你不需要写一个充满if和 for 循环的复杂代码。可能性通过类型表达,编译器会为你检查。让我们看看 Scala 语言中的一个简单的 monad 示例:

Scala 中的 Option Monad

我们将如下看到 Scala 中的 Option Monad:

$ scala
Welcome to Scala 2.11.8 (Java HotSpot(TM) 64-Bit Server VM, Java 1.8.0_77).
Type in expressions for evaluation. Or try :help.
scala> val a:Option[Int] = Some(1)
a: Option[Int] = Some(1)
scala> a.get
res0: Int = 1
scala> val b:Option[Int] = None
b: Option[Int] = None
scala> b.get
java.util.NoSuchElementException: None.get
 at scala.None$.get(Option.scala:347)
 at scala.None$.get(Option.scala:345)
 ... 32 elided
scala> b.getOrElse(0)
res2: Int = 0
scala> a == b
res3: Boolean = false
scala> 

在 Haskell 中,这被称为 Maybe monad。Option 表示可选值,因为我们不能 100%确定值是否存在。为了表达一个值,我们使用 Some 类型,为了表达值的缺失,我们使用 none。Option Monads 非常好,它们使你的代码更加明确,因为一个方法可能会接收或返回一个 option,这意味着你明确地表示这可能是 null。然而,这种技术不仅更加明确,而且更加安全,因为你不会得到一个 null 指针,因为你有一个容器包围着值。尽管如此,如果你在 Option 中调用 get 方法,并且它是 none,你将得到一个 NoSuchelementException。为了解决这个问题,你可以使用 getOrElse 方法,并且你可以提供一个回退值,在 none 的情况下将被使用。好吧,但你可能想知道 flatMap 方法在哪里。别担心,Scala 为我们实现了这个方法到 Option 抽象中,所以你可以无问题地使用它。

scala> val c = Some("one")
c: Some[String] = Some(one)
scala> c.flatMap( s => Some(s.toUpperCase) )
res6: Option[String] = Some(ONE)

Scala REPL 可以为你执行自动补全。如果你输入 C + Tab,你会看到 Some 类的所有可用方法。map 函数可供你使用,正如我之前所说的,Scala 中根本没有任何单元函数。然而,如果你在自己的 API 中添加它,这并不错误。

使用 Scala REPL 的所有方法列表

以下是使用 Scala REPL 的所有方法列表:

$ scala
Welcome to Scala 2.11.8 (Java HotSpot(TM) 64-Bit Server VM, Java 1.8.0_77).
Type in expressions for evaluation. Or try :help.
scala> val c = Some("one")
c: Some[String] = Some(one)
scala> c.
++             count       foreach              iterator     productArity   seq         toBuffer             unzip 
++:            drop        genericBuilder       last         productElement size        toIndexedSeq         unzip3 
/:             dropRight   get                  lastOption   productIterator slice      toIterable           view 
:\             dropWhile   getOrElse            map          productPrefix  sliding     toIterator           withFilter 
WithFilter     equals      groupBy              max          
reduce         span        toLeft               x 
addString      exists      grouped              maxBy        
reduceLeft     splitAt     toList               zip 
aggregate      filter      hasDefiniteSize      min          reduceLeftOption stringPrefix   toMap           zipAll 
canEqual       filterNot   hashCode             minBy        reduceOption   sum         toRight              zipWithIndex 
collect        find        head                 mkString     reduceRight    tail        toSeq 
collectFirst   
flatMap        headOption  nonEmpty             reduceRightOption   tails          toSet 
companion            flatten     
init           orElse      repr                 take           
toStream 
contains    fold                 inits                orNull         sameElements takeRight           toString 
copy           foldLeft    isDefined            par          
scan           takeWhile   toTraversable 
copyToArray    foldRight      isEmpty     partition            scanLeft            
to             toVector 
copyToBuffer         forall      isTraversableAgain product scanRight            toArray        
transpose 
scala> c

Scala 类、特性和面向对象编程

作为一种混合函数式语言,Scala 允许你编写面向对象的代码并创建类。现在我们将学习如何创建类和类内的函数,以及如何处理特性,特性在概念上类似于 Java 接口,但在实践中要强大得多。

Scala REPL 中的简单 Scala 类

我们将在 Scala REPL 中看到以下简单的 Scala 类:

$ scala
Welcome to Scala 2.11.8 (Java HotSpot(TM) 64-Bit Server VM, Java 1.8.0_77).
Type in expressions for evaluation. Or try :help.
scala> class Calculator {
 |    def add(a: Int, b: Int): Int = a + b
 |    def multiply(n: Int, f: Int): Int = n * f
 | }
defined class Calculator
scala> 
scala> val c = new Calculator
c: Calculator = Calculator@380fb434
scala> c.add(1,2)
res0: Int = 3
scala> c.multiply(3,2)
res1: Int = 6
scala>

初看,前面的代码看起来像 Java。但让我们添加构造函数、获取器和设置器,然后你就可以看到我们只需几行代码就能完成多少工作。

Scala REPL 中的 Scala 简单 Java 对象

以下是在 Scala REPL 中展示的 Scala 简单 Java 对象:

$ scala
Welcome to Scala 2.11.8 (Java HotSpot(TM) 64-Bit Server VM, Java 1.8.0_77).
Type in expressions for evaluation. Or try :help.
scala> class Person(
 |   @scala.beans.BeanProperty var name:String = "",
 |   @scala.beans.BeanProperty var age:Int = 0
 | ){
 |    name = name.toUpperCase
 |    override def toString = "name: " + name + " age: " + age
 | }
defined class Person
scala> 
scala> val p  = new Person("Diego",31)
p: Person = name: DIEGO age: 31
scala> val p1 = new Person(age = 31, name = "Diego")
p1: Person = name: DIEGO age: 31
scala> p.getAge
res0: Int = 31
scala> p1.getName
res1: String = DIEGO
scala> 

Scala 中的构造函数只是代码行。你可能意识到,我们在前面的例子中得到了 name 变量,并应用了一个函数将给定的名字转换为大写。如果你想,你可以放任意多的行,并且可以执行你想要的任何计算。

在相同的代码上,我们还执行了方法重写,因为我们重写了 toString 方法。在 Scala 中,为了进行重写,你需要在函数定义前使用 override 操作符。

我们只是用 Scala 写了一个只有几行代码的 纯 Java 对象POJO)。Scala 有一个特殊的注解叫做 @scala.beans.BeanProperty,它可以为你生成 getter 和 setter 方法。这非常有用,可以节省很多代码。然而,目标必须是公共的;你不能在私有 varval 对象上应用 BeanProperty 注解。

Java 中的 Person 类

以下是在 Java 中的 Person 类:

    package scalabook.javacode.chap1; 

    public class JavaPerson { 

      private String name; 
      private Integer age; 

      public JavaPerson() {} 

      public JavaPerson(String name, Integer age) { 
        super(); 
        this.name = name; 
        this.age = age; 
      } 

      public JavaPerson(String name) { 
        super(); 
        this.name = name; 
      } 

      public JavaPerson(Integer age) { 
        super(); 
        this.age = age; 
      } 

      public Integer getAge() { 
        return age; 
      } 

      public void setAge(Integer age) { 
        this.age = age; 
      } 

      public String getName() { 
        return name; 
      } 

      public void setName(String name) { 
        this.name = name; 
      } 

    } 

特质和继承

在 Scala 中也可以进行继承。对于这样的任务,你在类定义之后使用 extend 操作符。Scala 只允许你扩展一个类,就像 Java 一样。Java 不像 C++ 那样允许多重继承。然而,Scala 通过使用特质的混合技术允许这样做。Scala 特质类似于 Java 接口,但你也可以添加具体代码,并且你可以在代码中拥有任意数量的特质。

Scala 继承代码在 Scala REPL

以下是在 Scala REPL 中的 Scala 继承代码:

$ scala
Welcome to Scala 2.11.8 (Java HotSpot(TM) 64-Bit Server VM, Java 1.8.0_77).
Type in expressions for evaluation. Or try :help.
scala> class Person(
 |   @scala.beans.BeanProperty var name:String = "",
 |   @scala.beans.BeanProperty var age:Int = 0
 | ){
 |    name = name.toUpperCase
 |    override def toString = "name: " + name + " age: " + age
 | }
defined class Person
scala> 
scala> class LowerCasePerson(name:String,age:Int) extends Person(name,age) {
 |    setName(name.toLowerCase)
 | }
defined class LowerCasePerson
scala> 
scala> val p  = new LowerCasePerson("DIEGO PACHECO",31)
p: LowerCasePerson = name: diego pacheco age: 31
scala> p.getName
res0: String = diego pacheco
scala> 

Scala 不像 Java 那样支持构造函数的继承。因此,你需要重写构造函数并通过超类传递值。类内的所有代码都将作为次要构造函数。类定义中括号 () 内的所有代码都将作为主要构造函数。可以使用 this 操作符来拥有多个构造函数。对于这个特定的实现,我们改变了默认行为,并添加了新的构造函数代码,以便将给定的名称转换为小写,而不是 Person 超类定义的默认大写。

Scala 特质示例代码在 Scala REPL

以下是在 Scala REPL 中的 Scala 特质示例代码:

$ scala
Welcome to Scala 2.11.8 (Java HotSpot(TM) 64-Bit Server VM, Java 1.8.0_77).
Type in expressions for evaluation. Or try :help.
scala> trait Car
defined trait Car
scala> 
scala> trait SportCar {
 |    val brand:String 
 |    def run():String = "Rghhhhh Rghhhhh Rghhhhh...."
 | }
defined trait SportCar
scala> 
scala> trait Printable {
 |    def printIt:Unit 
 | }
defined trait Printable
scala> 
scala> class BMW extends Car with SportCar with Printable{
 |    override val brand = "BMW"
 |    override def printIt:Unit = println(brand + " does " + run() )
 | }
defined class BMW
scala> 
scala> val x1 = new BMW
x1: BMW = BMW@22a71081
scala> x1.printIt
BMW does Rghhhhh Rghhhhh Rghhhhh....
scala>

在前面的代码中,我们创建了多个特质。其中一个叫做 Car,是母特质。特质也支持继承,我们通过 SportCar 特质从 Car 特质扩展了它。SportCar 特质要求一个名为 brand 的变量,并定义了 run 函数的具体实现。最后,我们有一个名为 BMW 的类,它从多个特质扩展而来——这种技术称为 混合

在 Scala REPL 中使用变量混合技术的 Scala 特质

以下是在 Scala REPL 中使用变量混合技术的 Scala 特质示例:

$ scala
Welcome to Scala 2.11.8 (Java HotSpot(TM) 64-Bit Server VM, Java 1.8.0_77).
Type in expressions for evaluation. Or try :help.
scala> trait SportCar {
 |    def run():String = "Rghhhhh Rghhhhh Rghhhhh...."
 | }
defined trait SportCar
scala> 
scala> val bmw = new Object with SportCar
bmw: SportCar = $anon$1@ed17bee
scala> bmw.run
res0: String = Rghhhhh Rghhhhh Rghhhhh....
scala>

Scala 确实是一个非常强大的语言。你可以在运行时向变量添加特质。当你定义一个变量时,你可以在赋值之后使用 with 操作符。这是一个非常有用的特性,因为它使得函数组合更容易。你可以拥有多个专门的特质,并且可以根据需要将它们添加到你的变量中。

Scala 允许你创建 类型别名,这是一个非常简单的技术,可以提高你代码的可读性。它只是一个简单的别名。

在 Scala REPL 中的 Scala 类型别名示例

以下是在 Scala REPL 中的 Scala 类型别名示例:

$ scala
Welcome to Scala 2.11.8 (Java HotSpot(TM) 64-Bit Server VM, Java 1.8.0_77).
Type in expressions for evaluation. Or try :help.
scala> type Email = String
defined type alias Email
scala> 
scala> val e = new Email("me@mail.com.br")
e: String = me@mail.com.br
scala>

当你用 Scala 编码时,强烈建议你为一切使用 type 别名和特质,因为这样你将获得编译器的更多优势,并且可以避免编写不必要的代码和单元测试。

案例类

在 Scala 的面向对象特性方面,我们还没有完成;还有另一种非常有趣的方式来处理 Scala 中的类:所谓的案例类。案例类很棒,因为你可以拥有一个代码行数远少于常规类的类,并且案例类可以是模式匹配器的一部分。

Scala REPL 中的案例类特性

以下是在 Scala REPL 中的案例类特性:

$ scala
Welcome to Scala 2.11.8 (Java HotSpot(TM) 64-Bit Server VM, Java 1.8.0_77).
Type in expressions for evaluation. Or try :help.
scala> case class Person(name: String, age: Int)
defined class Person
scala> val p = Person("Diego",31)
p: Person = Person(Diego,31)
scala> val p2 = Person("Diego",32)
p2: Person = Person(Diego,32)
scala> p.name
res0: String = Diego
scala> p.age
res1: Int = 31
scala> p == p 
res2: Boolean = true
scala> p.toString
res3: String = Person(Diego,31)
scala> p.hashCode
res4: Int = 668670772
scala> p.equals(p2)
res5: Boolean = false
scala> p.equals(p)
res6: Boolean = true
scala>

这是 Scala 处理类的方式。因为这样既简单又紧凑,你几乎可以用一行代码创建一个类,并且可以免费获得 equalshashCode 方法。

模式匹配器

当你在 Java 中编码时,你可以使用 Switch 语句。然而,在 Scala 中,我们有一个更强大的特性,称为模式匹配,它是一种增强版的 switch。

Scala 中的简单模式匹配器

以下是一个 Scala 中的简单模式匹配器:

$ scala
Welcome to Scala 2.11.8 (Java HotSpot(TM) 64-Bit Server VM, Java 1.8.0_77).
Type in expressions for evaluation. Or try :help.
scala> def resolve(choice:Int):String = choice match {
 |     case 1 => "yes"
 |     case 0 => "no"
 |     case _ => throw new IllegalArgumentException("Valid arguments are: 0 or 1\. Your arg is: 
           " + choice)
 | }
resolve: (choice: Int)String
scala> println(resolve(0))
no
scala> println(resolve(1))
yes
scala> try {
 |   println(resolve(33))
 | } catch{
 |   case e:Exception => println("Something Went Worng. EX: " + e)
 | }
Something Went Worng. EX: java.lang.IllegalArgumentException: Valid arguments are: 0 or 1\. Your arg is: 33
scala>

Scala 使用模式匹配进行错误处理。Java 没有像 Scala 那样的模式匹配。它类似于 switch 语句;然而,模式匹配可以在方法返回语句中使用,就像前面代码中看到的那样。Scala 开发者可以指定一个特殊的操作符 _(下划线),它允许你在模式匹配器范围内指定任何内容。这种行为类似于 if 条件中的 else。然而,在 Scala 中,你可以在多个地方使用 _,而不仅仅是作为 Java switch 中的其他分支。

Scala 中的错误处理类似于 Java 中的错误处理。我们使用 try...catch 块。主要区别在于你必须在 Scala 中使用模式匹配,这很棒,因为它为你的代码增加了更多的灵活性。Scala 中的模式匹配可以对许多数据结构进行操作,如案例类、集合、整数和字符串。

前面的代码相当简单直接。接下来,我们将看到使用 Scala 模式匹配器特性的更复杂和高级的代码。

Scala REPL 中的高级模式匹配器

以下是在 Scala REPL 中使用的高级模式匹配器:

$ scala
Welcome to Scala 2.11.8 (Java HotSpot(TM) 64-Bit Server VM, Java 1.8.0_77).
Type in expressions for evaluation. Or try :help.
scala> def factorial(n:Int):Int = n match {
 |     case 0 => 1
 |     case n => n * factorial(n - 1)
 | }
factorial: (n: Int)Int
scala> 
scala> println(factorial(3))
6
scala> println(factorial(6))
720
scala> 

模式匹配可以非常函数式地使用。例如,在前面代码中,我们使用模式匹配进行递归。不需要创建变量来存储结果,我们可以直接将模式匹配放入函数返回,这非常方便,并且可以节省大量的代码行数。

Scala REPL 中的高级复杂模式匹配器

以下是在 Scala REPL 中使用的高级复杂模式匹配器:

$ scala
Welcome to Scala 2.11.8 (Java HotSpot(TM) 64-Bit Server VM, Java 1.8.0_77).
Type in expressions for evaluation. Or try :help.
scala> trait Color
defined trait Color
scala> case class Red(saturation: Int)   extends Color
defined class Red
scala> case class Green(saturation: Int) extends Color
defined class Green
scala> case class Blue(saturation: Int)  extends Color
defined class Blue
scala> def matcher(arg:Any): String = arg match {
 |   case "Scala"                            => "A Awesome Language"
 |   case x: Int                               => "An Int with value " + x
 |   case Red(100)                        => "Red sat 100"
 |   case Red(_)                            => "Any kind of RED sat"
 |   case Green(s) if s == 233       => "Green sat 233"
 |   case Green(s)                          => "Green sat " + s
 |   case c: Color                           => "Some Color: " + c
 |   case w: Any                            => "Whatever: " + w
 | }
matcher: (arg: Any)String
scala> println(matcher("Scala"))
A Awesome Language
scala> println(matcher(1))
An Int with value 1
scala> println(matcher(Red(100)))
Red sat 100
scala> println(matcher(Red(160)))
Any kind of RED sat
scala> println(matcher(Green(160)))
Green sat 160
scala> println(matcher(Green(233)))
Green sat 233
scala> println(matcher(Blue(111)))
Some Color: Blue(111)
scala> println(matcher(false))
Whatever: false
scala> println(matcher(new Object))
Whatever: java.lang.Object@b56c222
scala>

Scala 的模式匹配器非常神奇。我们只是在模式匹配器的中间使用了 if 语句,还使用了 _ 来指定对任何红色值的匹配。我们还在模式匹配器表达式中使用了案例类。

部分函数

部分函数非常适合函数组合。正如我们从模式匹配器中学到的那样,它们可以使用情况语句进行操作。在函数组合方面,部分函数非常出色。它们允许我们分步骤定义一个函数。Scala 框架和库大量使用这个特性来创建抽象和回调机制。还可以检查是否提供了部分函数。

部分函数是可预测的,因为调用者可以在之前检查值是否将被应用到部分函数中。部分函数可以带有或没有类似情况语句的代码。

Scala REPL 中的简单部分函数

下面是一个使用 Scala REPL 的简单部分函数示例:

$ scala
Welcome to Scala 2.11.8 (Java HotSpot(TM) 64-Bit Server VM, Java 1.8.0_77).
Type in expressions for evaluation. Or try :help.
scala> val positiveNumber = new PartialFunction[Int, Int] {
 |   def apply(n:Int) = n / n
 |   def isDefinedAt(n:Int) = n != 0
 | }
positiveNumber: PartialFunction[Int,Int] = <function1>
scala> 
scala> println( positiveNumber.isDefinedAt(6) )
true
scala> println( positiveNumber.isDefinedAt(0) )
false
scala> 
scala> println( positiveNumber(6) ) 
1
scala> println( positiveNumber(0) ) 
 java.lang.ArithmeticException: / by zero
 at $anon$1.apply$mcII$sp(<console>:12)
 ... 32 elided
scala>

部分函数是 Scala 类。它们有一些您需要提供的方法,例如applyisDefinedAtisDefinedAt函数由调用者使用,以检查PartialFunction是否会接受并操作提供的值。当PartialFunction由 Scala 执行时,apply函数将执行工作。

在 Scala REPL 中使用情况语句的 Scala 部分函数

下面是一个在 Scala REPL 中使用情况语句的 Scala 部分函数示例:

$ scala
Welcome to Scala 2.11.8 (Java HotSpot(TM) 64-Bit Server VM, Java 1.8.0_77).
Type in expressions for evaluation. Or try :help.
scala> val positiveNumber:PartialFunction[Int, Int]  =  {
 |   case n: Int if n != 0 => n / n
 | }
positiveNumber: PartialFunction[Int,Int] = <function1>
scala> 
scala> println( positiveNumber.isDefinedAt(6) )
true
scala> println( positiveNumber.isDefinedAt(0) )
false
scala> 
scala> println( positiveNumber(6) ) 
1
scala> println( positiveNumber(0) ) 
scala.MatchError: 0 (of class java.lang.Integer)
 at scala.PartialFunction$$anon$1.apply(PartialFunction.scala:253)
 at scala.PartialFunction$$anon$1.apply(PartialFunction.scala:251)
 at $anonfun$1.applyOrElse(<console>:11)
 at $anonfun$1.applyOrElse(<console>:11)
 at scala.runtime.AbstractPartialFunction$mcII$sp.apply$mcII$sp
  (AbstractPartialFunction.scala:36)
 ... 32 elided
scala>

使用情况语句,Scala 在处理PartialFunction时更加流畅。当您使用情况语句时,您不需要提供applyisDefinedAt函数,因为模式匹配器会处理这些。

Scala REPL 中的部分函数组合

下面是 Scala REPL 中的部分函数组合示例:

$ scala
Welcome to Scala 2.11.8 (Java HotSpot(TM) 64-Bit Server VM, Java 1.8.0_77).
Type in expressions for evaluation. Or try :help.
scala> val even:PartialFunction[Int, String] = {
 |   case i if i%2 == 0 => "even"
 | }
even: PartialFunction[Int,String] = <function1>
scala> 
scala> val odd:PartialFunction[Int, String] = { case _ => "odd"}
odd: PartialFunction[Int,String] = <function1>
scala> 
scala> val evenOrOdd:(Int => String) = even orElse odd
evenOrOdd: Int => String = <function1>
scala> 
scala> println( evenOrOdd(1) == "odd"  )
true
scala> println( evenOrOdd(2) == "even" )
true
scala>

Scala 允许我们组合任意多的PartialFunctionsPartialFunction的组合是通过orElse函数实现的。在前面的代码中,我们定义了一个不可变的变量even,用于验证偶数。其次,我们创建了一个名为odd的第二个不可变变量,用于检查奇数。然后我们进行了组合,并使用orElse运算符创建了一个名为evenOrOdd的第三个PartialFunction,它通过组合偶数和奇数。

包对象

Scala 有像 Java 一样的包。然而,Scala 的包也是对象,您可以在包内编写代码。在包方面,Java 没有 Scala 那样的能力。如果您向包中添加代码,它将对该包内的所有类和函数都可用。

package.scala

您的 package.scala 文件应包含以下代码

    package com.packait.scala.book 

    package object commons { 

      val PI = 3.1415926 

      object constraintsHolder { 
        val ODD = "Odd" 
        val EVEN = "Even" 
      } 

      def isOdd(n:Int):String  = if (n%2==0) constraintsHolder.ODD else 
      null   

      def isEven(n:Int):String = if (n%2!=0) constraintsHolder.EVEN 
      else null 

      def show(s:String) = println(s) 

    } 

这是一个 Scala 包对象。这里有一个特殊的标记,称为package对象,您可以使用它来定义所有在包或子包内部定义的类、对象和函数的公共代码。在这种情况下,我们定义 PI 的值作为一个常量,还有一个包含OddEven字符串值的对象持有者。还有三个辅助函数,这些函数将被包内的类使用。

MainApp.scala

您的 MainApp.scala 文件应包含以下代码

    package com.packait.scala.book.commons 

    object MainApp extends App { 

      show("PI is: " + PI) 
      show(constraintsHolder.getClass.toString()) 

      show( isOdd(2) ) 
      show( isOdd(6) ) 

      show( isEven(3) ) 
      show( isEven(7) ) 

    } 

正如您在前面的代码中所看到的,这个新对象被放置在包中:com.packait.scala.book.commons。另一个有趣的事实是,由于package对象特性,我们在这里没有任何导入语句。当您编译并运行此程序时,您将看到以下输出:

PI is: 3.1415926
class com.packait.scala.book.commons.package$constraintsHolder$
Odd
Odd
Even
Even

Scala 大量使用Package对象,为所有 Scala 开发者提供了许多快捷方式和便利。以下是 Scala package对象定义:

    /*                     __                                               
    *\ 
    **     ________ ___   / /  ___     Scala API                            
    ** 
    **    / __/ __// _ | / /  / _ |    (c) 2003-2013, LAMP/EPFL             
    ** 
    **  __\ \/ /__/ __ |/ /__/ __ |    http://scala-lang.org/               
    ** 
    ** /____/\___/_/ |_/____/_/ | |                                         
    ** 
    **                          |/                                          
    ** 
    \*                                                                      
    */ 

    /** 
     * Core Scala types. They are always available without an explicit 
     import. 
     * @contentDiagram hideNodes "scala.Serializable" 
     */ 
    package object scala { 
      type Throwable = java.lang.Throwable 
      type Exception = java.lang.Exception 
      type Error     = java.lang.Error 

      type RuntimeException                = java.lang.RuntimeException 
      type NullPointerException            = 
      java.lang.NullPointerException 
      type ClassCastException              = 
      java.lang.ClassCastException 
      type IndexOutOfBoundsException       = 
      java.lang.IndexOutOfBoundsException 
      type ArrayIndexOutOfBoundsException  = 
      java.lang.ArrayIndexOutOfBoundsException 
      type StringIndexOutOfBoundsException = 
      java.lang.StringIndexOutOfBoundsException 
      type UnsupportedOperationException   = 
      java.lang.UnsupportedOperationException 
      type IllegalArgumentException        = 
      java.lang.IllegalArgumentException 
      type NoSuchElementException          = 
      java.util.NoSuchElementException 
      type NumberFormatException           = 
      java.lang.NumberFormatException 
      type AbstractMethodError             = 
      java.lang.AbstractMethodError 
      type InterruptedException            = 
      java.lang.InterruptedException 

      // A dummy used by the specialization annotation. 
      val AnyRef = new Specializable { 
        override def toString = "object AnyRef" 
      } 

      type TraversableOnce[+A] = scala.collection.TraversableOnce[A] 

      type Traversable[+A] = scala.collection.Traversable[A] 
      val Traversable = scala.collection.Traversable 

      type Iterable[+A] = scala.collection.Iterable[A] 
      val Iterable = scala.collection.Iterable 

      type Seq[+A] = scala.collection.Seq[A] 
      val Seq = scala.collection.Seq 

      type IndexedSeq[+A] = scala.collection.IndexedSeq[A] 
      val IndexedSeq = scala.collection.IndexedSeq 

      type Iterator[+A] = scala.collection.Iterator[A] 
      val Iterator = scala.collection.Iterator 

      type BufferedIterator[+A] = scala.collection.BufferedIterator[A] 

      type List[+A] = scala.collection.immutable.List[A] 
      val List = scala.collection.immutable.List 

      val Nil = scala.collection.immutable.Nil 

      type ::[A] = scala.collection.immutable.::[A] 
      val :: = scala.collection.immutable.:: 

      val +: = scala.collection.+: 
      val :+ = scala.collection.:+ 

      type Stream[+A] = scala.collection.immutable.Stream[A] 
      val Stream = scala.collection.immutable.Stream 
      val #:: = scala.collection.immutable.Stream.#:: 

      type Vector[+A] = scala.collection.immutable.Vector[A] 
      val Vector = scala.collection.immutable.Vector 

      type StringBuilder = scala.collection.mutable.StringBuilder 
      val StringBuilder = scala.collection.mutable.StringBuilder 

      type Range = scala.collection.immutable.Range 
      val Range = scala.collection.immutable.Range 

      // Numeric types which were moved into scala.math.* 

      type BigDecimal = scala.math.BigDecimal 
      val BigDecimal = scala.math.BigDecimal 

      type BigInt = scala.math.BigInt 
      val BigInt = scala.math.BigInt 

      type Equiv[T] = scala.math.Equiv[T] 
      val Equiv = scala.math.Equiv 

      type Fractional[T] = scala.math.Fractional[T] 
      val Fractional = scala.math.Fractional 

      type Integral[T] = scala.math.Integral[T] 
      val Integral = scala.math.Integral 

      type Numeric[T] = scala.math.Numeric[T] 
      val Numeric = scala.math.Numeric 

      type Ordered[T] = scala.math.Ordered[T] 
      val Ordered = scala.math.Ordered 

      type Ordering[T] = scala.math.Ordering[T] 
      val Ordering = scala.math.Ordering 

      type PartialOrdering[T] = scala.math.PartialOrdering[T] 
      type PartiallyOrdered[T] = scala.math.PartiallyOrdered[T] 

      type Either[+A, +B] = scala.util.Either[A, B] 
      val Either = scala.util.Either 

      type Left[+A, +B] = scala.util.Left[A, B] 
      val Left = scala.util.Left 

      type Right[+A, +B] = scala.util.Right[A, B] 
      val Right = scala.util.Right 

      // Annotations which we might move to annotation.* 
    /* 
      type SerialVersionUID = annotation.SerialVersionUID 
      type deprecated = annotation.deprecated 
      type deprecatedName = annotation.deprecatedName 
      type inline = annotation.inline 
      type native = annotation.native 
      type noinline = annotation.noinline 
      type remote = annotation.remote 
      type specialized = annotation.specialized 
      type transient = annotation.transient 
      type throws  = annotation.throws 
      type unchecked = annotation.unchecked.unchecked 
      type volatile = annotation.volatile 
      */ 
    } 

函数

与任何伟大的函数式编程语言一样,Scala 有很多内置函数。这些函数使我们的代码更加流畅和函数式;现在是时候学习一些这些函数了:

$ scala
Welcome to Scala 2.11.8 (Java HotSpot(TM) 64-Bit Server VM, Java 1.8.0_77).
Type in expressions for evaluation. Or try :help.
scala> // Creates the numbers 1,2,3,4,5 and them multiply they by 2 and creates a new Vector
scala> println ((1 to 5).map(_*2)) 
Vector(2, 4, 6, 8, 10)
scala> 
scala> // Creates 1,2,3 and sum them all with each orher and return the total
scala> println ( (1 to 3).reduceLeft(_+_) )
6
scala> 
scala> // Creates 1,2,3 and multiply each number by it self and return a Vector
scala> println ( (1 to 3).map( x=> x*x ) )
Vector(1, 4, 9)
scala> 
scala> // Creates numbers 1,2,3,4 ans 5 filter only Odd numbers them multiply them odds by 2 and return a Vector
scala> println ( (1 to 5) filter { _%2 == 0 } map { _*2 } )
Vector(4, 8)
scala> 
scala> // Creates a List with 1 to 5 and them print each element being multiplyed by 2
scala> List(1,2,3,4,5).foreach ( (i:Int) => println(i * 2 ) )
2
4
6
8
10
scala> 
scala> // Creates a List with 1 to 5 and then print each element being multiplied by 2
scala> List(1,2,3,4,5).foreach ( i => println(i * 2) )
2
4
6
8
10
scala> 
scala> // Drops 3 elements from the lists
scala> println( List(2,3,4,5,6).drop(3))
List(5, 6)
scala> println( List(2,3,4,5,6) drop 3 )
List(5, 6)
scala> 
scala> // Zip 2 lists into a single one: It will take 1 element of each list and create a pair List
scala> println(  List(1,2,3,4).zip( List(6,7,8) )) 
List((1,6), (2,7), (3,8))
scala> 
scala> // Take nested lists and create a single list with flat elements
scala> println( List(List(1, 2), List(3, 4)).flatten )
List(1, 2, 3, 4)
scala> 
scala> // Finds a person in a List by Age
scala> case class Person(age:Int,name:String)
defined class Person
scala> println( List(Person(31,"Diego"),Person(40,"Nilseu")).find( (p:Person) => p.age <= 33 ) )
Some(Person(31,Diego))
scala>

部分应用

在 Scala 中,下划线(_)在不同的上下文中意味着不同的事情。下划线可以用来部分应用一个函数。这意味着稍后将会提供值。这个特性对于函数组合很有用,并允许您重用函数。让我们看看一些代码。

Scala REPL 中的部分函数

以下是一个使用部分函数的 Scala REPL 示例:

$ scala
Welcome to Scala 2.11.8 (Java HotSpot(TM) 64-Bit Server VM, Java 1.8.0_77).
Type in expressions for evaluation. Or try :help.
scala> def sum(a:Int,b:Int) = a+b
sum: (a: Int, b: Int)Int
scala> 
scala> val add6 = sum(6,_:Int)
add6: Int => Int = <function1>
scala> 
scala> println(add6(1))
7
scala>

在前面的代码中,首先,我们定义了一个名为sum的函数,它接受两个Int参数并计算这两个参数的和。稍后,我们定义了一个函数并将其存储在一个名为add6的变量中。对于add6函数的定义,我们只是调用 sum 函数,传递6_。Scala 将获取通过add6传递的参数,并将其传递给sum函数。

柯里化函数

这个特性在像 Haskell 这样的函数语言中非常流行。柯里化函数与部分应用类似,因为它们允许一些参数现在传递,其他参数稍后传递。然而,它们有一些不同之处。

柯里化函数 - Scala REPL

以下是一个使用柯里化函数的 Scala REPL 示例:

$ scala
Welcome to Scala 2.11.8 (Java HotSpot(TM) 64-Bit Server VM, Java 1.8.0_77).
Type in expressions for evaluation. Or try :help.
scala> // Function Definition
scala> def sum(x:Int)(y:Int):Int = x+y
sum: (x: Int)(y: Int)Int
scala> 
scala> // Function call - Calling a curried function 
scala> sum(2)(3)
res0: Int = 5
scala> 
scala> // Doing partial with Curried functions
scala> val add3 = sum(3) _
add3: Int => Int = <function1>
scala> 
scala> // Supply the last argument now
scala> add3(3)
res1: Int = 6
scala>

对于前面的代码,我们在函数定义中创建了一个柯里化函数。Scala 允许我们将常规/普通函数转换为柯里化函数。以下代码显示了curried函数的用法。

Scala REPL 中的柯里化转换

以下是一个使用柯里化转换的 Scala REPL 示例:

$ scala
Welcome to Scala 2.11.8 (Java HotSpot(TM) 64-Bit Server VM, Java 1.8.0_77).
Type in expressions for evaluation. Or try :help.
scala> def normalSum(x:Int,y:Int):Int=x+y
normalSum: (x: Int, y: Int)Int
scala> 
scala> val curriedSum = (normalSum _).curried
curriedSum: Int => (Int => Int) = <function1>
scala> 
scala> val add3= curriedSum(3)
add3: Int => Int = <function1>
scala> 
scala> println(add3(3))
6
scala>

运算符重载

与 C++类似,Scala 允许运算符重载。这个特性对于创建自定义领域特定语言DSL)非常有用,这对于创建更好的软件抽象或为开发人员或商业人士创建内部或外部 API 非常有用。您应该明智地使用这个特性——想象一下,如果所有框架都决定使用隐式参数重载相同的运算符!您可能会遇到麻烦。与 Java 相比,Scala 是一个非常灵活的语言。然而,您需要小心,否则您可能会创建难以维护或甚至与其他 Scala 应用程序、库或函数不兼容的代码。

Scala REPL 中的 Scala 运算符重载

以下是一个使用 Scala 运算符重载的 Scala REPL 示例:

$ scala
Welcome to Scala 2.11.8 (Java HotSpot(TM) 64-Bit Server VM, Java 1.8.0_77).
Type in expressions for evaluation. Or try :help.
scala> case class MyNumber(value:Int){
 | def +(that:MyNumber):MyNumber = new MyNumber(that.value + this.value)
 | def +(that:Int):MyNumber = new MyNumber(that + this.value)
 | }
defined class MyNumber
scala> val v = new MyNumber(5)
v: MyNumber = MyNumber(5)
scala> 
scala> println(v)
MyNumber(5)
scala> println(v + v)
MyNumber(10)
scala> println(v + new MyNumber(4))
MyNumber(9)
scala> println(v + 8)
MyNumber(13)
scala>

如您所见,我们有两个名为 + 的函数。其中一个函数接收一个 MyNumber 案例类,另一个接收一个 Int 值。如果您愿意,您也可以在 Scala 中使用面向对象编程(OO)和常规类及函数。我们在这里也倾向于使用不可变性,因为我们总是在操作 + 发生时创建一个新的 MyNumber 实例。

隐式参数

隐式参数允许您在 Scala 中施展魔法。权力越大,责任越大。隐式参数允许您创建非常强大的 DSL,但它们也允许您变得疯狂,所以请明智地使用。您可以使用隐式函数、类和对象。Scala 语言以及 Scala 生态系统中的其他核心框架,如 Akka 和 PlayFramework,都多次使用了隐式参数。

SCALA REPL 中的 Scala 隐式参数

$ scala
Welcome to Scala 2.11.8 (Java HotSpot(TM) 64-Bit Server VM, Java 1.8.0_77).
Type in expressions for evaluation. Or try :help.
scala> implicit def transformStringtoInt(n:String) = n.toInt
warning: there was one feature warning; re-run with -feature for details
transformStringtoInt: (n: String)Int
scala> 
scala> val s:String = "123456"
s: String = 123456
scala> println(s)
123456
scala> 
scala> val i:Int = s
i: Int = 123456
scala> println(i)
123456
scala>

要使用隐式参数,您需要在函数前使用关键字 implicit。Scala 将在适当的时候隐式调用该函数。对于这个例子,它将调用将 String 类型转换为 Int 类型的转换。

Scala REPL 中的隐式参数

$ scala
Welcome to Scala 2.11.8 (Java HotSpot(TM) 64-Bit Server VM, Java 1.8.0_77).
Type in expressions for evaluation. Or try :help.
scala> implicit val yValue:Int = 6
yValue: Int = 6
scala> def sum(x:Int)(implicit yValue:Int) = x + yValue
sum: (x: Int)(implicit yValue: Int)Int
scala> val result = sum(10)
result: Int = 16
scala> println(result)
16
scala>

对于最后给出的代码中的另一个情况,我们在 sum 函数中使用了隐式参数。我们在这里还使用了一个柯里化函数。我们首先定义了 implicit 函数,然后调用了 sum 函数。这种技术对于外部化函数配置和您希望避免硬编码的值很有用。它还节省了代码行数,因为您不必总是传递参数给所有函数,所以它非常方便。

Futures

Futures 提供了一种高效的方式来以非阻塞 I/O 风格编写并行操作。Futures 是可能尚未存在的值的占位符对象。Futures 是可组合的,并且它们使用回调而不是传统的阻塞代码。

Scala REPL 中的简单 Future 代码

$ scala
Welcome to Scala 2.11.8 (Java HotSpot(TM) 64-Bit Server VM, Java 1.8.0_77).
Type in expressions for evaluation. Or try :help.
scala> import concurrent.Future
import concurrent.Future
scala> import concurrent.ExecutionContext.Implicits.global
import concurrent.ExecutionContext.Implicits.global
scala> 
scala> val f: Future[String] = Future { "Hello world!" }
f: scala.concurrent.Future[String] = Success(Hello world!)
scala> 
scala> println("Result: " + f.value.get.get)
Result: Hello world!
scala> 
scala> println("Result: " + f)
Result: Success(Hello world!)
scala>

为了在 Scala 中使用 futures,我们必须导入 concurrent.Future。我们还需要一个执行器,这是一种处理线程的方式。Scala 有一个默认的执行服务集合。如果您喜欢,可以对其进行调整,但就目前而言,我们可以直接使用默认设置;为此,我们只需导入 concurrent.ExecutionContext.Implicits.global

可以检索 Future 的值。Scala 有一个非常明确的 API,这使得开发者的生活更加轻松,同时也为我们提供了如何编写我们自己的 API 的良好示例。Future 有一个名为 value 的方法,它返回 Option[scala.util.Try[A]],其中 A 是你为 future 使用的通用类型;在我们的例子中,它是一个 String ATry 是执行 try...catch 的另一种方式,这更安全,因为调用者事先知道他们调用的代码可能会失败。Try[Optional] 表示 Scala 将尝试运行一些代码,这些代码可能会失败——即使它们没有失败,你也可能会收到 NoneSome。这种类型的系统使每个人的生活都变得更好,因为你可以有 SomeNone 作为 Option 返回值。Futures 是一种回调。在我们的前一个示例代码中,结果获得得相当快,然而,我们经常使用 futures 来调用外部 API、REST 服务、微服务、SOAP Web 服务或任何需要时间运行且可能无法完成的代码。Futures 还与模式匹配器一起工作。让我们看看另一个示例代码。

在 Scala REPL 中完整的 Future 示例

$ scala
Welcome to Scala 2.11.8 (Java HotSpot(TM) 64-Bit Server VM, Java 1.8.0_77).
Type in expressions for evaluation. Or try :help.
scala> import concurrent.Future
import concurrent.Future
scala> import concurrent.ExecutionContext.Implicits.global
import concurrent.ExecutionContext.Implicits.global
scala> import scala.util.{Success, Failure}
import scala.util.{Success, Failure}
scala> def createFuture():Future[Int] = {
 | Future { 
 | val r = scala.util.Random
 | if (r.nextInt(100)%2==0) 0 else throw new RuntimeException("ODD numbers are not good here :( ")
 | }
 | }
createFuture: ()scala.concurrent.Future[Int]
scala> def evaluateFuture(f:Future[_]) {
 | f.onComplete {
 | case Success(i) => println(s"A Success $i ")
 | case Failure(e) => println(s"Something went wrong. Ex: ${e.getMessage}")
 | }
 | }
evaluateFuture: (f: scala.concurrent.Future[_])Unit
scala> evaluateFuture(createFuture)
scala> Something went wrong. Ex: ODD numbers are not good here :( 
evaluateFuture(createFuture)
A Success 0 
scala> evaluateFuture(createFuture)
Something went wrong. Ex: ODD numbers are not good here :( 
scala> evaluateFuture(createFuture)
Something went wrong. Ex: ODD numbers are not good here :( 
scala> evaluateFuture(createFuture)
A Success 0 
scala> evaluateFuture(createFuture)
A Success 0 
scala>

有一个名为 createFuture 的函数,每次调用它都会创建一个 Future[Int]。在上面的代码中,我们使用 scala.util.Random 生成介于 0 和 99 之间的随机数。如果数字是偶数,我们返回一个 0,表示成功。然而,如果数字是奇数,我们返回一个 RuntimeException,这表示失败。

有一个名为 evaluateFuture 的第二个函数,它接收任何 Future。我们允许函数的结果是任何类型的通用参数化类型,因为我们使用了魔法下划线 _。然后我们使用两个案例类 SuccessFailure 进行模式匹配。在两种情况下,我们只是在 stdin 上打印。我们还使用了另一个有趣且实用的 Scala 功能,称为字符串插值。我们需要在 "" 前以 s 开头字符串。这允许我们使用 $${} 来评估上下文中的任何变量。这是与我们迄今为止所做不同的字符串连接方法。稍后,我们为 evaluteFuture 函数调用了 6 次,每次传递由 createFuture 函数创建的新 Future。

反应式编程和 RxScala

响应式编程更好、可扩展,是构建应用程序的更快方式。响应式编程可以用面向对象的语言实现,然而,它们与函数式编程语言结合时更有意义。当函数式编程与响应式编程结合时,我们得到一个称为函数式响应式编程FRP)的东西。Scala FRP 可用于许多目的,如 GUI、机器人技术和音乐,因为它为你提供了一个更好的模型来模拟时间。响应式编程是一种新技术,它与流(也称为数据流)一起工作。流是一种以能够表达数据转换和流动的方式思考和编写应用程序的方法。主要思想是通过电路或流传播变化。是的,我们正在谈论一种新的异步编程方式。

用于响应式编程的主要库称为响应式扩展Rx) - reactivex.io/,最初由 Eric Meijer 为.NET 构建。它结合了观察者模式和迭代器模式以及函数式编程的最佳思想。Rx 为许多语言提供了实现,如 Scala、Java、Python、.NET、PHP 等(github.com/ReactiveX)。使用 Rx 进行编码很简单,你可以创建流,使用类似查询的运算符进行组合,还可以订阅任何可观测流以执行数据转换。Rx 被许多成功的公司使用,如 Netflix、GitHub、Microsoft、SoundCloud、Couchbase、Airbnb、Trello 等。在这本书中,我们将使用 RxScala,它是响应式流的 Scala 实现。

下表显示了您需要了解的主要类/概念,以便与 Rx 一起工作。

术语/类 概念
可观测对象 从源创建异步可组合的流。
观察者 一种回调函数类型。
订阅 订阅者和可观测对象之间的绑定。接收来自可观测对象的通告。

响应式流也是试图统一和标准化响应式流处理的通用规范名称。有几个实现,如 RxJava/RxScala、Reactor、Akka、Slick 和 Vert.x。更多信息请参阅github.com/reactive-streams/reactive-streams-jvm

回到可观测对象 -- 我们可以对可观测对象执行各种操作。例如,我们可以过滤、选择、聚合、组合、执行基于时间的操作,并应用背压。与回调相比,使用可观测对象有两个显著的优点。首先,可观测对象对底层 I/O 和线程的处理方式没有偏见,其次,当你编写复杂代码时,回调往往会嵌套,这时事情会变得丑陋且难以阅读。由于函数式编程(FP),可观测对象有简单的方法来进行组合。

Observables 在值可用时将值推送到消费者,这很好,因为这样值可以以同步或异步的方式到达。Rx 提供了一系列集合操作符,可以执行各种你可能需要的数据转换。现在让我们看看一些代码。我们将使用 RxScala 版本 0.26.1,它与 RxJava 版本 1.1.1+兼容。RxScala 只是 RxJava 的一个包装器(由 Netflix 创建)。为什么不直接使用 RxJava 呢?因为语法不会很愉快;使用 RxScala,我们可以获得流畅的 Scala 体验。RxJava 很棒,然而,Java 语法对于这个来说并不愉快——实际上,Scala 的语法相当糟糕。

简单的 RxScala Scala

package scalabook.rx.chap1 

import rx.lang.scala.Observable 
import scala.concurrent.duration._ 

object SimpleRX extends App { 

  val o = Observable. 
            interval(100 millis). 
            take(5)             

  o.subscribe( x => println(s"Got it: $x") )             
  Thread.sleep(1000)           

  Observable. 
      just(1, 2, 3, 4). 
      reduce(_+_). 
      subscribe( r => println(s"Sum 1,2,3,4 is $r in a Rx Way")) 

} 

如果你运行前面的 Scala 程序,你将看到以下输出:

简单的 RxScala Scala - 在控制台中的执行

Got it: 0 
Got it: 1 
Got it: 2 
Got it: 3 
Got it: 4 
Sum 1,2,3,4 is 10 in a Rx Way 

如果你尝试在 Scala REPL 中运行此代码,它将会失败,因为我们需要 RxScala 和 RxJava 依赖项。为此,我们需要 SBT 和依赖项管理。不要担心,我们将在下一章中介绍如何在 Scala 应用程序中使用 SBT。

回到 Observables,我们需要导入 Scala Observable。确保你从 Scala 包中获取它,因为如果你获取 Java 版本,你将会遇到问题:在代码的第一个部分,我们将从 0 开始每 100 毫秒获取一个数字,这段代码将会无限期地运行。为了避免这种情况,我们使用 take 函数将限制放入集合中,这样我们就会得到前五个值。然后,稍后,我们订阅观察者,当数据准备好时,我们的代码将会运行。对于第一个示例,它非常简单,我们只是打印出我们得到的值。在这个程序中有一个线程休眠,否则程序将会终止,你将看不到控制台上的任何值。

代码的第二部分做了更有趣的事情。首先,它从一个静态值列表中创建了一个 Observable,这些值是 1、2、3 和 4。我们将一个 reduce 函数应用到这些元素上,它会将所有元素相加,然后我们订阅并打印结果。

复杂的 Scala 与 RxScala Observables

package scalabook.rx.chap1 

import rx.lang.scala.Observable 

object ComplexRxScala extends App { 

  Observable. 
      just(1,2,3,4,5,6,7,8,9,10).       // 1,2,3,4,5,6,7,8,9,10 
      filter( x => x%2==0).             // 2,4,6,8,10 
      take(2).                          // 2,4 
      reduce(_+_).                      // 6 
      subscribe( r => println(s"#1 $r")) 

   val o1 = Observable. 
            just(1,2,3,4,5,6,7,8,9,10).  // 1,2,3,4,5,6,7,8,9,10 
            filter( x => x%2==0).        // 2, 4, 6, 8, 10 
            take(3).                     // 2, 4 ,6     
            map( n => n * n)             // 4, 16, 36 

   val o2 = Observable.                  
            just(1,2,3,4,5,6,7,8,9,10). // 1,2,3,4,5,6,7,8,9,10  
            filter( x => x%2!=0).       // 1, 3, 5, 7, 9    
            take(3).                    // 1, 3, 5 
            map( n => n * n)            // 1, 9, 25 

   val o3 = o1\. 
           merge(o2).                  // 2,16, 36, 1, 9, 25 
           subscribe( r => println(s"#2 $r")) 

} 

代码的前一部分创建了一个从 1 到 10 的数字 Observable,然后应用了一个filter函数,它只会获取偶数。然后它将它们相加,计算它们的总和,最后打印出解决方案。你可以将其可视化如下面的图像所示:

复杂的 Scala 与 RxScala Observables

对于代码的第二部分,我们创建了两个不同的 Observable。第一个是偶数,第二个是奇数。这两个 Observable 彼此解耦;你可以控制你想要的任意多个 Observable。稍后,代码使用 merge 函数将这两个 Observable 合并成一个第三和新的 Observable,它包含第一个和第二个 Observable 的内容。

复杂的 Scala 与 RxScala Observables

合并 2 个 Observables

有许多功能和选项,您可以在rxmarbles.com/github.com/ReactiveX/RxScala查看完整列表。为了简化,目前我们只使用数字。稍后,我们将使用这些知识进行更高级的合成,包括数据库调用和外部 Web 服务调用。

摘要

在本章中,我们学习了 FP(函数式编程)、响应式编程和 Scala 语言的基本概念。我们了解了 Scala 语言和函数式编程的基本结构,包括函数、集合、Scala 中的面向对象编程,以及使用 Futures 的并发编程。

接下来,我们将了解如何使用 SBT 构建 Scala 项目。我们将学习如何使用 SBT 编译和运行 Scala 应用程序。

第二章。创建你的应用程序架构并使用 SBT 引导

在上一章中,我们学习了函数式编程和 Scala。本章将专注于简单构建工具SBT)和 Activator,以便引导复杂的 Scala 和 Play 框架项目。使用 SBT 和 Activator,我们可以执行多个开发任务,例如构建、运行测试和部署应用程序(将在第十章中详细介绍,扩展)。让我们开始吧。

在本章中,我们将看到以下主题:

  • SBT 基础--安装、结构和依赖

  • Activator 基础--创建项目

  • 我们应用程序的整体架构

介绍 SBT

SBT 是构建和打包 Scala 应用程序的终极 Scala 解决方案。SBT 拥有许多插件,如 Eclipse 和 IntelliJ IDEA 项目生成,这在进行 Scala 开发时非常有帮助。SBT 是用 Scala 编写的,以便帮助你构建 Scala 应用程序。然而,如果你愿意,SBT 仍然可以用来构建 Java 应用程序。

SBT 的核心功能如下:

  • 基于 Scala 的构建定义

  • 增量编译

  • 持续编译和测试

  • 对 ScalaCheck、Specs、ScalaTest 和 JUnit 等测试库的出色支持

  • REPL 集成

  • 并行任务执行

在本章的后面部分,我们将使用 SBT 与 Typesafe Activator 一起引导我们的应用程序。在这样做之前,我们将使用 SBT 来学习为 Scala 应用程序设置构建项目的关键概念。在这本书中,我们将使用 SBT 版本 0.13.11。

在 Ubuntu Linux 上安装 SBT

请记住,在安装 SBT 之前,我们需要安装 Java 和 Scala。如果你还没有安装 Java 和 Scala,请回到第一章,函数式编程、响应式编程和 Scala 简介,并遵循安装说明。打开终端窗口,并运行以下命令以下载和安装 SBT:

$ cd /tmp
$ wget https://repo.typesafe.com/typesafe/ivy-releases/org.scala- 
sbt/sbt-launch/0.13.11/sbt-launch.jar?
_ga=1.44294116.1153786209.1462636319 -O sbt-launch.jar
$ chmod +x sbt-launch.jar
$ mkdir ~/bin/ && mkdir ~/bin/sbt/
$ mv sbt-launch.jar ~/bin/sbt/
$ cd ~/bin/sbt/
$ touch sbt

将以下内容添加到~/bin/sbt/sbt文件中:

#!/bin/bash
w

保存~/bin/sbt/sbt文件后,我们需要使用以下命令给予文件执行权限:

$ chmod u+x ~/bin/sbt/sbt

现在,我们需要将 SBT 放入操作系统的路径中,以便在任何 Linux 终端中执行。我们需要通过PATH命令将 SBT 导出到~/.bashrc文件中。使用你喜欢的编辑器打开~/.bashrc文件,并添加以下内容:

export SBT_HOME=~/bin/sbt/
export PATH=$PATH:$SBT_HOME

我们需要使用$ source ~/.bashrc命令来源文件。

现在,我们可以运行 SBT 并继续安装。当你现在在控制台输入$ sbt时,SBT 将下载所有必需的依赖项以运行自身。

开始使用 SBT

让我们创建一个名为hello-world-sbt的文件夹,并添加以下项目结构:

开始使用 SBT

对于build.properties,你需要以下内容:

    build.properties 

    sbt.version=0.13.11 

对于 hello_world.scala,我们将使用以下代码:

    hello_world.scala 

    object SbtScalaMainApp extends App { 
      println("Hello world SBT / Scala App ") 
    } 

目前我们将使用一个 SBT DSL。然而,由于 SBT 是用 Scala 编写的,如果我们愿意,我们可以使用build.scala格式。在某些情况下,这很方便,因为我们可以使用任何类型的 Scala 代码来使构建更加动态,并重用代码和任务。

我们将设置一些预定义的变量,但是你也可以创建自己的变量,这些变量可以用来避免重复代码。最后,让我们看看build.sbt文件的内容如下:

    build.scala 

    name := "hello-world-sbt" 

    version := "1.0" 

    scalaVersion := "2.11.8" 
    scalaVersion in ThisBuild := "2.11.8" 

在前面的代码中,我们有应用程序的名称、将在生成的 JAR 文件中使用的版本,以及应用程序和构建过程中使用的 Scala 版本。我们现在准备好构建这个项目,所以打开你的终端并输入 $ sbt compile

这个指令将使 SBT 编译我们的 Scala 代码,你应该会看到以下屏幕上的内容:

$ sbt compile 

使用 SBT 入门

恭喜!SBT 刚刚编译了我们的 Scala 应用程序。现在我们可以使用 SBT 运行应用程序。为了做到这一点,我们只需要输入 $ sbt run,如下所示:

$ sbt run 

使用 SBT 入门

SBT 使得测试和玩 Scala 应用程序变得更容易,因为 SBT 有一个像我们在第一章中玩过的 Scala REPL,FP、响应式和 Scala 简介。SBT REPL 使得项目下可能有的所有 Scala 代码在 REPL 中可用。

执行命令 $ sbt console

使用 SBT 入门

一旦你进入了 REPL,你可以输入任何 Scala 代码。正如你可能已经意识到的,我只是通过 $ SbtScalaMainApp.main(null) 直接调用了主 Scala 应用程序。

添加依赖项

任何构建工具都允许你解决依赖项。SBT 使用 Ivy / Maven2 模式来解决依赖项。所以,如果你熟悉 Maven2、Gradle 或 Ant/Ivy,你会意识到设置 SBT 依赖项与它们相同,尽管语法不同。依赖项在build.sbt文件中定义。没有单元测试就没有 Scala 开发。最受欢迎的测试库之一是 JUnit (junit.org/junit4/)。JUnit 与 Java 和 Scala 项目一起工作。SBT 将下载并将 JUnit 添加到你的 Scala 应用程序classpath参数中。我们需要编辑build.sbt文件,如下添加 JUnit 作为依赖项:

    build.sbt 
    name := "hello-world-sbt" 

    version := "1.0" 

    scalaVersion := "2.11.7" 
    scalaVersion in ThisBuild := "2.11.7" 

    libraryDependencies += "junit" % "junit" % "4.12" % Test 
    libraryDependencies += "com.novocode" % "junit-interface" % "0.11" 
    % "test" 

    testOptions += Tests.Argument(TestFrameworks.JUnit, "-q", "-v") 

如我之前提到的,SBT 使用与 Maven2 / Ivy 相同的模式:组 ID + artifactid + 版本。如果你不知道你想要添加的库的模式,你可以查看以下链接的 Maven 仓库网站(它们还生成 SBT 配置):mvnrepository.com/artifact/junit/junit/4.12

添加依赖项

SBT 有依赖项的作用域。我们不希望将 JUnit 作为源代码依赖项的一部分进行分发。这就是为什么在依赖定义后面有 % Test 的原因。

保存包含新内容的文件后,您可以运行 $ sbt compile。SBT 会为您下载 JUnit 并将 jar 文件存储在位于 /home/YOUR_USER/.ivy2/cache 的本地 Ivy 缓存文件中。有了依赖项,我们可以添加更多代码,并使用 SBT 来运行我们的测试,如下所示:

    src/main/scala/calc.scala 
    class Calculator{ 
      def sum(a:Int,b:Int):Int = { 
        return a + b 
      } 
      def multiply(a:Int,b:Int):Int = { 
        return a * b 
      } 
    } 

在前面的代码中,我们仅使用 Scala 创建了一个简单直接的计算器,它可以对两个整数进行加法运算,也可以对两个整数进行乘法运算。现在我们可以继续使用 JUnit 进行单元测试。测试需要位于 src/test/scala/ 文件夹中。看看以下代码:

    src/test/scala/calcTest.scala 

    import org.junit.Test 
    import org.junit.Assert._ 

    class CalcTest { 
      @Test 
      def testSumOK():Unit = { 
        val c:Calculator = new Calculator() 
        val result:Int = c.sum(1,5) 
        assertNotNull(c) 
        assertEquals(6,result) 
      } 

      @Test 
      def testSum0():Unit = { 
        val c:Calculator = new Calculator() 
        val result:Int = c.sum(0,0) 
        assertNotNull(c) 
        assertEquals(0,result) 
      } 

      @Test 
      def testMultiplyOk():Unit = { 
        val c:Calculator = new Calculator() 
        val result:Int = c.multiply(2,3) 
        assertNotNull(c) 
        assertEquals(6,result) 
      } 

      @Test 
      def testMultiply0():Unit = { 
        val c:Calculator = new Calculator() 
        val result:Int = c.multiply(5,0) 
        assertNotNull(c) 
        assertEquals(4,result) 
      } 

    } 

好的,现在我们可以使用命令 $ sbt test 来运行测试,如下所示:

添加依赖项

如您在前面的屏幕截图中所见,所有测试都在运行。当我们添加 Java 注解 @Test 时,就会创建一个测试,并且它需要是一个公共函数。有一个测试,名为 testMultiply0,它失败了,因为它期望结果是 4,但是 5 乘以 0 等于零,所以这个测试是错误的。让我们通过将断言更改为接受零来修复这个方法,如下面的代码所示,然后按照以下方式重新运行 $sbt test

    @Test 
    def testMultiply0():Unit = { 
      val c:Calculator = new Calculator() 
      val result:Int = c.multiply(5,0) 
      assertNotNull(c) 
      assertEquals(0,result) 
    } 

$ sbt test 会给出以下结果:

添加依赖项

哈喽!所有测试都通过了。默认情况下,SBT 会并行运行所有测试,这对于加快构建时间非常有用 - 没有人喜欢在构建时等待,而 Scala 也不是构建速度最快的科技。但是,如果您想禁用并行测试,可以在 build.sbt 中添加以下行:

    parallelExecution in Test := false 

从 SBT 生成 Eclipse 项目文件

SBT 通过插件可以生成 Eclipse 文件。您可以直接将这些插件添加到您的 build.sbt 文件中。然而,有一个更好的解决方案。您可以定义全局配置,这是理想的,因为您不需要在每一个简单的 build.sbt 文件中添加。如果您正在处理多个项目,或者您正在处理开源项目,这也非常有意义,因为这是一个个人偏好的问题,人们通常不会对 IDE 文件进行版本控制。

如果存在,请转到以下目录,否则请创建以下目录:/home/YOUR_USER/.sbt/0.13/plugins

现在创建一个包含以下内容的文件 build.sbt

/home/YOUR_USER/.sbt/0.13/plugins/build.sbt 全局配置文件

    resolvers += Classpaths.typesafeResolver 
    addSbtPlugin("com.typesafe.sbteclipse" % "sbteclipse-plugin" % 
    "4.0.0") 

保存包含此内容的文件后,我们可以通过执行 $ sbt reload 来重新加载我们的 SBT 应用程序,或者通过按 Ctrl + D 退出 SBT 控制台,然后使用 $ sbt. 再次打开 sbt。

$ sbt reload

从 SBT 生成 Eclipse 项目文件

现在我们可以使用命令 $ eclipse 来生成 Eclipse 文件。

从 SBT 生成 Eclipse 项目文件

生成完成后,你可以将生成的.project 文件导入到 Eclipse 中。

默认情况下,Eclipse 在生成 Eclipse 项目时不会附加源文件夹。如果你需要源代码(例如 JUnit 这样的第三方依赖项的源代码),你需要在你的build.sbt项目中添加额外的行。添加源文件夹通常是一个好主意,否则,没有源代码你无法进行适当的调试。

build.sbt

    EclipseKeys.withSource := true 

以下截图显示了导入到 Eclipse 中的 SBT Scala 应用程序:

从 SBT 生成 Eclipse 项目文件

应用程序分发

对于本节,我们将尝试三种不同的打包解决方案,如下所示:

  • 默认 SBT 打包器

  • SBT assembly 插件

  • SBT 原生打包器

SBT 默认可以生成 jar 文件。通过 SBT 插件,也可以生成 RPMs、DEBs,甚至 Docker 镜像。首先,让我们生成一个可执行的 jar 文件。这是通过 SBT 中的 package 任务完成的。打开你的 SBT 控制台,并运行$ sbt package。然而,我们想要生成一个FAT jar,这是一个包含应用程序所有其他依赖项(jar 文件)的 jar 文件。为了做到这一点,我们需要使用另一个名为 assembly 的插件。

应用程序分发

SBT 包可以生成一个 jar 文件,但它不包含依赖项。为了使用 assembly 插件,创建文件project/assembly.sbt,并添加以下内容:

    $ project/assembly.sbt 
    addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "0.11.2") 

在我们的build.sbt中,我们需要导入 assembly 插件,如下所示:

$ build.sbt(放入文件顶部)

    import AssemblyKeys._ 
    assemblySettings 

现在,我们可以运行$ sbt assembly来生成我们的FAT jar。

应用程序分发

就这样。现在我们可以通过使用命令java -jar将其作为一个普通的 Java 应用程序运行,如下所示:

    $ java -jar hello-world-sbt/target/scala-2.11/hello-world-sbt-
    assembly-1.0.jar 

Hello world SBT / Scala App

另有一个用于打包 Scala 应用程序的有用插件,名为sbt-native-packagersbt-native-packager可以为 Linux 操作系统生成包,如 DEB 和 RPM 文件。由于这是一个新插件,我们需要在project/中创建一个名为plugins.sbt的文件,如下所示:

    resolvers += "Typesafe repository" at 
    "http://repo.typesafe.com/typesafe/releases/" 
    addSbtPlugin("com.typesafe.sbt" %% "sbt-native-packager" % "1.0.4") 

在你的build.sbt的末尾,你需要添加以下行:

    enablePlugins(JavaAppPackaging) 

现在,我们可以使用$ sbt universal:packageBin$ sbt universal:packageZipTarball来生成带有sbt-native-packager的包。

Hello world SBT / Scala App

现在我们有了 ZIP 和 TGZ 文件,其中包含你的应用程序,位于hello-world-sbt/target/universal/文件夹中。在这个 ZIP/TGZ 文件中,我们有我们的应用程序,以 jar 格式存在,包含所有依赖项;目前我们只有 Scala,但如果更多的话,它们也会在那里。有 SH 和 BAT 脚本,可以在 Linux(SH)和 Windows(BAT)上轻松运行此应用程序。

sbt-native-packager还可以制作 Docker 镜像。这很棒,因为这使得将应用程序部署到生产环境更容易。我们的项目已经完全准备好制作 Docker 镜像。我们需要在 Linux 上安装 docker;你可以通过运行以下命令来完成:

sudo apt-get update
sudo apt-get install docker-engine
sudo service docker start
sudo docker run hello-world

如果您已成功安装 Docker,您应该会看到以下类似的截图:

Hello world SBT / Scala App

现在,您可以通过运行$ sbt,然后使用命令$ docker:publishLocal生成您的 docker 镜像。您将看到类似以下输出的内容:

> docker:publishLocal
[info] Wrote /home/diego/github/diegopacheco/Book_Building_Reactive_Functional_Scala_Applications/hello-world-sbt/target/scala-2.11/hello-world-sbt_2.11-1.0.pom
[info] Sending build context to Docker daemon 5.769 MB
[info] Step 1 : FROM java:latest
[info] Pulling repository docker.io/library/java
[info] 31ae46664586: Pulling image (latest) from docker.io/library/java
[info] 31ae46664586: Pulling image (latest) from docker.io/library/java, endpoint: https://registry-1.docker.io/v1/
[info] 31ae46664586: Pulling dependent layers
[info] e9fa146e2b2b: Pulling metadata
[info] Status: Downloaded newer image for java:latest
[info] docker.io/library/java: this image was pulled from a legacy registry. Important: This registry version will not be supported in future versions of docker.
[info] ---> 31ae46664586
[info] Step 2 : WORKDIR /opt/docker
[info] ---> Running in 74c3e354e9fd
[info] ---> d67542bcaa1c
[info] Removing intermediate container 74c3e354e9fd
[info] Step 3 : ADD opt /opt
[info] ---> f6cec2a2779f
[info] Removing intermediate container 0180e167ae2d
[info] Step 4 : RUN chown -R daemon:daemon .
[info] ---> Running in 837ecff2ffcc
[info] ---> 8a261bd9d88a
[info] Removing intermediate container 837ecff2ffcc
[info] Step 5 : USER daemon
[info] ---> Running in 6101bd5b482b
[info] ---> e03f5fa23bdf
[info] Removing intermediate container 6101bd5b482b
[info] Step 6 : ENTRYPOINT bin/hello-world-sbt
[info] ---> Running in 43de9335129c
[info] ---> eb3961f1e26b
[info] Removing intermediate container 43de9335129c
[info] Step 7 : CMD
[info] ---> Running in 302e1fcd0a3d
[info] ---> 04e7872e85fa
[info] Removing intermediate container 302e1fcd0a3d
[info] Successfully built 04e7872e85fa
[info] Built image hello-world-sbt:1.0
[success] Total time: 447 s, completed 07/05/2016 17:41:47
>

您可以通过运行命令$ docker ps:来确认系统中有一个新的 docker 镜像

Hello world SBT / Scala App

第一张图片是我们通过sbt-native-packager插件生成的 Scala 应用程序的 docker 镜像。恭喜!您的 Scala 应用程序正在运行的 docker 容器中。SBT Native Packager 功能强大,使用简单。您可以在官方文档网站上获取更多详细信息(www.scala-sbt.org/sbt-native-packager/gettingstarted.html)。

这些是我们构建专业 Scala 应用程序需要了解的 SBT(Scala Build Tool)的基本知识。SBT 还有许多其他功能和可能性,您可以在www.scala-sbt.org/0.13/docs/index.html查看。接下来,我们将学习 Typesafe Activator,它是一个围绕 SBT 的包装器,使得与 Play 框架应用程序一起使用变得容易。

使用 Activator 引导我们的 Play 框架应用程序

Lightbend(前 Typesafe)还有一个名为 Activator 的工具(www.lightbend.com/community/core-tools/activator-and-sbt),它是在 SBT 之上的包装器。Activator 使得使用 Scala、Akka 和 Play 框架创建响应式应用程序变得更容易。现在不用担心 Play 框架,因为我们将在第三章,使用 Play 框架开发 UI中更详细地介绍它。Akka 将在第八章,使用 Akka 开发聊天中详细讲解。

让我们下载并安装 Activator,并引导我们的架构。记住,我们需要已经安装 Java 8 和 Scala 2.11。如果您没有 Java 8 或 Scala 2.11,请回到第一章,函数式编程、响应式编程和 Scala 简介并安装它们。

首先,您需要从这里下载 activator:www.lightbend.com/activator/download

我建议您下载最小包,并让 Activator 为您下载和安装其他依赖项。您可以从这里下载最小包:downloads.typesafe.com/typesafe-activator/1.3.10/typesafe-activator-1.3.10-minimal.zip

对于这本书,我们将使用版本 1.3.10。我们需要将activator/bin文件夹放入 OS PATH 中。如果你想,你可以使用终端安装 Activator,如下所示:

如果你愿意,你可以使用终端安装 Activator,如下所示:

$ cd /usr/local/
$ wget https://downloads.typesafe.com/typesafe-
activator/1.3.10/typesafe-activator-1.3.10-minimal.zip
$ tar -xzf typesafe-activator-1.3.10-minimal.zip
$ rm -rf typesafe-activator-1.3.10-minimal.zip
$ sudo echo 'export PATH=$PATH:/usr/local/typesafe-activator-
1.3.10-minimal/bin' >> ~/.bashrc
$ source >> ~/.bashrc

为了测试你的安装,执行以下命令:

$ activator new ReactiveWebStore

上述命令将为你使用 Scala、Akka、Play 框架和 SBT 为你启动一个架构。

Activator 会问你一系列问题,比如你可能想使用哪些模板。有几个模板用于 Java 应用程序、Scala 应用程序、Akka 应用程序和 Play 应用程序。现在,我们将选择选项6) play-scala

第一次运行 Activator 时,可能需要一些时间,因为它将从网络下载所有依赖项。当 Activator 完成后,你应该在你的文件系统中看到一个名为ReactiveWebStore的文件夹。

命令$ activator new ReactiveWebStore显示以下结果:

使用 Activator 启动我们的 Play 框架应用程序

如果你输入$ ll到控制台,你应该进入ReactiveWebStore文件夹,你也应该看到以下结构:

diego@4winds:~/bin/activator-1.3.10-minimal/bin/ReactiveWebStore$ 
ll
total 52
drwxrwxr-x 9 diego diego 4096 Mai 14 19:03 ./
drwxr-xr-x 3 diego diego 4096 Mai 14 19:03 ../
drwxrwxr-x 6 diego diego 4096 Mai 14 19:03 app/
drwxrwxr-x 2 diego diego 4096 Mai 14 19:03 bin/
-rw-rw-r-- 1 diego diego 346 Mai 14 19:03 build.sbt
drwxrwxr-x 2 diego diego 4096 Mai 14 19:03 conf/
-rw-rw-r-- 1 diego diego 80 Mai 14 19:03 .gitignore
drwxrwxr-x 2 diego diego 4096 Mai 14 19:03 libexec/
-rw-rw-r-- 1 diego diego 591 Mai 14 19:03 LICENSE
drwxrwxr-x 2 diego diego 4096 Mai 14 19:03 project/
drwxrwxr-x 5 diego diego 4096 Mai 14 19:03 public/
-rw-rw-r-- 1 diego diego 1063 Mai 14 19:03 README
drwxrwxr-x 2 diego diego 4096 Mai 14 19:03 test/

内容如下解释:

  • app:这是 Play 框架应用程序文件夹,我们将在这里进行 Scala Web 开发。

  • build.sbt:这是构建文件;如你所见,Activator 为我们生成了 SBT 构建配置。

  • conf:这里存放应用程序配置文件,如日志和 Scala/Play 应用程序配置。

  • project:这是 SBT 项目文件夹,我们在其中定义 SBT 插件和 SBT 版本。

  • test:这里存放我们应用程序的测试源代码。

  • public:这里存放静态 HTML 资产,如图片、CSS 和 JavaScript 代码。

  • bin:这里存放 Linux/Mac 和 Windows 的 activator 脚本副本。

  • libexec:这里存放 Activator jar。这非常有用,因为 Activator 已经将我们的应用程序打包在一起。所以,比如说你将这个应用程序推送到 GitHub - 当有人需要访问这个应用程序并从 GitHub 下载它时,SBT 文件将存在,所以他们不需要从互联网上下载它。这在你在生产环境中配置和部署应用程序时特别有用,本书将在第十章[第十章。扩展]中详细讨论,扩展

Activator shell

Activator 允许你运行 REPL,就像我们在 Scala 和 SBT 中做的那样。为了获得 REPL 访问权限,你需要在控制台输入以下内容:

$ activator shell

Activator shell

Activator 提供了大量你可以使用的任务。为了了解所有可用的命令,你可以在控制台输入$ activator help

Activator shell

Activator - 编译、测试和运行

现在我们开始工作吧。我们将使用 Activator 和 SBT 编译、运行测试和运行我们的 Web 应用程序。首先,让我们编译。在控制台输入如下命令:$ activator compile

$ activator compile
diego@4winds:~/bin/activator-1.3.10-minimal/bin/ReactiveWebStore$ 
activator compile
[info] Loading global plugins from /home/diego/.sbt/0.13/plugins
[info] Loading project definition from /home/diego/bin/activator-
1.3.10-minimal/bin/ReactiveWebStore/project
[info] Set current project to ReactiveWebStore (in build 
file:/home/diego/bin/activator-1.3.10-
minimal/bin/ReactiveWebStore/)
[info] Updating {file:/home/diego/bin/activator-1.3.10-
minimal/bin/ReactiveWebStore/}root...
[info] Compiling 14 Scala sources and 1 Java source to 
/home/diego/bin/activator-1.3.10-
minimal/bin/ReactiveWebStore/target/scala-2.11/classes...
[success] Total time: 154 s, completed 14/05/2016 19:28:03
diego@4winds:~/bin/activator-1.3.10-minimal/bin/ReactiveWebStore$

现在让我们使用命令 $ activator test 来运行我们的测试。

Activator - 编译、测试和运行

最后,是时候运行你的应用程序了。在控制台输入 $ activator run

Activator - 编译、测试和运行

打开你的网页浏览器,并访问 URL:http://localhost:9000

你应该看到以下屏幕:

Activator - 编译、测试和运行

摘要

恭喜!你刚刚启动了第一个 Scala / Play 框架。Activator 让我们的生活变得更简单。正如你所见,我们只用了三个命令就能搭建起一个网站并使其运行。你也可以只用 SBT 来做同样的事情,但会花费更多的时间,因为我们需要获取所有依赖项,配置所有源代码结构,并添加一些示例 HTML 和 Scala 代码。多亏了 Activator,我们不需要做任何这些。然而,我们仍然可以按照我们的意愿更改所有的 SBT 文件和配置。Activator 并不紧密绑定到 Scala 或我们的应用程序代码,因为它更像是一个基于模板的代码生成器。

在下一章中,我们将通过添加验证、数据库持久化、使用 RxScala 和 Scala 调用反应式微服务以及更多功能来改进应用程序。

第三章:使用 Play 框架开发 UI

在上一章中,我们使用 Activator 对我们的应用程序进行了引导。在本章中,我们将继续使用 Scala 和 Play 框架开发我们的 Web 应用程序。Play 框架非常适合 Web 开发,因为它易于使用,同时非常强大。这是因为它在底层使用了一些顶级的反应式解决方案,如 spray、Akka 和 Akka Stream。对于本章,我们将通过添加验证和内存存储来创建我们反应式 Web 解决方案的一些基本 UI,这样你就可以感受到应用程序在工作。我们将使用一点 CSS 进行样式设计,以及 JavaScript 进行一些简单的可视化。

在本章中,我们将涵盖以下主题:

  • 使用 Scala 和 Play 框架进行 Web 开发的基础

  • 创建你的模型

  • 与视图和验证一起工作

  • 与会话作用域一起工作

入门

让我们看看Reactive Web Store的预览——我们将要构建的应用程序。

入门

目前,我们将构建三个简单的操作——创建、检索、更新删除CRUD),以管理产品、产品评论和产品图像。我们将为每个 CRUD 创建模型、控制器、视图和路由。

让我们开始吧。首先,我们需要定义我们的模型。模型需要位于ReactiveWebStore/app/models。模型是系统的核心,它们代表实体。我们将在第六章中稍后使用这个实体来存储和检索数据,使用 Slick 进行持久化。我们的模型不应该有任何 UI 逻辑,因为我们应该使用控制器来处理 UI 逻辑。

创建我们的模型

对于我们的产品模型,我们在Product.scala中有一个简单的 Scala case class,如下所示:

    package models 
    case class Product 
    ( var id:Option[Long], 
      var name:String, 
      var details:String, 
      var price:BigDecimal ) 
    { 
      override def toString:String =  
      { 
        "Product { id: " + id.getOrElse(0) + ",name: " + name + ", 
        details: "+ details + ", price: " + price + "}" 
      } 
    } 

一个产品可以有一个可选的 ID,一个名称,详细信息和一个价格。我们只是为了简化日志记录而重写toString方法。我们还需要定义图像和评论的模型。

以下是从Review.scala中获取的评论模型:

    package models 

    case class Review 
    (var id:Option[Long], 
      var productId:Option[Long], 
      var author:String, 
      var comment:String) 
    { 
      override def toString:String = { 
        "Review { id: " + id + " ,productId: " +  
        productId.getOrElse(0) + ",author: " + author + ",comment:  
        " + comment + " }" 
      } 
    } 

对于一个评论模型,我们有一个可选的 ID,一个可选的productId,一个作者和一个评论。验证将在视图上进行。现在让我们转向图像模型。

图像模型可以在Image.scala中找到,如下所示:

    package models 

    case class Image 
    (var id:Option[Long], 
      var productId:Option[Long],  
      var url:String){ 
        override def toString:String = { 
          "Image { productId: " + productId.getOrElse(0) + ",url: "  
          + url + "}" 
        } 
  } 

对于图像模型,我们有一个可选的 ID,一个可选的productId和图像 URL。

Play 框架负责路由,我们需要在ReactiveWebStore/conf/routes中定义路由。请记住,Play 框架将验证所有路由,因此您需要指定有效的包和类。Play 还创建了一个称为反向控制器的东西,我们将在本章后面使用。现在,让我们定义路由。反向控制器由 Play 框架生成,具有与原始控制器相同签名的方法,但它返回play.api.mvc.Call而不是play.api.mvc.Action

创建路由

Play 框架为产品、图片和评论的 CRUD 操作的路由如下:

    # Routes 
    # This file defines all application routes (Higher priority routes first) 
    # ~~~~ 

    GET / controllers.HomeController.index 
    GET /assets/*file controllers.Assets.at(path="/public", file) 

    # 
    # Complete CRUD for Product 
    # 
    GET /product controllers.ProductController.index 
    GET /product/add controllers.ProductController.blank 
    POST /product/ controllers.ProductController.insert 
    POST /product/:id controllers.ProductController.update(id:Long) 
    POST /product:id/remove controllers.ProductController.remove(id:Long) 
    GET /product/details/:id controllers.ProductController.details(id:Long) 

    # 
    # Complete GRUD for Review 
    # 
    GET /review controllers.ReviewController.index 
    GET /review/add controllers.ReviewController.blank 
    POST /review/ controllers.ReviewController.insert 
    POST /review/:id controllers.ReviewController.update(id:Long) 
    POST /review:id/remove controllers.ReviewController.remove(id:Long) 
    GET /review/details/:id controllers.ReviewController.details(id:Long) 

    # 
    # Complete CRUD for Image 
    # 
    GET /image controllers.ImageController.index 
    GET /image/add controllers.ImageController.blank 
    POST /image/ controllers.ImageController.insert 
    POST /image/:id controllers.ImageController.update(id:Long) 
    POST /image:id/remove controllers.ImageController.remove(id:Long) 
    GET /image/details/:id controllers.ImageController.details(id:Long) 

路由的工作原理是这样的--首先,你需要定义一个 REST 动词,例如GETPOSTPUTDELETE,然后你输入一个PATH,例如/image。最后,你指定哪个控制器函数将处理该路由。现在我们已经设置了路由,我们可以转向控制器。我们将为产品、图片和评论定义控制器。

所有路由遵循相同的逻辑。首先,我们将用户发送到我们列出所有项目的网页(产品、图片、评论)--这由GET /resource表示,其中资源可以是图片、产品或评论,例如。为了获取一个特定的资源,通常是通过 ID,我们给出命令GET /resource (product, review or image)/IDPOST /resource用于执行UPDATE

为了创建一个新项目(产品、评论或图片),模式是GET /resource/addPOST /resource/。你可能想知道为什么有两个路由来执行插入操作。好吧,那是因为首先我们需要加载网页,其次,当表单提交时,我们需要一个新的路由来处理值。更新操作也有两个路由,原因相同。如果你想DELETE一个资源,模式是POST /resource/ID/remove。最后,我们有操作细节,它用于显示与特定项目相关的详细信息--模式是GET /resource/details/ID。有了六个路由,我们可以为产品、图片、评论或其他任何未来可能添加到该应用程序或您自己的应用程序的资源执行完整的 CRUD 操作。

创建我们的控制器

现在,让我们转向之前路由中使用的控制器。控制器需要位于ReactiveWebStore/app/controllers。控制器在视图(UI)、模型和服务之间绑定,它们负责业务操作。始终重要的是将 UI 逻辑(通常是特定的)与业务逻辑(通常是更通用的,并且通常更重要)分开。

让我们看一下以下代码中的ProductController.scala中的产品控制器:

    @Singleton 
    class ProductController @Inject() (val messagesApi:MessagesApi,val 
    service:IProductService) extends Controller with I18nSupport { 

      val productForm: Form[Product] = Form( 
        mapping( 
          "id" -> optional(longNumber), 
          "name" -> nonEmptyText, 
          "details" -> text, 
          "price" -> bigDecimal 
        )(models.Product.apply)(models.Product.unapply)) 

      def index = Action { implicit request => 
        val products = service.findAll().getOrElse(Seq()) 
        Logger.info("index called. Products: " + products) 
        Ok(views.html.product_index(products)) 
      } 

      def blank = Action { implicit request => 
        Logger.info("blank called. ") 
        Ok(views.html.product_details(None, productForm)) 
      } 

      def details(id: Long) = Action { implicit request => 
        Logger.info("details called. id: " + id) 
        val product = service.findById(id).get 
        Ok(views.html.product_details(Some(id),  
        productForm.fill(product))) 
      } 

      def insert()= Action { implicit request => 
        Logger.info("insert called.") 
        productForm.bindFromRequest.fold( 
          form => { 
            BadRequest(views.html.product_details(None, form)) 
          }, 
        product => { 
          val id = service.insert(product) 
          Redirect(routes.ProductController.index).flashing("success"  
          -> Messages("success.insert", id)) 
        }) 
      } 

      def update(id: Long) = Action { implicit request => 
        Logger.info("updated called. id: " + id) 
        productForm.bindFromRequest.fold( 
          form => { 
            Ok(views.html.product_details(Some(id),  
            form)).flashing("error" -> "Fix the errors!") 
          }, 
          product => { 
            service.update(id,product) 
            Redirect(routes.ProductController.index). 
            flashing("success" -> Messages("success.update",  
            product.name)) 
          }) 
      } 

      def remove(id: Long)= Action { 
        service.findById(id).map { product => 
        service.remove(id) 
        Redirect(routes.ProductController.index).flashing("success" ->  
        Messages("success.delete", product.name)) 
      }.getOrElse(NotFound) 

      } 
    } 

The Play framework uses dependency injection and inversion of control using Google Guice. So, you can see at the top of the controller that we have the annotations @Singletonand@Inject. Singleton means that Guice will create a single instance of the class to handle all requests. Inject means we are injecting other dependencies into our controller, for instance, we inject MessagesApiin order to have the Play framework internalization support for string messages, andIProductService, that is, the product service that we will cover later in this chapter.

We also need to extend the Play class, play.api.mvc.Controller. Each function in a controller needs to return an action. This action could be a view. The Play framework compiles all the views into Scala classes, so you can safely reference them into your controllers code.

All business operations are delegated to a trait called IProductService, which we will cover later in this chapter. We also log some information using the Logger class. Play Framework uses Logback as the default logging solution. Let's take a closer look at each controller function now.

The index function calls IProductService`, and finds all the available products. If there are no products available, it returns an empty sequence, and then calls the product UI passing the collection of products.``

The blank function renders a blank product form, so the user can have a blank product form on the UI in order to add data (insert operation). Play framework works with form binding. So, in each controller, you need to define how your form looks on the UI. That form mapping is done using play.api.data.Form. You can see the mapping on the immutable variable called productForm. The mapping is between the view(UI) and the model called product. Keep in mind that the name field is mapped as NonEmptyText, which means Play won't accept null or blank values. This is a great future, because we can do validations in a declarative way without having to write code. Price is defined as BigDecimal, so Play won't accept text, but only numbers.

The details function retrieves a product using IProductService, and redirects to the view. However, before doing the redirect, it binds the data with the form so the UI will load with all the data into the HTML inputs.

我们还有插入和更新方法。它们都是使用fold方法构建的。fold方法有左右两个方向,这意味着错误或成功。fold函数是从映射表单中调用的,如果没有验证错误,它将向右移动,但如果存在验证错误,它将向左移动。这是一个非常简单和干净的方式来编写updateinsert流程。使用fold,我们不需要编写if语句。一旦验证通过,我们调用IProductService来进行插入或更新,然后我们执行视图的重定向。消息通过作用域传递。Play 有作用域选项——会话或 Flash。会话用于多个请求,值将存储在客户端。Flash 是一个请求作用域,大多数情况下这就是你需要使用的。这里我们使用的是Flash作用域,所以它只会在那个特定的请求期间退出。这个功能用于传递国际化消息i18n),这是动作的结果。所有 i18n 消息都需要在ReactiveWebStore/conf/messages中定义,如下所示:

    success.delete = OK '{0}' deleted! 
    success.insert = OK '{0}' created! 
    success.update = OK '{0}' updated! 
    error.notFound = Nothing Found with ID {0,number,0} 
    error.number = Not a valid number 
    error.required = Missing value here 

最后,我们有删除方法。首先,我们需要确保产品存在,所以我们使用IProductService进行findById,然后我们应用一个映射函数。如果产品不存在,Play 框架有预构建的 HTTP 错误代码消息,如NotFound。如果产品存在,我们使用IProductService删除它,然后我们通过带有闪烁信息的 UI 进行重定向。现在让我们看看图像和审查控制器。

审查控制器,ReviewController.scala,如下所示:

    @Singleton 
    class ReviewController @Inject() 
    (val messagesApi:MessagesApi, 
      val productService:IProductService, 
      val service:IReviewService) 
    extends Controller with I18nSupport { 
      val reviewForm:Form[Review] = Form( 
        mapping( 
          "id" -> optional(longNumber), 
          "productId" -> optional(longNumber), 
          "author" -> nonEmptyText, 
          "comment" -> nonEmptyText 
        )(models.Review.apply)(models.Review.unapply)) 

      def index = Action { implicit request => 
        val reviews = service.findAll().getOrElse(Seq()) 
        Logger.info("index called. Reviews: " + reviews) 
        Ok(views.html.review_index(reviews)) 
      } 

      def blank = Action { implicit request => 
        Logger.info("blank called. ") 
        Ok(views.html.review_details(None,  
          reviewForm,productService.findAllProducts)) 
      } 

      def details(id: Long) = Action { implicit request => 
        Logger.info("details called. id: " + id) 
        val review = service.findById(id).get 
        Ok(views.html.review_details(Some(id),  
        reviewForm.fill(review),productService.findAllProducts)) 
      } 

      def insert()= Action { implicit request => 
        Logger.info("insert called.") 
        reviewForm.bindFromRequest.fold( 
        form => { 
          BadRequest(views.html.review_details(None,  
          form,productService.findAllProducts)) 
      }, 
      review => { 
        if (review.productId==null ||   
        review.productId.getOrElse(0)==0) { 
          Redirect(routes.ReviewController.blank).flashing("error" ->  
          "Product ID Cannot be Null!") 
        }else { 
          Logger.info("Review: " + review) 
          if (review.productId==null ||  
          review.productId.getOrElse(0)==0) throw new  
          IllegalArgumentException("Product  Id Cannot Be Null") 
          val id = service.insert(review) 
          Redirect(routes.ReviewController.index).flashing("success" - 
          > Messages("success.insert", id)) 
        } 
      }) 
    } 

    def update(id: Long) = Action { implicit request => 
      Logger.info("updated called. id: " + id) 
      reviewForm.bindFromRequest.fold( 
        form => { 
          Ok(views.html.review_details(Some(id),  
            form,productService.findAllProducts)).flashing("error" ->  
          "Fix the errors!") 
        }, 
        review => { 
          service.update(id,review) 
          Redirect(routes.ReviewController.index).flashing("success" - 
          >Messages("success.update", review.productId)) 
        }) 
      } 

      def remove(id: Long)= Action { 
        service.findById(id).map { review => 
          service.remove(id) 
          Redirect(routes.ReviewController.index).flashing("success" - 
          >Messages("success.delete", review.productId)) 
        }.getOrElse(NotFound) 
      } 

    } 

审查控制器遵循与产品控制器相同的思想和结构。唯一的区别是,这里我们需要注入IProductService,因为一个审查需要属于一个产品。然后我们需要使用IProductService来执行findAllProduct,因为在审查视图中,我们将有一个包含所有可用产品的SelectBox

图像控制器,ImageController.scala,如下所示:

    @Singleton 
    class ImageController @Inject() 
    (val messagesApi:MessagesApi, 
      val productService:IProductService, 
      val service:IImageService) 
    extends Controller with I18nSupport { 

      val imageForm:Form[Image] = Form( 
        mapping( 
          "id" -> optional(longNumber), 
          "productId" -> optional(longNumber), 
          "url" -> text 
        )(models.Image.apply)(models.Image.unapply)) 

        def index = Action { implicit request => 
          val images = service.findAll().getOrElse(Seq()) 
          Logger.info("index called. Images: " + images) 
          Ok(views.html.image_index(images)) 
        } 

        def blank = Action { implicit request => 
          Logger.info("blank called. ") 
          Ok(views.html.image_details(None,  
          imageForm,productService.findAllProducts)) 
        } 

        def details(id: Long) = Action { implicit request => 
          Logger.info("details called. id: " + id) 
          val image = service.findById(id).get 
          Ok(views.html.image_details(Some(id),  
          imageForm.fill(image),productService.findAllProducts)) 
        } 

        def insert()= Action { implicit request => 
          Logger.info("insert called.") 
          imageForm.bindFromRequest.fold( 
            form => { 
              BadRequest(views.html.image_details(None, form,  
              productService.findAllProducts)) 
            }, 
            image => { 
              If (image.productId==null ||  
              image.productId.getOrElse(0)==0) { 
                Redirect(routes.ImageController.blank). 
                flashing("error" -> "Product ID Cannot be Null!") 
              }else { 
                if (image.url==null || "".equals(image.url)) image.url  
                = "/assets/images/default_product.png" 
                val id = service.insert(image) 
                Redirect(routes.ImageController.index). 
                flashing("success" -> Messages("success.insert", id)) 
              } 
            }) 
        } 

        def update(id: Long) = Action { implicit request => 
          Logger.info("updated called. id: " + id) 
          imageForm.bindFromRequest.fold( 
            form => { 
              Ok(views.html.image_details(Some(id), form,  
              null)).flashing("error" -> "Fix the errors!") 
            }, 
            image => { 
              service.update(id,image) 
              Redirect(routes.ImageController.index). 
              flashing("success" -> Messages("success.update",  
              image.id)) 
            }) 
        } 

    def remove(id: Long)= Action { 
      service.findById(id).map { image => 
        service.remove(id) 
        Redirect(routes.ImageController.index).flashing("success"  
        -> Messages("success.delete", image.id)) 
      }.getOrElse(NotFound) 
    } 
} 

图像审查与ReviewController类似工作。我们需要IProductService来获取所有服务。

与服务一起工作

服务是我们放置业务逻辑的地方。我们将在第六章中查看反应式持久化,使用 Slick 的持久化。目前,我们没有数据库来持久化信息,所以,现在我们将进行内存持久化。

首先,我们将定义我们服务的契约。这是我们将在控制器中使用的基本 API。让我们看看BaseService.scala中的以下特质:

    package services 

    import java.util.concurrent.atomic.AtomicLong 
    import scala.collection.mutable.HashMap 

    trait BaseService[A] { 

      var inMemoryDB = new HashMap[Long,A] 
      var idCounter = new AtomicLong(0) 

      def insert(a:A):Long 
      def update(id:Long,a:A):Boolean  
      def remove(id:Long):Boolean  
      def findById(id:Long):Option[A]  
      def findAll():Option[List[A]] 
    } 

在前面的代码中,我们有一个内存中的可变 HashMap,这是我们内存数据库的一部分,我们将在这里存储产品、图片和评论。我们还有一个原子计数器,我们可以用它为我们的模型生成 ID。这是一个使用泛型的特质--正如你所见,这里我们有所有与 A 相关的操作,这些将在稍后指定。现在我们可以移动产品、评论和图片的服务实现。

ProductService.scala 包如下所示:

    package services 

    import models.Product 
    import javax.inject._ 

    trait IProductService extends BaseService[Product]{ 
      def insert(product:Product):Long 
      def update(id:Long,product:Product):Boolean 
      def remove(id:Long):Boolean 
      def findById(id:Long):Option[Product] 
      def findAll():Option[List[Product]] 
      def findAllProducts():Seq[(String,String)] 
    } 

    @Singleton 
    class ProductService extends IProductService{ 

      def insert(product:Product):Long = { 
        val id = idCounter.incrementAndGet() 
        product.id = Some(id) 
        inMemoryDB.put(id, product) 
        id 
      } 

      def update(id:Long,product:Product):Boolean = { 
        validateId(id) 
        product.id = Some(id) 
        inMemoryDB.put(id, product) 
        true 
      } 

      def remove(id:Long):Boolean = { 
        validateId(id) 
        inMemoryDB.remove(id) 
        true 
      } 

      def findById(id:Long):Option[Product] = { 
        inMemoryDB.get(id) 
      } 

      def findAll():Option[List[Product]] = { 
        if (inMemoryDB.values == Nil ||  
        inMemoryDB.values.toList.size==0) return None 
        Some(inMemoryDB.values.toList) 
      } 

      private def validateId(id:Long):Unit = { 
        val entry = inMemoryDB.get(id) 
        if (entry==null) throw new RuntimeException("Could not find  
        Product: " + id) 
      } 

      def findAllProducts():Seq[(String,String)] = { 
        val products:Seq[(String,String)] = this 
        .findAll() 
        .getOrElse(List(Product(Some(0),"","",0))) 
        .toSeq 
        .map { product => (product.id.get.toString,product.name) } 
        return products 
      } 
    } 

在最后的代码中,我们定义了一个名为 IProductService 的特质,它通过泛型应用到产品上扩展了 BaseServiceProductService 包实现了 IProductService。在 Scala 中,我们可以在同一个 Scala 文件中有多个类,因此不需要创建不同的文件。

代码非常直接。这里有一个名为 findAllProducts 的实用方法,它被评论和图像控制器使用。我们在这里获取内存哈希表上的所有元素。如果没有元素,我们返回一个空产品的列表。然后我们将列表映射到一个元组 Seq 上,这是我们在视图(UI)中将要拥有的 SelectBox 复选框所必需的。现在让我们继续图像和评论服务,如下所示:

    package services 

    import javax.inject._ 
    import models.Image 
    import scala.collection.mutable.HashMap 
    import java.util.concurrent.atomic.AtomicLong 

    trait IImageService extends BaseService[Image]{ 
      def insert(image:Image):Long 
      def update(id:Long,image:Image):Boolean 
      def remove(id:Long):Boolean 
      def findById(id:Long):Option[Image] 
      def findAll():Option[List[Image]] 
} 

    @Singleton 
    class ImageService extends IImageService{ 

      def insert(image:Image):Long = { 
        val id = idCounter.incrementAndGet(); 
        image.id = Some(id) 
        inMemoryDB.put(id, image) 
        id 
      } 

      def update(id:Long,image:Image):Boolean = { 
        validateId(id) 
        image.id = Some(id) 
        inMemoryDB.put(id, image) 
        true 
      } 

      def remove(id:Long):Boolean = { 
        validateId(id) 
        inMemoryDB.remove(id) 
        true 
      } 

      def findById(id:Long):Option[Image] = { 
        inMemoryDB.get(id) 
      } 

      def findAll():Option[List[Image]] = { 
        if (inMemoryDB.values.toList == null ||  
        inMemoryDB.values.toList.size==0) return None 
        Some(inMemoryDB.values.toList) 
      } 

      private def validateId(id:Long):Unit = { 
        val entry = inMemoryDB.get(id) 
        If (entry==null) throw new RuntimeException("Could not find  
        Image: " + id) 
      } 

    } 

在前面的代码中,我们有与 ProductService 类似的东西。我们有一个名为 IImageService 的特质和 ImageService 实现。现在让我们在 ReviewService.scala 中实现评论服务,如下所示:

    package services 

    import javax.inject._ 
    import models.Review 
    import scala.collection.mutable.HashMap 
    import java.util.concurrent.atomic.AtomicLong 

    trait IReviewService extends BaseService[Review]{ 
      def insert(review:Review):Long 
      def update(id:Long,review:Review):Boolean 
      def remove(id:Long):Boolean 
      def findById(id:Long):Option[Review] 
      def findAll():Option[List[Review]] 
    } 

    @Singleton 
    class ReviewService extends IReviewService{ 

      def insert(review:Review):Long = { 
        val id = idCounter.incrementAndGet(); 
        review.id = Some(id) 
        inMemoryDB.put(id, review) 
        id 
      } 

      def update(id:Long,review:Review):Boolean = { 
        validateId(id) 
        review.id = Some(id) 
        inMemoryDB.put(id, review) 
        true 
      } 

      def remove(id:Long):Boolean = { 
        validateId(id) 
        inMemoryDB.remove(id) 
        true 
      } 

      def findById(id:Long):Option[Review] = { 
        inMemoryDB.get(id) 
      } 

      def findAll():Option[List[Review]] = { 
        if (inMemoryDB.values.toList == null ||  
        inMemoryDB.values.toList.size==0) return None 
        Some(inMemoryDB.values.toList) 
      } 

      private def validateId(id:Long):Unit = { 
        val entry = inMemoryDB.get(id) 
        If (entry==null) throw new RuntimeException("Could not find  
        Review: " + id) 
      } 
    } 

在前面的代码中,我们有 IReviewService 特质和 ReviewService 实现。我们在服务上也有验证和良好的实践。

配置 Guice 模块

我们在控制器中使用了 @Inject 注入类。注入是基于特质的;我们需要为注入的特质定义一个具体的实现。Play 框架在 ReactiveWebStore/app/Module.scala 位置寻找 Guice 注入。好的,那么让我们定义我们为刚刚创建的三个控制器创建的注入。

Guice 模块在 Module.scala 中如下所示:

    import com.google.inject.AbstractModule 
    import java.time.Clock 
    import services.{ApplicationTimer} 
    import services.IProductService 
    import services.ProductService 
    import services.ReviewService 
    import services.IReviewService 
    import services.ImageService 
    import services.IImageService 

    /** 
    * This class is a Guice module that tells Guice how to bind several 
    * different types. This Guice module is created when the Play 
    * application starts. 

    * Play will automatically use any class called `Module` that is in 
    * the root package. You can create modules in other locations by 
    * adding `play.modules.enabled` settings to the `application.conf` 
    * configuration file. 
    */ 
    class Module extends AbstractModule { 

      override def configure() = { 
        // Use the system clock as the default implementation of Clock 
        bind(classOf[Clock]).toInstance(Clock.systemDefaultZone) 
        // Ask Guice to create an instance of ApplicationTimer  
        // when the application starts. 
        bind(classOf[ApplicationTimer]).asEagerSingleton() 
        bind(classOf[IProductService]).to(classOf[ProductService]). 
        asEagerSingleton() 
        bind(classOf[IReviewService]).to(classOf[ReviewService]). 
        asEagerSingleton() 
        bind(classOf[IImageService]).to(classOf[ImageService]). 
        asEagerSingleton() 
      } 
    } 

因此,我们只需要为控制器添加 bind 与我们的特质,然后指向控制器实现。它们也应该被创建为单例,因为 Play 框架启动我们的应用程序。在这里,你也可以定义任何其他我们的应用程序可能需要的配置或注入。最后的代码定义了三个服务:产品服务、IReviewServiceIImageService

与视图(UI)一起工作

Play 框架与一个基于 Scala 的模板引擎 Twirl 一起工作。Twirl 是由 ASP.NET Razor 启发的。Twirl 紧凑且表达性强;你会发现我们可以用更少的代码做更多的事情。Twirl 模板文件是简单的文本文件,然而,Play 框架会编译这些模板并将它们转换为 Scala 类。你可以在 Twirl 中无缝地混合 HTML 和 Scala。

UI 将被编译成一个 Scala 类,这个类可以在我们的控制器中被引用,因为我们可以在视图中进行路由。它的好处是这使得我们的编码方式更安全,因为编译器会为我们进行检查。坏消息是,你需要编译你的 UI,否则你的控制器将找不到它。

在本章前面,我们为产品、图片和评论定义了控制器,并编写了以下代码:

    Ok(views.html.product_details(None, productForm)) 

通过前面的代码,我们将用户重定向到一个空白的产品页面,以便用户可以创建新产品。我们还可以将参数传递给 UI。由于它全部是 Scala 代码,你实际上只是在调用一个函数,如下所示:

    val product = service.findById(id).get 
    Ok(views.html.product_details(Some(id), productForm.fill(product))) 

在前面的代码中,我们调用服务通过 ID 检索产品,然后将对象传递给 UI,并填写表单。

让我们继续构建我们的应用程序,并为产品、评论和图片创建 UI。由于我们正在进行 CRUD 操作,每个 CRUD 将需要多个模板文件。我们需要以下结构:

  • 索引模板

    • 列出所有项目

    • 链接编辑单个项目

    • 删除单个项目的链接

    • 创建新项目的链接

      • 详细模板

        • 用于创建新项目的 HTML 表单

        • 用于编辑现有项目(更新)的 HTML 表单

说到这里,我们将有以下文件:

  • 对于产品:

    • product_index.scala.html

    • product_details.scala.html

      • 对于图片:

        • image_index.scala.html

        • image_details.scala.html

          • 对于评论:

            • review_index.scala.html

            • review_details.scala.html

为了代码复用,我们将创建另一个文件,包含我们 UI 的基本结构,例如 CSS 导入(CSS 需要位于ReactiveWebStore\public\stylesheets),JavaScript 导入,以及页面标题,这样我们就不需要在每个 CRUD 的模板中重复这些内容。这个页面将被称为:main.scala.html

所有的 UI 代码都应该位于ReactiveWebStore/app/views

包含所有 CRUD 操作的 UI 索引的主 Scala 代码在main.scala.html中,如下所示:

    @(title: String)(content: Html)(implicit flash: Flash) 

    <!DOCTYPE html> 
      <html lang="en"> 
        <head> 
          <title>@title</title> 
          <link rel="shortcut icon" type="image/png"  
          href="@routes.Assets.at("images/favicon.png")"> 
          <link rel="stylesheet" media="screen"  
          href="@routes.Assets.at("stylesheets/main.css")"> 
          <link rel="stylesheet" media="screen"  
          href="@routes.Assets.at("stylesheets/bootstrap.min.css")"> 
          <script src="img/@routes.Assets.at("javascripts/jquery- 
          1.9.0.min.js")" type="text/javascript"></script> 
          <script src="img/@routes.Assets.at("javascripts/bootstrap.js")"  
          type="text/javascript"></script> 
          <script src="img/@routes.Assets.at("javascripts/image.js")"  
          type="text/javascript"></script> 
        </head> 
        <body> 
          <center><a href='/'><img height='42' width='42'  
          src='@routes.Assets.at("images/rws.png")'></a> 
          <h3>@title</h3></center> 
          <div class="container"> 
            @alert(alertType: String) = { 
              @flash.get(alertType).map { message => 
                <div class="alert alert-@alertType"> 
                  <button type="button" class="close" data- 
                  dismiss="alert">&times;</button> 
                  @message 
                </div> 
              } 
            } 
          @alert("error") 
          @alert("success") 
          @content 
          <a href="/"></a><BR> 
          <button type="submit" class="btn btn-primary"  
          onclick="window.location.href='/'; " > 
          Reactive Web Store - Home 
          </button> 
        </div> 
      </body> 
    </html> 

在前面的代码中,首先在最顶部有如下一行:

    @(title: String)(content: Html)(implicit flash: Flash) 

这意味着我们定义了此 UI 可以接收的参数。这里我们期望一个字符串标题,它将是页面标题,还有一些 currying 变量。你可以在第一章中了解更多关于 currying 的细节,FP、响应式和 Scala 简介。所以在 currying 中,有两件事:第一是 HTML,这意味着你可以将 HTML 代码传递给这个函数,第二,我们有Flash,Play 框架会为我们传递。Flash用于获取请求之间的参数。

如您所见,在代码的后面我们有@title,这意味着我们检索参数的标题,并将其添加到 HTML 中。我们还使用@alert打印任何错误消息或验证问题,如果有的话。我们导入 JQuery 和 Twitter Bootstrap,但我们不使用硬编码的路径。相反,我们使用路由器如@routes.Assets.at。JavaScript 仍然需要位于ReactiveWebStore\public\javascripts

现在其他模板可以使用@main(..),并且它们不需要任何 JavaScript 或 CSS 的声明。它们可以添加额外的 HTML 代码,这些代码将通过@content在之前的代码上渲染。因此,对于产品,内容将是 HTML 产品内容,对于评论和图片也是如此。现在我们可以转向产品的 UI。

产品索引:产品的 UI 索引product_index.scala.html

    @(products:Seq[Product])(implicit flash: Flash) 

    @main("Products") { 

      @if(!products.isEmpty) { 
        <table class="table table-striped"> 
          <tr> 
            <th>Name</th> 
            <th>Details</th> 
            <th>Price</th> 
            <th></th> 
          </tr> 
          @for(product <- products) { 
            <tr> 
              <td><a href="@routes.ProductController. 
              details(product.id.get)">@product.name</a></td> 
              <td>@product.details</td> 
              <td>@product.price</td> 
              <td><form method="post" action= 
              "@routes.ProductController.remove(product.id.get)"> 
              <button class="btn btn-link" type="submit"> 
              <i class="icon-trash"></i>Delete</button> 
              </form></td> 
            </tr> 
          } 
        </table> 
      } 
      <p><a href="@routes.ProductController.blank" class= 
      "btn btn-success"><i class="icon-plus icon-white"> 
      </i>Add Product</a></p> 
    } 

如您所见,Scala 代码中混合了 HTML。每次需要运行 HTML 时,只需运行它,需要运行 Scala 代码时,需要使用特殊字符@。在模板的顶部,您可以看到以下代码:

    @(products:Seq[Product])(implicit flash: Flash) 

由于这是最终的 Scala 代码,并且将被编译,我们需要定义这个 UI 模板可以接收哪些参数。在这里,我们期望一个产品序列。还有一个名为Flash的 currying 隐式变量,它将由 Play 框架提供,我们将用它来显示消息。我们还有代码@main("Products") { .. }。这意味着我们调用主 Scala 模板并添加额外的 HTML——产品 HTML。对于这个产品 UI,我们根据产品序列列出所有产品。如您所见,我们定义了一个 HTML 表格。在列出所有产品之前,我们还验证序列是否为空。

现在我们可以进入product_details.scala.html中的产品详情页面,如下所示:

    @(id: Option[Long],product:Form[Product])(implicit flash:Flash) 

    @import play.api.i18n.Messages.Implicits._ 
    @import play.api.Play.current 

    @main("Product: " + product("name").value.getOrElse("")){  

      @if(product.hasErrors) { 
        <div class="alert alert-error"> 
          <button type="button" class="close" data- 
          dismiss="alert">&times;</button> 
          Sorry! Some information does not look right. Could you  
          review it please and re-submit? 
        </div> 
      } 

      @helper.form(action = if (id.isDefined)  
        routes.ProductController.update(id.get) else  
        routes.ProductController.insert)  
      { 
        @helper.inputText(product("name"),    '_label -> "Product  
        Name") 
        @helper.inputText(product("details"), '_label -> "Product  
        Details") 
        @helper.inputText(product("price"),   '_label -> "Price") 
        <div class="form-actions"> 
          <button type="submit" class="btn btn-primary"> 
            @if(id.isDefined) { Update Product } else { New Product } 
          </button> 
        </div> 
      } 

    } 

对于这个先前的 UI,在最顶部,我们有以下一行:

    @(id: Option[Long],product:Form[Product])(implicit flash:Flash) 

这意味着我们期望一个完全可选的 ID。这个 ID 用于知道我们是在处理插入场景还是更新场景,因为我们使用相同的 UI 来处理插入和更新。我们还获取产品表单,它将通过ProductController传递,并且我们接收Flash,它将由 Play 框架提供。

我们需要在 UI 上进行一些导入,以便我们可以访问 Play 框架的 i18n 支持。这是通过以下方式完成的:

    @import play.api.i18n.Messages.Implicits._ 
    @import play.api.Play.current 

与之前的 UI 一样,我们在主 Scala UI 中渲染这个 UI。因此,我们不需要再次指定 JavaScript 和 CSS。这是通过以下代码完成的:

    @main("Product: " + product("name").value.getOrElse("")){ .. } 

接下来,我们检查是否有任何验证错误。如果有,用户在继续之前需要修复这些错误。这是通过以下代码完成的:

    @if(product.hasErrors) { 
      <div class="alert alert-error"> 
        <button type="button" class="close" data- 
        dismiss="alert">&times;</button> 
        Sorry! Some information does not look right. Could you review  
        it please and re-submit? 
      </div> 
    } 

现在是创建产品表单的时候了,最终它将被映射到 HTML 输入框。我们通过以下代码来完成:

    @helper.form(action = if (id.isDefined) 
    routes.ProductController.update(id.get) else 
    routes.ProductController.insert) { 
      @helper.inputText(product("name"),    '_label -> "Product Name") 
      @helper.inputText(product("details"), '_label -> "Product  
      Details") 
      @helper.inputText(product("price"),   '_label -> "Price") 
      <div class="form-actions"> 
        <button type="submit" class="btn btn-primary"> 
          @if(id.isDefined) { Update Product } else { New Product } 
        </button> 
      </div> 
    } 

@helper.form 是 Play 框架提供的一个特殊助手,用于轻松创建 HTML 表单。因此,动作是表单将提交的目标。我们需要在这里做一次 if 判断,因为我们需要知道这是一个更新还是一个插入。然后我们使用以下代码将我们的产品模型的所有字段映射到所有这些字段上:

    @helper.inputText(product("name"),    '_label -> "Product Name") 
    @helper.inputText(product("details"), '_label -> "Product Details") 
    @helper.inputText(product("price"),   '_label -> "Price") 

记住,产品表单来自产品控制器。对于助手,我们只需要告诉它哪个产品字段对应哪个 HTML 标签,就这样。这将产生以下 UI。

以下图像显示了空的产品索引 UI:

与视图(UI)一起工作

产品详情的插入 UI 表单如下所示:

与视图(UI)一起工作

添加产品后,产品索引 UI 如下所示:

与视图(UI)一起工作

现在我们可以转向审查部分。让我们看看 UI。

review_index.scala.html 中的审查索引 UI 如下所示:

    @(reviews:Seq[Review])(implicit flash: Flash) 

    @main("Reviews") { 

      @if(!reviews.isEmpty) { 
        <table class="table table-striped"> 
          <tr> 
            <th>ProductId</th> 
            <th>Author</th> 
            <th>Comment</th> 
            <th></th> 
          </tr> 
          @for(review <- reviews) { 
            <tr> 
              <td><a href="@routes.ReviewController.details 
              (review.id.get)">@review.productId</a></td> 
              <td>@review.author</td> 
              <td>@review.comment</td> 
              <td> 
              <form method="post" action="@routes.ReviewController. 
              remove(review.id.get)"> 
                <button class="btn btn-link" type="submit"><i class= 
                "icon-trash"></i>Delete</button> 
              </form></td> 
            </tr> 
          } 
        </table> 
      } 
      <p><a href="@routes.ReviewController.blank" class="btn btn- 
      success"><i class="icon-plus icon-white"></i>Add Review</a></p> 
    } 

所以这里我们有与产品相同的东西。现在让我们看看审查的详情页面。您可以在 review_details.scala.html 中找到它。

    @(id: Option[Long],review:Form[Review],products:
    Seq[(String,String)])(implicit flash:Flash) 

    @import play.api.i18n.Messages.Implicits._ 
    @import play.api.Play.current 

    @main("review: " + review("name").value.getOrElse("")){  

      @if(review.hasErrors) { 
        <div class="alert alert-error"> 
          <button type="button" class="close" data- 
          dismiss="alert">&times;</button> 
          Sorry! Some information does not look right. Could you  
          review it please and re-submit? 
        </div> 
      } 

      @helper.form(action = if (id.isDefined)  
      routes.ReviewController.update(id.get) else  
      routes.ReviewController.insert) { 
        @helper.select( 
          field = review("productId"), 
          options = products, 
          '_label -> "Product Name",  
          '_default -> review("productId").value.getOrElse("Choose  
          One")) 
          @helper.inputText(review("author"),     '_label -> "Author") 
          @helper.inputText(review("comment"),    '_label ->  
          "Comment") 
          <div class="form-actions"> 
            <button type="submit" class="btn btn-primary"> 
            @if(id.isDefined) { Update review } else { New review } 
          </button> 
          </div> 
      } 

    } 

在这里,在这段最后的代码中,我们几乎拥有与产品相同的所有内容,然而,有一个很大的不同。审查需要与产品 ID 相关联。这就是为什么我们需要有一个产品选择框,它由 products:Seq[(String,String)] 提供。这来自 ReviewController 代码。这段代码产生了以下 UI。

空的审查索引 UI 如下所示:

与视图(UI)一起工作

插入审查详情 UI 如下所示:

与视图(UI)一起工作

带有审查的审查索引 UI 如下所示:

与视图(UI)一起工作

现在我们可以转向最后一个:图片 UI。图片 UI 与审查 UI 非常相似,因为它也依赖于产品 ID。让我们看看它。

图片索引 UI 在 image_index.scala.html 中的代码如下:

    @(images:Seq[Image])(implicit flash:Flash) 
    @main("Images") { 
      @if(!images.isEmpty) { 
        <table class="table table-striped"> 
          <tr> 
            <th>ProductID</th> 
            <th>URL</th> 
            <th></th> 
          </tr> 
          @for(image <- images) { 
            <tr> 
              <td><a href="@routes.ImageController.details 
              (image.id.get)">@image.id</a></td> 
              <td>@image.productId</td> 
              <td>@image.url</td> 
              <td><form method="post" action= 
              "@routes.ImageController.remove(image.id.get)"> 
                <button class="btn btn-link" type="submit"> 
                <i class="icon-trash"></i>Delete</button> 
              </form></td> 
            </tr> 
          } 
        </table> 
      } 
      <p><a href="@routes.ImageController.blank" class= 
      "btn btn-success"><i class="icon-plus icon-white"> 
      </i>Add Image</a></p> 
    } 

    Image Details UI [image_details.scala.html] 

    @(id: Option[Long],image:Form[Image],products:Seq[(String,String)])
    (implicit flash:Flash) 
    @import play.api.i18n.Messages.Implicits._ 
    @import play.api.Play.current 
    @main("Image: " + image("productId").value.getOrElse("")){  
      @if(image.hasErrors) { 
        <div class="alert alert-error"> 
          <button type="button" class="close" data- 
          dismiss="alert">&times;</button> 
          Sorry! Some information does not look right. Could you image  
          it please and re-submit? 
        </div> 
      } 

      @helper.form(action = if (id.isDefined)  
      routes.ImageController.update(id.get) else  
      routes.ImageController.insert) { 
        @helper.select(field = image("productId"), 
          options = products, 
          '_label -> "Product Name",  
          '_default -> image("productId").value.getOrElse("Choose  
          One") 
        ) 
        @helper.inputText(  
          image("url"),  
          '_label       -> "URL", 
          '_placeholder -> "/assets/images/default_product.png", 
          'onchange     -> "javascript:loadImage();" 
        ) 
        Visualization<br> 
        <img id="imgProduct" height="42" width="42"  
        src="img/@image("url").value"></img> 
        <div class="form-actions"> 
          <button type="submit" class="btn btn-primary"> 
            @if(id.isDefined) { Update Image } else { New Image } 
          </button> 
        </div> 
      } 

    } 

这个 UI 模板将创建以下 HTML 页面:

空的图片索引 UI 如下所示:

与视图(UI)一起工作

图片详情的插入 UI 如下所示:

与视图(UI)一起工作

以下是有项的图片索引 UI 的图像:

与视图(UI)一起工作

现在我们有一个完整的运行 UI 应用程序。这里有控制器、模型、视图,以及简单的服务。我们还有所有的验证。

摘要

在这一章中,你学习了如何创建控制器、模型、服务、视图(使用 Twirl 模板)、Guice 注入和路由。我们使用了 Play 框架覆盖了 Scala Web 开发的原理。到本章结束时,我们得到了一个运行中的 Play 框架应用程序。

在下一章中,我们将学习更多关于服务的内容。正如你可能意识到的,在本章中我们已经为产品、评论和图片进行了一些简单的服务,但现在我们将继续与服务进行工作。

第四章:开发反应式后端服务

在上一章中,你学习了如何使用 Activator 引导你的应用程序,我们使用 Scala 和 Play 框架开发了我们的 Web 应用程序。现在我们将进入 RxJava 和 RxScala 的反应式世界。

在本章中,我们将涵盖以下主题:

  • 反应式编程原则和反应式宣言

  • 理解非阻塞 I/O 的重要性

  • 可观测性、函数和 Rx 中的错误处理

  • 重构我们的控制器和模型以调用我们的服务

  • 将 RxScala 添加到我们的服务中

  • 添加日志

开始使用反应式编程

现在构建应用程序比以前更难。现在的一切都更加复杂:我们必须在处理器中使用更多的核心,并且我们有了数百台机器的单个服务云原生应用程序。并发编程一直很难,而且它将始终如此,因为建模时间很困难。为了解决这个问题,我们需要有一个反应式架构风格。为了能够处理更多用户并扩展我们的应用程序,我们需要利用异步和非阻塞 I/O。为了帮助我们完成这项任务,我们可以依赖 RxJava 和 RxScala。反应式不仅关乎代码,也关乎架构原则。

反应式宣言很好地捕捉了这些原则,并且有一些技术遵循这些原则以实现完全的反应式。

反应式宣言可以表示如下图:

开始使用反应式编程

更多信息,您可以访问www.reactivemanifesto.org/

反应式宣言描述了这种反应式架构/系统看起来是什么样子。基本上,以下是支撑反应式理念的四个核心原则:

好的,让我们在应用中使用 RxScala 将这些原则付诸实践。RxScala 只是 RxJava 的 Scala 包装器,但使用它更好,因为它使代码更具有函数式,而且你不需要创建像Action1这样的对象。

在我们的应用中,我们有三个主要资源:产品、评论和图片。所有产品都必须有一个价格,所以我们将使用 Play 框架、RxScala 和 Scala 现在构建一个完全反应式的价格生成器。

首先,我们将在我们的 Play 应用中玩转 RxScala,然后我们将创建一个独立的微服务,对该微服务进行反应式调用,并检索该服务的价格建议。所有数据流转换都使用可观察对象。

让我们在ReactiveWebStore/conf/routes创建这个控制器的路由,如下所示:

    # 
    # Services 
    # 
    GET   /rx/prices   controllers.RxController.prices 
    GET   /rx/aprices  controllers.RxController.pricesAsync 

这里有两个路由:一个用于常规操作,另一个用于异步操作,它将返回一个 Scala Future。让我们创建一个新的控制器,称为Rx Controller.scala。这个控制器需要位于ReactiveWebStore/app/controller

让我们看看RxController,这是我们反应式的 RxScala 简单控制器:

    @Singleton 
    class RxController @Inject()(priceService:IPriceSerice) extends 
    Controller { 
      def prices = Action { implicit request => 
        Logger.info("RX called. ") 
        import ExecutionContext.Implicits.global 
        val sourceObservable = priceService.generatePrices 
        val rxResult = Observable.create { sourceObservable.subscribe  
      } 
      .subscribeOn(IOScheduler()) 
      .take(1) 
      .flatMap { x => println(x) ; Observable.just(x) } 
      .toBlocking 
      .first 
      Ok("RxScala Price suggested is = " + rxResult) 
    } 

      def pricesAsync = Action.async { implicit request => 
        Logger.info("RX Async called. ") 
        import play.api.libs.concurrent.Execution.Implicits. 
        defaultContext 
        val sourceObservable = priceService.generatePrices 
        val rxResult = Observable.create { sourceObservable.subscribe  
        } 
        .subscribeOn(IOScheduler()) 
        .take(1) 
        .flatMap { x => println(x) ; Observable.just(x) } 
        .toBlocking 
        .first 
        Future { Ok("RxScala Price sugested is = " + rxResult) } 
      } 
    } 

所以,在第一个名为prices的方法中,我们返回一个常规的 Play 框架 Action。我们通过依赖注入接收IPriceService。这个IPriceService是一个反应式服务,因为它使用了可观察对象。所以我们调用一个方法,generatePrices,它将返回Observable[Double]。这将是我们计算的数据源,即我们的数据源。继续前进,我们创建一个新的可观察对象,订阅到源可观察对象,然后应用一些转换。例如,我们只取一个元素,然后我们可以使用flatMap进行转换。在这个案例中,我们实际上并没有应用转换。我们使用flatMap简单地打印我们得到的内容,然后继续链式操作。下一步是调用toBlocking,这将阻塞线程直到数据返回。一旦数据返回,我们得到第一个元素,它将是一个 double 类型,然后我们返回Ok

阻塞听起来很糟糕,我们不希望这样。作为替代,我们可以在 Play 框架中使用async控制器,它不会阻塞线程并返回一个 Future。所以这是第二种方法,称为pricesAsync。这里我们有类似的可观察对象代码。然而,最后我们返回一个非阻塞的 Future。然而,我们调用可观察对象的toBlocking,这将阻塞调用,因此使其与之前的方法相同。为了清楚起见,Action 并不坏。默认情况下,Play 框架中一切都是异步的,因为即使你没有返回一个明确的 Future,Play 框架也会为y创建一个 promise,并使你编写异步代码。使用 HTTP,你会在某个点阻塞线程。如果你想要从头到尾 100%非阻塞,你需要考虑一个不同的协议,例如 web sockets。

现在我们来看看这个服务。这个服务以及其他服务都需要位于ReactiveWebStore/apps/services。首先,我们将创建trait来定义服务行为。

IPriceService - Scala trait

如以下代码所示,我们只定义了一个操作IPriceService,即generatePrices,它返回Observable[Double]。现在的下一步是定义服务实现。这段代码需要位于与之前 trait 相同的 services 文件夹中:

    trait IPriceSerice{ 
      def generatePrices:Observable[Double] 
    } 

PriceService - RxScala PriceService 实现

首先,我们创建PublishSubject,这是一种将数据生成到可观察对象中的方法。Scala 有一个很好的方法,使用Stream.continually生成无限序列。因此,我们传递一个函数,该函数从 0 到 1,000 生成双随机数。这将永远发生,因为这个计算很昂贵,所以我们将其运行在 Future 中。正确的方法是在Stream之后使用一个方法,因为这将完成计算。为了练习的目的,我们暂时保持这种方式。

每个双随机数都通过onNext方法发布到PublishSubject。现在让我们转到generatePrices方法,该方法使用三个可观察对象为我们生成数字。为了清楚起见,当然我们可以在这里做一个更简单的解决方案。然而,我们这样做是为了展示可观察对象的力量以及如何在实践中使用它们。

我们有EvenOdd可观察对象,它们都订阅了PublishSubject,因此它们将接收到无限的双随机数。有一个flatMap操作来给数字加10。记住,你做的每一件事都需要在可观察对象中完成。所以当你用flatMap进行转换时,你总是需要返回一个可观察对象。

最后,我们在Even可观察对象上应用过滤函数以获取偶数,在Odd可观察对象上获取奇数。所有这些都在并行发生。EvenOdd可观察对象不会互相等待。

下一步是将两个可观察量合并。我们创建一个空的第三个可观察量,然后将来自Even可观察量的无限双数与来自Odd可观察量的无限双数合并。现在是将计算限制在只有 10 个数字的时候了。由于异步操作,我们不知道会有多少奇数或偶数。如果你希望控制奇数和偶数的数量,你需要在每个可观察量上应用take函数。

最后,我们应用foldLeft来求和所有数字并得到总数。然而,当我们这样做时,我们只得到了 90%的数字。这个最后的可观察量是返回给控制器的。这里没有任何阻塞,一切都是异步和反应式的。

你可能想知道为什么Stream.Continuously总是生成不同的值。这是因为我们在 Scala 中使用了一个按名调用(Call-by-Name)函数。我们导入了nextDouble函数,并传递一个函数而不是函数的值:

    @Singleton 
    class PriceService extends IPriceSerice{ 
      var doubleInfiniteStreamSubject = PublishSubject.apply[Double]() 
      Future { 
        Stream.continually(nextDouble * 1000.0 ).foreach { 
          x => Thread.sleep(1000);  
          doubleInfiniteStreamSubject.onNext(x) 
        } 
      } 
      override def generatePrices:Observable[Double] = { 
        var observableEven = Observable.create {  
        doubleInfiniteStreamSubject.subscribe } 
        .subscribeOn(IOScheduler()) 
        .flatMap { x => Observable.from( Iterable.fill(1)(x + 10) )  
        } 
        .filter { x => x.toInt % 2 == 0 } 
        var observableOdd = Observable.create {  
        doubleInfiniteStreamSubject.subscribe } 
        .subscribeOn(IOScheduler()) 
        .flatMap { x => Observable.from( Iterable.fill(1)(x + 10) )  
        } 
        .filter { x => x.toInt % 2 != 0 } 
        var mergeObservable = Observable 
        .empty 
        .subscribeOn(IOScheduler()) 
        .merge(observableEven) 
        .merge(observableOdd) 
        .take(10) 
        .foldLeft(0.0)(_+_) 
        .flatMap { x => Observable.just( x - (x * 0.9) ) } 
         return mergeObservable 
      } 
    } 

我们需要在Module.scala中注册这个服务,Module.scala位于ReactiveWebStore/app默认包中。

Guice 注入 - Module.scala

你的Module.scala文件应该看起来像这样:

    class Module extends AbstractModule { 
      override def configure() = { 
        // Use the system clock as the default implementation of Clock 
        bind(classOf[Clock]).toInstance(Clock.systemDefaultZone) 
        // Ask Guice to create an instance of ApplicationTimer when  
        //the 
        // application starts. 
        bind(classOf[ApplicationTimer]).asEagerSingleton() 
        bind(classOf[IProductService]).to(classOf[ProductService]). 
        asEagerSingleton() 
        bind(classOf[IReviewService]).to(classOf[ReviewService]). 
        asEagerSingleton() 
        bind(classOf[IImageService]).to(classOf[ImageService]). 
        asEagerSingleton() 
        bind(classOf[IPriceSerice]).to(classOf[PriceService]). 
        asEagerSingleton() 
      } 
    } 

为了编译和运行前面的代码,我们需要添加一个额外的 SBT 依赖项。打开build.sbt,并添加 RxScala。我们还将添加另一个依赖项,即 ws,这是一个用于进行 Web 服务调用的 Play 库。我们将在本章的后面使用它。

你的build.sbt应该看起来像这样:


    name := """ReactiveWebStore""" 
    version := "1.0-SNAPSHOT" 
    lazy val root = (project in file(".")).enablePlugins(PlayScala) 
    scalaVersion := "2.11.7" 

    libraryDependencies ++= Seq( 
      jdbc, 
      cache, 
      ws, 
      "org.scalatestplus.play" %% "scalatestplus-play" % "1.5.1" %      
      Test, 
      "com.netflix.rxjava" % "rxjava-scala" % "0.20.7" 
    ) 

    resolvers += "scalaz-bintray" at 
    "http://dl.bintray.com/scalaz/releases" 
    resolvers += DefaultMavenRepository 

现在,我们可以使用activator run编译并运行这段代码。

我们现在可以使用 CURL 调用调用这个新的路由。如果你愿意,你也可以直接在浏览器中完成。

curl -v http://localhost:9000/rx/prices 
curl -v http://localhost:9000/rx/aprices

我们将看到以下结果:

Guice 注入 - Module.scala

太好了!我们已经让 RxScala 与 Play 框架一起工作。现在,我们将重构我们的代码,使其更加有趣。所以我们将使用 Play 框架创建一个微服务,并将这个随机数生成器外部化到微服务中。

我们需要创建一个新的 Play 框架应用程序。我们将选择选项号6) play-scala应用程序模板。打开你的控制台,然后输入以下命令:

$ activator new ng-microservice

你将看到以下结果:

Guice 注入 - Module.scala

让我们在ng-microservice上创建路由。这里不会有任何 UI,因为这将是一个微服务。我们需要在ng-microservice/conf/routes中添加一个路由:

    # Routes 
    # This file defines all application routes (Higher priority routes 
    first) 
    # ~~~~ 

    GET /double      controllers.NGServiceEndpoint.double 
    GET /doubles/:n  controllers.NGServiceEndpoint.doubles(n:Int) 

现在让我们定义控制器。这不是一个常规控制器,因为这个控制器不会提供 UI 视图。相反,它将为微服务消费者提供 JSON。这里我们只有一个消费者,它将是ReactiveWebStore。然而,你可以有任意多的消费者,比如其他微服务或甚至是移动应用程序。

NGServiceEndpoint

对于这个控制器,我们只有两个路由。这些路由是doubledoubles。第一个路由从服务返回一个双精度浮点数,第二个路由返回一个批量生成的双精度浮点数的列表。对于第二个方法,我们获取一个双精度浮点数的列表,并使用 Play 框架的实用库Json将这个列表转换为 JSON 格式:

    class NGServiceEndpoint @Inject()(service:NGContract) extends 
    Controller { 
      def double = Action { 
        Ok(service.generateDouble.toString()) 
      } 
      def doubles(n:Int) = Action { 
        val json = Json.toJson(service.generateDoubleBatch(n)) 
        Ok(json) 
      } 
    } 

下一步是为微服务创建trait。在面向服务架构SOA)的术语中,这个trait也是服务合同,即微服务提供的功能。

NGContract.scala文件应类似于以下内容:

    trait NGContract { 
       def generateDouble:Double 
       def generateDoubleBatch(n:Int):List[Double] 
    } 

让我们看看这个微服务的服务实现:

    package services 
    import scala.util.Random 
    import scala.util.Random.nextDouble 
    class NGServiceImpl extends NGContract{ 
      override def generateDouble:Double = { 
        Stream.continually(nextDouble * 1000.0 ) 
        .take(1) 
      } 
      override def generateDoubleBatch(n:Int):List[Double] = { 
        require(n >= 1, "Number must be bigger than 0") 
        val nTimes:Option[Int] = Option(n) 
        nTimes match { 
          case Some(number:Int) => 
          Stream.continually(nextDouble * 1000.0 ) 
          .take(n) 
          .toList 
          case None => 
          throw new IllegalArgumentException("You need provide a valid  
          number of doubles you want.") 
        } 
      } 
    } 

这个服务实现没有使用任何 RxScala 代码。然而,它非常函数式。我们在这里实现了两个方法。这些方法是generateDoublegenerateDoubleBatch,它们通过参数接收你希望它为你生成的双精度浮点数的数量。对于第一个操作(generateDouble),我们使用Stream.continually生成无限随机双精度浮点数,然后我们将这些数字乘以 1,000,然后只取 1 并返回它。

第二个操作非常相似。然而,我们必须添加一些验证以确保双精度浮点数的数量存在。有几种方法可以做到这一点。一种方法是在 Scala 中使用 assert 方法。第二种方法是模式匹配器,它很棒,因为我们不需要编写if语句。

这种技术在 Scala 社区中非常常见。因此,我们创建了一个选项,它接受一个数字,然后进行模式匹配。如果有数字存在,将触发Some方法,否则将调用None

经过这些验证后,我们可以使用Stream生成所需数量的数字。在我们运行代码之前,我们需要定义 Guice 注入。此文件位于默认包ng-microservice/app/中:

    class Module extends AbstractModule { 
      override def configure() = { 
        // Use the system clock as the default implementation of Clock 
        bind(classOf[Clock]).toInstance(Clock.systemDefaultZone) 
        bind(classOf[NGContract]).to(classOf[NGServiceImpl]). 
        asEagerSingleton() 
      } 
    } 

现在是编译并运行我们的微服务的时候了。由于我们已经在9000端口上运行了一个名为ReactiveWebStore的 Play 应用程序,如果你直接运行微服务,你会遇到麻烦。为了解决这个问题,我们需要在不同的端口上运行它。让我们为微服务使用9090端口。打开控制台,执行命令$ activator后跟$ run 9090

ng-microservice$ activator -Dsbt.task.forcegc=false 
[info] Loading global plugins from /home/diego/.sbt/0.13/plugins 
[info] Loading project definition from /home/diego/github/diegopacheco/Book_Building_Reactive_Functional_Scala_Applications/Chap4/ng-microservice/project 
[info] Set current project to ng-microservice (in build file:/home/diego/github/diegopacheco/Book_Building_Reactive_Functional_Scala_Applications/Chap4/ng-microservice/) 
[ng-microservice] $ run 9090 

--- (Running the application, auto-reloading is enabled) --- 

[info] p.c.s.NettyServer - Listening for HTTP on /0:0:0:0:0:0:0:0:9090 

(Server started, use Ctrl+D to stop and go back to the console...)

我们可以通过调用我们拥有的两个操作来测试我们的微服务。所以让我们打开网页浏览器并执行它。

http://localhost:9090/doubledouble微服务调用如下:

NGServiceEndpoint

每次调用此操作时,你都会看到一个不同的随机双精度浮点数。现在我们可以尝试下一个操作:传递给数字的数量。此操作将返回一个包含双精度浮点数的 JSON 格式的列表。

http://localhost:9090/doubles/100的批量double微服务调用如下所示:

NGServiceEndpoint

它工作了!这里有 100 个双精度浮点数。现在我们有一个微服务正在运行,我们可以回到我们的ReactiveWebStore并更改我们的 RxScala 代码。我们将创建新的控制器。我们还将更新现有代码以调用我们的新代码,并在 UI 上为用户建议价格,所有这些都在反应式方式下完成。请记住,你需要让ng-microservice运行;否则,ReactiveWebStore将无法检索双精度浮点数。

Play 框架和高 CPU 使用率

如果你注意到你的 CPU 使用率高于应有的水平,不要担心;有一个修复方法。实际上,这个问题与 SBT 有关。只需确保在运行 Activator 时,你传递以下参数:

$ activator -Dsbt.task.forcegc=false

回到ReactiveWebStore,让我们创建新的路由。打开ReactiveWebStore/conf/routes

    GET   /rnd/double  
    controllers.RndDoubleGeneratorController.rndDouble 
    GET   /rnd/call    controllers.RndDoubleGeneratorController.rndCall 
    GET   /rnd/rx      controllers.RndDoubleGeneratorController.rxCall 
    GET   /rnd/rxbat      
    controllers.RndDoubleGeneratorController.rxScalaCallBatch 

一旦我们有了新的路由,我们需要创建新的控制器。这个控制器需要与其他控制器一起位于ReactiveWebStore/app/controllers

RndDoubleGeneratorController

RndDoubleGeneratorController类文件应该看起来像这样:

    @Singleton 
    class RndDoubleGeneratorController @Inject() (service:IRndService)     
    extends Controller { 
      import play.api.libs.concurrent.Execution. 
      Implicits.defaultContext 
      def rndDouble = Action { implicit request => 
        Ok( service.next().toString() ) 
      } 
      def rndCall = Action.async { implicit request => 
        service.call().map { res => Ok(res) } 
      } 
      def rxCall = Action { implicit request => 
        Ok(service.rxScalaCall().toBlocking.first.toString()) 
      } 
      def rxScalaCallBatch = Action { implicit request => 
        Ok(service.rxScalaCallBatch().toBlocking.first.toString()) 
      } 
    } 

在前面的控制器中,所有方法都调用服务IRndServiceRndService中的所有操作都调用ng-microservice。这里有一些操作变体,我们将在探索服务实现时详细讨论。

这里有一些有趣的事情:例如,对于第二个操作rndCall,我们看到使用了Action.async,这意味着我们的控制器将返回一个 Future,而这个 Future 来自服务。我们还执行了一个Map来将结果转换为Ok

最后一个名为rxScalaCallBatch的操作是最有趣的,也是我们将用于我们的 UI 的操作。然而,如果你愿意,你也可以使用其他的,因为它们都返回双精度浮点数,这是很好的。

IRndService.scala - Scala trait

让我们看看服务定义。首先,我们需要为服务定义一个特质,该特质将定义我们需要的操作:

    trait IRndService { 
      def next():Double 
      def call():Future[String] 
      def rxScalaCall():Observable[Double] 
      def rxScalaCallBatch():Observable[Double] 
    } 

RndService.scala - RndService 实现

现在我们可以转向真正的服务实现了。这需要位于ReactiveWebStore/app/services

    @Singleton 
    class RndService @Inject() (ws: WSClient) extends IRndService { 
      import play.api.libs.concurrent.Execution.Implicits. 
      defaultContext 
      override def next():Double = { 
        val future = ws.url("http://localhost:9090/double").get().map  
        { res => res.body.toDouble } 
        Await.result(future, 5.seconds) 
      } 
      override def call():Future[String] = { 
        ws.url("http://localhost:9090/double").get().map  
        { res => res.body } 
      } 
      override def rxScalaCall():Observable[Double] = { 
        val doubleFuture:Future[Double] =  
        ws.url("http://localhost:9090/double").get().map { x =>  
          x.body.toDouble } 
        Observable.from(doubleFuture) 
      } 
     // Continue ... 

为了调用我们的微服务(ng-microservice),我们需要注入一个特殊的 Play 框架库ws,这是一个用于调用 Web 服务的实用库。我们通过在类定义中添加代码(ws:WSClient)来注入它。

当你使用ws调用某个东西时,它返回一个 Future。我们需要有 Future 执行器。这就是为什么defaultContext的导入非常重要,你不能跳过它。

对于这个方法,正如你所看到的,我们接下来调用我们的微服务http://localhost:9090/double以获取一个单独的双精度浮点数。我们映射这个结果,并获取结果的主体,这将是我们需要的双精度浮点数本身。

对于这个方法,我们使用Await.result,这将阻塞并等待结果。如果结果在五秒内没有返回,这段代码将失败。

第二个方法名为 call,它执行相同的操作,但主要区别是我们没有阻塞服务;相反,我们向控制器返回一个 Future。

最后,最后一个方法名为 rxScalaCall,它执行相同的操作:使用 ws 库调用我们的微服务。然而,我们返回一个可观测对象。可观测对象很棒,因为它们可以用作 Future。

现在是时候检查最终操作和最有趣的操作了。对于这个相同的类,我们需要添加另一个方法,如下所示:

RndService.scala 中的 rxScalaCallBatch 方法如下:

    override def rxScalaCallBatch():Observable[Double] = { 
      val doubleInfiniteStreamSubject = PublishSubject.apply[Double]() 
      val future = ws.url("http://localhost:9090/doubles/10") 
      .get() 
      .map { x => Json.parse(x.body).as[List[Double]] } 
      future.onComplete { 
        case Success(l:List[Double]) => l.foreach { e =>  
        doubleInfiniteStreamSubject.onNext(e) } 
        case Failure(e:Exception) =>  
        doubleInfiniteStreamSubject.onError(e) 
      } 
      var observableEven = Observable.create {  
        doubleInfiniteStreamSubject.subscribe } 
      .onErrorReturn { x => 2.0 } 
      .flatMap { x => Observable.from( Iterable.fill(1)(x + 10) ) } 
      .filter { x => x.toInt % 2 == 0 } 
      .flatMap { x => println("ODD: " + x) ; Observable.just(x) } 
      var observableOdd = Observable.create {  
        doubleInfiniteStreamSubject.subscribe } 
        .onErrorReturn { x => 1.0 } 
        .flatMap { x => Observable.from( Iterable.fill(1)(x + 10) ) } 
        .filter { x => x.toInt % 2 != 0 } 
        .flatMap { x => println("EVEN: " + x) ; Observable.just(x) } 
        var mergeObservable = Observable 
        .empty 
        .merge(observableEven) 
        .merge(observableOdd) 
        .take(10) 
        .foldLeft(0.0)(_+_) 
        .flatMap { x => Observable.just( x - (x * 0.9) ) } 
        mergeObservable 
      } 

因此,首先我们创建 PublishSubject 以便能够为可观测对象生成数据。然后我们调用微服务的 ws 方法。现在的主要区别是我们调用批处理操作并排序 10 个双精度浮点数。这段代码发生在 Future 中,所以它是非阻塞的。

我们然后使用 Map 函数来转换结果。ng-microservice 函数将返回 JSON,因此我们需要将此 JSON 反序列化为 Scala 对象。最后,我们在 Future 结果中运行模式匹配器。如果结果是成功的,这意味着一切正常。因此,对于每个双精度浮点数,我们使用 PublishSubject 将其发布到可观测对象中。如果服务已关闭或我们遇到问题,我们将错误发布到下游的可观测对象中。

接下来我们创建三个可观测对象:一个用于奇数,一个用于偶数,第三个将合并前两个并执行额外的计算。我们在 Future 和可观测对象之间进行转换的方式是理想的,因为它是非阻塞的。

在这里,我们有与之前 Rx 控制器相似的代码。主要区别是我们有错误处理,因为 ng-microservice 可能永远不会返回,因为它可能已经关闭或根本不起作用。因此,我们需要开始使用回退。良好的回退对于反应式应用程序的错误处理至关重要。回退应该是某种静态的;换句话说,它们不应该失败。

我们提供了两种回退方法:一个用于 Odd 可观测对象,另一个用于 Even 可观测对象。这些回退是通过设置 OnErrorReturn 方法完成的。所以对于偶数,回退是静态的,值为 2,而对于奇数,值为 1。这很好,因为即使出现故障,我们的应用程序仍然可以继续工作。

你可能会意识到这次我们没有使用 take 函数。那么这段代码会永远运行吗?不会的,因为 ng-microservice 只返回 10 个双精度浮点数。最后,我们将可观测对象合并成一个单一的可观测对象,将所有数字相加,获取值的 90%,然后返回一个可观测对象。

Module.scala - Guice 注入

现在,将这个新服务连接到 Guice。让我们更改位于 ReactiveWebStore/apps/Module.scala 的 Guice Module.scala

    class Module extends AbstractModule { 
      override def configure() = { 
        bind(classOf[Clock]).toInstance(Clock.systemDefaultZone)  
         bind(classOf[ApplicationTimer]).asEagerSingleton() 
         bind(classOf[IProductService]).to(classOf[ProductService]). 
         asEagerSingleton() 
         bind(classOf[IReviewService]).to(classOf[ReviewService]). 
         asEagerSingleton() 
         bind(classOf[IImageService]).to(classOf[ImageService]). 
         asEagerSingleton() 
         bind(classOf[IPriceSerice]).to(classOf[PriceService]). 
         asEagerSingleton() 
         bind(classOf[IRndService]).to(classOf[RndService]). 
         asEagerSingleton() 
      }} 

接下来我们需要在 JavaScript 中创建一个 jQuery 函数来调用我们新的控制器。这个函数需要位于 ReactiveWebStore/public/javascripts

下面的代码是 price.js,这是一个调用我们控制器的 jQuery 函数:

    /** 
     * This functions loads the price in the HTML component. 
    */ 
    function loadPrice(doc){ 
      jQuery.get( "http://localhost:9000/rnd/rxbat", function(  
      response ) { 
        doc.getElementById("price").value = parseFloat(response) 
      }).fail(function(e) { 
        alert('Wops! We was not able to call  
        http://localhost:9000/rnd/rxba. Error: ' + e.statusText); 
      }); 
    } 

我们只有一个名为 loadPrice 的函数,它接收一个文档。我们使用 JQuery.get 方法调用我们的控制器,并解析响应,将其添加到名为 price 的 HTML 文本框中。如果出现问题,我们会通过 alert 弹窗通知用户无法加载价格。

main.scala.html

我们需要更改位于 ReactiveWebStore/app/views/main.scala.htmlmain.scala 代码,以便导入一个新的 JavaScript 函数:

    @(title: String)(content: Html)(implicit flash: Flash) 
    <!DOCTYPE html> 
    <html lang="en"> 
      <head> 
        <title>@title</title> 
        <link rel="shortcut icon" type="image/png"  
        href="@routes.Assets.at("images/favicon.png")"> 
        <link rel="stylesheet" media="screen"  
        href="@routes.Assets.at("stylesheets/main.css")"> 
        <link rel="stylesheet" media="screen"  
        href="@routes.Assets.at("stylesheets/bootstrap.min.css")"> 
        <script src="img/@routes.Assets.at("javascripts/jquery- 
        1.9.0.min.js")" type="text/javascript"></script> 
        <script src="img/@routes.Assets.at("javascripts/bootstrap.js")"  
        type="text/javascript"></script> 
        <script src="img/@routes.Assets.at("javascripts/image.js")"  
        type="text/javascript"></script> 
        <script src="img/@routes.Assets.at("javascripts/price.js")"  
        type="text/javascript"></script> 
      </head> 
      <body> 
        <center><a href='/'><img height='42' width='42'  
        src='@routes.Assets.at("images/rws.png")'></a> 
        <h3>@title</h3></center> 
        <div class="container"> 
          @alert(alertType: String) = { 
            @flash.get(alertType).map { message => 
              <div class="alert alert-@alertType"> 
                <button type="button" class="close" data- 
                dismiss="alert">&times;</button> 
                @message 
              </div> 
            } 
          }
          @alert("error") 
          @alert("success") 
          @content 
          <a href="/"></a><BR> 
          <button type="submit" class="btn btn-primary"  
            onclick="window.location.href='/'; " > 
           Reactive Web Store - Home 
          </button> 
        </div> 
      </body> 
    </html> 

product_details.scala.html

最后,我们需要更改我们的产品视图,以便添加一个从控制器加载价格的按钮。让我们更改 product_details 视图,位于 ReactiveWebStore/app/views/product_details.scala.html

    @(id: Option[Long],product:Form[Product])(implicit flash:Flash) 
    @import play.api.i18n.Messages.Implicits._ 
    @import play.api.Play.current 
    @main("Product: " + product("name").value.getOrElse("")){ 
      @if(product.hasErrors) { 
        <div class="alert alert-error"> 
          <button type="button" class="close" data- 
          dismiss="alert">&times;</button> 
          Sorry! Some information does not look right. Could you  
          review it please and re-submit? 
        </div> 
      } 

      @helper.form(action = if (id.isDefined)  
      routes.ProductController.update(id.get) else  
      routes.ProductController.insert) { 
        @helper.inputText(product("name"), '_label -> "Product Name") 
        @helper.inputText(product("details"), '_label -> "Product  
        Details") 
        @helper.inputText(product("price"), '_label -> "Price") 
        <div class="form-actions"> 
        <button type="button" class="btn btn-primary"  
        onclick="javascript:loadPrice(document);" >Load Rnd  
        Price</button> 
        <button type="submit" class="btn btn-primary"> 
          @if(id.isDefined) { Update Product } else { New Product } 
        </button> 
        </div> 
      } 
    } 

太好了!现在我们有一个按钮,使用 JQuery 从控制器加载数据。你可以看到按钮 Load Rnd Price 有一个 onClick 属性,它调用我们的 JavaScript 函数。

现在,你需要打开你的控制台,并输入 $ activator run 来编译和运行更改,就像我们对 ReactiveWebStore 做的那样。

此命令将产生以下结果:

ReactiveWebStore$ activator -Dsbt.task.forcegc=false
[info] Loading global plugins from /home/diego/.sbt/0.13/plugins
[info] Loading project definition from /home/diego/github/diegopacheco/Book_Building_Reactive_Functional_Scala_Applications/Chap4/ReactiveWebStore/project
[info] Set current project to ReactiveWebStore (in build file:/home/diego/github/diegopacheco/Book_Building_Reactive_Functional_Scala_Applications/Chap4/ReactiveWebStore/)
[ReactiveWebStore] $ run
--- (Running the application, auto-reloading is enabled) ---
[info] p.c.s.NettyServer - Listening for HTTP on /0:0:0:0:0:0:0:0:9000
(Server started, use Ctrl+D to stop and go back to the console...)
[info] application - ApplicationTimer demo: Starting application at 2016-07-03T02:35:54.479Z.
[info] play.api.Play - Application started (Dev)

因此,打开你的浏览器到 http://localhost:9000,并转到产品页面以查看我们的需求功能集成并完美运行。请记住,你需要在一个另一个控制台窗口中让 ng-microservice 运行;否则,我们的应用程序将使用静态回退。

新的产品功能将在 http://localhost:9000/product/add 上显示,如下面的截图所示:

product_details.scala.html

所以,如果你点击 Load Rnd Price 按钮,你会看到类似以下的内容:

product_details.scala.html

如果你查看 activator 控制台中的应用程序日志,你会看到类似以下的内容:

[info] application - ApplicationTimer demo: Starting application at 2016-07-03T02:35:54.479Z.
[info] play.api.Play - Application started (Dev)
[info] application - index called. Products: List()
[info] application - blank called. 
ODD: 722.8017048639501
EVEN: 863.8229024202085
ODD: 380.5549208988492
EVEN: 947.6312814830953
ODD: 362.2984794191124
ODD: 676.978825910585
ODD: 752.7412673916701
EVEN: 505.3293481709368
EVEN: 849.9768444508936
EVEN: 99.56583617819769

好的,这就完成了。一切都在运行中!

摘要

在本章中,你学习了由 Reactive Manifesto 引导的响应式应用程序的核心原则。你还学习了如何使用 RxScala 创建响应式应用程序。然后我们解释了如何使用 ws 库调用其他内部和外部 Web 服务。然后你学习了如何使用 Json 库序列化和反序列化 JSON。继续前进,你学习了如何使用 Play 框架支持创建简单的微服务。

在下一章中,我们将继续构建我们的应用程序,并学习如何使用 JUnit 和 ScalaTest 测试我们的应用程序。

第五章:测试你的应用程序

在到目前为止的章节中,我们使用 Activator 启动了我们的应用程序,使用 Scala 和 Play 框架开发了我们的 Web 应用程序,并使用 RxScala 添加了一个响应式微服务调用以进行数据流计算。现在我们将继续进行单元测试和控制器测试,使用 BDD 和 Play 框架。

在本章中,我们将涵盖以下主题:

  • 单元测试原则

  • 使用 JUnit 测试 Scala 应用程序

  • 行为驱动开发原则

  • 使用 ScalaTest 进行测试

  • 使用 ScalaTest 测试 Play 框架应用程序

  • 在 Activator / SBT 中运行测试

testingTest的需求是软件开发的一个基本且非常重要的部分。没有测试,我们无法确保我们的代码是正确的。我们应该对我们产生的几乎所有代码进行测试。有些东西不适合测试,例如,案例类和仅代表结构对象的类,换句话说,没有函数的类。如果你有应用计算、转换和验证的代码,你肯定希望使用良好的代码覆盖率来测试这些代码,这指的是功能或任何重要的代码,而不仅仅是结构性的。

测试覆盖率很重要,因为它允许我们带着信任进行重构(通过减少代码、创建通用代码,甚至删除代码来提高应用程序代码质量)。这是因为,如果你有测试,并且你意外地做错了什么,你的测试会告诉你。这全部关于拥有短周期的反馈。越早越好;你希望尽早、尽可能接近开发地发现你引入的 bug。没有人喜欢在生产中发现那些可以通过简单测试捕获的 bug。

单元测试原则

单元测试是你可能应用的最小测试单元。你需要将其应用于类级别。因此,一个单元测试将覆盖你拥有的所有函数的类。但是,等等,一个类通常有依赖项,而这些依赖项可能有其他的依赖项,那么你该如何测试呢?我们需要有模拟,简单的哑对象,它们模拟其他类的行为。这是隔离代码并允许单元测试的重要技术。

使代码可测试

单元测试很简单:基本上,我们通过向函数传递参数来调用它,然后检查输出是否与我们的预期相符。这也被称为断言或断言。因此,单元测试是关于断言的。有时,你的代码可能不可测试。例如,假设你有一个没有参数且返回单位的函数。这非常难以测试,因为它意味着函数充满了副作用。如果你还记得我们在第一章中讨论的内容,函数式编程、响应式编程和 Scala 简介,这是违反函数式编程原则的。因此,如果我们遇到这种情况,我们需要重构代码,使函数返回某些内容,然后我们才能对其进行测试。

隔离和自包含测试

单元测试应该是自包含的,这意味着单元测试中的类不应该依赖于任何特定的执行顺序。假设你有一个包含两个测试函数的单元测试类。因此,每个测试应该一次只测试一个函数,并且这两个函数应该能够以任何顺序运行。否则,测试将变得脆弱,长期维护起来也会很困难。

有效的命名

有效的命名是至关重要的。测试函数需要确切地说明测试做了什么。这很重要,因为当测试失败时,更容易找出出错的原因和原因。遵循同样的思路,当你进行断言时,你应该一次只断言一件事情。想象一下,你需要测试一个网络服务是否返回有效的 JSON。这个特定的 JSON 可能有两个字段:名字和姓氏。因此,你将为一个名字进行一次断言,为姓氏进行另一次断言。这样,将更容易理解测试做了什么,以及当测试失败时如何进行故障排除。

测试层次

当我们运行测试时,我们通常分层进行。单元测试是基本层;然而,还有其他层次,如控制器测试、集成测试、UI 测试、端到端测试、压力测试,等等。对于这本书,我们将使用 JUnit 和ScalaTest,Play 框架的支持,来介绍单元测试、控制器测试和 UI 测试。

使用 JUnit 进行测试

如果你来自 Java 背景,你很可能已经使用过 JUnit。你可以使用 Scala 和 Play 框架进行 JUnit 测试。然而,当我们使用 Play 框架创建应用程序时,这并不是最佳实践,因为它更倾向于使用 Scala Spec 进行行为驱动开发BDD)测试。对于本章,我们将介绍如何使用 BDD 和 Play 进行各种测试。现在,在我们转向 BDD 之前,让我们看看如何使用 JUnit 进行单元测试。

    @RunWith(classOf[Suite]) 
    @Suite.SuiteClasses(Array(classOf[JunitSimpleTest])) 
    class JunitSimpleSuiteTest 
    class JunitSimpleTest extends PlaySpec with AssertionsForJUnit { 
      @Test def testBaseService() { 
        val s = new ProductService 
        val result = s.findAll() 
        assertEquals(None, result) 
        assertTrue( result != null) 
        println("All good junit works fine with ScalaTest and Play") 
      } 
    } 

因此,在前面代码中,我们有一个扩展 PlaySpec 并添加了一个名为 AssertionForJunit 的特质的类。为什么这里没有经典的 JUnit 类?因为 Play 框架已经设置为运行 Scala 测试,所以这个桥梁允许我们通过 ScalaTest Play 框架构造来运行 JUnit。

然后,我们有一个名为 testBaseServer 的测试函数,它使用 JUnit 的 @Test 注解。在测试方法内部,我们创建一个 ProductService 实例,然后调用 findAll 函数。

最后,我们有断言将检查结果是否符合我们的预期。因此,我们没有产品,因为我们之前没有插入它们。因此,我们期望响应为 None,结果也不应该是 null

您可以使用以下命令在您的控制台中运行此操作:

$ activator "test-only JunitSimpleTest"

您将在下一张截图看到结果:

使用 Junit 进行测试

如您所见,我们的测试执行没有任何问题。您也可以在 Eclipse IDE 中使用 Junit 运行此测试和正常测试。您只需右键单击文件,然后选择 运行方式:Scala Junit 测试;**,请参考以下截图:

使用 Junit 进行测试

行为驱动开发 - BDD

行为驱动开发 (BDD) 是一种敏捷开发技术,它关注开发人员与非技术人员(如业务的产品负责人)之间的互动。这个想法很简单:使用业务使用的相同语言来提取你正在构建的代码最初存在的原因。BDD 最终最小化了技术语言与业务语言之间的翻译,创造了更多协同作用,减少了信息技术与业务之间的噪音。

BDD 测试描述了应用程序需要做什么,以及它的行为。使用业务人员和开发人员之间的结对编程来编写这些测试是非常常见的。ScalaTest 是一个 BDD 框架。Play 框架与 ScalaTest 集成得很好。让我们现在就开始使用 ScalaTest 和 Play 吧。

MyFirstPlaySpec.scala - 使用 ScalaTest 和 Play 框架的首次 BDD

MyFirstPlaySpec.scala 类应包含以下代码:

    class MyFirstPlaySpec extends PlaySpec { 
      "x + 1 " must { 
        "be 2 if x=1 " in { 
          val sum = 1 + 1 
          sum mustBe 2 
        } 
        "be 0 if x=-1 " in { 
          val sum = -1 + 1 
          sum mustBe 0 
        } 
      } 
    } 

因此,您创建了一个名为 MyFirstPlaySpec 的类,并从 PlaySpec 扩展它以获取 Play 框架 ScalaTest 支持。然后我们创建了两个测试两个数字之和的函数。在第一个测试中,1 + 1 应该等于 2,在第二个测试中,-1 + 1 应该等于 0。当我们执行 mustBe 时,它与在 JUnit 中进行断言相同。这里的主要区别是测试在 Spec 上有明确的行为。现在我们可以通过键入以下内容来运行测试:

$ activator "test-only MyFirstPlaySpec"

您将看到以下结果:

MyFirstPlaySpec.scala - 使用 ScalaTest 和 Play 框架的首次 BDD

使用 Play 框架支持进行测试

现在我们将继续构建我们的应用程序。让我们在我们的应用程序中添加 BDD 测试。我们将开始对你的服务进行测试。我们必须测试ProductServiceImageServiceReviewService

你的ProductServiceTestSpec.scala文件应该包含以下代码:

    class ProductServiceTestSpec extends PlaySpec { 
      "ProductService" must { 
        val service:IProductService = new ProductService 
        "insert a product properly" in { 
          val product = new models.Product(Some(1), 
          "Ball","Awesome Basketball",19.75) 
          service.insert(product) 
        } 
        "update a product" in { 
          val product = new models.Product(Some(1), 
          "Blue Ball","Awesome Blue Basketball",19.99) 
          service.update(1, product) 
        } 
        "not update because does not exit" in { 
          intercept[RuntimeException]{ 
            service.update(333,null) 
          } 
        } 
        "find the product 1" in { 
           val product = service.findById(1) 
           product.get.id mustBe Some(1) 
           product.get.name mustBe "Blue Ball" 
           product.get.details mustBe "Awesome Blue Basketball" 
           product.get.price mustBe 19.99 
        } 
        "find all" in { 
          val products = service.findAll() 
          products.get.length mustBe 1 
          products.get(0).id mustBe Some(1) 
          products.get(0).name mustBe "Blue Ball" 
          products.get(0).details mustBe "Awesome Blue Basketball" 
          products.get(0).price mustBe 19.99 
        } 
        "find all products" in { 
          val products = service.findAllProducts() 
          products.length mustBe 1 
          products(0)._1 mustBe "1" 
          products(0)._2 mustBe "Blue Ball" 
        } 
        "remove 1 product" in { 
          val product = service.remove(1) 
          product mustBe true 
          val oldProduct = service.findById(1) 
          oldProduct mustBe None 
        } 
        "not remove because does not exist" in { 
          intercept[RuntimeException]{ 
            service.remove(-1) 
          } 
        } 
      } 
    } 

对于这次测试,我们正在测试ProductService中所有可用的公共函数。测试非常直接:我们调用特定的服务操作,例如findById,然后我们检查结果以确保所有应该存在的数据都存在。

有一些场景下,服务应该返回一个异常,例如,如果你尝试删除一个不存在的东西。如果你看一下最后一个测试函数"not remove because does not exist",我们应该得到一个异常。然而,服务代码中有一个 bug。运行测试,然后你就会看到它。

ProductService.scala - 修复代码问题

测试的伟大之处在于:它们可以显示我们代码中的问题,这样我们就可以在代码进入生产环境并影响用户体验之前修复它们。为了修复最后一个测试,我们需要进入ProductService类并修复一个方法。这就是我们修复它的方法:

    private def validateId(id:Long):Unit = { 
      val entry = inMemoryDB.get(id) 
      if (entry==null || entry.equals(None)) throw new  
      RuntimeException("Could not find Product: " + id) 
    } 

现在一切都准备好了,一切都正常。Play 框架支持使用intercept函数测试预期的异常,让我们使用activator命令在控制台中运行测试。

$ activator "test-only ProductServiceTestSpec"

执行此命令后,我们得到以下结果:

ProductService.scala - 修复代码问题

ImageServiceTestSpec.scala - ImageService 测试

好的,现在我们可以添加ImageService的测试,如下所示:

    class ImageServiceTestSpec extends PlaySpec { 
      "ImageService" must { 
        val service:IImageService = new ImageService 
        "insert a image properly" in { 
          val image = new models.Image(Some(1),Some(1), 
          "http://www.google.com.br/myimage") 
          service.insert(image) 
        } 
        "update a image" in { 
          val image = new models.Image(Some(2),Some(1), 
          "http://www.google.com.br/myimage") 
          service.update(1, image) 
        } 
        "not update because does not exist" in { 
          intercept[RuntimeException]{ 
            service.update(333,null) 
          } 
        } 
        "find the image1" in { 
          val image = service.findById(1) 
          image.get.id mustBe Some(1) 
          image.get.productId mustBe Some(1) 
          image.get.url mustBe "http://www.google.com.br/myimage" 
        } 
        "find all" in { 
          val reviews = service.findAll() 
          reviews.get.length mustBe 1 
          reviews.get(0).id mustBe Some(1) 
          reviews.get(0).productId mustBe Some(1) 
          reviews.get(0).url mustBe "http://www.google.com.br/myimage" 
        } 
        "remove 1 image" in { 
          val image = service.remove(1) 
          image mustBe true 
          val oldImage = service.findById(1) 
          oldImage mustBe None 
        } 
        "not remove because does not exist" in { 
          intercept[RuntimeException]{ 
            service.remove(-1) 
          } 
        } 
      } 
    } 

所以这些是ImageService的 BDD 测试。我们已经覆盖了服务上所有可用的函数。就像在ProductService类中一样,我们也有针对不幸场景的测试,在这些场景中我们期望发生异常。

有时候,我们需要调用多个函数来测试特定的函数或特定的测试用例。例如,在"删除 1 张图片"中,我们首先删除一张图片。我们的测试用例检查是否存在图片。让我们在 Activator 控制台中运行测试。

$ activator "test-only ImageServiceTestSpec"

将获得以下结果:

ImageServiceTestSpec.scala - ImageService 测试

ReviewServiceTestSpec.scala - ReviewService 测试

接下来,我们需要为审查服务添加测试。这里我们开始。

    class ReviewServiceTestSpec extends PlaySpec { 
      "ReviewService" must { 
        val service:IReviewService = new ReviewService 
        "insert a review properly" in { 
          val review = new  
          models.Review(Some(1),Some(1),"diegopacheco", 
          "Testing is Cool") 
          service.insert(review) 
        } 
        "update a review" in { 
          val review = new models.Review(Some(1),Some(1), 
          "diegopacheco","Testing so so Cool") 
          service.update(1, review) 
        } 
        "not update because does not exist" in { 
          intercept[RuntimeException]{ 
            service.update(333,null) 
          } 
        } 
        "find the review 1" in { 
          val review = service.findById(1) 
          review.get.id mustBe Some(1) 
          review.get.author mustBe "diegopacheco" 
          review.get.comment mustBe "Testing so so Cool" 
          review.get.productId mustBe Some(1) 
        } 
        "find all" in { 
          val reviews = service.findAll() 
          reviews.get.length mustBe 1 
          reviews.get(0).id mustBe Some(1) 
          reviews.get(0).author mustBe "diegopacheco" 
          reviews.get(0).comment mustBe "Testing so so Cool" 
          reviews.get(0).productId mustBe Some(1) 
        } 
        "remove 1 review" in { 
          val review = service.remove(1) 
          review mustBe true 
          val oldReview= service.findById(1) 
          oldReview mustBe None 
        } 
        "not remove because does not exist" in { 
          intercept[RuntimeException]{ 
            service.remove(-1) 
          } 
        } 
      } 
    } 

好的,我们已经有了审查服务的测试。我们现在可以运行它们了。

$ activator "test-only ReviewServiceTestSpec"

这里是输出结果:

ReviewServiceTestSpec.scala - ReviewService 测试

测试路由

Play 框架允许我们测试路由。这是好事,因为随着我们的应用程序的增长和代码的重构,我们可以 100%确信我们的路由正在正常工作。路由测试很容易与控制器测试混淆。主要区别在于,在路由测试中,我们应该测试我们是否能够到达路由,仅此而已。在路由测试之后,我们将详细介绍控制器测试。

RoutesTestingSpec.scala - Play 框架路由测试

你的 RoutesTestingSpec.scala 文件应该包含以下代码:

    class RoutesTestingSpec extends PlaySpec with OneAppPerTest { 
      "Root Controller" should { 
        "route to index page" in { 
          val result = route(app, FakeRequest(GET, "/")).get 
          status(result) mustBe OK 
          contentType(result) mustBe Some("text/html") 
          contentAsString(result) must include ("Welcome to Reactive  
          Web Store") 
        } 
      } 
      "Product Controller" should { 
        "route to index page" in { 
          val result = route(app, FakeRequest(GET, "/product")).get 
          status(result) mustBe OK 
          contentType(result) mustBe Some("text/html") 
          contentAsString(result) must include ("Product") 
        } 
        "route to new product page" in { 
          val result = route(app, FakeRequest(GET,  
          "/product/add")).get 
          status(result) mustBe OK 
          contentType(result) mustBe Some("text/html") 
          contentAsString(result) must include ("Product") 
        } 
        "route to product 1 details page page" in {  
          try{ 
            route(app, FakeRequest(GET, "/product/details/1")).get 
            }catch{ 
              case e:Exception => Unit 
            } 
          } 
        } 
        "Review Controller" should { 
          "route to index page" in { 
            val result = route(app, FakeRequest(GET, "/review")).get 
            status(result) mustBe OK 
            contentType(result) mustBe Some("text/html") 
            contentAsString(result) must include ("Review") 
          } 
          "route to new review page" in { 
            val result = route(app, FakeRequest(GET,  
            "/review/add")).get 
            status(result) mustBe OK 
            contentType(result) mustBe Some("text/html") 
            contentAsString(result) must include ("review") 
          } 
          "route to review 1 details page page" in { 
            try{ 
              route(app, FakeRequest(GET, "/review/details/1")).get 
            }catch{ 
              case e:Exception => Unit 
            } 
          } 
        } 
        "Image Controller" should { 
          "route to index page" in { 
            val result = route(app, FakeRequest(GET, "/image")).get 
            status(result) mustBe OK 
            contentType(result) mustBe Some("text/html") 
            contentAsString(result) must include ("Image") 
          } 
          "route to new image page" in { 
            val result = route(app, FakeRequest 
            (GET, "/image/add")).get 
            status(result) mustBe OK 
            contentType(result) mustBe Some("text/html") 
            contentAsString(result) must include ("image") 
          } 
          "route to image 1 details page page" in { 
          try{ 
            route(app, FakeRequest(GET, "/image/details/1")).get 
          }catch{ 
            case e:Exception => Unit  
          } 
        } 
      }  
    } 

因此,这里我们有对所有主要控制器的测试,这些控制器是根、产品、评论和图像。RootController 是当你访问 http://localhost:9000 时的主页控制器。

在 Play 框架中有一个特殊的辅助函数叫做 route,它帮助我们测试路由。然后我们使用 FakeRequest 将路径传递给路由。我们可以测试路由器将我们的请求路由到的页面的状态码和内容类型。

对于产品、图像和评论控制器,你可以看到我们正在尝试调用一个不存在的项目。这就是为什么我们有 try...catch,因为我们预计那里会发生异常。

$  activator "test-only RoutesTestingSpec"

执行前面的命令会产生以下结果:

RoutesTestingSpec.scala - Play 框架路由测试

控制器测试

我们进行了单元测试,进行了路由测试,现在是添加控制器测试的时候了。控制器测试与路由测试类似,但它们并不相同。例如,我们的控制器总是响应 UI 页面,因此我们预计将为每个方法创建特定的 HTML 页面。Play 框架与 Selenium 集成,Selenium 是一个用于 UI 的测试框架,它允许你模拟网络浏览器,你可以做很多事情,就像一个真实用户点击页面一样。

那么,让我们开始吧。首先,我们将从 RndDoubleGeneratorControllerTestSpec 开始。

RndDoubleGeneratorControllerTestSpec.scala - RndDoubleGeneratorController 测试

RndDoubleGeneratorControllerTestSpec.scala 文件应该包含以下代码:

    class RndDoubleGeneratorControllerTestSpec 
    extends PlaySpec  
    with OneServerPerSuite with OneBrowserPerSuite with HtmlUnitFactory 
    { 
      val injector = new GuiceApplicationBuilder() 
      .injector 
      val ws:WSClient = injector.instanceOf(classOf[WSClient]) 
      import play.api.libs.concurrent.Execution. 
      Implicits.defaultContext 
      "Assuming ng-microservice is down rx number should be" must { 
        "work" in { 
          val future = ws.url(s"http://localhost:${port}/rnd/rxbat") 
          .get().map { res => res.body } 
          val response = Await.result(future, 15.seconds) 
          response mustBe "2.3000000000000007" 
        } 
      } 
    } 

这个类有一些有趣的东西。例如,我们使用 GuiceApplicationBuilder 通过 Google Guice 注入 WSClient。其次,我们假设我们在上一章中创建的 ng-microservice 已经关闭,因此我们可以预测来自回退的响应。

我们使用 WSClient 调用控制器,然后将响应映射回返回的正文内容作为字符串。所以这将是一个异步未来,为了获取结果,我们使用 Await 等待五秒钟以等待响应返回。一旦响应返回,我们确保结果是 2.3。如果结果在 15 秒内没有返回,测试将失败。运行以下命令:

$ activator "test-only RndDoubleGeneratorControllerTestSpec"

现在,你将看到以下结果:

RndDoubleGeneratorControllerTestSpec.scala - RndDoubleGeneratorController 测试

好的,现在我们已经使用 Guice 注入和 WSClient Play 框架库完全实现了控制器测试。现在让我们为产品、图像和评论控制器编写控制器测试。

IntegrationSpec.scala

我们可以测试我们的主页以检查它是否正常。这是一个非常简单的测试,为接下来的测试做好准备。所以,我们开始吧。

    class IntegrationSpec extends PlaySpec with OneServerPerTest with  
    OneBrowserPerTest with HtmlUnitFactory { 

      "Application" should { 
        "work from within a browser" in {  
           go to ("http://localhost:" + port) 
           pageSource must include ("Welcome to Reactive Web Store") 
        } 
      } 
    } 

此测试非常简单。我们只需调用主页,并检查它是否包含文本 欢迎来到 Reactive WebStore。让我们运行它。

$ activator "test-only IntegrationSpec"

运行此测试后的结果显示在下图中:

IntegrationSpec.scala

ProductControllerTestSpec.scala

现在我们将查看产品控制器测试规范:

    class ProductControllerTestSpec extends PlaySpec with 
    OneServerPerSuite with OneBrowserPerSuite with HtmlUnitFactory { 
      "ProductController" should { 
        "insert a new product should be ok" in { 
          goTo(s"http://localhost:${port}/product/add") 
          click on id("name") 
          enter("Blue Ball") 
          click on id("details") 
          enter("Blue Ball is a Awesome and simple product") 
          click on id("price") 
          enter("17.55") 
          submit() 
        } 
        "details from the product 1 should be ok" in { 
          goTo(s"http://localhost:${port}/product/details/1") 
          textField("name").value mustBe "Blue Ball" 
          textField("details").value mustBe "Blue Ball is a Awesome  
          and simple product" 
          textField("price").value mustBe "17.55" 
        } 
        "update product 1 should be ok" in { 
          goTo(s"http://localhost:${port}/product/details/1") 
          textField("name").value = "Blue Ball 2" 
          textField("details").value = "Blue Ball is a Awesome and  
          simple product 2 " 
          textField("price").value = "17.66" 
          submit() 
          goTo(s"http://localhost:${port}/product/details/1") 
          textField("name").value mustBe "Blue Ball 2" 
          textField("details").value mustBe "Blue Ball is a Awesome  
          and simple product 2 " 
          textField("price").value mustBe "17.66" 
        } 
        "delete a product should be ok" in { 
          goTo(s"http://localhost:${port}/product/add") 
          click on id("name") 
          enter("Blue Ball") 
          click on id("details") 
          enter("Blue Ball is a Awesome and simple product") 
          click on id("price") 
          enter("17.55") 
          submit() 
          goTo(s"http://localhost:${port}/product") 
          click on id("btnDelete") 
        } 
      } 
    } 

因此,对于产品控制器,我们使用 Selenium Play 框架的支持模拟一个网络浏览器。我们测试基本控制器功能,如插入新产品、特定产品的详细信息以及更新和删除产品。

对于插入,我们使用 goTo 转到新产品表单。我们使用 $port 作为变量。我们这样做是因为 Play 框架将为我们启动应用程序,但我们不知道它在哪个端口。因此,我们需要使用这个变量来访问产品控制器。

然后,我们使用 click 函数点击每个文本字段,并使用 enter 函数输入值。填写完整个表单后,我们使用 submit 函数提交。

对于详情,我们只需转到产品详情页面,并检查文本字段是否具有我们期望的值。我们使用 textField.value 函数来做这件事。

为了检查产品更新功能,我们首先需要更新产品定义,然后进入详情查看我们更改的值是否在那里。

最后,我们测试 delete 函数。对于此函数,我们需要点击一个按钮。我们需要设置按钮的 ID 以使其工作。我们需要在我们的 UI 中进行一些小的重构以使 ID 存在。

product_index.scala.html

您的 product_index.scala.html 文件应包含以下代码:

    @(products:Seq[Product])(implicit flash: Flash) 
    @main("Products") { 
      @if(!products.isEmpty) { 
        <table class="table table-striped"> 
          <tr> 
            <th>Name</th> 
            <th>Details</th>  
            <th>Price</th> 
            <th></th> 
          </tr> 
          @for(product <- products) { 
            <tr> 
              <td><a href="@routes.ProductController.details 
              (product.id.get)">@product.name</a></td> 
              <td>@product.details</td> 
              <td>@product.price</td> 
              <td><form method="post" action= 
              "@routes.ProductController.remove(product.id.get)"> 
                <button id="btnDelete" name="btnDelete" class="btn  
                btn-link" type="submit"><i class="icon- 
                trash"></i>Delete</button> 
              </form></td>  
            </tr> 
          } 
        </table> 
      } 
      <p><a href="@routes.ProductController.blank"  
      class="btn btn-success"><i class="icon-plus icon-white"> 
      </i>Add Product</a></p> 
    } 

一切准备就绪。现在我们可以使用控制台在我们的 Activators 上运行我们的测试。

$ activator "test-only ProductControllerTestSpec"

以下命令显示了以下截图中的结果:

product_index.scala.html

由于此测试实际运行应用程序并模拟网络浏览器调用控制器,此测试可能需要一些时间。现在是我们转向 ImageController 测试的时候了。

ImageControllerTestSpec.scala

您的 product_index.scala.html 应包含以下代码:

    class ImageControllerTestSpec extends PlaySpec with OneServerPerSuite with OneBrowserPerSuite with 
    HtmlUnitFactory { 
      "ImageController" should { 
        "insert a new image should be ok" in { 
          goTo(s"http://localhost:${port}/product/add") 
          click on id("name") 
          enter("Blue Ball") 
          click on id("details") 
          enter("Blue Ball is a Awesome and simple product") 
          click on id("price") 
          enter("17.55") 
          submit() 
          goTo(s"http://localhost:${port}/image/add") 
          singleSel("productId").value = "1" 
          click on id("url") 
          enter("http://myimage.com/img.jpg") 
          submit() 
        } 
        "details from the image 1 should be ok" in { 
          goTo(s"http://localhost:${port}/image/details/1") 
          textField("url").value mustBe "http://myimage.com/img.jpg" 
        } 
        "update image 1 should be ok" in { 
          goTo(s"http://localhost:${port}/image/details/1") 
          textField("url").value = "http://myimage.com/img2.jpg" 
          submit() 
          goTo(s"http://localhost:${port}/image/details/1") 
          textField("url").value mustBe "http://myimage.com/img2.jpg" 
        } 
        "delete a image should be ok" in { 
          goTo(s"http://localhost:${port}/image/add") 
          singleSel("productId").value = "1" 
          click on id("url") 
          enter("http://myimage.com/img.jpg") 
          submit() 
          goTo(s"http://localhost:${port}/image") 
          click on id("btnDelete") 
        } 
      } 
    } 

首先,我们需要转到产品控制器以插入一个产品;否则,我们无法进行图像操作,因为它们都需要产品 ID。

image_index.scala.html

您的 image_index.scala.html 文件应包含以下代码:

    @(images:Seq[Image])(implicit flash:Flash) 
    @main("Images") { 
      @if(!images.isEmpty) { 
        <table class="table table-striped"> 
          <tr> 
            <th>ProductID</th> 
            <th>URL</th> 
            <th></th> 
          </tr> 
          @for(image <- images) { 
            <tr> 
              <td><a href="@routes.ImageController.details 
              (image.id.get)">@image.id</a></td> 
              <td>@image.productId</td> 
              <td>@image.url</td> 
              <td><form method="post" action= 
              "@routes.ImageController.remove(image.id.get)"> 
                <button id="btnDelete" name="btnDelete" class="btn  
                btn-link" type="submit"><i class="icon- 
                trash"></i>Delete</button></form> 
              </td> 
            </tr> 
          } 
        </table> 
      } 
      <p><a href="@routes.ImageController.blank"  
      class="btn btn-success"><i class="icon-plus icon-white"> 
      </i>Add Image</a></p> 
    } 

一切准备就绪。现在我们可以运行 ImageController 测试。

$ activator "test-only ImageControllerTestSpec"

结果显示在下图中:

image_index.scala.html

ImageController 已通过所有测试。现在我们将转向 ReviewController 测试。

ReviewControllerTestSpec.scala

您的 ReviewControllerTestSpec.scala 文件应包含以下代码:

    class ReviewControllerTestSpec extends PlaySpec with OneServerPerSuite with OneBrowserPerSuite 
    with HtmlUnitFactory { 
      "ReviewController" should { 
        "insert a new review should be ok" in { 
          goTo(s"http://localhost:${port}/product/add") 
          click on id("name") 
          enter("Blue Ball") 
          click on id("details") 
          enter("Blue Ball is a Awesome and simple product") 
          click on id("price") 
          enter("17.55") 
          submit() 
          goTo(s"http://localhost:${port}/review/add") 
          singleSel("productId").value = "1" 
          click on id("author") 
          enter("diegopacheco") 
          click on id("comment") 
          enter("Tests are amazing!") 
          submit() 
        } 
        "details from the review 1 should be ok" in { 
          goTo(s"http://localhost:${port}/review/details/1") 
          textField("author").value mustBe "diegopacheco" 
          textField("comment").value mustBe "Tests are amazing!" 
        } 
        "update review 1 should be ok" in { 
          goTo(s"http://localhost:${port}/review/details/1") 
          textField("author").value = "diegopacheco2" 
          textField("comment").value = "Tests are amazing 2!" 
          submit() 
          goTo(s"http://localhost:${port}/review/details/1") 
          textField("author").value mustBe "diegopacheco2" 
          textField("comment").value mustBe "Tests are amazing 2!" 
        } 
        "delete a review should be ok" in { 
          goTo(s"http://localhost:${port}/review/add") 
          singleSel("productId").value = "1" 
          click on id("author") 
          enter("diegopacheco") 
          click on id("comment") 
          enter("Tests are amazing!") 
          submit() 
          goTo(s"http://localhost:${port}/review") 
          click on id("btnDelete") 
          } 
    } 

首先,我们需要转到产品控制器以插入一个产品;否则,我们无法进行图像操作,因为它们都需要产品 ID。

对于插入,我们使用 goto 转到新产品表单。我们使用 $port 作为变量。我们这样做是因为 Play 框架会为我们启动应用程序,但我们不知道在哪个端口,因此我们需要使用这个变量来访问产品控制器。

然后我们使用 click 函数点击每个文本字段,并使用 enter 函数输入值。填写完整个表单后,我们使用 submit 函数提交它。

详细信息,我们只需转到产品详情页面,检查文本字段是否有我们期望的值。我们使用 textField.value 函数来完成这个操作。

为了检查产品更新功能,我们首先需要更新产品定义,然后转到详情页面查看我们更改的值是否在那里。

最后,我们测试 delete 函数。对于这个函数,我们需要点击一个按钮。我们需要设置按钮的 ID 以使其工作。然后我们在 UI 中进行一些小的重构,以便在那里设置 ID。

review_index.scala.html

您的 review_index.scala.html 文件应包含以下代码:

    @(reviews:Seq[Review])(implicit flash: Flash) 
    @main("Reviews") { 
      @if(!reviews.isEmpty) { 
        <table class="table table-striped"> 
          <tr> 
            <th>ProductId</th> 
            <th>Author</th> 
            <th>Comment</th> 
            <th></th> 
          </tr> 
          @for(review <- reviews) { 
            <tr> 
              <td><a href="@routes.ReviewController. 
              details(review.id.get)">@review.productId</a></td> 
              <td>@review.author</td> 
              <td>@review.comment</td> 
              <td> 
                <form method="post" action="@routes. 
                ReviewController.remove(review.id.get)"> 
                  <button id="btnDelete" name="btnDelete"  
                  class="btn btn-link" type="submit"><i class="icon- 
                  trash"></i>Delete</button> 
                </form> 
              </td> 
            </tr> 
          } 
        </table> 
      } 
      <p><a href="@routes.ReviewController.blank" class="btn btn- 
      success"><i class="icon-plus icon-white"></i>Add Review</a></p> 
    } 

最后,我们可以在控制台运行测试。

$ activator "test-only ReviewControllerTestSpec"

测试将显示以下输出:

review_index.scala.html

好的,ReviewController 已经通过了我们所有的测试。

将测试按类型分开是一个非常好的实践。然而,如果您愿意,您可以将所有测试,如单元测试、控制器测试和路由测试混合在一个单独的文件中。

ApplicationSpec.scala

您的 ApplicationSpec.scala 应包含以下代码:

    class ApplicationSpec extends PlaySpec with OneAppPerTest { 
      "Routes" should { 
        "send 404 on a bad request" in { 
          route(app, FakeRequest(GET, "/boum")).map(status(_)) mustBe  
          Some(NOT_FOUND) 
        } 
      } 
      "HomeController" should { 
        "render the index page" in { 
          val home = route(app, FakeRequest(GET, "/")).get 
          status(home) mustBe OK 
          contentType(home) mustBe Some("text/html") 
          contentAsString(home) must include ("Welcome to Reactive Web  
          Store") 
        } 
      } 
      "RndController" should { 
        "return a random number" in { 
          // Assuming ng-microservice is down otherwise will fail. 
          contentAsString(route(app, FakeRequest(GET,  
          "/rnd/rxbat")).get) mustBe "2.3000000000000007" 
        } 
      } 
    } 

我们可以运行这些混合测试,并且它们都会通过。

$ activator "test-only ApplicationSpec"

您将看到以下结果:

ApplicationSpec.scala

好的,我们几乎完成了。我们只需为名为 ng-microservice 的微服务添加一些测试,我们在 第四章 开发响应式后端服务 中创建了它。

NGServiceImplTestSpec.scala

您的 NGServiceImplTestSpec.scala 文件应包含以下代码:

    class NGServiceImplTestSpec extends PlaySpec { 
      "The NGServiceImpl" must { 
        "Generate a Ramdon number" in { 
          val service:NGContract = new NGServiceImpl 
          val double = service.generateDouble 
          assert( double >= 1 ) 
        } 
        "Generate a list of 3 Ramdon numbers" in { 
          val service:NGContract = new NGServiceImpl 
          val doubles = service.generateDoubleBatch(3) 
          doubles.size mustBe 3 
          assert( doubles(0) >= 1 ) 
          assert( doubles(1) >= 1 ) 
          assert( doubles(2) >= 1 ) 
        } 
      } 
    } 

因此,在前面代码中,我们有两种方法来测试我们微服务中的两个操作。首先我们生成一个双精度浮点数,然后请求三个双精度浮点数的列表。如您所见,我们只是检查是否从服务中得到了一个正的双精度浮点数,就这样。由于结果不可预测,这是一种很好的测试方式。有时,即使结果可预测,您也想进行这样的测试。为什么?因为这使测试更可靠,而且通常,当我们使用太多的硬编码值时,这些值可能会改变并破坏我们的测试,这可不是什么好事。让我们在控制台运行它。

$  activator "test-only NGServiceImplTestSpec"

这里是我们得到的结果:

NGServiceImplTestSpec.scala

现在,让我们转到控制器,做一些控制器测试。

NGServiceEndpointControllerTest.scala

您的 NGServiceEndpointControllerTest.scala 文件应包含以下代码:

    class NGServiceEndpointControllerTest extends PlaySpec with OneServerPerSuite with 
    OneBrowserPerSuite with HtmlUnitFactory { 
      val injector = new GuiceApplicationBuilder() 
      .injector 
      val ws:WSClient = injector.instanceOf(classOf[WSClient]) 
      import play.api.libs.concurrent.Execution. 
      Implicits.defaultContext 
      "NGServiceEndpointController" must { 
        "return a single double" in { 
          val future = ws.url(s"http://localhost:${port}/double") 
          .get().map { res => res.body } 
          val response = Await.result(future, 15.seconds) 
          response must not be empty 
          assert( new java.lang.Double(response) >= 1 ) 
        } 
        "return a list of 3 doubles" in { 
          val future = ws.url(s"http://localhost:${port}/doubles/3") 
          .get().map { res => res.body } 
          val response = Await.result(future, 15.seconds) 
          response must (not be empty and include ("[") and  
          include ("]")) 
        } 
      } 
    } 

在这里,我们必须注入 WSClient 库,这样我们就可以调用控制器。这个控制器有两个方法,就像我们之前测试的服务一样。第二个方法返回一个 JSON 结构。然后我们检查 "["和 "]",以确保数组存在,因为这是一个包含三个数字的列表。

我们使用 assert 函数来检查来自控制器的响应,并且要确保一切正常。让我们运行它。

$ activator "test-only NGServiceEndpointControllerTest"

参考以下截图查看测试结果:

NGServiceEndpointControllerTest.scala

太好了!我们几乎涵盖了所有内容。

在本章中,我们运行了各种各样的测试。我们总是使用命令 $ activator "test-only xxx";这样做的原因是为了节省时间。然而,运行所有测试是非常常见的。你可以在两个项目中都这样做;我们只需要输入 $ activator test

当运行 ng-microservice 项目的所有测试时,我们得到以下截图所示的成果:

NGServiceEndpointControllerTest.scala

另一方面,运行 ReactiveWebStore 项目的所有测试会得到下一张截图所示的成果:

NGServiceEndpointControllerTest.scala

摘要

在本章中,你学习了如何进行测试。我们为你的 Scala 和 Play 框架项目添加了几个测试。你还了解了单元测试原则、使用 JUnit 测试 Scala 应用程序、BDD 原则、使用 ScalaTest 进行测试、使用 ScalaTest 测试 Play 框架应用程序,以及在 Activator / SBT 中运行测试。

在下一章中,你将学习如何使用响应式的 Slick 来了解更多关于持久性的知识。我们还将稍微修改一下测试,以便与数据库一起工作。

第六章. 使用 Slick 的持续改进

在前面的章节中,你学习了如何使用 Activator 启动你的应用程序,我们使用 Scala 和 Play 框架开发了我们的 Web 应用程序,并使用 RxScala 添加了响应式微服务调用以进行数据流计算。我们使用 BDD 和 Play 框架进行了单元测试和控制器测试。

在本章中,你将学习如何实现关系型数据库持久化。到目前为止,我们的应用程序已经运行起来。然而,我们正在使用 HashMap 进行内存持久化。现在我们将升级我们的应用程序以使用适当的持久化。为了实现这一点,我们将使用一个名为 Slick 的响应式数据库持久化框架。

在本章中,我们将涵盖以下主题:

  • 使用 Slick 进行数据库持久化的原则

  • 在我们的应用程序中处理功能关系映射

  • 使用 SQL 支持创建查询

  • 使用异步数据库操作改进代码

我们将在我们的应用程序中进行一些重构。逐步地,我们将创建所有必要的表和持久化类,以便 Slick 能够与我们的 Play 框架应用程序一起工作。测试也将进行重构,以便测试应用程序逻辑,并跳过数据库持久化部分。

介绍 Slick 框架

Scala 语言集成连接套件Slick)是一个 Scala 现代框架,它允许使用与 Scala 集合非常相似的抽象来处理数据。你可以使用 SQL 和 Scala 代码编写数据库查询。用 Scala 代码代替 SQL 更好,因为我们利用了编译器,因此这种方法更不容易出错。此外,代码的维护也变得更简单,因为编译器会帮助你指出代码出错的地方,如果发生这种情况。

Slick 是一个功能关系映射FRM)库。那些来自 Java 背景并且熟悉 Hibernate 等对象关系映射ORM)框架的人会发现 Slick 有类似的概念。基本上,你创建一个 Scala 类,该类将明确映射到一个关系型表。Slick FRM 理念受到了微软的 LINQ 框架的启发。

Slick 在设计上就是反应式的,并且使用异步非阻塞 I/O 模型工作。使用 Slick,你将拥有以下优势:

  • 弹性:一个常见问题是数据库和应用程序的负载过重会创建更多的线程,从而使情况变得更糟。Slick 可以解决这个问题,因为它在数据库中排队等待数据库操作。

  • 高效资源利用:Slick 可以根据活动作业和挂起的数据库会话的数量进行并行调整。Slick 还有清晰的 I/O 和 CPU 密集型代码之间的界限。

MySQL 设置

我们将使用 Slick 和 MySQL 5.6。然而,Slick 支持其他关系型数据库,如 Oracle、SQL Server、DB2 和 Postgres。首先,我们需要在我们的机器上安装 MySQL。打开控制台,执行以下步骤(对于 Ubuntu Linux、其他操作系统(Windows/Mac)和发行版,请参阅dev.mysql.com/downloads/mysql/):

$ sudo apt-get install mysql-server -y
$ mysql --version
$ service mysql status

在使用apt-get安装后,当你运行其他两个命令时,你应该看到如下输出:

MySQL 设置

MySQL 安装

安装完成后,MySQL 服务器启动并运行,我们可以继续创建数据库。为了实现这一点,我们需要打开 MySQL 控制台。出于开发目的,我没有为 root 用户设置密码。然而,对于生产环境,强烈建议您使用强密码。

执行以下命令:

$ mysql -u root -p

这将给出以下输出:

MySQL 设置

MySQL 控制台

一旦进入 MySQL 控制台,你就可以创建数据库。我们将使用以下命令创建一个名为RWS_DB的数据库:

$ CREATE DATABASE RWS_DB; 

你将看到以下结果:

MySQL 设置

你可以输入$ SHOW DATABASES;来获取 MySQL 中所有可用数据库的列表。一切准备就绪,我们的数据库已经启动并运行。

在我们的 Play 框架应用程序中配置 Slick

首先,我们需要向build.sbt文件中添加依赖项。我们需要移除或注释掉名为JDBC的库,并添加play-slick库和 MySQL Java 驱动。

这可以这样完成:

    name := """ReactiveWebStore""" 
    version := "1.0-SNAPSHOT" 
    lazy val root = (project in file(".")).enablePlugins(PlayScala) 
    scalaVersion := "2.11.7" 
    libraryDependencies ++= Seq(   //jdbc, 
      cache, 
      ws, 
      "org.scalatestplus.play" %% "scalatestplus-play" % "1.5.1" %  
      Test, 
      "com.netflix.rxjava" % "rxjava-scala" % "0.20.7", 
      "com.typesafe.play" %% "play-slick" % "2.0.0", 
      "com.typesafe.play" %% "play-slick-evolutions" % "2.0.0", 
      "mysql" % "mysql-connector-java" % "6.0.3" 
    ) 
    resolvers += "scalaz-bintray" at "http://dl.bintray.com/scalaz/releases" 
    resolvers += DefaultMavenRepository 

如前述代码所示,我们注释掉了JDBC库,并添加了三个新的依赖项:

    "com.typesafe.play" %% "play-slick" % "2.0.0", 
    "com.typesafe.play" %% "play-slick-evolutions" % "2.0.0", 
    "mysql" % "mysql-connector-java" % "6.0.3" 

你可以进入控制台并运行命令$ activator$ reload$ compile,以便让 SBT 下载所有新的依赖项。

配置数据库连接

Slick 需要配置以访问我们创建的 MySQL 数据库。在ReactiveWebStore/conf文件夹下,我们需要编辑application.conf文件,并添加以下数据库连接 URL 和设置:

    # Default database configuration 
    slick.dbs.default.driver="slick.driver.MySQLDriver$" 
    slick.dbs.default.db.driver="com.mysql.cj.jdbc.Driver" 
    slick.dbs.default.db.url="jdbc:mysql://127.0.0.1:3306/RWS_DB?
    useUnicode=true&useJDBCCompliantTimezoneShift=
    true&useLegacyDatetimeCode=false&serverTimezone=UTC" 
    slick.dbs.default.db.user="root" 
    slick.dbs.default.db.password="" 

FPM 映射

下一步是在ReactiveWebStore/app下的 Scala 代码和 MySQL 表之间创建 FPM 映射,我们将在dao下创建一个新的包。DAO 代表数据库访问对象DAO),这是一个众所周知的面向对象模式。因此,我们将在这里创建一些 DAO 类。首先,我们将定义一个基特质,它将为每个dao包定义行为和代码能力。

我们将从BaseDao.scala开始:

    package dao 
    import slick.lifted.TableQuery 
    import scala.concurrent.Future 
    /** 
    * Defines base dao structure every dao should have. 
    */ 
    trait BaseDao[T] { 
      def toTable():TableQuery[_]   
      def findAll():Future[Seq[T]] 
      def remove(id:Long):Future[Int] 
      def insert(o:T):Future[Unit] 
      def update(o:T):Future[Unit] 
      def findById(id:Long):Future[Option[T]] 
    } 

我们将拥有三个dao包:ProductDaoImageDaoReviewDao。每个dao都将能够执行操作,但针对不同的 MySQL 表。根据我们刚刚定义的特质,我们将能够执行以下操作:

  • 查找所有:查找特定表中的所有数据

  • 删除:通过 ID 删除表中的项

  • 插入:向表中添加新数据

  • 更新:更新表中的数据

  • 通过 ID 查找:通过 ID 获取表中的特定记录

  • toTable: 返回该 dao 的表 FRM 映射。

ProductDao

我们将从查看 ProductDao.scala 开始:

    package dao 
    trait IProductDao extends BaseDao[Product]{ 
      def findAll(): Future[Seq[Product]] 
      def findById(id:Long): Future[Option[Product]] 
      def remove(id:Long): Future[Int] 
      def insert(p:Product): Future[Unit] 
      def update(p2:Product): Future[Unit] 
    } 
    class ProductDao @Inject() (protected val dbConfigProvider: DatabaseConfigProvider)  
    extends  
    HasDatabaseConfigProvider[JdbcProfile] with IProductDao { 
      import driver.api._   
      class ProductTable(tag: Tag) extends TableProduct { 
        def id = column[Option[Long]]("ID", O.PrimaryKey) 
        def name = columnString 
        def details = columnString 
        def price = columnBigDecimal 
        def * = (id, name, details, price) <> (Product.tupled,  
        Product.unapply _) 
      } 
      override def toTable = TableQuery[ProductTable] 
      private val Products = toTable() 
      override def findAll(): Future[Seq[Product]] =  
      db.run(Products.result)  
      override def findById(id:Long): Future[Option[Product]] =  
      db.run(Products.filter( _.id === id).result.headOption) 
      override def remove(id:Long): Future[Int] =  
      db.run(Products.filter( _.id === id).delete)  
      override def insert(p:Product): Future[Unit] = db.run(Products    
      += p).map { _ => () } 
      override def update(p2:Product) = Future[Unit] { 
        db.run( 
          Products.filter(_.id === p2.id) 
          .map(p => (p.name,p.details, p.price)) 
          .update((p2.name,p2.details,p2.price)) 
        ) 
      } 
    } 

这是 Productdao 实现。这里有很多新的概念,所以让我们一步一步地看看每个步骤。正如你所看到的,有一个名为 IProductDao 的特质,它通过泛型从 BaseDao 扩展,以指定模型 Product

这个特质对于使用 Guice 进行依赖注入非常重要。对于每个模型,我们将有两个 dao 实现:一个使用 Slick 和 MySQL,另一个使用我们之前的 inMemory 数据库进行测试。

那里有一个名为 ProductDao 的类,它是使用 Slick 的 dao 实现。我们需要 Guice 注入一个类,称为 DatabaseConfigProvider,它将用于执行数据库操作。ProductDao 还需要扩展 HasDatabaseConfigProvider[JdbcProfile] 以与数据库一起工作。

我们还需要通过以下命令导入 driver api:

Import driver.api._

下一步是使用名为 ProductTable 的类创建 FRM 映射,它扩展了表,传递了模型,在我们的例子中是一个产品。你还需要宣布 MySQL 表的名称。为了获取表名,我们使用一个伴随对象,我们需要在我们的模型周围创建它。我们这样做是为了避免在所有地方重复 MySQL 表名。

ProductTable 表中,你可以看到一些函数,如 idnamepricedetails。这些都是 model.Product 字段的准确名称。然而,我们必须在右侧添加到 MySQL 表的映射。我们使用一个名为 column 的函数来完成,我们传递类型和确切的 MySQL 字段名称。

最后,我们需要运行一个特殊的投影函数 * 来传递模型上的所有字段,这些字段被映射到关系数据库中。

现在,我们可以转向 dao 操作。正如你所看到的,所有函数都使用 db.run 来执行数据访问。这很好,因为,正如你所意识到的,它们返回一个 Future,所以 dao 不会阻塞,你可以做其他事情,例如,更多的数据库操作,预优化和验证。

一旦我们有了 ProductTable 表,我们可以使用它创建一个 Slick TableQuery 来执行数据库操作,就像它们是 Scala 函数一样。为了列出所有可用的产品,我们只需使用这个命令:

db.run(Products.result)

就这么简单。这段代码将返回一个 Future [Seq[Products]]。我们也可以通过 ID 进行过滤:

db.run(Products.filter( _.id === id).result.headOption)

因此,first _.id 是数据库中的 id,而 id 是通过参数传入的。在获取结果后,你可以看到我们调用了另一个名为 headOption 的函数,确保我们得到的结果是一个选项。这是一个值得信赖的模式,因为数据可能不在表中,我们避免了得到 NoSuchElementException

移除产品也是相当简单的。我们只需使用以下方法:

db.run(Products.filter( _.id === id).delete)

上述代码返回 Future[Int],计算被删除的项目数量。如果记录 ID 在数据库中找不到,结果将是 0。我们期望它总是 1,因为我们将要按 ID 删除。然而,API 是通用的,如果你比如说按名称或其他字段删除,可能会有多个删除。这就是为什么它是一个 Int 而不是一个布尔值。

插入数据也很简单;我们只需给出以下命令:

db.run(Products += p).map { _ => () }

如您所见,这是一个非常简单的 map 函数,就像我们向列表中添加一个元素一样。此代码返回 unit,这意味着没有返回值。然而,我们仍然有一个 Future,所以这段代码不是阻塞的。

要执行更新操作,代码稍微多一点,但最终仍然很简单。

    db.run( 
      Products.filter(_.id === p2.id) 
      .map(p => (p.name,p.details, p.price)) 
      .update((p2.name,p2.details,p2.price)) 
    ) 

首先,我们需要应用一个过滤器来选择我们将要更新的记录。我们传递 ID,因为我们只想更新单个记录。然后我们需要应用一个 map 函数来选择我们想要更新的字段;最后,我们执行更新,将新值传递给 update 函数。

让我们看看产品模型的伴生对象。

下面是 Models.Product.scala 的代码:

    object ProductDef{ 
      def toTable:String = "Product" 
    } 

如您所见,这是一个简单的辅助伴生对象,用于存储 MySQL 表名。

ReviewDAO

我们已经完成了 ProductDao,现在我们需要转向评论模型并创建评论的 dao。我们将执行与产品类似的步骤。

ReviewDao.scala 的代码如下:

    package dao 
    trait IReviewDao extends BaseDao[Review]{ 
      def findAll(): Future[Seq[Review]] 
      def findById(id:Long): Future[Option[Review]] 
      def remove(id:Long): Future[Int] 
      def insert(p:Review): Future[Unit] 
      def update(p2:Review): Future[Unit] 
    } 
    class ReviewDao @Inject() (protected val dbConfigProvider: DatabaseConfigProvider)  
    extends HasDatabaseConfigProvider[JdbcProfile] with IReviewDao { 
      import driver.api._ 
      class ReviewTable(tag: Tag) extends TableReview { 
        def id = column[Option[Long]]("ID", O.PrimaryKey)   
        def productId = column[Option[Long]]("PRODUCT_ID") 
        def author = columnString 
        def comment = columnString 
        def * = (id, productId, author,comment) <> (Review.tupled,  
        Review.unapply _) 
      } 
      override def toTable = TableQuery[ReviewTable] 
      private val Reviews = toTable() 
      override def findAll(): Future[Seq[Review]] =  
      db.run(Reviews.result)  
      override def findById(id:Long): Future[Option[Review]] =  
      db.run(Reviews.filter( _.id === id).result.headOption) 
      override def remove(id:Long): Future[Int] =  
      db.run(Reviews.filter( _.id === id).delete) 
      override def insert(r:Review): Future[Unit] =  
      db.run(Reviews += r).map { _ => () } 
      override def update(r2:Review) = Future[Unit] { 
        db.run( 
          Reviews.filter(_.id === r2.id) 
          .map(i => (i.productId,i.author, i.comment)) 
          .update((r2.productId,r2.author,r2.comment)) 
        ) 
      } 
    } 

在前面的代码中,我们有我们在 ProductDao 中看到的元素。有一个名为 dao 的接口,称为 IReviewDao,它使用评论模型扩展了 BaseDao。我们有 ReviewDao 实现,使用 ReviewTable FRM 映射。我们还有一个评论模型的伴生对象。

Review.scala 的代码如下:

    object ReviewDef{ 
      def toTable:String = "Review" 
    } 

ImageDao

现在我们需要转向我们的最后一个 daoImageDao。像 ProductDaoReviewDao 一样,我们将通过实现过程来探讨相同的思想和概念。

我们现在将查看 ImageDao.scala

    package dao 
    trait IImageDao extends BaseDao[Image]{ 
      def findAll(): Future[Seq[Image]] 
      def findById(id:Long): Future[Option[Image]] 
      def remove(id:Long): Future[Int] 
      def insert(p:Image): Future[Unit] 
      def update(p2:Image): Future[Unit] 
    } 
    class ImageDao @Inject() (protected val dbConfigProvider: DatabaseConfigProvider)  
    extends    
    HasDatabaseConfigProvider[JdbcProfile]  
    with IImageDao { 
      import driver.api._ 
      class ImageTable(tag: Tag) extends TableImage {  
        def id = column[Option[Long]]("ID", O.PrimaryKey) 
        def productId = column[Option[Long]]("PRODUCT_ID")  
        def url = columnString 
        def * = (id, productId, url) <> (Image.tupled, Image.unapply  
        _) 
      } 
      override def toTable = TableQuery[ImageTable] 
      private val Images = toTable() 
      override def findAll(): Future[Seq[Image]] =  
      db.run(Images.result) 
      override def findById(id:Long): Future[Option[Image]] =  
      db.run(Images.filter( _.id === id).result.headOption) 
      override def remove(id:Long): Future[Int] =  
      db.run(Images.filter( _.id === id).delete) 
      override def insert(i:Image): Future[Unit] =  
      db.run(Images += i).map { _ => () } 
      override def update(i2:Image) = Future[Unit]  
      {db.run( 
        Images.filter(_.id === i2.id)   
        .map(i => (i.productId,i.url))   
        .update((i2.productId,i2.url)) 
      )} 
    } 

我们还需要为图片有一个伴生对象辅助器。

Image.scala 的代码如下:

    object ImageDef{ 
      def toTable:String = "Image" 
    } 

Slick evolutions

Slick 不会为我们创建表,除非我们创建一个演变。Slick 跟踪数据库状态,并为我们创建和执行 SQL 命令。演变需要位于 ReactiveWebStore/conf/evolutions/default,其中 default 是我们在 application.conf 中配置的数据库名称。演变需要按顺序命名,以便我们可以保持顺序,Slick 可以跟踪更改。现在,我们将为 ProductDao 创建一个演变,因为我们需要一个产品表。

以下是与 1.sql 命名的代码:

# --- !Ups
CREATE TABLE Product (ID INT NOT NULL AUTO_INCREMENT,NAME VARCHAR(100) NOT NULL,DETAILS VARCHAR(250),PRICE DOUBLE NOT NULL,PRIMARY KEY ( ID ));
# --- !Downs
# drop table "Product";

我们还需要对评论和图片进行演变。因此,我们需要为图片创建 2.sql,为评论创建 3.sql

以下是与 2.sql 命名的代码:

# --- !Ups
CREATE TABLE Image (ID INT NOT NULL AUTO_INCREMENT,PRODUCT_ID INT NOT NULL,URL VARCHAR(250),PRIMARY KEY ( ID ));
# --- !Downs
# drop table "Product";

以下是与 3.sql 命名的代码:

# --- !Ups
CREATE TABLE Review (ID INT NOT NULL AUTO_INCREMENT,PRODUCT_ID INT NOT NULL,AUTHOR VARCHAR(250),COMMENT VARCHAR(250),PRIMARY KEY ( ID ));
# --- !Downs
# drop table "Review";

重构服务

我们需要将我们的dao包的默认基础特质更改为现在返回 Futures。

让我们从BaseServices.scala开始:

    package services 
    import scala.concurrent.Future 
    trait BaseService[A] { 
      def insert(a:A):Future[Unit] 
      def update(id:Long,a:A):Future[Unit] 
      def remove(id:Long):Future[Int] 
      def findById(id:Long):Future[Option[A]] 
      def findAll():Future[Option[Seq[A]]] 
    } 

这个最后的实现反映了dao包中发生的事情。现在我们可以转到服务实现,并继续我们的重构。

接下来我们看到ProductService.scala

    package services 
    trait IProductService extends BaseService[Product]{ 
      def insert(product:Product):Future[Unit] 
      def update(id:Long,product:Product):Future[Unit] 
      def remove(id:Long):Future[Int] 
      def findById(id:Long):Future[Option[Product]] 
      def findAll():Future[Option[Seq[Product]]] 
      def findAllProducts():Seq[(String,String)] 
    } 
    @Singleton 
    class ProductService  
    @Inject() (dao:IProductDao)  
    extends IProductService{ 
      import play.api.libs.concurrent.Execution.Implicits. 
      defaultContext 
      def insert(product:Product):Future[Unit] = { 
        dao.insert(product); 
      } 
      def update(id:Long,product:Product):Future[Unit] = { 
        product.id = Option(id.toInt) 
        dao.update(product) 
      } 
      def remove(id:Long):Future[Int] = { 
        dao.remove(id) 
      } 
      def findById(id:Long):Future[Option[Product]] = { 
        dao.findById(id) 
      } 
      def findAll():Future[Option[Seq[Product]]] = { 
        dao.findAll().map { x => Option(x) } 
      } 
      private def validateId(id:Long):Unit = { 
        val future = findById(id) 
        val entry = Awaits.get(5, future)   
        if (entry==null || entry.equals(None)) throw new  
        RuntimeException("Could not find Product: " + id) 
      } 
      def findAllProducts():Seq[(String,String)] = { 
        val future = this.findAll() 
        val result = Awaits.get(5, future)   
        val products:Seq[(String,String)] = result 
        .getOrElse(Seq(Product(Some(0),"","",0))) 
        .toSeq 
        .map { product => (product.id.get.toString,product.name) } 
        return products 
      } 
    } 

这里有一些更改。首先,我们注入一个IProductDao,让 Guice 确定我们需要的正确注入,以便能够使用我们稍后在本章中将要介绍的旧的in-memory HashMap 实现进行测试。

变更涉及新的函数签名,使用Awaits,以及使用Seq而不是List

现在让我们转到ReviewService.scala

    package services 
    trait IReviewService extends BaseService[Review]{ 
      def insert(review:Review):Future[Unit] 
      def update(id:Long,review:Review):Future[Unit] 
      def remove(id:Long):Future[Int] 
      def findById(id:Long):Future[Option[Review]] 
      def findAll():Future[Option[Seq[Review]]] 
    } 
    @Singleton 
    class ReviewService @Inject() (dao:IReviewDao)  
    extends IReviewService{ 
      import play.api.libs.concurrent.Execution. 
      Implicits.defaultContext 
      def insert(review:Review):Future[Unit] = { 
      dao.insert(review);} 
      def update(id:Long,review:Review):Future[Unit] = { 
        review.id = Option(id.toInt) 
        dao.update(review)   
      } 
      def remove(id:Long):Future[Int] = { 
        dao.remove(id) 
      } 
      def findById(id:Long):Future[Option[Review]] = { 
        dao.findById(id) 
      } 
      def findAll():Future[Option[Seq[Review]]] = { 
        dao.findAll().map { x => Option(x) } 
      } 
      private def validateId(id:Long):Unit = { 
        val future = findById(id) 
        val entry = Awaits.get(5, future) 
        if (entry==null || entry.equals(None)) throw new  
        RuntimeException("Could not find Review: " + id) 
      }  
    }     

在前述代码中,我们有与产品相同的更改。让我们转到ImageService.scala,这是我们最后一个服务。

    package services 
    trait IImageService extends BaseService[Image]{ 
      def insert(image:Image):Future[Unit] 
      def update(id:Long,image:Image):Future[Unit] 
      def remove(id:Long):Future[Int] 
      def findById(id:Long):Future[Option[Image]] 
      def findAll():Future[Option[Seq[Image]]] 
    } 
    @Singleton 
    class ImageService @Inject() (dao:IImageDao)  
    extends IImageService { 
      import play.api.libs.concurrent.Execution. 
      Implicits.defaultContext 
      def insert(image:Image):Future[Unit] = { 
        dao.insert(image) 
      } 
      def update(id:Long,image:Image):Future[Unit] = { 
        image.id = Option(id.toInt) 
        dao.update(image)  
      } 
      def remove(id:Long):Future[Int] = { 
        dao.remove(id) 
      } 
      def findById(id:Long):Future[Option[Image]] = { 
        dao.findById(id) 
      } 
      def findAll():Future[Option[Seq[Image]]] = { 
        dao.findAll().map { x => Option(x) } 
      } 
      private def validateId(id:Long):Unit = { 
        val future = findById(id) 
        val entry = Awaits.get(5, future) 
        if (entry==null ||entry.equals(None) ) throw new  
        RuntimeException("Could not find Image: " + id)  
      } 
    } 

我们已经重构了所有服务以使用新的dao包实现。下一步是将控制器迁移过来。

重构控制器

现在我们已经实现了所有dao包与相应的数据库演变。然而,我们的服务期望一个不同的契约,因为我们之前使用的是内存数据库。让我们重构产品控制器:

    package controllers 
    @Singleton 
    class ProductController @Inject() (val messagesApi:MessagesApi,val service:IProductService)  
    extends Controller with I18nSupport { 
      val productForm: Form[Product] = Form( 
        mapping( 
          "id" -> optional(longNumber), 
          "name" -> nonEmptyText, 
          "details" -> text, 
          "price" -> bigDecimal 
        )(models.Product.apply)(models.Product.unapply) 
      ) 
      def index = Action { implicit request => 
        val products = Awaits.get(5,service.findAll()) 
        .getOrElse(Seq())   
        Logger.info("index called. Products: " + products) 
        Ok(views.html.product_index(products)) 
      } 
      def blank = Action { implicit request => 
        Logger.info("blank called. ") 
        Ok(views.html.product_details(None, productForm)) 
      } 
      def details(id: Long) = Action { implicit request => 
        Logger.info("details called. id: " + id) 
        val product = Awaits.get(5,service.findById(id)).get 
        Ok(views.html.product_details(Some(id),  
        productForm.fill(product))) 
      } 
      def insert()= Action { implicit request => 
        Logger.info("insert called.") 
        productForm.bindFromRequest.fold( 
          form => {  
            BadRequest(views.html.product_details(None, form)) 
          }, 
          product => {  
            service.insert(product) 
            Redirect(routes.ProductController.index). 
            flashing("success" -> Messages("success.insert",  
            "new product created")) 
          } 
        ) 
      } 
      def update(id: Long) = Action { implicit request => 
        Logger.info("updated called. id: " + id) 
        productForm.bindFromRequest.fold( 
          form => { 
            Ok(views.html.product_details(Some(id),  
            form)).flashing("error" -> "Fix the errors!") 
          }, 
          product => { 
            service.update(id,product)  
            Redirect(routes.ProductController.index).flashing  
            ("success" -> Messages("success.update", product.name)) 
          } 
        ) 
      } 
      def remove(id: Long)= Action { 
        import play.api.libs.concurrent.Execution. 
        Implicits.defaultContext 
        val result = Awaits.get(5,service.findById(id)) 
        result.map { product => 
          service.remove(id) 
          Redirect(routes.ProductController.index).flashing("success"  
          -> Messages("success.delete", product.name)) 
        }.getOrElse(NotFound) 
      } 
    } 

尽管有新的函数签名,前述代码中有两个大的变化。首先,我们从一个名为Awaits的类中使用一个名为get的实用函数。这是必要的,以便我们等待结果从数据库返回。其次,当我们闪现结果时,我们不再显示id,我们只显示一条文本消息。让我们看看Utils.Awaits.scala中的Awaits实现,如下所示:

    package utils 
    object Awaits { 
      def getT:T = { 
        Await.resultT 
      } 
    } 

Awaits只是一个简单的实用类,它等待一段时间以获取 Future 结果。我们还需要在ReviewControllerImageController中添加一些调整。

我们将首先探索ReviewController.scala

    package controllers 
    @Singleton 
    class ReviewController @Inject() 
    (val messagesApi:MessagesApi,val productService:IProductService, 
    val service:IReviewService) 
    extends Controller with I18nSupport { 
      val reviewForm:Form[Review] = Form( 
        mapping( 
          "id" -> optional(longNumber), 
          "productId" -> optional(longNumber), 
          "author" -> nonEmptyText, 
          "comment" -> nonEmptyText 
        )(models.Review.apply)(models.Review.unapply)  
      ) 
      def index = Action { implicit request => 
        val reviews = Awaits.get(5,service.findAll()).getOrElse(Seq()) 
        Logger.info("index called. Reviews: " + reviews) 
        Ok(views.html.review_index(reviews)) 
      } 
      def blank = Action { implicit request => 
        Logger.info("blank called. ") 
        Ok(views.html.review_details(None,  
        reviewForm,productService.findAllProducts)) 
      } 
      def details(id: Long) = Action { implicit request => 
        Logger.info("details called. id: " + id) 
        val review = Awaits.get(5,service.findById(id)).get 
        Ok(views.html.review_details(Some(id),  
        reviewForm.fill(review),productService.findAllProducts)) 
      } 
      def insert()= Action { implicit request => 
        Logger.info("insert called.") 
        reviewForm.bindFromRequest.fold( 
          form => { 
            BadRequest(views.html.review_details(None,  
            form,productService.findAllProducts)) 
          }, 
          review => { 
            if (review.productId==null || review.productId.isEmpty) { 
              Redirect(routes.ReviewController.blank).flashing("error"  
              -> "Product ID Cannot be Null!") 
            }else { 
              Logger.info("Review: " + review) 
              if (review.productId==null ||  
              review.productId.getOrElse(0)==0) throw new  
              IllegalArgumentException("Product Id Cannot Be Null") 
              service.insert(review)   
              Redirect(routes.ReviewController.index). 
              flashing("success" -> Messages("success.insert",  
              "new Review")) 
            } 
          } 
        ) 
      } 
      def update(id: Long) = Action { implicit request => 
        Logger.info("updated called. id: " + id)  
        reviewForm.bindFromRequest.fold( 
          form => { 
            Ok(views.html.review_details(Some(id),  
            form,productService.findAllProducts)). 
            flashing("error" -> "Fix the errors!") 
          }, 
          review => { 
            service.update(id,review)  
            Redirect(routes.ReviewController.index). 
            flashing("success" -> Messages("success.update",  
            review.productId)) 
          } 
        ) 
      } 
      def remove(id: Long)= Action {  
        import play.api.libs.concurrent.Execution. 
        Implicits.defaultContext 
        val result = Awaits.get(5,service.findById(id)) 
        result.map { review => 
          service.remove(id) 
          Redirect(routes.ReviewController.index).flashing("success" - 
          > Messages("success.delete", review.productId)) 
        }.getOrElse(NotFound) 
      } 
    } 

对于ReviewController,我们进行了与产品相同的更改,即使用Awaits和闪现返回的标签。

让我们转到最后的控制器:ImageController.scala

    package controllers 
    @Singleton 
    class ImageController @Inject() 
    (val messagesApi:MessagesApi, 
    val productService:IProductService, 
    val service:IImageService) 
    extends Controller with I18nSupport { 
      val imageForm:Form[Image] = Form( 
        mapping( 
          "id" -> optional(longNumber), 
          "productId" -> optional(longNumber), 
          "url" -> text 
        )(models.Image.apply)(models.Image.unapply) 
      ) 
      def index = Action { implicit request => 
        val images = Awaits.get(5,service.findAll()).getOrElse(Seq()) 
        Logger.info("index called. Images: " + images) 
        Ok(views.html.image_index(images)) 
      } 
      def blank = Action { implicit request => 
        Logger.info("blank called. ") 
        Ok(views.html.image_details(None,  
        imageForm,productService.findAllProducts)) 
      } 
      def details(id: Long) = Action { implicit request => 
        Logger.info("details called. id: " + id) 
        val image = Awaits.get(5,service.findById(id)).get 
        Ok(views.html.image_details(Some(id),  
        imageForm.fill(image),productService.findAllProducts)) 
      } 
      def insert()= Action { implicit request => 
        Logger.info("insert called.") 
        imageForm.bindFromRequest.fold( 
          form => { 
            BadRequest(views.html.image_details(None, form,  
            productService.findAllProducts)) 
          }, 
          image => { 
            if (image.productId==null ||  
            image.productId.getOrElse(0)==0) { 
              Redirect(routes.ImageController.blank).flashing 
              ("error" -> "Product ID Cannot be Null!") 
            }else { 
              if (image.url==null || "".equals(image.url))  
              image.url = "/assets/images/default_product.png" 
              service.insert(image) 
              Redirect(routes.ImageController.index). 
              flashing("success" -> Messages("success.insert",  
              "new image")) 
            } 
          } 
        ) 
      } 
      def update(id: Long) = Action { implicit request => 
        Logger.info("updated called. id: " + id) 
        imageForm.bindFromRequest.fold( 
          form => { 
            Ok(views.html.image_details(Some(id), form,  
            null)).flashing("error" -> "Fix the errors!") 
          }, 
          image => { 
            service.update(id,image) 
            Redirect(routes.ImageController.index).flashing 
            ("success" -> Messages("success.update", image.id)) 
          } 
        ) 
      } 
      def remove(id: Long)= Action { 
        import play.api.libs.concurrent.Execution. 
        Implicits.defaultContext 
        val result = Awaits.get(5,service.findById(id)) 
        result.map { image => 
          service.remove(id) 
          Redirect(routes.ImageController.index).flashing("success"  
          -> Messages("success.delete", image.id)) 
        }.getOrElse(NotFound) 
      } 
    } 

在 Guice 中配置 DAO 包

我们需要配置我们创建的三个新dao包的注入。因此,我们需要在Module.scala文件中添加三行。请打开您的 IDE 中的文件,并添加以下内容:

    bind(classOf[IProductDao]).to(classOf[ProductDao]).asEagerSingleton() 
    bind(classOf[IImageDao]).to(classOf[ImageDao]).asEagerSingleton() 
    bind(classOf[IReviewDao]).to(classOf[ReviewDao]).asEagerSingleton 

整个文件,Module.scala,应该看起来像这样:

    /** 
    * This class is a Guice module that tells Guice how to bind several 
    * different types. This Guice module is created when the Play 
    * application starts. 
    * Play will automatically use any class called `Module` that is in 
    * the root package. You can create modules in other locations by 
    * adding `play.modules.enabled` settings to the `application.conf` 
    * configuration file. 
    */ 
    class Module extends AbstractModule { 
      override def configure() = { 
        // Use the system clock as the default implementation of Clock 
        bind(classOf[Clock]).toInstance(Clock.systemDefaultZone) 
        // Ask Guice to create an instance of ApplicationTimer when  
        //the application starts. 
        bind(classOf[ApplicationTimer]).asEagerSingleton() 
        bind(classOf[IProductService]). 
        to(classOf[ProductService]).asEagerSingleton() 
        bind(classOf[IReviewService]). 
        to(classOf[ReviewService]).asEagerSingleton() 
        bind(classOf[IImageService]). 
        to(classOf[ImageService]).asEagerSingleton() 
        bind(classOf[IPriceSerice]). 
        to(classOf[PriceService]).asEagerSingleton() 
        bind(classOf[IRndService]). 
        to(classOf[RndService]).asEagerSingleton() 
        bind(classOf[IProductDao]). 
        to(classOf[ProductDao]).asEagerSingleton() 
        bind(classOf[IImageDao]). 
        to(classOf[ImageDao]).asEagerSingleton() 
        bind(classOf[IReviewDao]). 
        to(classOf[ReviewDao]).asEagerSingleton() 
      } 
    } 

重构测试

如你所料,大多数测试现在不再工作。我们在这里也需要进行一些重构。我们将重构我们之前的dao使其通用,它将被用于集成测试(端到端测试)。

由于我们将为了端到端测试的目的在内存中创建一个通用的 dao 系统,我们需要稍微改变我们的模型。首先,我们需要为所有模型创建一个基础特质。这是必要的,这样我们就可以将我们的模型视为相等。

让我们看看models.BaseModel.scala

    package models 
    trait BaseModel { 
      def getId:Option[Long] 
      def setId(id:Option[Long]):Unit 
    } 

我们还需要让所有模型实现这个新特质。因此,我们需要更改产品、图片和评论的 Scala 代码。这非常简单:我们只需为 id 字段添加一个获取器和设置器。您也可以使用 scala.bean.BeanProperty 而不是自己编写。

您的 models.Product.scala 文件应该看起来像这样:

    package models 
    case class Product 
    ( var id:Option[Long], 
      var name:String, 
      var details:String, 
    var price:BigDecimal ) 
    extends BaseModel{ 
      override def toString:String = { 
        "Product { id: " + id.getOrElse(0) + ",name: " + name + ",  
        details: "+ details + ", price: " + price + "}" 
      } 
      override def getId:Option[Long] = id 
      override def setId(id:Option[Long]):Unit = this.id = id 
    } 
    object ProductDef{ 
      def toTable:String = "Product" 
    } 

如前述代码所示,我们扩展了 BaseModel 方法,并实现了 getIdsetId。我们需要为评论和图片模型做同样的操作。

您的 models.Review.scala 文件应该看起来像这样:

    package models 
    case class Review 
    (var id:Option[Long], 
      var productId:Option[Long], 
      var author:String, 
    var comment:String) 
    extends BaseModel{ 
      override def toString:String = { 
        "Review { id: " + id + " ,productId: " +  
          productId.getOrElse(0) + ",author: " + author + ",comment: "  
        + comment + " }" 
      } 
      override def getId:Option[Long] = id  
      override def setId(id:Option[Long]):Unit = this.id = id 
    } 
    object ReviewDef{ 
      def toTable:String = "Review" 
    } 

现在我们转到最后一个模型。我们需要在 Image.scala 中实现它。

您的 models.Image.scala 文件应该看起来像这样:

package models 
case class Image 
(var id:Option[Long],  
  var productId:Option[Long], 
var url:String) 
extends BaseModel { 
  override def toString:String = { 
    "Image { productId: " + productId.getOrElse(0) + ",url: " +  
    url + "}" 
  } 
  override def getId:Option[Long] = id 
  override def setId(id:Option[Long]):Unit = this.id = id 
} 
object ImageDef{ 
  def toTable:String = "Image" 
} 

通用模拟

现在我们有了创建通用模拟实现并模拟所有 dao 包所需的一切。在 ReactiveWebStore/test/ 位置下,我们将创建一个名为 mocks 的包,并创建一个名为 GenericMockedDao 的调用。

您的 GenericMockedDao.scala 文件应该看起来像这样:

package mocks 
import models.BaseModel 
class GenericMockedDao[T <: BaseModel] { 
  import java.util.concurrent.atomic.AtomicLong 
  import scala.collection.mutable.HashMap 
  import scala.concurrent._ 
  import play.api.libs.concurrent.Execution. 
  Implicits.defaultContext 
  var inMemoryDB = new HashMap[Long,T] 
  var idCounter = new AtomicLong(0) 
  def findAll(): Future[Seq[T]] = { 
    Future { 
      if (inMemoryDB.isEmpty) Seq() 
      inMemoryDB.values.toSeq 
    } 
  } 
  def findById(id:Long): Future[Option[T]] = { 
    Future { 
      inMemoryDB.get(id) 
    } 
  } 
  def remove(id:Long): Future[Int] = { 
    Future { 
      validateId(id) 
      inMemoryDB.remove(id) 
      1 
    } 
  } 
  def insert(t:T): Future[Unit] = { 
    Future { 
      val id = idCounter.incrementAndGet(); 
      t.setId(Some(id)) 
      inMemoryDB.put(id, t) 
      Unit 
    } 
  } 
  def update(t:T): Future[Unit] = { 
    Future { 
      validateId(t.getId.get) 
      inMemoryDB.put(t.getId.get, t) 
      Unit 
    } 
  } 
  private def validateId(id:Long):Unit = { 
    val entry = inMemoryDB.get(id) 
    if (entry==null || entry.equals(None)) throw new  
    RuntimeException("Could not find Product: " + id) 
  } 
} 

因此,GenericMockedDao 调用期望 Generic 参数,这可以是任何继承自 BaseModel 的类。然后我们使用内存中的 HashMap 实现和一个计数器来模拟数据库操作。我们在 Futures 中运行所有操作,这样我们就不破坏代码期望的新签名。现在我们可以为每个需要的模型创建三个 MockedDaos:产品、评论和图片。

您的 mocks.ProductMockedDao.scala 文件应该看起来像这样:

package mocks 
class ProductMockedDao extends IProductDao { 
  val dao:GenericMockedDao[Product] = new  
  GenericMockedDao[Product]() 
  override def findAll(): Future[Seq[Product]] = { 
    dao.findAll() 
  } 
  override def findById(id:Long): Future[Option[Product]] = { 
    dao.findById(id) 
  } 
  override def remove(id:Long): Future[Int] = { 
    dao.remove(id) 
  } 
  override def insert(p:Product): Future[Unit] = { 
    dao.insert(p) 
  } 
  override def update(p2:Product): Future[Unit] = { 
    dao.update(p2) 
  } 
  override def toTable:TableQuery[_] = { 
    null 
  } 
} 

如此可见,我们实现了 IProdutDao 特质,并将所有操作委托给 genericMockedDao。由于所有内容都在内存中,我们不需要实现 toTable 函数。评论和图片也需要做同样的操作。

您的 mocks.ReviewMockedDao.scala 文件应该看起来像这样:

package mocks 
class ReviewMockedDao extends IReviewDao { 
  val dao:GenericMockedDao[Review] = new  
  GenericMockedDao[Review]() 
  override def findAll(): Future[Seq[Review]] = { 
    dao.findAll() 
  } 
  override def findById(id:Long): Future[Option[Review]] = { 
    dao.findById(id) 
  } 
  override def remove(id:Long): Future[Int] = { 
    dao.remove(id) 
  } 
  override def insert(p:Review): Future[Unit] = { 
    dao.insert(p) 
  } 
  override def update(p2:Review): Future[Unit] = { 
    dao.update(p2) 
  } 
  override def toTable:TableQuery[_] = { 
    null 
  } 
} 

就像产品一样,我们将所有操作委托给 GenericMockedDao。现在让我们转到最后一个,图片,然后我们可以修复测试。

您的 mocks.ImageMockedDao.scala 文件应该看起来像这样:

package mocks 
class ImageMockedDao extends IImageDao { 
  val dao:GenericMockedDao[Image] = new GenericMockedDao[Image]() 
  override def findAll(): Future[Seq[Image]] = { 
    dao.findAll() 
  } 
  override def findById(id:Long): Future[Option[Image]] = { 
    dao.findById(id) 
  } 
  override def remove(id:Long): Future[Int] = { 
    dao.remove(id) 
  } 
  override def insert(p:Image): Future[Unit] = { 
    dao.insert(p) 
  } 
  override def update(p2:Image): Future[Unit] = { 
    dao.update(p2) 
  } 
  override def toTable:TableQuery[_] = { 
    null 
  } 
} 

好的,我们现在有了所有需要的模拟。我们可以继续修复测试规范。我们需要修复服务测试和控制器测试。服务测试将使用模拟。然而,控制器测试将使用真实的数据库实现。我们需要为控制器测试使用其他实用工具类。位于测试源文件夹中,我们需要创建一个名为 utils 的包。

您的 utils.DBCleaner.scala 文件应该看起来像这样:

    package utils 
    object DBCleaner { 
      val pool = Executors.newCachedThreadPool() 
      implicit val ec = ExecutionContext.fromExecutorService(pool) 
      def cleanUp():Unit = { 
        Class.forName("com.mysql.cj.jdbc.Driver") 
        val db =   Database.forURL 
        ("jdbc:mysql://127.0.0.1:3306/RWS_DB?useUnicode= 
        true&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=     
        false&serverTimezone=UTC", 
        "root", "") 
        try{ 
          Await.result( 
            db.run( 
              DBIO.seq( 
                sqlu""" DELETE FROM Product; """, 
                sqlu""" DELETE FROM Image; """, 
                sqlu""" DELETE FROM Review; """, 
                sqlu""" ALTER TABLE Product AUTO_INCREMENT = 1 """, 
                sqlu""" ALTER TABLE Image AUTO_INCREMENT = 1 """, 
                sqlu""" ALTER TABLE Review AUTO_INCREMENT = 1 """ 
              ) 
            ) 
          ,20 seconds) 
        }catch{ 
          case e:Exception => Unit 
        } 
      } 
    } 

DBCleaner 将连接到真实数据库并执行删除语句以清理所有表数据。在删除表中的所有数据后,我们还需要重置数据库中的序列;否则,我们的测试将不会有我们进行断言所需的可预测性。

db.run 中所示,我们可以使用 DBIO.seq,它允许我们在数据库上执行多个指令。这里我们不是使用 Scala 代码。我们使用纯 SQL 语句,因为我们需要使用非常特定的 MySQL 函数来重置序列。

如果需要,您可以在您的应用程序中使用所有这些功能。如果您需要使用特定的数据库功能,如果您有一个非常复杂的查询,或者有时,因为存在性能问题,这很有用。

我们现在所做的大多数修复都集中在使用 Awaits 等待 Future 结果,以及使用我们新的模拟。对于控制器测试,我们需要调用 DBCleaner 函数。

服务测试

现在,我们将为服务创建测试以测试它们。让我们开始吧。

您的 ProductServiceTestSpec.scala 文件应如下所示:

    class ProductServiceTestSpec extends PlaySpec { 
      "ProductService" must { 
        val service:IProductService = new ProductService(new  
        ProductMockedDao) 
        "insert a product properly" in { 
          val product = new models.Product(Some(1),"Ball","Awesome  
          Basketball",19.75) 
          service.insert(product) 
        } 
        "update a product" in { 
          val product = new models.Product(Some(1),"Blue  
          Ball","Awesome Blue Basketball",19.99) 
          service.update(1, product) 
        } 
        "not update because does not exit" in { 
          intercept[RuntimeException]{ 
            service.update(333,null) 
          } 
        } 
        "find the product 1" in { 
          val product = Awaits.get(5, service.findById(1)) 
          product.get.id mustBe Some(1) 
          product.get.name mustBe "Blue Ball" 
          product.get.details mustBe "Awesome Blue Basketball" 
          product.get.price mustBe 19.99 
        } 
        "find all" in { 
          val products = Awaits.get(5, service.findAll()) 
          products.get.length mustBe 1 
          products.get(0).id mustBe Some(1) 
          products.get(0).name mustBe "Blue Ball" 
          products.get(0).details mustBe "Awesome Blue Basketball" 
          products.get(0).price mustBe 19.99 
        } 
        "find all products" in { 
          val products = service.findAllProducts() 
          products.length mustBe 1 
          products(0)._1 mustBe "1" 
          products(0)._2 mustBe "Blue Ball" 
        } 
        "remove 1 product" in { 
          val product = Awaits.get(5, service.remove(1)) 
          product mustBe 1 
          val oldProduct = Awaits.get(5,service.findById(1)) 
          oldProduct mustBe None 
        } 
        "not remove because does not exist" in { 
          intercept[RuntimeException]{ 
            Awaits.get(5,service.remove(-1)) 
          } 
        } 
      } 
    } 

如前述代码所示,大多数修复都集中在新的签名以及我们正在使用 Futures 并需要使用 Awaits 工具和模拟的事实。我们通过以下代码测试服务而不进行数据库调用:

val service:IProductService = new ProductService(new ProductMockedDao) 

我们可以继续到下一个服务,这将是我们将要审查的服务。

您的 ReviewServiceTestSpec.scala 文件应如下所示:

    class ReviewServiceTestSpec extends PlaySpec { 
      "ReviewService" must { 
        val service:IReviewService = new ReviewService(new  
        ReviewMockedDao) 
        "insert a review properly" in { 
          val review = new models.Review 
          (Some(1),Some(1),"diegopacheco","Testing is Cool") 
          service.insert(review) 
        } 
        "update a reviewt" in { 
          val review = new models.Review 
          (Some(1),Some(1),"diegopacheco","Testing so so Cool") 
          service.update(1, review) 
        } 
        "not update because does not exist" in { 
          intercept[RuntimeException]{ 
            Awaits.get(5, service.update(333,null)) 
          } 
        } 
        "find the review 1" in { 
          val review = Awaits.get(5,service.findById(1)) 
          review.get.id mustBe Some(1) 
          review.get.author mustBe "diegopacheco" 
          review.get.comment mustBe "Testing so so Cool" 
          review.get.productId mustBe Some(1) 
        } 
        "find all" in { 
          val reviews = Awaits.get(5,service.findAll()) 
          reviews.get.length mustBe 1 
          reviews.get(0).id mustBe Some(1) 
          reviews.get(0).author mustBe "diegopacheco" 
          reviews.get(0).comment mustBe "Testing so so Cool" 
          reviews.get(0).productId mustBe Some(1) 
        } 
        "remove 1 review" in { 
          val review = Awaits.get(5, service.remove(1)) 
          review mustBe 1 
          val oldReview= Awaits.get(5, service.findById(1)) 
          oldReview mustBe None 
        } 
        "not remove because does not exist" in { 
          intercept[RuntimeException]{ 
            Awaits.get(5, service.remove(-1)) 
          } 
        } 
      } 
    } 

那就是审查规范服务测试代码。我们应用与产品相同的更改。现在我们需要继续到最后一个服务测试,即图像测试。

您的 ImageServiceTestSpec.scala 文件应如下所示:

    class ImageServiceTestSpec extends PlaySpec { 
      "ImageService" must { 
        val service:IImageService = new ImageService(new  
        ImageMockedDao) 
        "insert a image properly" in { 
          val image = new models.Image 
          (Some(1),Some(1),"http://www.google.com.br/myimage") 
          service.insert(image) 
        } 
        "update a image" in { 
          val image = new models.Image 
          (Some(2),Some(1),"http://www.google.com.br/myimage") 
          service.update(1, image) 
        } 
        "not update because does not exist" in { 
          intercept[RuntimeException]{ 
            Awaits.get(5, service.update(333,null)) 
          } 
        } 
        "find the image" in { 
          val image = Awaits.get(5,service.findById(1)) 
          image.get.id mustBe Some(1) 
          image.get.productId mustBe Some(1) 
          image.get.url mustBe "http://www.google.com.br/myimage" 
        } 
        "find all" in { 
          val reviews = Awaits.get(5,service.findAll()) 
          reviews.get.length mustBe 1 
          reviews.get(0).id mustBe Some(1) 
          reviews.get(0).productId mustBe Some(1) 
          reviews.get(0).url mustBe "http://www.google.com.br/myimage" 
        } 
        "remove 1 image" in { 
          val image = Awaits.get(5,service.remove(1)) 
          image mustBe 1 
          val oldImage = Awaits.get(5,service.findById(1)) 
          oldImage mustBe None 
        } 
        "not remove because does not exist" in { 
          intercept[RuntimeException]{ 
            Awaits.get(5,service.remove(-1)) 
          } 
        } 
      } 
    } 

我们已经修复了所有服务测试。现在我们需要修复控制器测试。

控制器测试

现在让我们修复控制器测试。第一个将是产品控制器。

您的 ProductControllerTestSpec.scala 文件应如下所示:

    class ProductControllerTestSpec 
    extends  
    PlaySpec  
    with OneServerPerSuite with OneBrowserPerSuite with HtmlUnitFactory { 
      "ProductController" should { 
        DBCleaner.cleanUp() 
        "insert a new product should be ok" in { 
          goTo(s"http://localhost:${port}/product/add") 
          click on id("name") 
          enter("Blue Ball") 
          click on id("details") 
          enter("Blue Ball is a Awesome and simple product") 
          click on id("price") 
          enter("17.55") 
          submit() 
        } 
        "details from the product 1 should be ok" in { 
          goTo(s"http://localhost:${port}/product/details/1") 
          textField("name").value mustBe "Blue Ball" 
          textField("details").value mustBe "Blue Ball is a Awesome  
          and simple product" 
          textField("price").value mustBe "17.55" 
        } 
        "update product 1 should be ok" in { 
          goTo(s"http://localhost:${port}/product/details/1") 
          textField("name").value = "Blue Ball 2" 
          textField("details").value = "Blue Ball is a Awesome and  
          simple product 2 " 
          textField("price").value = "17.66" 
          submit() 
          goTo(s"http://localhost:${port}/product/details/1") 
          textField("name").value mustBe "Blue Ball 2" 
          textField("details").value mustBe "Blue Ball is a Awesome  
          and simple product 2 " 
          textField("price").value mustBe "17.66" 
        } 
        "delete a product should be ok" in { 
          goTo(s"http://localhost:${port}/product/add") 
          click on id("name") 
          enter("Blue Ball") 
          click on id("details") 
          enter("Blue Ball is a Awesome and simple product") 
          click on id("price") 
          enter("17.55") 
          submit() 
          goTo(s"http://localhost:${port}/product") 
          click on id("btnDelete") 
        } 
        "Cleanup db in the end" in { 
          DBCleaner.cleanUp() 
        } 
      } 
    } 

控制器产品测试需要在测试开始时调用 DBCleaner 函数以确保数据库处于已知状态;此外,在运行所有测试后,我们还需要清理数据库以确保安全。

我们现在将应用相同的更改以进行审查和图像控制器测试。

您的 ReviewControllerTestSpec 文件应如下所示:

    class ReviewControllerTestSpec  
    extends PlaySpec  
    with OneServerPerSuite with OneBrowserPerSuite with HtmlUnitFactory { 
      DBCleaner.cleanUp() 
      "ReviewController" should { 
        "insert a new review should be ok" in { 
          goTo(s"http://localhost:${port}/product/add") 
          click on id("name") 
          enter("Blue Ball") 
          click on id("details") 
          enter("Blue Ball is a Awesome and simple product") 
          click on id("price") 
          enter("17.55") 
          submit() 
          goTo(s"http://localhost:${port}/review/add") 
          singleSel("productId").value = "1" 
          click on id("author") 
          enter("diegopacheco") 
          click on id("comment") 
          enter("Tests are amazing!") 
          submit() 
        } 
        "details from the review 1 should be ok" in { 
          goTo(s"http://localhost:${port}/review/details/1") 
          textField("author").value mustBe "diegopacheco" 
          textField("comment").value mustBe "Tests are amazing!" 
        } 
        "update review 1 should be ok" in { 
          goTo(s"http://localhost:${port}/review/details/1") 
          textField("author").value = "diegopacheco2" 
          textField("comment").value = "Tests are amazing 2!" 
          submit() 
          goTo(s"http://localhost:${port}/review/details/1") 
          textField("author").value mustBe "diegopacheco2" 
          textField("comment").value mustBe "Tests are amazing 2!" 
        } 
        "delete a review should be ok" in { 
          goTo(s"http://localhost:${port}/review/add") 
          singleSel("productId").value = "1" 
          click on id("author") 
          enter("diegopacheco") 
          click on id("comment") 
          enter("Tests are amazing!") 
          submit() 
          goTo(s"http://localhost:${port}/review") 
        click on id("btnDelete")} 
        "Cleanup db in the end" in { 
          DBCleaner.cleanUp() 
        } 
      } 
    } 

好的,我们已经修复了审查控制器测试。现在我们可以移动到最后一个控制器测试,即图像测试。

您的 ImageControllerTestSpec.scala 文件应如下所示:

    class ImageControllerTestSpec  
    extends PlaySpec  
    with OneServerPerSuite with OneBrowserPerSuite with HtmlUnitFactory { 
      DBCleaner.cleanUp() 
      "ImageController" should { 
        "insert a new image should be ok" in { 
          goTo(s"http://localhost:${port}/product/add") 
          click on id("name") 
          enter("Blue Ball") 
          click on id("details") 
          enter("Blue Ball is a Awesome and simple product") 
          click on id("price") 
          enter("17.55") 
          submit() 
          goTo(s"http://localhost:${port}/image/add") 
          singleSel("productId").value = "1" 
          click on id("url") 
          enter("https://thegoalisthering.files.wordpress.com/2012/01/       
          bluetennisball_display_image.jpg") 
          submit() 
        } 
        "details from the image 1 should be ok" in { 
          goTo(s"http://localhost:${port}/image/details/1") 
          textField("url").value mustBe             
          "https://thegoalisthering.files.wordpress.com/2012/01/       
          bluetennisball_display_image.jpg" 
        } 
        "update image 1 should be ok" in { 
          goTo(s"http://localhost:${port}/image/details/1") 
          textField("url").value =        
          "https://thegoalisthering.files.wordpress.com/2012/01/       
          bluetennisball_display_image2.jpg" 
          submit() 
          goTo(s"http://localhost:${port}/image/details/1") 
          textField("url").value mustBe        
          "https://thegoalisthering.files.wordpress.com/2012/01/      
          bluetennisball_display_image2.jpg" 
        } 
        "delete a image should be ok" in { 
          goTo(s"http://localhost:${port}/image/add") 
          singleSel("productId").value = "1" 
          click on id("url") 
          enter("https://thegoalisthering.files.wordpress.com/2012/01/       
          bluetennisball_display_image.jpg") 
          submit() 
          goTo(s"http://localhost:${port}/image") 
          click on id("btnDelete") 
        } 
        "Cleanup db in the end" in { 
          DBCleaner.cleanUp() 
        } 
      } 
    } 

好的,所有的控制器测试现在都已修复。我们可以运行所有测试以再次检查是否一切正常。

运行以下命令:

$ activator test

您将得到以下截图所示的输出:

控制器测试

如果您在运行应用程序时遇到问题(下一节将介绍),请应用进化,然后您可以再次运行测试。测试可能需要一些时间,具体取决于您的硬件。

运行应用程序

现在是时候使用 $ activator run 运行应用程序了。打开您的网络浏览器,并转到 http://localhost:9000/。一旦这样做,Play 将检测到应用程序需要进化,并将应用我们拥有的三个进化(1.sql2.sql3.sql)。然而,您需要点击按钮来应用进化。

运行应用程序

在您点击红色按钮后,立即应用此脚本,Slick 将创建表格,并将您重定向到应用程序。

运行应用程序

摘要

有了这些,我们就到达了本章的结尾。你学习了如何使用 Slick 来执行数据库持久化。你还学习了如何进行 FRM 映射,并且我们对应用程序和测试进行了重构,以便它们能够与反应式持久化和 Play 框架一起工作。然后我们解释了如何使用 Scala 代码访问数据库,并使用 SQL 执行操作。

在下一章中,我们将了解更多关于报告的内容,并且我们将使用我们的数据库来根据我们的 Play 框架应用程序生成报告。

第七章:创建报告

到目前为止,我们学习了如何使用 Activator 启动我们的应用程序,使用 Scala 和 Play 框架开发我们的 Web 应用程序,并使用 RxScala 添加反应式微服务调用进行数据流计算。我们还使用 BDD 和 Play 框架进行了单元测试和控制器测试。然后,我们使用 Slick 将数据持久化到 MySQL 中。现在我们将继续我们的应用程序。

在本章中,您将学习如何使用 JasperReports 编写报告。JasperReports 是一个针对 Java 的非常稳定的报告解决方案,并且可以很容易地在 Scala 中使用。我们将使用 Jasper 创建数据库报告,并将我们的应用程序更改为具有这种功能。

在本章中,我们将涵盖以下主题:

  • 理解 JasperReports

  • 将数据库报告添加到我们的应用程序中

介绍 JasperReports

JasperReports (community.jaspersoft.com/project/jasperreports-library) 是一个非常流行且稳定的报告解决方案,可以生成多种格式的报告,例如:

  • HTML

  • Excel

  • Word

  • Open Office 格式

  • PDF

为了获取您的报告,您有一个名为 Jaspersoft Studio 的视觉工具,在其中您可以拖放标签、图像、数据字段等元素。Jasper 会将这些元数据(报告定义)存储在一个 XML 文件中,也称为JRXML。如果您愿意,您可以在没有任何编辑器的情况下编辑和操作这个 XML;然而,使用 Jaspersoft Studio 工具来提高生产力会更好。

Jasper 可以与多个数据源一起工作,例如数据库、XML,甚至是内存中的对象。对于这本书,我们将使用数据库数据源来访问我们的 MySQL 数据库。

JasperReports 工作流程

JasperReports 具有相同的执行阶段,包括编译您的报告并在特定格式(例如 HTML)中渲染。第一个阶段是报告设计。如果您没有使用 Jaspersoft Studio 视觉工具,我们假设您已经有了您的 JRXML 文件。下一步是将 JRXML 编译成 Jasper 文件。这个编译阶段不需要每次都发生;只有在您更改 JRXML 时才需要;否则,您可以使用相同的 Jasper 文件。有一些策略可以缓存 Jasper 文件,所以基本上您可以在构建时进行缓存,或者您可以在应用程序中按需缓存。对于我们的应用程序,我们将使用第二种方法--在应用程序中按需缓存。

下一个阶段是渲染或导出。您可以将报告导出为 Jasper 支持的多种格式,例如 HTML、EXCEL 或 PDF 等。您可以使用相同的报告布局并导出为任何您喜欢的格式。对于我们的应用程序,我们将使用 PDF 格式。

Jasper 会话

一个 JRXML 有不同方式评估和在不同时间评估的部分。以下图表显示了所有可用的会话:

Jasper 会话

不同的部分如下:

  • 标题:这是打印一次

  • 页面页眉:这是打印在所有打印页的开头

  • 列标题:这是打印在每个详细列的开头

  • 详细:这是从数据源读取的每个记录打印的地方

  • 列页脚:这是打印在每个详细列的末尾

  • 页面页脚:这是打印在所有打印页的末尾

  • 摘要:这是打印在报告的末尾,通常用于显示计算、总计和一般总结

Jasper 是一个非常灵活的报告解决方案,它还允许我们在 Jasper 报告内部运行 Groovy 脚本来进行动态计算以及动态布局。如果您不想根据某些条件打印页面,或者基于数据库中的内容,或者您不想显示某些数据,这将非常有用。

接下来,我们将安装 Jaspersoft Studio 并开始为我们应用程序创建报告。

安装 Jaspersoft Studio 6

为了做到这一点,您需要安装 Java 8。如果您没有安装,请回到第一章,FP、响应式和 Scala 简介,并遵循设置说明。Jasper 真的非常棒,因为它可以在多个平台上运行;然而,它在 Windows 上运行得更好。我们使用 Linux,因此我们需要处理字体。JasperReports 使用许多微软的核心字体,例如 Arial 和 Times New Roman。Linux 上有一些选项可以拥有源文件。您可以在 Linux 上查找 mscorefonts 安装程序,或者只需从 Windows 复制字体。

如果您有双启动 Linux/Windows 安装,您可以前往 Windows 驱动器上的位置 WindowsDrive/Windows/Fonts。您需要将所有字体文件复制到 Linux 上的 /usr/share/fonts 并运行 $ sudo fc-cache -fv。这可能需要一些时间——对于我的 Windows 安装,大约是 ~300 MB 的字体。您可以在 Linux 上测试是否拥有 Windows 核心字体。打开 writer 并检查字体。您应该看到类似以下的内容:

安装 Jaspersoft Studio 6

为什么这如此重要?因为如果您没有放置正确的字体,Jasper 将无法工作。它将抛出一些随机异常,这些异常没有意义,但很可能与缺少字体有关。

一旦我们有了字体,我们就可以继续下载 Jaspersoft Studio 6。对于这本书,我们将使用 6.2.2 版本。您可以从 community.jaspersoft.com/project/jasperreports-library/releases 下载它。如果您在 Linux 上,强烈建议使用 DEB 软件包;否则,您将需要安装几个其他依赖项。

一旦您下载并安装了 Jaspersoft Studio 并打开程序,您将看到与此类似的用户界面:

安装 Jaspersoft Studio 6

我们已经成功安装了 Jaspersoft Studio。现在,我们需要配置我们的 MySQL 数据源,以便开始为我们的应用程序创建报告。

在 Jaspersoft Studio 中配置 MySQL 数据适配器

打开 Jaspersoft Studio 6 并点击文件 | 新建 | 数据适配器向导。您将看到以下屏幕:

在 Jaspersoft Studio 中配置 MySQL 数据适配器

文件名应为 MYSQL_DATAADAPTER.xml,然后您可以点击下一步 >

接下来,我们需要选择数据库适配器的类型。有几个选项,例如 Cassandra、MongoDB、HBase、JSON 文件等。

在 Jaspersoft Studio 中配置 MySQL 数据适配器

我们需要选择数据库 JDBC 连接并点击下一步 >

现在,我们需要配置连接详情。

在 Jaspersoft Studio 中配置 MySQL 数据适配器

字段应填写如下:

  • 名称:MySQL

  • JDBC 驱动程序:com.mysql.jdbc.Driver

  • JDBC URL:jdbc:mysql://localhost/RWS_DB?useUnicode=true&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=UTC

  • 用户名:root

  • 密码:此字段需要为空,或者如果您正在使用密码,请输入密码。

我们还需要将驱动程序配置到 Jaspersoft Studio 的类路径中。由于我们在同一个盒子上运行应用程序,我们已经在 ~/.ivy2/cache/mysql/mysql-connector-java/jars/mysql-connector-java-6.0.3.jar 文件夹中下载了 MySQL 驱动程序,使用 SBT。我们只需要在第三个标签页上指出它,该标签页称为 Driver Classpath

在 Jaspersoft Studio 中配置 MySQL 数据适配器

现在我们可以测试连接,看看是否一切正常。

在 Jaspersoft Studio 中配置 MySQL 数据适配器

太好了!现在我们已经配置好了我们的 MySQL 数据库适配器,准备开始为您的应用程序创建报告。

创建产品报告

要创建产品报告,请点击文件 | 新建 | Jasper 报告。然后选择发票模板。

创建产品报告

现在您可以点击下一步 >,我们将设置报告的名称。文件名将是 Products.jrxml。点击下一步 >。然后,我们需要选择数据源: MySQL

创建产品报告

现在,您需要运行 Select name, details, price from Product; 查询。

在设置完 SQL 查询后,您可以点击下一步 >

创建产品报告

接下来,您需要选择将在报告中使用的字段。从左侧列表中选择所有字段并将它们移动到右侧列表。然后点击下一步 >。对于这个报告,我们不需要分组排序,所以只需跳过分组并再次点击下一步 >

恭喜!我们已经完成了设置。我们可以使用 Jaspersoft Studio 预览报告。只需点击名为 Products.jxml 的新报告。我们将移除所有不需要的字段以及标志。然后报告将看起来像这样:

创建产品报告

我们将更改标题为 Products 并删除所有其他信息,但保留 名称详情价格标题,这些将被保留。我们还将保留来自 MySQL 数据库的 \(F{NAME}**、**\)F{DETAILS}$F{PRICE} 字段。

创建产品报告

现在,我们可以看到报告预览。我们需要点击底部名为 预览 的选项卡。有几个预览选项。我们必须从屏幕顶部选择 MySQL 作为数据源以及导出格式;在这里,我们使用 Java 来查看 UI。您也可以选择其他格式,例如 PDF

接下来,我们需要创建审查和图像的报告。

创建审查报告

现在,我们将创建审查报告。我们将使用与产品报告非常相似的过程。让我们开始创建审查报告:

  1. 点击 文件 | 新建 | Jasper 报告。选择 发票 模板并点击 下一步 >

  2. 文件名将是 Reviews.jrxml。然后点击 下一步 >

  3. 数据适配器 中选择 MySQL 并点击 下一步 >

  4. 查询(文本) 应包含以下代码片段:

            Select p.name, r.author, r.comment 
            from Product p, Review r 
            where p.id = r.product_id; 
    
    
  5. 然后点击 下一步 >

  6. 选择所有字段:名称作者评论,然后点击 下一步。>

  7. 让我们跳过 分组 部分,点击 下一步 然后点击 完成

  8. 我们将移除所有模板标签和字段,只保留数据库字段,所以我们应该有类似这样的内容:

创建审查报告

就这样!我们已经有了审查报告。如果您愿意,可以点击屏幕底部的 预览 选项卡,并选择 MySQLJava 来查看报告。请记住,您将需要数据;否则,它将是空的。

创建图像报告

现在,我们将创建图像报告。我们将遵循与产品报告和审查报告非常相似的过程。由于我们有图像 URL,我们还将显示图像,因此我们需要使用不同的组件。让我们开始创建图像报告:

  1. 点击 文件 | 新建 | Jasper 报告

  2. 选择 发票 模板并点击 下一步

  3. 文件名将是 Images.jrxml。然后点击 下一步

  4. 数据适配器 中选择 MySQL 并点击 下一步 >

  5. 查询(文本) 应包含以下代码片段:

            Select p.name, i.url 
            from Image i, Product p 
            where p.id = i.product_id; 
    
    
  6. 然后点击 下一步 >

  7. 选择所有字段:名称url,然后点击 下一步 >

  8. 让我们跳过分组部分,点击 下一步 > 然后点击 完成

现在,我们需要移除所有标签和字段,就像我们对其他报告所做的那样,只保留数据适配器中的标签和字段。

我们需要添加一个名为 Image 的图像组件。你可以在右侧的 基本元素 面板中找到它。只需将其拖放到详细带中,如图所示:

创建图像报告

选择一个自定义表达式,然后输入 $F{url}

就这样!现在我们已经有了包含图像的图像报告,是时候更改 Play 框架应用程序,以便在那里以 PDF 格式渲染此报告了。

将 JasperReports 集成到 Play 框架中

我们需要在 ReactiveWebStore/app 下创建一个新的文件夹,名为 reports。然后,我们将从 Jaspersoft Studio 复制所有三个新的 .jrxml 文件到这个文件夹,并设置构建依赖项。

build.sbt

首先,我们需要在 build.sbt 文件中添加新的依赖项。

在添加 Jasper 依赖项后,你的 build.sbt 文件应该看起来像这样:

    libraryDependencies ++= Seq( 
      // .... Other dependencies .... 
      "net.sf.jasperreports" % "jasperreports" % "6.2.2"  withSources() 
      ,"net.sf.jasperreports" % "jasperreports-functions" % "6.2.2", 
      "net.sf.jasperreports" % "jasperreports-chart-themes" % "6.2.2" 
    ) 
    resolvers += "Jasper" at 
    "https://jaspersoft.artifactoryonline.com/jaspersoft/repo/" 
    resolvers += "JasperSoft" at 
    "https://jaspersoft.artifactoryonline.com/jaspersoft/jaspersoft-
    repo/" 
    resolvers += "Jasper3rd" at 
    "https://jaspersoft.artifactoryonline.com/jaspersoft/
    jaspersoft-3rd-party/" 
    resolvers += "mondrian-repo-cache" at 
    "https://jaspersoft.artifactoryonline.com/jaspersoft/
    mondrian-repo-cache/" 
    resolvers += "spring-mil" at "http://repo.spring.io/libs-milestone" 
    resolvers += "spring-rel" at "http://repo.spring.io/libs-release" 
    resolvers += "oss" at 
    "https://oss.sonatype.org/content/groups/public/" 

因此,基本上,我们添加了所有的 JasperReports 依赖和解析器,这些解析器是一系列远程仓库,SBT 可以在其中查找 jar 文件。你可以在控制台运行 $ activator compile 命令来重新加载新的依赖项。运行 compile 后,再次生成 eclipse 文件是很重要的,所以你需要运行 $ activator eclipse

通用报告生成器

现在是时候用 Scala 编码了。我们将在 Scala 中创建一个通用报告生成器。在 ReactiveWebStore/app/reports 下,我们将创建一个新的 Scala 类,名为 ReportBuilder.scala

你的 ReportBuilder.scala 文件应该包含以下代码:

    package reports 
    object ReportBuilder { 
      private var reportCache:scala.collection.Map[String,Boolean] =  
      new scala.collection.mutable.HashMap[String,Boolean].empty 
      def generateCompileFileName(jrxml:String): String =  
      "/tmp/report_" + jrxml + "_.jasper" 
      def compile(jrxml:String){ 
        if(reportCache.get(jrxml).getOrElse(true)){ 
          JasperCompileManager.compileReportToFile( new  
          File(".").getCanonicalFile +     "/app/reports/" + jrxml ,  
          generateCompileFileName(jrxml)) 
          reportCache += (jrxml -> false) 
        } 
      } 
      def toPdf(jrxml:String):ByteArrayInputStream = { 
        try { 
          val os:OutputStream = new ByteArrayOutputStream() 
          val reportParams:java.util.Map[String,Object] = new  
          java.util.HashMap() 
          val con:Connection = DriverManager.getConnection 
          ("jdbc:mysql://localhost/RWS_DB?user=root&password       
            =&useUnicode=true&useJDBCCompliantTimezoneShift       
          =true&useLegacyDatetimeCode=false&serverTimezone=UTC") 
          compile(jrxml) 
          val jrprint:JasperPrint = JasperFillManager.fillReport 
          (generateCompileFileName(jrxml),  reportParams, con) 
            val exporter:JRPdfExporter = new JRPdfExporter() 
          exporter.setExporterInput(new SimpleExporterInput(jrprint)) 
          exporter.setExporterOutput 
          (new SimpleOutputStreamExporterOutput(os)); 
          exporter.exportReport() 
          new ByteArrayInputStream 
          ((os.asInstanceOf[ByteArrayOutputStream]).toByteArray()) 
        }catch { 
          case e:Exception => throw new RuntimeException(e) 
        } 
      } 
    } 

首先,我们在 generateCompileFileName 函数中设置一个临时目录来存储 Jasper 编译文件。正如你所见,我们将编译后的报告存储在 /tmp/。如果你不使用 Linux,你需要更改此路径。

接下来,我们有编译函数,它接收一个作为参数的 JRXML 报告。有一个报告缓存 Map 对象,用于对 Jasper 文件进行按需缓存。这个映射以 JRXML 报告作为键,布尔文件作为值。这个解决方案允许你按需编译报告。

最后,我们有 toPdf 函数,它将接收 jrxml 函数并编译所需的报告。此函数使用 DriverManager 获取 SQL 连接,以便将连接发送到 Jasper 引擎。最后,有由 JasperFillManager 管理的填充过程,它将接收 Jasper 文件和报告参数(对我们来说是一个空映射)以及 SQL 连接。

在用数据库中的数据填充报告后,我们可以使用 JRPdfExporter 命令将报告导出为 PDF。由于这是一个通用函数,我们将返回一个 ByteArrayInputStream,这是一个内存中的流结构。

接下来,下一步是更改我们的控制器,以便能够为产品、评论和图像生成报告。

将报告添加到产品控制器

我们需要更改产品控制器以公开新的报告功能。

在添加报告功能后,您的 ProductController.scala 文件应该看起来像这样:

    @Singleton 
    class ProductController @Inject() (val messagesApi:MessagesApi,val 
    service:IProductService) extends Controller with I18nSupport { 
      //... rest of the controller code... 
      def report() = Action { 
        import play.api.libs.concurrent. 
        Execution.Implicits.defaultContext 
        Ok.chunked( Enumerator.fromStream(  
        ReportBuilder.toPdf("Products.jrxml") ) ) 
        .withHeaders(CONTENT_TYPE -> "application/octet-stream") 
        .withHeaders(CONTENT_DISPOSITION -> "attachment;  
          filename=report-products.pdf" 
        ) 
      }
    }

就在这里,我们有一个名为 report 的新函数。我们需要使用我们的 ReportBuilder 方法,并将 Products.jrxml 作为参数传递。我们使用 Ok.chunked 函数以便能够将报告流式传输到浏览器。我们还设置了一些响应头,例如内容类型和文件名,这些将被报告为 products.pdf

现在,我们将将相同的代码应用到评论和图片控制器上。

将报告添加到评论控制器

现在是时候为评论控制器创建报告函数了。让我们开始吧。

在添加报告功能后,您的 ReviewController.scala 文件应该看起来像这样:

    @Singleton 
    class ReviewController @Inject() 
    (val messagesApi:MessagesApi, 
      val productService:IProductService, 
    val service:IReviewService) 
    extends Controller with I18nSupport { 
      //... rest of the controller code... 
      def report() = Action { 
        import play.api.libs.concurrent.Execution. 
        Implicits.defaultContext 
        Ok.chunked( Enumerator.fromStream(  
        ReportBuilder.toPdf("Reviews.jrxml") ) ) 
        .withHeaders(CONTENT_TYPE -> "application/octet-stream") 
        .withHeaders(CONTENT_DISPOSITION -> "attachment;  
        filename=report-reviews.pdf") 
      } 
    } 

这里与产品控制器相同的逻辑。主要区别是 jrxml 文件和文件名响应头。现在,我们可以移动到最后一个控制器--图片控制器。

将报告添加到图片控制器

最后,我们将像对产品控制器和评论控制器所做的那样应用相同的逻辑,但现在是我们更改图片控制器的时候了。

在添加报告功能后,您的 ImageController.scala 文件应该看起来像这样:

    @Singleton 
    class ImageController @Inject() 
    (val messagesApi:MessagesApi, 
      val productService:IProductService, 
    val service:IImageService) 
    extends Controller with I18nSupport { 
      // ... rest of the controller code ... 
      def report() = Action { 
        import play.api.libs.concurrent.Execution. 
        Implicits.defaultContext 
        Ok.chunked( Enumerator.fromStream(  
        ReportBuilder.toPdf("Images.jrxml") ) ) 
        .withHeaders(CONTENT_TYPE -> "application/octet-stream") 
        .withHeaders(CONTENT_DISPOSITION -> "attachment;  
        filename=report-images.pdf") 
      } 
    } 

好的,我们已经完成了所有的控制器。然而,我们还需要配置路由,否则我们无法调用控制器--这是下一步。

路由 - 添加新的报告路由

现在,我们需要添加报告的新路由。为此,我们将编辑 conf/routes 文件,如下所示:

    GET     /reports           controllers.HomeController.reports  
    # 
    # Reports 
    # 
    GET  /product/report       controllers.ProductController.report 
    GET  /review/report        controllers.ReviewController.report 
    GET  /image/report         controllers.ImageController.report 

现在我们已经完成了路由,我们需要更改 UI 以便公开新的报告功能。我们将创建一个包含所有报告的新视图,并且为了方便起见,我们还将为每个资源 UI(产品、评论和图片)添加一个按钮。

新的集中式报告 UI

我们需要在 ReactiveWebStore/views/reports_index.scala.html 创建一个新的视图。

您的 reports_index.scala.html 文件应该看起来像这样:

    @()(implicit flash: Flash) 
    @main("Reports") { 
      <a href="/product/report"><img height="42" width="42"   
      src="img/@routes.Assets.at("images/product.png")"> Products  
      Report</a><BR> 
      <a href="/review/report"><img height="42" width="42"  
      src="img/@routes.Assets.at("images/review.png")"> Reviews Report  
      </a><BR> 
      <a href="/image/report"><img height="42" width="42"  
      src="img/@routes.Assets.at("images/image.png")"> Images  
      Report</a><BR> 
    } 

因此,这里我们将基本上列出所有资源--产品、评论和图片,并将相关控制器链接起来,当用户点击相应的链接时,将下载 PDF 报告。现在我们需要编辑每个资源(产品、图片和评论)视图,以便在那里添加报告链接。

为每个视图添加报告按钮

让我们先编辑产品视图。

您的 product_index.scala.html 文件应该看起来像这样:

    @(products:Seq[Product])(implicit flash: Flash) 
    @main("Products") { 
      // ... rest of the ui code ... 
      <p> 
        <a href="@routes.ProductController.blank" class="btn btn- 
        success"> 
        <i class="icon-plus icon-white"></i>Add Product</a>  
        <a href="@routes.ProductController.report" class="btn btn- 
        success"> 
        <i class="icon-plus icon-white"></i>Products Report</a> 
      </p> 
    } 

如您所见,我们添加了一个指向新报告函数的新按钮。我们还需要为评论和图片 UI 执行相同的操作。

您的 review_index.scala.html 文件应该看起来像这样:

    @(reviews:Seq[Review])(implicit flash: Flash) 
    @main("Reviews") { 
      // ... rest of the ui code ... 
      <p> 
        <a href="@routes.ReviewController.blank" class="btn btn- 
        success"><i class="icon-plus icon-white"></i>Add Review</a>  
        <a href="@routes.ReviewController.report" class="btn btn- 
        success"><i class="icon-plus icon-white"></i>Review Report</a>  
      </p> 
    } 

现在我们可以向图片视图添加最终的按钮。

您的 image_index.scala.html 文件应该看起来像这样:

    @(images:Seq[Image])(implicit flash:Flash) 
    @main("Images") { 
      // ... rest of the ui template ... 
      <p> 
        <a href="@routes.ImageController.blank" class=
        "btn btn-success"><i class="icon-plus icon-white"></i>Add 
        Image</a>  
        <a href="@routes.ImageController.report" class=
        "btn btn-success"><i class="icon-plus icon-white"></i>
        Images Report</a>  
      </p> 
    } 

一切准备就绪!现在我们可以运行 $ activator run 来查看新的用户界面和报告按钮。访问 http://localhost:9000/

为每个视图添加报告按钮

如果你访问 http://localhost:9000/reports 或点击 报告,你会看到以下内容:

为每个视图添加报告按钮

就这样!我们在 Play 框架应用程序上已经使所有报告都正常工作。

摘要

在本章中,你学习了如何使用 Jaspersoft Studio 和 JasperReports 创建自定义报告。此外,你还修改了应用程序以集成 Play 框架和 JasperReports。

在下一章中,你将学习如何使用 Akka 框架。我们将继续构建我们的应用程序,并采用演员模型为你的应用程序带来一个全新的杀手级特性。

第八章:使用 Akka 开发聊天

在前面的章节中,我们使用 Slick 将数据持久化到 MySQL,并使用 Jasper reports 编写 PDF 报告。现在,我们将使用 Akka 在我们的应用程序中添加更多功能。

在本章中,您将学习如何使用 Akka 框架创建 Actors。我们将结合使用 Play 框架和 WebSockets 来实现聊天功能。

本章将涵盖以下主题:

  • 理解 Actor 模型

  • Actor 系统、Actor 路由和调度器

  • 邮箱、Actor 配置和持久化

  • 创建我们的聊天应用程序

  • 测试我们的 Actors

添加 Akka 的新 UI 介绍

Akka (akka.io/) 是一个用于在 Scala、Java 和 .NET 中构建并发、分布式和弹性消息驱动应用程序的框架。使用 Akka 构建应用程序具有以下优点:

  • 高性能:Akka 在具有 ~2.5 百万 Actors 每 GB RAM 的通用硬件上,每秒可以处理高达 5000 万条消息。

  • 设计上的弹性:Akka 系统具有本地和远程 Actors 的自我修复特性。

  • 分布式和弹性:Akka 拥有所有扩展应用程序的机制,例如集群、负载均衡、分区和分片。Akka 允许您根据需求扩展或缩小 Actors。

Akka 框架为并发、异步和分布式编程提供了良好的抽象,例如 Actors、Streams 和 Futures。在生产环境中有许多成功的案例,例如 BBC、Amazon、eBay、Cisco、The Guardian、Blizzard、Gilt、HP、HSBC、Netflix 以及许多其他公司。

Akka 是一个真正的反应式框架,因为从发送和接收消息到 Actors 的所有操作都是无锁的、非阻塞的 IO 和异步的。

Actor 模型的介绍

并发编程的关键是避免共享可变状态。共享状态通常需要锁和同步,这使得您的代码并发性降低,复杂性增加。Actors 不共享任何东西;它们有内部状态,但它们不共享它们的内部状态。

Actors 具有位置透明性;它们可以在本地或远程系统以及集群中运行。混合本地和远程 Actors 也是可能的——这对于可扩展性来说非常好,并且非常适合云环境。Actors 可以在任何地方运行,从您的本地设备、云、裸金属数据中心和 Linux 容器。

什么是 Actor?

Actors 可以作为线程、回调监听器、单例服务、企业 JavaBeans (EJB)、路由器、负载均衡器或池以及有限状态机 (FSM)的替代品。Actor 模型概念并非全新;它是由 Carl Hewitt 在 1973 年创建的。Actor 模型在电信行业中得到了广泛的应用,例如在 Erlang 这样的坚固技术中。Erlang 和 Actor 模型在 Ericsson 和 Facebook 等公司取得了巨大的成功。

Actors 有一种简单的工作方式:

  • 代码组织单元:

    • 处理

    • 存储

    • 通信

      • 它们管理内部状态

      • 它们有一个邮箱

      • 它们通过消息与其他 Actor 进行通信

      • 它们可以在运行时改变行为

消息交换和邮箱

Actor 通过消息相互交谈。有两种模式:一种称为 ask,另一种称为 fire and forget。两种方法都是异步和非阻塞 IO。当一个 Actor 向另一个 Actor 发送消息时,它并不是直接发送给另一个 Actor;实际上,它是发送到 Actor 的邮箱中。

消息以时间顺序的方式入队到 Actor 邮箱中。Akka 中有不同的邮箱实现。默认的是基于先进先出FIFO)的。这是一个好的默认设置;然而,你可能需要一个不同的算法,这是可以的,因为如果你需要,你可以更改邮箱。更多详细信息可以在官方文档中找到(doc.akka.io/docs/akka/2.4.9/scala/mailboxes.html#mailboxes-scala)。Actor 存在于 Actor 系统中。在一个集群中你可以有多个 Actor 系统:

消息交换和邮箱

Akka 将 actor 状态封装在邮箱中,并将其与 Actor 行为解耦。Actor 行为是你将在 Actor 内部拥有的代码。你需要将 Actor 和 Akka 视为一个协议。所以,基本上,你需要定义你将有多少个 Actor,以及每个 Actor 在代码、责任和行为方面的作用。Actor 系统有 Actor 和监督者。监督者是 Akka 机制之一,用于提供容错性和弹性。监督者负责 Actor 实例,并且可以根据需要重启、终止或创建更多的 Actor。

Actor 模型非常适合并发和可伸缩性;然而,就像计算机科学中的每一件事一样,都有权衡和缺点。例如,Actor 需要新的思维方式和不同的思考方式。

没有万能的解决方案。一旦你有了你的协议,可能很难在协议外部重用你的 Actor。一般来说,与面向对象的类或函数式编程中的函数相比,Actor 可能更难组合。

使用 Akka 编码 Actor

让我们看看以下使用 Akka 框架和 Scala 编写的 Actor 代码:

    import akka.actor._ 
    case object HelloMessage 
    class HelloWorldActor extends Actor { 
      def receive = { 
        case HelloMessage => sender() ! "Hello World" 
        case a:Any => sender() ! "I don't know: " + a + " - Sorry!" 
      } 
    } 
    object SimpleActorMainApp extends App{  
      val system = ActorSystem("SimpleActorSystem") 
      val actor = system.actorOf(Props[HelloWorldActor])  
      import scala.concurrent.duration._ 
      import akka.util.Timeout 
      import akka.pattern.ask 
      import scala.concurrent.Await 
      implicit val timeout = Timeout(20 seconds)  
      val future = actor ? HelloMessage 
      val result = Await.result(future, 
      timeout.duration).asInstanceOf[String] 
      println("Actor says: " + result )  
      val future2 = actor ? "Cobol" 
      val result2 = Await.result(future2, 
      timeout.duration).asInstanceOf[String] 
      println("Actor says: " + result2 )    
      system.terminate() 
    } 

如果你将这段 Akka 代码在你的控制台中的sbt上运行,你会看到类似以下的输出:

$ sbt run 

使用 Akka 编码 Actor

让我们更仔细地看看我们刚刚编写的这段 Akka 代码,其中我们定义了一个名为HelloWorldActor的 Scala 类。为了使这个类成为 Actor,我们需要扩展 Actor。Actor 默认是反应式的,这意味着它们正在等待接收消息以对消息做出反应。你需要在一个事件循环中编写你的行为。在 Akka 中,这是通过在 Scala 中用模式匹配器编写receive函数来完成的。

模式匹配器将定义参与者可以做什么。你需要编写所有你希望该参与者处理的消息类型。正如我之前提到的,你需要有一个协议;所以你的协议有一个名为HelloMessage的消息。在 Akka 中,使用 Scala 对象作为消息是一种常见的做法。然而,你可以传递几乎所有类型作为消息。甚至可以发送带有参数的 case 类。

好的,我们已经有了我们的协议,即我们的参与者以及他们可以交换的消息。现在我们需要创建一个参与者系统并启动我们的应用程序。正如你所看到的,我们将使用ActorSystem对象来创建参与者系统。参与者系统需要有一个名称,可以是任何你喜欢的字符串,只要它包含任何字母[a-z, A-Z, 0-9]以及非开头的'-'或'_'。

在创建系统之后,你可以创建参与者。系统有一个名为actorOf的函数,可以用来创建参与者。你需要使用一个特殊对象Props并传递actor类。为什么我们需要这样做呢?这是因为 Akka 管理参与者状态。你不应该尝试自己管理参与者实例。这是危险的,因为你可能会破坏引用透明性,你的代码可能不会工作。

对于这段代码,我们使用的是 ask 模式。我们将使用它来向参与者发送消息,并想知道参与者将返回什么。正如之前提到的,Akka 以异步和非阻塞的方式进行所有操作。然而,有时你想要立即得到答案,不幸的是,你将需要阻塞。

为了立即得到答案,我们需要定义一个超时并使用Await对象。当你使用?(ask 模式)向参与者发送消息时,Akka 会为你返回一个 Future。然后,你可以将带有超时的 Future 传递给Await,如果答案在超时之前返回,你将得到参与者的响应。

再次强调,我们在这里阻塞是因为我们想要立即得到答案,而且我们处于参与者系统之外。记住,当一个参与者在与参与者系统内的另一个参与者交谈时,它永远不应该阻塞。所以请小心使用Await

在这段代码中,另一个重要的事情是参与者内部的sender()方法接收一个函数。这意味着你想要获取发送消息给你的参与者的引用。由于我们正在执行sender() !方法,我们正在将答案发送回调用者。sender()函数是 Akka 对处理其他参与者或函数调用者的响应消息的抽象。

我们还有一个使用Any的情况,这意味着所有其他消息都将由该代码块处理。

ask 模式是向参与者发送消息的一种方式。还有一种叫做FireAndForget "!"的模式。fire and forget 将发送一个消息,不会阻塞并等待答案。所以,没有答案——换句话说,Unit。

让我们看看使用FireAndForget消息交换的一些代码:

    import akka.actor._  
    object Message  
    class PrinterActor extends Actor { 
      def receive = { 
        case a:Any => 
        println("Print: " + a) 
      } 
    } 
    object FireAndForgetActorMainApp extends App{ 
      val system = ActorSystem("SimpleActorSystem") 
      val actor = system.actorOf(Props[PrinterActor])     
      val voidReturn = actor ! Message 
      println("Actor says: " + voidReturn ) 
      system.terminate() 
    } 

如果你使用$ sbt run运行此代码,你将看到以下输出:

使用 Akka 编码 Actor

在这里,我们有一个PrinterActor方法,它几乎可以接受任何东西并在控制台上打印。然后,我们将创建一个 Actor 系统,并使用“fire and forget”模式(即“!”)向我们的 Actor 发送消息,正如你所看到的,我们将收到 Unit;最后,我们将使用terminate选项等待 Actor 系统的关闭。

Actor 路由

Akka 提供了路由功能。从业务角度来看,这很有用,因为你可以根据业务逻辑和行为将消息路由到正确的 Actor。对于架构,我们可以用它作为负载均衡,并将消息路由到更多的 Actor 以实现容错和可伸缩性。

Akka 提供了几种路由选项,如下所示:

  • RoundRobin:这是一个随机逻辑,将消息发送到池中的每个不同的 Actor。

  • SmallestMailbox:这会将消息发送到消息较少的 Actor。

  • Consistent Hashing:这根据哈希 ID 对 Actor 进行分区。

  • ScatterGather:这会将消息发送到所有 Actor,第一个回复的获胜。

  • TailChopping:这会将消息随机发送到路由,如果一秒内没有收到回复,它会选择新的路由并再次发送,依此类推。

让我们看看以下代码的实际应用:

    import akka.actor._ 
    import akka.routing.RoundRobinPool  
    class ActorUpperCasePrinter extends Actor { 
      def receive = { 
        case s:Any => 
        println("Msg: " + s.toString().toUpperCase() + " - " + 
        self.path) 
      } 
    } 
    object RoutingActorApp extends App {    
      val system = ActorSystem("SimpleActorSystem") 
      val actor:ActorRef = system.actorOf(   
        RoundRobinPool(5).props(Props[ActorUpperCasePrinter]),name = 
      "actor")  
      try{ 
        actor ! "works 1" 
        actor ! "works 2" 
        actor ! "works 3" 
        actor ! "works 4" 
        actor ! "works 5" 
        actor ! "works 6"  
      }catch{ 
        case e:RuntimeException => println(e.getMessage()) 
      }   
      system.terminate() 
    } 

如果你使用sbt运行此代码,即使用$ sbt run,你将得到以下输出:

Actor 路由

因此,这里我们有一个ActorUppercasePrinter函数,它会打印接收到的任何内容,并调用toString函数,然后toUpperCase。最后,它还会打印self.path Actor,这将 Actor 的地址。Actor 以类似于文件系统的层次结构组织。

使用 Akka 有多种方式 - Akka 支持代码或配置(application.conf文件)。在这里,我们创建了一个具有五个路由的循环池 Actor。我们将目标 Actor 传递给路由器,它将成为我们的打印 Actor。

如你所见,当我们使用“fire and forget”模式发送消息时,每个消息都会被发送到不同的 Actor。

持久化

Akka 在内存上工作。然而,使用持久化是可能的。在 Akka 中,持久化仍然处于某种实验阶段。但是,它是稳定的。对于生产环境,你可以使用高级持久化插件,例如 Apache Cassandra。为了开发和教育的目的,我们将在文件系统中使用 Google leveldb。Akka 提供了多种持久化选项,例如视图和持久化 Actor。

让我们看看使用 Google leveldb 和文件系统的持久化 Actor 的例子:

    import akka.actor._ 
    import akka.persistence._ 
    import scala.concurrent.duration._ 
    class PersistenceActor extends PersistentActor{  
      override def persistenceId = "sample-id-1" 
      var state:String = "myState" 
      var count = 0    
      def receiveCommand: Receive = { 
        case payload: String => 
        println(s"PersistenceActor received ${payload} (nr = 
        ${count})") 
        persist(payload + count) { evt => 
          count += 1 
        } 
      }        
      def receiveRecover: Receive = { 
        case _: String => 
        println("recover...") 
        count += 1 
      } 
    }  
    object PersistentViewsApp extends App { 
      val system = ActorSystem("SimpleActorSystem") 
      val persistentActor = 
      system.actorOf(Props(classOf[PersistenceActor]))     
      import system.dispatcher 
      system.scheduler.schedule(Duration.Zero, 2.seconds, 
      persistentActor, "scheduled") 
    } 

执行$ sbt run命令将给出以下输出:

持久化

如果你使用$ sbt run运行此代码,然后停止并再次运行,你将看到每次停止和启动时都会存储和恢复数据。

如你所见,你的 Actor 需要扩展PersistentActor以获得持久化支持。你还需要提供一个persistenceID

在这里,你需要实现两个接收函数。一个是命令(也称为消息),另一个是恢复。当这个 Actor 接收消息时,将激活命令的接收循环,而当 Actor 启动时,将激活恢复循环并从数据库中读取持久化数据。

因此,这个 Actor 有一个计数器来计算它接收到的每个消息,并在控制台上打印出它接收到的每个消息。就是这样;正如你所看到的,它非常简单。为了使用这个功能,你还需要配置你的application.conf

你的application.conf文件应该看起来像这样:

    akka { 
      system = "SimpleActorSystem" 
      remote { 
        log-remote-lifecycle-events = off 
        netty.tcp { 
          hostname = "127.0.0.1" 
          port = 0 
        } 
      } 
    } 
    akka.cluster.metrics.enabled=off  
    akka.persistence.journal.plugin = 
    "akka.persistence.journal.leveldb" 
    akka.persistence.snapshot-store.plugin = 
    "akka.persistence.snapshot-store.local"  
    akka.persistence.journal.leveldb.dir = "target/persistence/journal" 
    akka.persistence.snapshot-store.local.dir = 
    "target/persistence/snapshots"  
    # DO NOT USE THIS IN PRODUCTION !!! 
    # See also https://github.com/typesafehub/activator/issues/287 
    akka.persistence.journal.leveldb.native = false 

因此,我们在这里定义了一个简单的 Akka 系统(本地模式),并配置了 Google leveldb 的持久化。正如你所看到的,我们需要提供一个持久化的路径,并且这个路径必须在操作系统上存在。

由于我们使用了额外的功能,我们还需要更改build.sbt以导入我们需要的所有 jar 文件,包括 Akka、持久化和 leveldb。

你的build.sbt文件应该看起来像这样:

    // rest of the build.sbt file ...   
    val akkaVersion = "2.4.9"                
    libraryDependencies += "com.typesafe.akka" %% "akka-actor" % 
    akkaVersion 
    libraryDependencies += "com.typesafe.akka" %% "akka-kernel" % 
    akkaVersion 
    libraryDependencies += "com.typesafe.akka" %% "akka-remote" % 
    akkaVersion 
    libraryDependencies += "com.typesafe.akka" %% "akka-cluster" % 
    akkaVersion 
    libraryDependencies += "com.typesafe.akka" %% "akka-contrib" % 
    akkaVersion 
    libraryDependencies += "com.typesafe.akka" %% "akka-persistence" % 
    akkaVersion 
    libraryDependencies += "org.iq80.leveldb" % "leveldb" % "0.7" 
    libraryDependencies += "org.iq80.leveldb" % "leveldb-api" % "0.7" 
    libraryDependencies += "org.fusesource.leveldbjni" % "leveldbjni" % 
    "1.8" 
    libraryDependencies += "org.fusesource.leveldbjni" % "leveldbjni-
    linux64" % "1.8" 
    libraryDependencies += "org.fusesource" % "sigar" % "1.6.4" 
    libraryDependencies += "org.scalatest" % "scalatest_2.11" % "2.2.6" 

就这样。这就是我们需要持久化 Actor 状态的所有内容。

注意

Akka 有更多的功能。更多内容,请查看默认文档doc.akka.io/docs/akka/2.4/scala.html?_ga=1.12480951.247092618.1472108365

创建我们的聊天应用程序

现在我们对 Akka 有了更好的了解,我们将继续开发我们的应用程序。Akka 与 Play 框架有很好的集成。我们现在将使用 Akka 和 Play 框架中的 Actors。让我们为我们的应用程序构建一个简单的聊天功能。我们将更改代码以添加新的 UI,并使用 Akka 测试套件测试 Actors。

Play 框架已经为我们包含了 Akka 在类路径中,所以我们不需要担心它。然而,我们需要将 Akka 测试套件依赖项添加到build.sbt文件中,以便在我们的类路径中有这些类。

你的build.sbt应该看起来像这样:

    // rest of the build.stb ...  
    libraryDependencies ++= Seq( 
      "com.typesafe.akka" %% "akka-testkit" % "2.4.4" % Test, 
    // rest of the deps ... 
    )  
    // rest of the build.stb ... 

好的,现在你可以去控制台输入$ activator$ reload,然后$ compile。这将强制 sbt 下载新的依赖项。

现在我们需要创建一个名为 Actors 的包。这个包需要位于ReactiveWebStore/app/目录下。我们将开始创建一个ActorHelper实用对象,以便拥有一个通用的 ask 模式函数,这是我们之前看到的。这是一个 Actor 辅助的通用 ask 模式实用工具。

你的ActorHelper.scala文件应该看起来像这样:

    package actors  
    object ActorHelper {  
      import play.api.libs.concurrent.
      Execution.Implicits.defaultContext 
      import scala.concurrent.duration._ 
      import akka.pattern.ask 
      import akka.actor.ActorRef 
      import akka.util.Timeout 
      import scala.concurrent.Future 
      import scala.concurrent.Await    
      def get(msg:Any,actor:ActorRef):String = { 
        implicit val timeout = Timeout(5 seconds) 
        val result = (actor ? msg).mapTo[String].map { result => 
        result.toString } 
        Await.result(result, 5.seconds) 
      } 
    } 

ActorHelper只有一个函数:get。这个函数将从任何消息中给出的任何 Actor 获取答案。然而,正如你所看到的,我们有一个五秒的超时。如果结果在这段时间内没有返回,将会抛出一个异常。

在此代码中,我们还将 Actor 结果映射到 String,通过在结果 future 中调用 toString 函数。这不是很多代码;然而,有很多导入,这使得代码更简洁,我们可以用更少的代码和更少的导入从 Actors 获取答案。

聊天协议

现在我们需要定义我们的协议。为此功能,我们需要三个 Actor。我们创建的 Actor 将如下所示:

  • ChatRoom:这将包含聊天室中所有用户的引用。

  • ChatUser:这将每个用户(活动浏览器)有一个实例。

  • ChatBotAdmin:这个简单的 Bot Admin 将提供关于聊天室的状态。

ChatUserActor 需要加入 JoinChatRoom 对象以开始聊天。ChatUserActor 还需要向 ChatMessage 类发送消息给 ChatRoomActor,该类将向所有用户广播消息。ChatBotAdmin 将从 ChatRoomActorGetStats 对象中获得报告。

让我们开始编写这个协议。首先,我们需要定义这些 Actor 之间交换的消息,如下面的代码片段所示:

    package actors  
    case class ChatMessage(name:String,text: String) 
    case class Stats(users:Set[String])  
    object JoinChatRoom 
    object Tick 
    object GetStats 

如您所见,我们有一个名为 ChatMessage 的类,其中包含一个名称和文本。这将是在聊天中每个用户发送的消息。然后,我们将有一个统计类,其中包含一组用户--这将是在聊天应用程序中登录的所有用户。

最后,我们有一些动作消息,例如 JoinChatRoomTickGetStats。所以,JoinChatRoom 将由 ChatUserActor 发送到 ChatRoomActor 以加入聊天。Tick 将是一个定期发生的预定消息,以便 ChatBotAdmin 向所有已登录用户发送关于聊天室的状态。GetStatsChatBotAdminActor 发送到 ChatRoomActor 的消息,以获取关于谁在房间中的信息。

现在我们来编写我们的三个演员。

ChatRoomActor.scala 文件应该看起来像这样:

    package actors  
    import akka.actor.Props 
    import akka.actor.Terminated 
    import akka.actor.ActorLogging 
    import akka.event.LoggingReceive 
    import akka.actor.Actor 
    import akka.actor.ActorRef 
    import play.libs.Akka 
    import akka.actor.ActorSystem  
    class ChatRoomActor extends Actor with ActorLogging {  
      var users = Set[ActorRef]()  
      def receive = LoggingReceive { 
        case msg: ChatMessage => 
        users foreach { _ ! msg } 
        case JoinChatRoom => 
        users += sender 
        context watch sender   
        case GetStats => 
        val stats:String = "online users[" + users.size + "] - users[" 
        + users.map( a =>        a.hashCode().mkString("|") + "]" 
        sender ! stats 
        case Terminated(user) => 
        users -= user 
      } 
    } 
    object ChatRoomActor { 
      var room:ActorRef = null 
      def apply(system:ActorSystem) = { 
        this.synchronized { 
          if (room==null) room = system.actorOf(Props[ChatRoomActor]) 
          room 
        } 
      } 
    } 

ChatRoomActor 有一个名为 users 的变量,它是一个 ActorRef 的集合。ActorRef 是对任何演员的泛型引用。我们有一个接收函数,包含三个情况:ChatMessageJoinChatRoomGetStats

ChatUserActor 方法将发送一个 JoinChatRoom 来加入房间。如您所见,我们通过 sender() 函数从发送者 Actor 获取 ActorRef 方法,并将此引用添加到用户集合中。这样,ActorRef 集合就代表了当前聊天室中在线登录的用户。

另一个情况是与 ChatMessage 方法相关。基本上,我们将消息广播给聊天中的所有用户。我们这样做是因为我们有所有演员的引用。然后,我们将调用 foreach 函数逐个迭代所有用户,然后我们将使用操作符下划线 _ 表示的每个用户 Actor 发送消息,使用 FireAndForget "!"

GetStats 案例创建了一个包含所有聊天室统计信息的字符串。目前,统计信息只是在线用户的数量,这是通过在用户对象上调用 size 函数计算出来的。我们还展示了所有标识已登录 Actor 的哈希码,只是为了好玩。

这就是我们的 ChatRoomActor 实现。正如你所见,不描述另一个 Actor 就很难谈论一个 Actor,因为协议总是有点耦合。你可能也会想知道为什么我们为 ChatRoomActor 方法有一个伴随对象。

这个对象是为了提供一个创建 Actor 实例的简单方法。我们为我们的设计创建了一个单独的房间;我们不希望有多个聊天室,所以我们需要控制房间 Actor 的创建。

如果房间为空,我们将创建一个新的房间;否则,我们将返回内存中已经获取的房间缓存的实例。我们需要一个 Actor 系统的实例来创建演员,这就是为什么我们在 apply 函数中接收系统。当有人编写 ChatRoomActor(mySystem) 这样的代码时,apply 函数将被调用。

现在,让我们转向 ChatUserActor 的实现。

ChatUserActor.scala 文件应该看起来像这样:

    package actors  
    import akka.actor.ActorRef 
    import akka.actor.Actor 
    import akka.actor.ActorLogging 
    import akka.event.LoggingReceive 
    import akka.actor.ActorSystem 
    import akka.actor.Props  
    class ChatUserActor(room:ActorRef, out:ActorRef) extends Actor with 
    ActorLogging { 
      override def preStart() = { 
        room ! JoinChatRoom 
      }  
      def receive = LoggingReceive { 
        case ChatMessage(name, text) if sender == room => 
        val result:String = name + ":" + text 
        out ! result 
        case (text:String) => 
        room ! ChatMessage(text.split(":")(0), text.split(":")(1)) 
        case other => 
        log.error("issue - not expected: " + other) 
      } 
    }  
    object ChatUserActor { 
      def props(system:ActorSystem)(out:ActorRef) = Props(new       
      ChatUserActor(ChatRoomActor(system), out)) 
    } 

这个演员比上一个稍微容易一些。ChatUserActor 接收一个参数,即房间演员引用和一个输出演员。房间将是一个用户将用来与其他用户通信的房间实例。被调用的 ActorRef 方法是 Play 框架的 Actor,负责将答案发送回控制器和 UI。

我们基本上只有两种情况:一种是我们接收一个 ChatMessage,另一种是聊天室中的 ChatUserActors 方法。所以,我们只需要通过输出演员发送回 UI。这就是为什么有一个带有结果的 fire-and-forget 消息给输出 Actor。使用新的 Actor 模型可能会有危险,请参阅doc.akka.io/docs/akka/current/scala/actors.html 了解更多。

另一种情况是只接收一个字符串,这个字符串将是该 Actor 自身的消息。记住,每个 Actor 代表一个用户和一个通过 WebSocket 实现的全双工连接的浏览器。现在不用担心 WebSocket;我们将在本章的后面更详细地介绍它。

对于这个案例函数,我们将 ChatMessage 方法发送到房间。我们将消息分成两部分:用户名和文本,它们通过 : 分隔。

这里,我们也为了良好的实践而有一个伴随对象。所以,你可以调用 ChatUserActor,传递 Actor 系统和一个 curried 参数给输出演员。

现在,我们将转向最后一个 Actor:Bot Admin Actor,它应该看起来像这样:

    package actors  
    import akka.actor.ActorRef 
    import akka.actor.Actor 
    import akka.actor.ActorLogging 
    import akka.event.LoggingReceive 
    import akka.actor.ActorSystem 
    import akka.actor.Props 
    import scala.concurrent.duration._  
    class ChatBotAdminActor(system:ActorSystem) extends Actor with 
    ActorLogging { 
      import play.api.libs.concurrent.Execution.
      Implicits.defaultContext 
      val room:ActorRef = ChatRoomActor(system) 
      val cancellable = system.scheduler.schedule(0 seconds, 
      10 seconds, self , Tick) 
      override def preStart() = { 
        room ! JoinChatRoom 
      }  
      def receive = LoggingReceive { 
        case ChatMessage(name, text) => Unit 
        case (text:String) => room ! ChatMessage(text.split(":")(0), 
        text.split(":")(1)) 
        case Tick => 
        val response:String = "AdminBot:" + ActorHelper.get
        (GetStats, room) 
        sender() ! response 
        case other => 
        log.error("issue - not expected: " + other) 
      } 
    } 
    object ChatBotAdminActor { 
      var bot:ActorRef = null 
      def apply(system:ActorSystem) = { 
        this.synchronized { 
          if (bot==null) bot = system.actorOf(Props
          (new ChatBotAdminActor(system))) 
          bot 
        } 
      } 
    } 

如您所见,这个 Actor 接收聊天室引用作为参数。使用 Actor 系统,它获取聊天室 Actor 的引用。这个 Actor 现在已经接收到了ActorSystem消息。

使用名为 system 的 Actor 系统变量,我们还将为这个 Actor 每十秒安排一个Tick。这次,窗口间隔将是机器人通知聊天室当前状态的时间。

我们还将重写preStart函数。Akka 将在 Actor 在 actor 系统中创建时调用此函数。此实现将向房间发送一条消息,即JoinChatRoom

如同所有 Actor 一样,有接收函数的实现。第一个情况ChatMessage返回 Unit。如果您想使这个机器人对人们做出响应,请移除 Unit 并编写您想要的适当的 Scala 代码。

在第二种情况下,我们将有一个要发送到聊天室的消息字符串。最后,在这个情况之后,我们将有一个每十秒出现的Tick方法。因此,我们将使用ActorHelper从房间获取统计数据,然后我们将发送一个包含房间信息的字符串消息。这将触发第二种情况并向整个房间广播消息。

最后,我们有一个伴生对象。我们不希望有两个机器人实例,这就是为什么我们将通过设计控制这个对象创建。我们已经完成了 Actor 的实现。接下来,我们需要为聊天 Actor 工作一个新的控制器。

聊天控制器

我们需要创建一个新的控制器。这个控制器将位于ReactiveWebStore/app/controllers

实现聊天控制器

ChatController.scala应类似于以下内容:

    package controllers  
    import akka.actor.ActorSystem 
    import akka.stream.Materializer 
    import javax.inject._ 
    import play.api._ 
    import play.api.mvc._ 
    import play.api.libs.streams._ 
    import actors.ChatUserActor 
    import actors.ChatBotAdminActor  
    @Singleton 
    class ChatController @Inject() (implicit val system: ActorSystem,
    materializer: Materializer) 
    extends Controller {  
      import play.api.libs.concurrent.Execution.
      Implicits.defaultContext 
      ChatBotAdminActor(system)  
      def index_socket = Action { request => 
        Ok(views.html.chat_index()(Flash(Map()))) 
      } 
      def ws = WebSocket.accept[String, String] { request => 
        ActorFlow.actorRef(out => ChatUserActor.props(system)(out)) 
      }
    } 

ChatController方法将使用 Google Guice 来获取 Actor System 和 Actor materializer 实例的注入实例。需要一个 materializer,因为它将为系统中的每个用户提供出 Actor 的实例。

如您所见,我们将创建一个ChatBotAdmin方法的实例,通过 actor 系统传递 Google Guice 为我们注入的。对于这个控制器,我们只有一个函数:一个用于渲染聊天 UI 的函数,另一个用于服务 WebSocket。

Play 框架已经提供了与 Akka 和 WebSockets 的内置集成。因此,我们只需使用actorRef函数中的ActorFlow方法来获取出 Actor 即可。

在这里,我们将调用ChatUserActor伴生对象,并为 websocket 创建一个聊天用户,传递控制器拥有的 Actor 系统。如您所见,这返回WebSocket.accept,这是浏览器和后端之间的全双工连接。

配置路由

接下来,我们需要将我们的控制器函数暴露给用户界面。我们需要向ReactiveWebStore/conf/routes文件中添加更多路由:

    routes  
    # 
    # Akka and Websockets  
    # 
    GET /chat/index_socket   controllers.ChatController.index_socket 
    GET /chat/ws                    controllers.ChatController.ws 

路由已完成。

正在处理用户界面

现在,是时候在 UI 上编写代码了,包括 HTML 布局和 JavaScript 中的 WebSocket 代码。我们需要创建一个新文件,位于 ReactiveWebStore/app/views

您的 chat_index.scala.html 文件应该看起来像这样:

    @()(implicit flash:Flash)  
    @main("Chat"){  
    <!DOCTYPE html> 
      <meta charset="utf-8" /> 
      <title>Chat Room</title> 
      <script type="text/javascript"> 
        var output; 
        var websocket = new WebSocket("ws://localhost:9000/chat/ws");     
        function init(){ 
          output = document.getElementById("output");     
          websocket.onmessage = function(evt) { 
            writeToScreen('<span style="color:blue;">' + evt.data+ 
            '</span>'); 
          };           
          websocket.onerror = function(evt) { 
            writeToScreen('<span style="color: red;">ERROR:</span> ' + 
            evt.data); 
          }; 
        }     
        function doSend(message){ 
          websocket.send(message); 
        }     
        function writeToScreen(message){ 
          var pre = document.createElement("p"); 
          pre.style.wordWrap = "break-word"; 
          pre.innerHTML = message; 
          $('#output').prepend(pre); 
        }     
        window.addEventListener("load", init, false); 
      </script>  
      <h3>Messages</h3> 
      <div id="output" style="width: 800px; height: 250px; overflow-y: 
      scroll;" > 
      </div>  
      <div id="contentMessage">   
        <BR> 
        user:      &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 
        <input type="text" name="txtUser" id="txtUser" /> <BR><BR> 
        message: <input type="text" name="txtMessage" 
        id="txtMessage" /> 
        <BR> 
        <BR>     
        <a href="#" class="btn btn-success" 
        onclick="doSend( document.getElementById('txtUser').value + ':' 
        + document.getElementById('txtMessage').value );"> 
        <i class="icon-plus icon-white"></i>Send Message</a> 
      </div> 
    } 

UI 非常简单。有一个输入框供您输入您的名字,还有一个用于输入文本消息的输入框,以及一个发送按钮。正如您在 JavaScript 代码中所看到的,我们首先要做的事情是打开一个 WebSocket 连接到 ws://localhost:9000/chat/ws URL。然后,我们将注册 init 函数,以便在浏览器准备好后运行。

JavaScript 中的 init 函数将为我们的 WebSocket 创建两个函数。一个函数会在发生任何错误时运行,另一个函数会在 Akka 后端发出每条消息时运行。

我们将在 JavaScript 中有一个 doSend 函数,用于向 WebSocket 发送消息。这条消息将被传递到控制器,然后到 Akka Actor。您还可以看到一些用于在 UI 上创建新元素的 jQuery 和 HTML 代码。这是为了在聊天室中显示消息。

好的,还有一件事我们需要做——在我们的应用程序主页上添加对聊天 UI 的引用。

您的 Index.scala.html 应该看起来像这样:

    @(message: String)(implicit flash:Flash)  
    @main("Welcome to Reactive Web Store"){ 
    <div class="row-fluid"> 
      <BR> 
      <div class="span12">           
        <div class="row-fluid"> 
          <div class="span6"> 
            <a href="/product"><img height="42" width="42" 
            src="img/@routes.Assets.at("images/product.png")"> Manage 
            Products</a><BR> 
            <a href="/review"><img height="42" width="42" 
            src="img/@routes.Assets.at("images/review.png")"> Manage 
            Reviews</a><BR> 
            <a href="/image"><img height="42" width="42" 
            src="img/@routes.Assets.at("images/image.png")"> Manage 
            Images</a><BR> 
          </div> 
          <div class="span6"> 
            <a href="/reports"><img height="42" width="42" 
            src="img/@routes.Assets.at("images/reports.png")"> Reports </a>
            <BR> 
            <a href="/chat/index_socket"><img height="42" width="42" 
            src="img/@routes.Assets.at("images/chat.png")"> Chat Room </a>
              <BR> 
            </div> 
          </div> 
        </div>             
      </div> 
    } 

我们还将利用这个机会稍微改进一下 UI 设计,使用 Twitter Bootstrap 列表设计。在最后一行,您可以看到我们链接到聊天 UI 的链接。现在,我们可以运行应用程序并看到我们的聊天功能正在工作。运行 $ activator run

正在处理 UI

如您所见,我们的新聊天 UI 链接在那里。现在,让我们享受这个新功能带来的乐趣。打开四个新的浏览器(模拟四个不同的用户),然后转到 http://localhost:9000/chat/index_socket URL,让我们聊一聊。您应该看到类似以下的内容:

正在处理 UI

几乎完成了。我们的聊天功能已经工作;然而,我们不仅需要在 UI 上进行功能性的黑盒测试,还需要进行单元测试。幸运的是,我们有 Akka 测试套件,它允许我们轻松地测试 Actor。

添加 Akka 测试

我们将创建另外三个测试:每个 Actor 一个。它们位于 ReactiveWebStore/test/

Akka Actor 的 Scala 测试

ChatUserActorSpec.scala 应该看起来像这样:

    class OutActor extends Actor { 
      def receive = { 
        case a:Any => Unit 
      } 
    }  
    class ChatUserActorSpec extends PlaySpec {   
      class Actors extends TestKit(ActorSystem("test"))  
      "ChatUserActor" should { 
        "joins the chat room and send a message" in new Actors { 
          val probe1 = new TestProbe(system) 
          val actorOutRef = TestActorRefOutActor 
          val actorRef = TestActorRef[ChatUserActor]
          (ChatUserActor.props(system)(actorOutRef)) 
          val userActor = actorRef.underlyingActor 
          assert(userActor.context != null) 
          val msg = "testUser:test msg" 
          probe1.send(actorRef,msg) 
          actorRef.receive(msg) 
          receiveOne(2000 millis) 
        } 
      } 
    } 

Akka 测试套件非常酷,因为它允许我们使用非常简单的领域特定语言DSL)来测试 Actor。我们可以检查 Actor 的邮箱、Actor 的内部状态以及更多。我们需要做的一个小技巧是因为我们需要扩展一个类;为了使 Play 与 Scala 测试一起工作,我们需要使用 PlaySpec。然而,我们还需要扩展一个类来使 Akka 测试套件工作,这个类是 TestKit。我们不能同时扩展两个,但不用担心,总有解决方案。

这里的解决方案是创建一个 case 类,让这个 case 类扩展TestKit,然后在 spec 上下文中使用它,即在新的Actor {}中。

在这里,我们检查ChatUserActor是否能正确加入聊天室。这是通过简单地创建演员来完成的,因为演员有一个preStart方法,它将自动加入房间。

我们需要在这里创建一个假的OutActor实现,这就是为什么我们有OutActor实现的原因。我们将创建一个探针来测试演员系统,并且我们还将使用一个特殊的功能来测试演员,称为TestActorRef。这个抽象提供了一种通过actorRef.underlyingActor访问演员状态的方法,这很有用,因为你可以检查演员的内部状态以验证代码。其余的代码是正常的 Akka 和 Scala 测试代码。让我们进行下一个测试。

聊天室演员测试

ChatRoonActorSpec.scala文件应类似于以下内容:

    class ChatRoomActorSpec extends PlaySpec {    
      class Actors extends TestKit(ActorSystem("test"))  
      "ChatRoomActor" should { 
        "accept joins the chat rooms" in new Actors { 
          val probe1 = new TestProbe(system) 
          val probe2 = new TestProbe(system) 
          val actorRef = TestActorRef[ChatRoomActor]
          (Props[ChatRoomActor]) 
          val roomActor = actorRef.underlyingActor 
          assert(roomActor.users.size == 0) 
          probe1.send(actorRef, JoinChatRoom) 
          probe2.send(actorRef, JoinChatRoom) 
          awaitCond(roomActor.users.size == 2, 100 millis) 
          assert(roomActor.users.contains(probe1.ref)) 
          assert(roomActor.users.contains(probe2.ref)) 
        } 
        "get stats from the chat room" in new Actors { 
          val probe1 = new TestProbe(system) 
          val actorRef = TestActorRef[ChatRoomActor]
          (Props[ChatRoomActor]) 
          val roomActor = actorRef.underlyingActor 
          assert(roomActor.users.size == 0) 
          probe1.send(actorRef, JoinChatRoom) 
          awaitCond(roomActor.users.size == 1, 100 millis) 
          assert(roomActor.users.contains(probe1.ref)) 
          probe1.send(actorRef, GetStats) 
          receiveOne(2000 millis) 
        } 
        "and broadcast messages" in new Actors { 
          val probe1 = new TestProbe(system) 
          val probe2 = new TestProbe(system) 
          val actorRef = TestActorRef[ChatRoomActor]
          (Props[ChatRoomActor]) 
          val roomActor = actorRef.underlyingActor 
          probe1.send(actorRef, JoinChatRoom) 
          probe2.send(actorRef, JoinChatRoom) 
          awaitCond(roomActor.users.size == 2, 100 millis) 
          val msg = ChatMessage("sender", "test message") 
          actorRef.receive(msg) 
          probe1.expectMsg(msg) 
          probe2.expectMsg(msg) 
        } 
        "and track users ref and counts" in new Actors { 
          val probe1 = new TestProbe(system) 
          val probe2 = new TestProbe(system)  
          val actorRef = TestActorRef[ChatRoomActor]
          (Props[ChatRoomActor]) 
          val roomActor = actorRef.underlyingActor 
          probe1.send(actorRef, JoinChatRoom) 
          probe2.send(actorRef, JoinChatRoom) 
          awaitCond(roomActor.users.size == 2, 100 millis) 
          probe2.ref ! PoisonPill 
          awaitCond(roomActor.users.size == 1, 100 millis) 
        }
      }
    } 

因此,这里我们有与其他测试相同的概念。然而,我们使用了更多的 Akka 测试 kit DSL。例如,我们正在使用expectMsg在探针上检查演员是否接收到了特定的消息。我们还在断言中使用awaitCond来检查演员的内部状态。

现在是测试最后一个演员方法的时候了。

聊天机器人管理员演员测试

ChatBotAdminActorSpec.scala文件应类似于以下内容:

    class ChatBotAdminActorSpec extends TestKit(ActorSystem("test")) 
    with ImplicitSender 
    with WordSpecLike with Matchers with BeforeAndAfterAll {  
      "ChatBotAdminActor" should { 
        "be able to create Bot Admin in the Chat Room and Tick" in { 
          val probe1 = new TestProbe(system) 
          val actorRef = TestActorRefChatBotAdminActor)) 
          val botActor = actorRef.underlyingActor 
          assert(botActor.context != null) 
          awaitCond(botActor.room != null ) 
        } 
      } 
    } 

对于这次测试,我们将检查演员上下文是否不为空,以及房间是否已创建,调度器也不为空。一切准备就绪。

好的,这就结束了!这是最后一个演员测试。现在我们已经全部完成。你可以使用$ activator test运行这个测试,或者,如果你更喜欢 activator,那么使用"test-only TESTCLASSNAME" -Dsbt.task.forcegc=false来运行特定的测试用例。

摘要

在本章中,你学习了如何使用 Akka 演员,并使用 Akka、Play 框架和 WebSockets 创建了一个网络聊天。Akka 是一个真正强大的解决方案,可以与或无需 Play 框架一起使用。此外,你还了解了演员模型、邮箱、路由、持久化、Akka 配置、消息模式,以及如何在 Scala 和 Play 中使用演员编写代码。

在下一章中,你将了解更多关于 REST、JSON 以及如何建模 REST API 的内容,以及如何为你的 REST 服务创建 Scala 客户端。

第九章:设计你的 REST API

在上一章中,我们使用 Akka 在我们的应用程序中添加了一个新的聊天功能。我们的 Web 应用程序接近完成。本章将在我们的 Play 框架应用程序中添加 REST API。

我们还将使用 Play 框架中的ws库创建一个 Scala 客户端来调用我们的 REST API。在本章的后面,我们将添加 Swagger 支持并将 Swagger UI 嵌入到我们的应用程序中。

在本章中,我们将涵盖以下主题:

  • REST 和 API 设计

  • 使用 REST 和 JSON 创建我们的 API

  • 创建 Scala 客户端

  • 添加验证

  • 添加回压

  • 添加 Swagger 支持

REST 简介

表征状态转移REST)是一种架构风格。它由 Roy Fielding 在他的博士论文中定义。REST 通过 HTTP 1.1 协议使用 HTTP 动词进行,例如GETPOSTDELETEPUT统一资源标识符URI),例如/users/profile/1sales/cart/add/2

REST 架构有以下属性:

  • 简单性:几乎所有的语言都有库来操作 HTTP URI。

  • 互操作性:REST 是语言、平台和操作系统无关的。

  • 可扩展和可靠:因为 REST 基于 HTTP,你可以使用 HTTP 服务器与 HTTP 负载均衡器、HTTP 缓存和 HTTP DNS 一起扩展你的应用程序。

  • 关注点分离SOC):因为你有一个 URI,这是你的合同,而不是代码、底层后端或数据库。这意味着你可以更改数据库或语言,而不会影响代码。

  • 客户端/服务器:有一个提供 REST 接口的服务器,以及调用 REST 端点的客户端。

采纳 REST 原则的 Web 服务通常被称为 RESTful。

REST API 设计

当你使用 REST 时,有一些原则你应该记住,并且这些原则应该为你在进行 API 设计时的设计选择提供指导。

HTTP 动词设计

这些是 HTTP 中找到的以下动词:

  • GET:这通常用于回答查询

  • PUT:这通常用于插入数据

  • POST:这通常用于更新数据

  • DELETE:这通常用于删除数据

为什么我们总是说“通常”?好吧,在大小限制方面有一些例外。例如,对于GET动词,我们无法有一个大于 8192 字节或 8 KB 的请求。如果你需要发送更大的有效负载,我们需要使用POST动词。

统一 API

REST 使用统一的 API。例如,考虑以下代码片段:

    GET          /users/1   = List information about user id 1 
    PUT          /users/1   = Insert user 1 
    POST         /users/1   = Update user 1 
    DELETE       /users/1   =  Delete user 1 
    GET          /users/    = Lists All users 

如果我们将资源从用户更改为销售,API 几乎相同。数据检索使用GET进行,更新通过POST完成,因此这是一个统一的 API。

带有 HTTP 状态码的响应

REST 使用 HTTP 1.1 状态码运行错误处理器。例如:

  • 200 -> OK:这通常与GET动词一起使用

  • 201 -> 已创建:这通常由PUT/POST动词使用

  • 204 -> 无内容:这通常用于DELETE动词

  • 400 -> 无效请求:这通常意味着对 POST/PUT 动词的无效请求

  • 404 -> 未找到:这通常与 GET 动词一起使用

  • 500 -> 内部服务器错误 - 意外服务器错误:这通常由所有动词使用

REST API 模式

对于良好的和清晰的 REST API 设计,有一些常见的模式,如下所示:

  • 使用名词;不要使用动词:通常,你可以使用标准的 URI,例如 /cars//members/。你不应该使用 /getCars/getMembers/,因为你在使用带有动词的 URI,而动词已经说明了动作。

  • GET 方法不应改变状态:如果你想改变服务器的状态,你需要使用动词,如 PUTPOSTDELETEGET 不应改变服务器的状态,因此可以安全地多次调用 GET。这被称为幂等。

  • 优先使用子资源关系:假设我们有一个名为 /users/ 的资源,一个用户有项目。始终使用子资源,如 /users/1/projects/2 是一个好主意,因为我们有用户和项目之间的关系。

  • 使用 HTTP 头部:HTTP 头部应该用于序列化、安全性和应用程序需要的所有类型的元数据。HTTP 头部通常用于内容协商。例如,你可能做以下操作:

    HTTP HEADER Content-Type = XML - GET /cars/1  
    HTTP HEADER Content-Type = JSON - GET /cars/1  

  • URI 是相同的;然而,根据头部类型,它将以 XML 或 JSON 格式返回数据。

  • 过滤、排序和分页:有时,你的数据可能很大。提供排序、过滤和分页的机制总是一个好主意,如下所示:

    GET /projects/1/tasks?by=priority             -> Sorting  
    GET /projects/1/tasks?status=done             -> Filter 
    GET /projects/1/tasks?limit=30&offset=5       -> Pagination 

API 版本化

有两种执行 API 版本化的方式。第一种策略是通过对端点进行显式版本化,例如 /v1/cars。第二种策略基于元数据,例如 /cars/,但此时你需要传递一个 HTTP 头部版本作为 v1。

这两种策略都有优点和缺点。显式版本化更清晰,你总是可以创建一个新版本而不会破坏你的消费者。头部策略更优雅;然而,它可能会变得难以管理。

需要避免的一些反模式

在 REST API 设计中存在一些陷阱,但以下事项需要避免:

  • GET 动词用于所有事物

  • 忽略 HTTP 头部,如 MIME 类型

  • 发生错误时返回 200

  • 对于无效参数或缺失参数返回 500

使用 REST 和 JSON 创建我们的 API

好的,现在是时候为你的 Play 框架应用程序设计一个 REST API 了。我们将创建一个 API 来导出系统中的所有数据。这个 API 将是只读的;然而,如果你愿意,可以添加写操作。

在本章的后面部分,我们将添加一些背压来限制消费者的 API REST 速率,并为我们的 REST API 创建一个 Scala 客户端应用程序。所以,首先,让我们从 Play 框架(服务器)开始。

在我们的 Play 框架应用程序中创建 REST API,我们不需要任何额外的库。我们只需要一个新的控制器和新的路由。此外,我们将利用前几章中编写的代码。

RestApiContoller

让我们在 ReactiveWebStore/app/controllers 中创建一个新的控制器。

REST API 前端控制器实现

RestApiController.scala 文件应类似于以下内容:

    package controllers  
    @Singleton 
    class RestAPIController @Inject() 
    (val productService:IProductService, 
      val reviewService:IReviewService, 
    val imageService:IImageService) extends Controller {  
      import play.api.libs.concurrent.Execution.
      Implicits.defaultContext  
      def listAllProducts = Action { 
        val future = productService.findAll() 
        val products = Awaits.get(5,future) 
        val json = ProductsJson.toJson(products) 
        Ok(json) 
      }  
      def listAllReviews = Action { 
        val future = reviewService.findAll() 
        val reviews = Awaits.get(5,future) 
        val json = ReviewsJson.toJson(reviews) 
        Ok(json) 
      } 
      def processImages = { 
        val future = imageService.findAll() 
        val images = Awaits.get(5,future) 
        val json = ImagesJson.toJson(images) 
        json 
      } 
      def listAllImages = Action { 
        Ok(processImages) 
      }
    } 

基本上,这里有三个函数。这些函数列出所有产品、图片和评论。如您在控制器顶部所见,我们正在注入用于产品、图片和评论的三个服务。

所有函数的代码相当直接。首先,我们将调用适当的服务,然后使用 await 对象等待结果。一旦我们有了数据,我们将调用一个函数将数据转换为 JSON。

让我们看看这里使用的 JSON 辅助对象。

JSON 映射

我们的 REST 控制器使用了 JSON 辅助对象将对象映射到 JSON。首先,我们将从 Products JSON 辅助对象开始。

ProductsJson 位于 ReactiveWebStore/app/controllers/Product.scala

    object ProductsJson {  
      import play.api.libs.json._ 
      import play.api.libs.json.Reads._ 
      import play.api.libs.functional.syntax._  
      implicit val productWrites: Writes[Product] = ( 
        (JsPath \ "id").write[Option[Long]] and 
        (JsPath \ "name").write[String] and 
        (JsPath \ "details").write[String] and 
        (JsPath \ "price").write[BigDecimal] 
      )(unlift(Product.unapply)) 
      implicit val productReads: Reads[Product] = ( 
        (JsPath \ "id").readNullable[Long] and 
        (JsPath \ "name").read[String] and 
        (JsPath \ "details").read[String] and 
        (JsPath \ "price").read[BigDecimal] 
      )(Product.apply _)  
      def toJson(products:Option[Seq[Product]]) = Json.toJson(products) 
    } 

基本上,这里有三个重要的概念。首先,我们有 productsWrites,它将 JSON 映射到 model,以及用于写入的 Product,这同样也被称为反序列化。我们还有一个用于序列化的映射,称为 productsReads,它将对象转换为 JSON。

如您所见,我们需要映射模型中存在的所有字段,例如 ID、名称、详情和价格。这种映射必须匹配适当的类型。ID 映射使用 readNullable,因为 ID 是可选的。

最后,我们有一个将 JSON 转换为对象的函数,称为 toJson,它使用一个名为 JSON 的通用 Play 框架库。让我们转到下一个辅助对象——评论。

ReviewsJson 位于 ReactiveWebStore/app/controllers/Review.scala,其结构应类似于以下内容:

    object ReviewsJson {  
      import play.api.libs.json._ 
      import play.api.libs.json.Reads._ 
      import play.api.libs.functional.syntax._  
      implicit val reviewWrites: Writes[Review] = ( 
        (JsPath \ "id").write[Option[Long]] and 
        (JsPath \ "productId").write[Option[Long]] and 
        (JsPath \ "author").write[String] and 
        (JsPath \ "comment").write[String] 
      )(unlift(Review.unapply)) 
      implicit val reviewReads: Reads[Review] = ( 
        (JsPath \ "id").readNullable[Long] and 
        (JsPath \ "productId").readNullable[Long] and 
        (JsPath \ "author").read[String] and 
        (JsPath \ "comment").read[String] 
      )(Review.apply _)  
      def toJson(reviews:Option[Seq[Review]]) = Json.toJson(reviews) 
    } 

在这里,我们看到了之前在 Products JSON 辅助对象中看到的相同概念。我们有一个用于读取和写入的映射,以及一个将 model.Review 转换为 JSON 的函数。让我们转到最后的辅助对象,即 ImageJson

ImagesJson 位于 ReactiveWebStore/app/controllers/Image.scala,其结构应类似于以下内容:

    object ImagesJson {  
      import play.api.libs.json._ 
      import play.api.libs.json.Reads._ 
      import play.api.libs.functional.syntax._  
      implicit val imagesWrites: Writes[Image] = ( 
        (JsPath \ "id").write[Option[Long]] and 
        (JsPath \ "productId").write[Option[Long]] and 
        (JsPath \ "url").write[String] 
      )(unlift(Image.unapply))     
      implicit val imagesReads: Reads[Image] = ( 
        (JsPath \ "id").readNullable[Long] and 
        (JsPath \ "productId").readNullable[Long] and 
        (JsPath \ "url").read[String] 
      )(Image.apply _)  
      def toJson(images:Option[Seq[Image]]) = Json.toJson(images) 
    } 

就像其他两个映射器一样,我们有读取、写入、映射和 toJson 函数。我们已经完成了映射器,所以下一步是创建新的路由。

配置新路由

我们需要为我们的 REST API 添加以下三个新路由,该 API 位于 ReactiveWebStore/conf/routes

    # 
    # REST API 
    # 
    GET /REST/api/product/all 
    controllers.RestAPIController.listAllProducts 
    GET /REST/api/review/all 
    controllers.RestAPIController.listAllReviews 
    GET /REST/api/image/all controllers.RestAPIController.listAllImages 

如您所见,我们已将刚刚创建的所有列表操作进行了映射。

使用浏览器测试 API

现在,我们可以运行 $ activator run 并使用我们的网络浏览器测试新的 REST API。

访问 http://localhost:9000/REST/api/product/all;您应该会看到以下截图类似的内容:

使用浏览器测试 API

让我们看看评论 API。

前往 http://localhost:9000/REST/api/review/all;您应该看到类似于以下截图的结果:

使用浏览器测试 API

最后,让我们查看 REST API 的图片。

前往 http://localhost:9000/REST/api/image/all;您应该看到类似于以下截图的结果:

使用浏览器测试 API

好的。现在我们将继续使用 REST。我们刚刚完成了服务器;然而,创建一个 REST 客户端来消费这些 REST API 是很重要的。

创建 Scala 客户端

首先,您需要创建一个新的项目。进入您的文件系统,创建一个名为 rest-client 的文件夹。然后,在 rest-client 内创建另一个名为 project 的文件夹。在 project 内,您需要添加以下两个文件:

  • build.properties:此文件包含 SBT 配置,例如版本

  • Plugins.sbt:此文件包含 SBT 插件配置

让我们从 build.properties 开始:

    sbt.version=0.13.11 

如您所见,我们正在配置此项目以使用 SBT 版本 0.13.11。现在,我们可以转到插件文件。

配置 plugins.sbt

您的 plugins.sbt 文件应该看起来像这样:

    addSbtPlugin("com.typesafe.sbteclipse" % "sbteclipse-plugin" % 
      "2.5.0") 
    addSbtPlugin("com.github.mpeltonen" % "sbt-idea" % "1.6.0") 

在这里,我们添加了 Eclipse 和 IntelliJ 的支持。对于这本书,我们使用 Eclipse,但请随意使用您喜欢的任何东西。

project 文件夹外部,在 rest-client 下,我们需要配置 build.sbt 文件。

配置 build.sbt

您的 build.sbt 文件应该看起来像这样:

    name := "rest-client"  
    version := "1.0" 
    scalaVersion := "2.11.7" 
    scalaVersion in ThisBuild := "2.11.7"  
    resolvers += DefaultMavenRepository 
    resolvers += JavaNet1Repository 
    resolvers += "OSSSonatype" at 
    "https://oss.sonatype.org/content/repositories/releases" 
    resolvers += "Sonatype OSS Snapshots" at 
    "https://oss.sonatype.org/content/repositories/snapshots" 
    resolvers += "Sonatype OSS Snapshots" at 
    "https://oss.sonatype.org/content/repositories/snapshots" 
    resolvers += "amateras-repo" at 
    "http://amateras.sourceforge.jp/mvn/"  
    libraryDependencies += "com.typesafe.play" % "play-ws_2.11" % 
    "2.5.6" 
    libraryDependencies += "org.scalatest" % "scalatest_2.11" % "2.2.6" 
    % Test 

因此,我们在这里使用 Scala 版本 2.11.7,并且我们只声明了两个依赖项。一个依赖项是用于测试的 scala-test,另一个依赖项是 Play 框架的 ws 库,我们将使用它来调用我们的 REST API。

让我们也创建两个源文件夹,如下所示:

  • src/main/scala:这是 Scala 源代码

  • src/test/scala:这是 Scala 测试源代码

好的。现在我们可以运行 $ sbt clean compile eclipse 以从网络下载依赖项并创建我们需要的所有 Eclipse 项目文件。

配置 build.sbt

现在,我们可以将此代码导入 Eclipse 并继续下一步。

Scala 客户端代码

首先,我们需要创建一个 Factory 来实例化 WS Play 框架库以调用 webservices。在 rest-client/src/main/scala 位置下,让我们创建一个名为 client 的包,并在 WSFactory.scala 下添加以下代码:

    package client  
    object WSFactory {  
      import akka.actor.ActorSystem 
      import akka.stream.ActorMaterializer  
      def ws = {  
        implicit val system = ActorSystem() 
        implicit val materializer = ActorMaterializer()  
        import com.typesafe.config.ConfigFactory 
        import play.api._ 
        import play.api.libs.ws._ 
        import play.api.libs.ws.ahc.{AhcWSClient, AhcWSClientConfig} 
        import play.api.libs.ws.ahc.AhcConfigBuilder 
        import org.asynchttpclient.AsyncHttpClientConfig 
        import java.io.File  
        val configuration = Configuration.reference ++ 
        Configuration(ConfigFactory.parseString( 
          """ 
          |ws.followRedirects = true 
        """.stripMargin))  
        val parser = new WSConfigParser(configuration, 
        play.api.Environment.simple(
        new File("/tmp/"), null)) 
        val config = new AhcWSClientConfig(wsClientConfig = 
        parser.parse()) 
        val builder = new AhcConfigBuilder(config) 
        val logging = new 
        AsyncHttpClientConfig.AdditionalChannelInitializer() { 
          override def initChannel(channel: io.netty.channel.Channel): 
          Unit = { 
            channel.pipeline.addFirst("log", new 
            io.netty.handler.logging.LoggingHandler("debug")) 
          }
        }  
        val ahcBuilder = builder.configure() 
        ahcBuilder.setHttpAdditionalChannelInitializer(logging) 
        val ahcConfig = ahcBuilder.build() 
        new AhcWSClient(ahcConfig) 
      }
    } 

上述代码仅是技术性的。这些是在 Play 框架外部实例化 WSClient 所需的步骤。如果这个客户端是一个使用 Play 框架的 Web 应用程序,那么会容易得多,因为我们可以直接使用 Google Guice 并注入所需的内容。

您需要记住的主要思想是,您需要使用 Akka 和 ActorSystem 来使用此功能。如您所见,所有这些代码都锁定在一个名为 ws 的单个函数中的对象内。

我们需要一个用于处理 Future 的实用类。因为我们使用ws库调用 REST API,它返回 Future。所以,让我们创建一个新的包叫做utils

你的Awaits.scala文件应该看起来像这样:

    package utils  
    import scala.concurrent.Future 
    import scala.concurrent.duration._ 
    import scala.concurrent.Await  
    object Awaits { 
      def getT:T = { 
        Await.resultT 
      }
    } 

前面的代码相当简单。我们使用了Await对象,并使用了一个通用的T来将结果转换为泛型参数化类型。通过使用这个参数,我们还将接收到在超时前应该等待多少秒。

创建我们的 REST 客户端代理

我们现在将进行 REST 调用;然而,我们将创建一个 Scala API。因此,使用我们的rest-client的开发者不需要处理 REST,只需执行 Scala 代码。这有很多好处,其中一些如下:

  • SOC:我们仍然在 Play 框架和客户端应用之间有职责分离

  • 隔离:如果 REST API 发生变化,我们将在代理层处理它

  • 抽象:客户端代码的其余部分只使用 Scala,并且不知道任何关于 REST 或 HTTP 调用的事情

这些技术现在在微服务中非常常见。这些技术也可以被称为驱动程序或厚客户端。现在,我们需要创建三个代理,每个 REST API 上的资源一个,让我们创建一个新的包叫做proxy

你的ProductProxy.scala文件应该看起来像这样:

    package proxy  
    import client.WSFactory 
    import utils.Awaits  
    case class Product 
    ( var id:Option[Long], 
      var name:String, 
      var details:String, 
    var price:BigDecimal ) { 
      override def toString:String = { 
        "Product { id: " + id.getOrElse(0) + ",name: " + name + ", 
        details: "+ details + ", price: 
        " + price + "}" 
      }
    }  
    object ProductsJson {  
      import play.api.libs.json._ 
      import play.api.libs.json.Reads._ 
      import play.api.libs.functional.syntax._ 
      implicit val productWrites: Writes[Product] = (  
        (JsPath \ "id").write[Option[Long]] and 
        (JsPath \ "name").write[String] and 
        (JsPath \ "details").write[String] and 
        (JsPath \ "price").write[BigDecimal] 
      )(unlift(Product.unapply)) 
      implicit val productReads: Reads[Product] = ( 
        (JsPath \ "id").readNullable[Long] and 
        (JsPath \ "name").read[String] and 
        (JsPath \ "details").read[String] and 
        (JsPath \ "price").read[BigDecimal] 
      )(Product.apply _)  
      def toJson(products:Option[Seq[Product]]) = Json.toJson(products) 
    }  
    object ProductProxy {  
      import scala.concurrent.Future 
      import play.api.libs.json._ 
      import ProductsJson._  
      val url = "http://localhost:9000/REST/api/product/all" 
      implicit val context = 
      play.api.libs.concurrent.Execution.Implicits.defaultContext  
      def listAll():Option[List[Product]] = { 
        val ws = WSFactory.ws 
        val futureResult:Future[Option[List[Product]]] = 
        ws.url(url).withHeaders("Accept" -> 
        "application/json").get().map( 
          response => 
          Json.parse(response.body).validate[List[Product]].asOpt 
        )  
        val products = Awaits.get(10, futureResult) 
        ws.close 
        products 
      }
    } 

在这个代码中,我们有三个主要概念。首先,我们有一个表示产品的case类。前面的代码与我们在 Play 框架应用中的代码非常相似。然而,如果你仔细观察,你会发现它要干净得多,因为我们没有围绕持久性的任何元数据。

你可能会想,这是重复的代码!是的,这是完全正确的。重复的代码是解耦的。记住,我们有一个 REST 接口,还有一个在客户端代码其余部分的代理,因此我们至少有两层间接处理,可以应对变化。如果这两个代码库共享相同的类,我们就会有耦合,并且容纳变化的空间会减少。

这里第二个主要概念是映射。我们将接收 JSON,并希望将其转换为我们的case类,因此我们将有与在 Play 框架应用中类似的映射。

最后,我们有代理实现。我们将使用我们的工厂实例化 Play 框架的WS库,并调用ws函数。然后,我们将使用url函数传递产品的 REST API URI,并定义一个头以接受 JSON。我们也是使用 HTTP 动词GET来做这件事。响应通过Json.parse传递response.body进行映射。此外,我们将调用验证函数以确保这个 JSON 与我们的case类匹配。这种验证很重要,因为这样我们可以确保格式没有改变,并且一切正常。WS将返回这个作为 Future,所以我们将使用我们的Awaits辅助函数来获取结果。

让我们转到下一个代理,即审查。

您的 ReviewProxy.scala 文件应该看起来像这样:

    package proxy  
    import client.WSFactory 
    import utils.Awaits  
    case class Review 
    (var id:Option[Long], 
      var productId:Option[Long], 
      var author:String, 
    var comment:String) 
    { 
      override def toString:String = { 
        "Review { id: " + id + " ,productId: " + productId.getOrElse(0) 
        + ",author: " + author + 
        ",comment: " + comment + " }" 
      } 
    }  
    object ReviewsJson {  
      import play.api.libs.json._ 
      import play.api.libs.json.Reads._ 
      import play.api.libs.functional.syntax._  
      implicit val reviewWrites: Writes[Review] = ( 
        (JsPath \ "id").write[Option[Long]] and 
        (JsPath \ "productId").write[Option[Long]] and 
        (JsPath \ "author").write[String] and 
        (JsPath \ "comment").write[String] 
      )(unlift(Review.unapply))  
      implicit val reviewReads: Reads[Review] = ( 
        (JsPath \ "id").readNullable[Long] and 
        (JsPath \ "productId").readNullable[Long] and 
        (JsPath \ "author").read[String] and 
        (JsPath \ "comment").read[String] 
      )(Review.apply _) 
      def toJson(reviews:Option[Seq[Review]]) = Json.toJson(reviews) 
    }  
    object ReviewProxy { 
      import scala.concurrent.Future 
      import play.api.libs.json._ 
      import ReviewsJson._ 
      val url = "http://localhost:9000/REST/api/review/all" 
      implicit val context = 
      play.api.libs.concurrent.Execution.Implicits.defaultContext 
      def listAll():Option[List[Review]] = { 
        val ws = WSFactory.ws 
        val futureResult:Future[Option[List[Review]]] = 
        ws.url(url).withHeaders("Accept" -> 
        "application/json").get().map( 
          response => 
          Json.parse(response.body).validate[List[Review]].asOpt 
        ) 
        val reviews = Awaits.get(10, futureResult) 
        ws.close 
        reviews 
      }
    } 

这里,我们有与产品代理相同的原理,但这次是为了审查。正如您所看到的,我们将调用不同的 URI。现在,让我们转到最后一个代理--ImageProxy.scala 文件。

您的 ImageProxy.scala 文件应该看起来像这样:

    package proxy  
    import client.WSFactory 
    import utils.Awaits  
    case class Image 
    (var id:Option[Long], 
      var productId:Option[Long], 
    var url:String) 
    { 
      override def toString:String = { 
        "Image { productId: " + productId.getOrElse(0) + ",url: " + url 
        + "}" 
      } 
    }  
    object ImagesJson {  
      import play.api.libs.json._ 
      import play.api.libs.json.Reads._ 
      import play.api.libs.functional.syntax._  
      implicit val imagesWrites: Writes[Image] = ( 
        (JsPath \ "id").write[Option[Long]] and 
        (JsPath \ "productId").write[Option[Long]] and 
        (JsPath \ "url").write[String] 
      )(unlift(Image.unapply))  
      implicit val imagesReads: Reads[Image] = ( 
        (JsPath \ "id").readNullable[Long] and 
        (JsPath \ "productId").readNullable[Long] and 
        (JsPath \ "url").read[String] 
      )(Image.apply _) 
      def toJson(images:Option[Seq[Image]]) = Json.toJson(images) 
    } 
    object ImageProxy { 
      import scala.concurrent.Future 
      import play.api.libs.json._ 
      import ImagesJson._ 
      val url = "http://localhost:9000/REST/api/image/all" 
      implicit val context =  
      play.api.libs.concurrent.Execution.Implicits.defaultContext 
      def listAll():Option[List[Image]] = { 
        val ws = WSFactory.ws 
        val futureResult:Future[Option[List[Image]]] = 
        ws.url(url).withHeaders("Accept" -> 
        "application/json").get().map( 
          response => 
          Json.parse(response.body).validate[List[Image]].asOpt 
        ) 
        val images = Awaits.get(10, futureResult) 
        ws.close 
        images 
      }
    } 

就这样。我们与产品和审查有相同的概念。我们已经完成了所有代理。现在,是时候测试我们的代理实现了。最好的方式是通过测试,所以让我们为这三个实现创建 Scala 测试。

为代理创建 ScalaTest 测试

/src/test/scala 源文件夹下,我们需要创建一个名为 proxy.test 的包。

您的 ProductProxtTestSpec.scala 应该看起来像这样:

    package proxy.test  
    import org.scalatest._ 
    import proxy.ProductProxy  
    class ProductProxtTestSpec extends FlatSpec with Matchers {  
      "A Product Rest proxy " should "return all products" in { 
        val products = ProductProxy.listAll().get 
        products shouldNot(be(null)) 
        products shouldNot(be(empty))  
      }
    } 

测试相当简单;我们只需在我们的产品代理中调用 listAll 操作,然后添加一些断言以确保结果不是 null。我们还会在控制台显示所有产品。

现在,我们需要为审查代理创建测试,这将与产品类似。

您的 ReviewProxyTestSpec.scala 文件应该看起来像这样:

    package proxy.test  
    import org.scalatest._ 
    import proxy.ReviewProxy  
    class ReviewProxyTestSpec extends FlatSpec with Matchers {  
      "A Review REST Proxy " should "return all reviews" in { 
        val reviews = ReviewProxy.listAll().get 
        reviews shouldNot(be(null)) 
        reviews shouldNot(be(empty)) 
        for( r <- reviews){ 
          println(r) 
        }
      }
    } 

在这里,我们使用了代理思想来测试审查。我们使用 listAll 函数调用代理以获取所有审查。稍后,我们将检查审查是否不为 null。我们将打印所有审查。最后,是时候转到最后一个代理测试--图像代理。

您的 ImageProxyTestSpec.scala 应该看起来像这样:

    package proxy.test  
    import org.scalatest._ 
    import proxy.ImageProxy 
    import scala.concurrent.Future 
    import play.api.libs.concurrent.Execution.Implicits.defaultContext 
    import java.util.concurrent.CountDownLatch  
    class ImageProxyTestSpec extends FlatSpec with Matchers {  
      "A Image REST Proxy " should "return all images" in { 
        val images = ImageProxy.listAll().get 
        images shouldNot(be(null)) 
        images shouldNot(be(empty)) 
        for( i <- images){ 
          println(i) 
        }
      }
    } 

对于图像代理也是同样的情况。我们已经有所有测试;现在,我们可以运行测试。您需要确保我们的 ReactiveWebStore Play 框架应用正在运行。

让我们使用 sbt 运行这个测试:

打开您的控制台,输入 $ sbt test

为代理创建 ScalaTest 测试

好的,一切正常!我们的下一步将是添加反压。

添加反压

反压是汽车行业中的一个众所周知的概念。如今,这个术语在软件工程中也被使用。在汽车世界中,反压是指在一个封闭空间(如管道)中与期望的气体流动方向相反的压力。对于软件工程,它通常与减缓生产者(可以是应用程序、流处理引擎,甚至是用户本身)有关。

当我们执行 REST 时,很容易达到客户端可以饱和服务器的情况。这也可以是一种安全漏洞,也被称为拒绝服务DOS)攻击。

有两种架构场景。在第一种场景中,您的 REST API 是内部的,您公司中只有消费者。在第二种场景中,您将 REST API 作为公共 API,使其对整个互联网开放。对于这种情况,您真的应该有反压,也称为节流。

我们可以扩展我们的架构以处理更多用户。我们将在第十章扩展中讨论这一点,以及可扩展性技术。

目前,有几种方法可以应用背压。例如,如果我们的代码是纯 RxScala/RxJava,我们可以在可观察对象上应用背压。更多详情可以在github.com/ReactiveX/RxJava/wiki/Backpressure找到。

由于我们正在公开 REST 接口,我们将在控制器上添加背压,因此我们需要创建一个新的类来包含背压代码。

有一些算法用于背压;我们将使用漏桶算法。该算法本身非常简单——只有 30 行 Scala 代码。

漏桶算法

漏桶的隐喻相当简单。让我们在下面的图中看看它:

漏桶算法

该算法背后的隐喻是基于一个带孔的桶。水流入或滴入桶中,并通过桶的孔泄漏。如果水太多,桶满了,水就会从桶中溢出——换句话说,它将被丢弃。

此算法用于网络编程,也被电信行业所采用。API 管理解决方案也是此算法的应用场景。

此概念允许速率限制约束。我们可以用每时间请求的数量来表示背压速率限制。在这种情况下,时间通常以秒或分钟来衡量,因此我们有每秒请求数(RPS)或每分钟请求数(RPM)。

你可以用队列来实现这个算法。然而,在我们的实现中,我们不会使用队列;我们将使用时间来控制流量。我们的实现也将是无锁的,或非阻塞的,因为我们不会使用线程或外部资源。

现在是时候用 Scala 编写漏桶算法了。首先,我们将为 ReactiveWebStore 应用程序创建此代码。我们需要在 ReactiveWebStore/app 位置创建一个新的包。新包的名称将是 backpressure

Scala 漏桶实现

你的 LeakyBucket.scala 文件应该包含以下内容:

    package backpresurre  
    import scala.concurrent.duration._ 
    import java.util.Date  
    class LeakyBucket(var rate: Int, var perDuration: FiniteDuration) {  
      var numDropsInBucket: Int = 0 
      var timeOfLastDropLeak:Date = null 
      var msDropLeaks = perDuration.toMillis  
      def dropToBucket():Boolean = { 
        synchronized { 
          var now = new Date()  
          if (timeOfLastDropLeak != null) { 
            var deltaT = now.getTime() - timeOfLastDropLeak.getTime() 
            var numberToLeak:Long = deltaT / msDropLeaks  
              if (numberToLeak > 0) { 
                if (numDropsInBucket <= numberToLeak) { 
                  numDropsInBucket -= numberToLeak.toInt 
                } else { 
                numDropsInBucket = 0 
              } 
              timeOfLastDropLeak = now 
            } 
          }else{ 
            timeOfLastDropLeak = now   
          }  
          if (numDropsInBucket < rate) { 
            numDropsInBucket = numDropsInBucket + 1 
            return true; 
          } 
          return false; 
        }
      }
    } 

如你所见,我们创建了一个 Scala 类,它接收两个参数:rateperDuration。速率是一个整数,表示在应用背压之前我们能处理多少个请求。"PerDuration" 是 Scala 的 FiniteDuration,可以是任何时间度量,如毫秒、秒、分钟或小时。

此算法跟踪桶中最后滴水的时刻。如你所见,代码是同步的,但这是可以的,因为我们不会调用外部资源或线程。

首先,我们将使用 new Date() 获取当前时间。第一次运行算法时,我们将在 else 语句上失败,并将当前时间作为最后泄漏的时间。

第二次运行时,它将进入第一个 If 语句。然后,我们将计算上一次漏桶和现在之间的差值(diff)。这个差值将除以你在 perDuration 上传递的毫秒数。如果差值大于 0,那么我们将泄漏;否则我们丢弃。然后,我们将再次捕获上一次泄漏的时间。

最后,我们将检查掉落率。如果速率较小,我们将增加并返回 true,这意味着请求可以继续进行;否则,我们将返回 false,请求不应继续。

现在我们已经用 Scala 编码了这个算法,我们可以调用我们的一个控制器。我们将在这个图像 REST API 上添加这个背压。

你的 RestApiController.scala 应该看起来像这样:

    package controllers  
    class RestAPIController @Inject() 
    (val productService:IProductService, 
      val reviewService:IReviewService, 
      val imageService:IImageService) extends Controller {  
        import 
        play.api.libs.concurrent.Execution.Implicits.defaultContext  
        // REST of the Controller...  
        import scala.concurrent.duration._  
        var bucket = new LeakyBucket(5, 60 seconds)  
        def processImages = { 
          val future = imageService.findAll() 
          val images = Awaits.get(5,future) 
          val json = ImagesJson.toJson(images) 
          json 
        }  
        def processFailure = { 
          Json.toJson("Too Many Requests - Try Again later... ") 
        } 
        def listAllImages = Action { 
          bucket.dropToBucket() match { 
            case true => Ok(processImages) 
            case false => 
            InternalServerError(processFailure.toString()) 
          }
        }
     } 

这里,我们将创建一个每分钟五个请求的漏桶。我们有两个函数:一个用于处理调用服务并将对象转换为 JSON 的图像,另一个用于处理失败。processFailure 方法将只发送一条消息,说明请求太多,我们现在无法接受请求。

因此,对于 listAllImages 函数,我们只需尝试调用漏桶并使用 Scala 模式匹配器来处理适当的响应。如果响应为 true,我们将返回带有 200 HTTP 代码的 JSON。否则,我们将返回 500 内部错误并拒绝该请求。在这里,我们实现了一个全局速率限制器;然而,大多数时候,人们按用户执行此操作。现在,让我们打开网页浏览器并尝试在一分钟内发出超过五个请求。你应该会在 http://localhost:9000/REST/api/images/all 看到以下截图:

Scala 漏桶实现

好的,它工作了!如果你等待一分钟再次发出请求,你会看到流量恢复正常。下一步是添加一个新的客户端测试,因为我们知道如果我们过多地调用我们的 REST API 中的图像,我们将会被节流。

我们需要在 rest-client Scala 项目中添加一个额外的测试。为此,我们需要更改 ImageProxyTestSpec

测试背压

你的 ImageProxyTestSpec.scala 应该看起来像这样:

    package proxy.test  
    class ImageProxyTestSpec extends FlatSpec with Matchers {  
      // REST of the tests...  
      "A Image REST Proxy " should "suffer backpressure" in { 
        val latch = new CountDownLatch(10) 
        var errorCount:Int = 0 
        for(i <- 1 to 10){ 
          Future{ 
            try{ 
              val images = ImageProxy.listAll().get 
              images shouldNot(be(null)) 
              for( i <- images){ 
                println(i) 
              } 
            }catch{ 
              case t:Throwable => errorCount += 1 
            } 
            latch.countDown() 
          }
        }     
        while( latch.getCount >= 1 ) 
        latch.await() 
        errorCount should be >=5 
      } 
    } 

因此,对于这个测试,我们将调用 ImageProxy 十次。我们知道并非所有请求都会被服务,因为我们服务器上有背压。在这里,我们可以使用 try...catch 块调用代理,并有一个错误计数器。每次失败,我们都可以增加它。所以,这里,我们预计至少会失败五次。

我们用特性创建代码,因为我们希望请求同时发生。我们需要使用 CountDownLatch 函数,这是一个 Java 工具类,它允许我们在所有 Future 完成之前等待。这是通过 countDown 函数完成的。每次我们执行 countdown,我们都会减少内部计数器。正如你所看到的,我们用十创建了 CountDownLatch 函数。

最后,我们有一个while循环块来等待,直到计数器有挂起的 Futures。现在我们等待。一旦所有操作都完成,我们可以检查错误计数;它应该至少是五个。就这样。我们已经测试了我们的背压机制,并且一切正常!

现在,是我们转向下一个将在我们的应用程序中实现的功能的时候了:Swagger--我们将为我们的 REST API 添加 Swagger 支持。

添加 Swagger 支持

Swagger (swagger.io/) 是一个简单的 JSON 和 UI 表示工具,用于 REST API。它可以在多种语言中生成代码。它还创建了一份非常棒的文档,这份文档也是一个可运行的 Swagger 代码,允许您从它生成的文档中调用 REST 网络服务。为了使 Swagger 在 Play 框架中运行,我们需要在我们的 Play 框架应用程序中进行一些更改。

首先,我们需要将 Swagger 依赖项添加到build.sbt中:

    // Rest of build file...   
    libraryDependencies ++= Seq( 
      // Rest of other deps...  
      "io.swagger" %% "swagger-play2" % "1.5.2-SNAPSHOT" 
    ) 

如您所见,我们正在使用快照版本。为什么使用快照版本?目前,它不支持在稳定版本上。为了解决这个依赖项,我们需要使用 Git 并克隆另一个项目。您可以在github.com/CreditCardsCom/swagger-play获取更多详细信息。基本上,您需要编写如下命令:

$ git clone https://github.com/CreditCardsCom/swagger-play.git
$ cd swagger-play/
$ sbt publishLocal

现在,我们需要在ReactiveWebStore/conf/application.conf中启用 Swagger。

您的application.conf文件应该看起来像这样:

    play.modules { 
      enabled += "play.modules.swagger.SwaggerModule" 
    } 

接下来,我们可以更改我们的控制器以添加 Swagger 支持。Swagger 有注解来映射 REST 操作。

您的RestAPIController.scala文件应该看起来像这样:

    package controllers  
    @Singleton 
    @Api(value = "/REST/api", description = "REST operations on 
    Products, Images and Reviews. ") 
    class RestAPIController @Inject() 
    (val productService:IProductService, 
      val reviewService:IReviewService, 
      val imageService:IImageService) extends Controller { 
        import 
        play.api.libs.concurrent.Execution.Implicits.defaultContext  
        @ApiOperation( 
          nickname = "listAllProducts", 
          value = "Find All Products", 
          notes = "Returns all Products", 
          response = classOf[models.Product], 
          httpMethod = "GET" 
        ) 
        @ApiResponses(Array( 
          new ApiResponse(code = 500, message = "Internal Server 
          Error"), 
          new ApiResponse(code = 200, message = "JSON response with 
          data") 
        )
      ) 
      def listAllProducts = Action { 
        val future = productService.findAll() 
        val products = Awaits.get(5,future) 
        val json = ProductsJson.toJson(products) 
        Ok(json) 
      }  
      @ApiOperation( 
        nickname = "listAllReviews", 
        value = "Find All Reviews", 
        notes = "Returns all Reviews", 
        response = classOf[models.Review], 
        httpMethod = "GET" 
      ) 
      @ApiResponses(Array( 
        new ApiResponse(code = 500, message = "Internal Server Error"), 
        new ApiResponse(code = 200, message = "JSON response with 
        data") 
      )
      ) 
      def listAllReviews = Action { 
        val future = reviewService.findAll() 
        val reviews = Awaits.get(5,future) 
        val json = ReviewsJson.toJson(reviews) 
        Ok(json) 
      }  
      import scala.concurrent.duration._ 
      var bucket = new LeakyBucket(5, 60 seconds) 
      def processImages = { 
        val future = imageService.findAll() 
        val images = Awaits.get(5,future) 
        val json = ImagesJson.toJson(images) 
        json 
      }  
      def processFailure = { 
        Json.toJson("Too Many Requests - Try Again later... ") 
      }  
      @ApiOperation( 
        nickname = "listAllImages", 
        value = "Find All Images", 
        notes = "Returns all Images - There is throttling of 5 
        reqs/sec", 
        response = classOf[models.Image], 
        httpMethod = "GET" 
      ) 
      @ApiResponses(Array( 
        new ApiResponse(code = 500, message = "Internal Server Error"), 
        new ApiResponse(code = 200, message = "JSON response with 
        data") 
      )
      )  
      def listAllImages = Action { 
        bucket.dropToBucket() match { 
          case true => Ok(processImages) 
          case false => InternalServerError(processFailure.toString()) 
        }
      }
    } 

在这里,我们有几个注解。首先,我们在类的顶部有一个@Api注解。通过这个注解,我们将定义 REST API 的根路径。然后,对于每个 REST API 操作,我们有@ApiOperation@ApiResponses注解。@ApiOperation定义了 REST API 本身,您可以在其中定义参数和 HTTP 动词,还可以添加一些注释(文档)。它还可能描述结果;在我们的案例中,它将是模型的 JSON 表示。

就这样!我们已经将控制器映射到 Swagger。下一步是添加 Swagger 的路由。这需要通过在ReactiveWebStore/conf/routes中添加新行来完成,如下所示:

    // REST of the other routes..  
    GET     /swagger.json    controllers.ApiHelpController.getResources 

Swagger UI

Swagger 将为我们的 REST API 生成 JSON 响应。可以使用 Swagger UI,它非常棒,为开发者提供了许多便利。我们可以以两种方式与 Swagger UI 一起工作:我们可以将其作为独立应用使用,或者将其嵌入到我们的 Play 框架应用程序中。

我们将采取的策略是在我们的应用程序中嵌入 Swagger UI。如果您有多个 REST API 和多个 Play 应用程序或微服务,拥有独立的 swagger UI 安装是一个好主意。

在之前的步骤中,我们在应用程序中启用了 Swagger。打开您的浏览器并输入 http://localhost:9000/swagger.json。您可以遵循 swagger.io/swagger-ui/ 上的说明。总之,您将得到以下输出:

Swagger UI

构建 和 安装 Swagger Standalone

现在,我们将下载、构建和安装 Swagger Standalone。让我们通过编写以下代码行开始:

$ sudo apt-get update $ sudo apt-get install nodej $ sudo apt-get install npm 
$ git clone https://github.com/swagger-api/swagger-ui.git $ cd swagger-ui/ $ sudo -E npm install -g $ sudo -E npm run build $ npm run serve $ GOTO: http://localhost:8080/

一旦您启动 Swagger UI,您可以在浏览器中看到以下输出:

构建和安装 Swagger Standalone

现在,让我们将 Swagger UI 集成到我们的 Play 框架应用程序中。

我们需要将 /swagger-ui/dist/ 中的内容复制到我们的 Play 框架应用程序的 ReactiveWebStore/public 下。然后,我们将创建一个名为 swaggerui 的文件夹。

我们需要编辑一个文件,以便将我们的 swagger JSON URI 放入其中。打开 ReactiveWebStore/public/swaggerui/index.html 并将 40 改为以下代码行:

    url = "http://localhost:9000/swagger.json"; 

就这样。现在,我们需要从我们的应用程序中创建一个链接来嵌入 Swagger UI。所以,让我们更改 ReactiveWebStore/app/views/index.scala.html

您的 index.scala.html 文件应该看起来像这样:

    @(message: String)(implicit flash:Flash)  
    @main("Welcome to Reactive Web Store"){ 
      <div class="row-fluid"> 
        <BR> 
        <div class="span12"> 
          <div class="row-fluid"> 
            <div class="span6"> 
              <a href="/product"><img height="42" width="42" 
              src="img/@routes.Assets.at("images/product.png")"> Manage 
              Products</a><BR> 
              <a href="/review"><img height="42" width="42" 
              src="img/@routes.Assets.at("images/review.png")"> Manage 
              Reviews</a><BR> 
              <a href="/image"><img height="42" width="42" 
              src="img/@routes.Assets.at("images/image.png")"> Manage 
              Images</a><BR> 
            </div> 
            <div class="span6"> 
            <a href="/reports"><img height="42" width="42" 
            src="img/@routes.Assets.at("images/reports.png")"> Reports </a>
            <BR> 
            <a href="/chat/index_socket"><img height="42" width="42" 
            src="img/@routes.Assets.at("images/chat.png")"> Chat Room </a>
            <BR> 
            <a href="/assets/swaggerui/index.html"><img height="42" 
            width="42" 
            src="img/@routes.Assets.at("images/swagger.png")"> Swagger REST 
            API </a><BR> 
          </div> 
        </div> 
      </div>             
    </div>} 

现在,我们可以使用 $ activator run 运行我们的 Play 应用程序。

打开浏览器并访问 http://localhost:9000/。您将看到以下截图:

构建和安装 Swagger Standalone

现在,我们可以通过点击 Swagger REST API 链接或直接访问 http://localhost:9000/assets/swaggerui/index.html 来打开 Swagger UI。它看起来应该像这样:

构建和安装 Swagger Standalone

如您所见,Swagger UI 非常不错。您可以点击每个操作,并查看更多关于它们如何工作、它们使用的 HTTP 动词以及 URI 的详细信息。有一个 试试看! 按钮。让我们点击 试试看! 按钮来查看产品,它看起来应该像这样:

构建和安装 Swagger Standalone

如您所见,我们有我们的 JSON 结果,还有一些 CURL 示例。

摘要

在本章中,您学习了如何设计 REST API 并更改您的 Play 框架应用程序以支持 Swagger。您使用代理技术创建了一个 Scala 客户端库,以及用于 API 的 Scala 测试。此外,您还介绍了使用漏桶算法的背压。

在下一章,也就是最后一章,您将学习关于软件架构和可伸缩性/弹性技术,例如可发现性、负载均衡器、缓存、Akka Cluster 和亚马逊云。

第十章。扩展

在前面的章节中,我们构建了一个 Scala 和 Play 框架应用程序。我们使用了 Scala 生态系统中最有效的框架和工具,如 Play 框架和 Akka;我们使用了 Reactive 和函数式编程技术,使用 Futures 和 RxScala。此外,我们使用 Jasper 和 WebSocket 进行聊天。这是最后一章,我们将学习如何部署和扩展我们的应用程序。

在本章中,我们将涵盖以下主题:

  • 独立部署

  • 架构原则

  • 反应式驱动和可发现性

  • 中间层负载均衡器、超时、背压和缓存

  • 使用 Akka 集群扩展微服务

  • 使用 Docker 和 AWS 云扩展基础设施

独立部署

在整本书中,我们使用了 Activator 和 SBT 构建和开发工具。然而,当我们谈论生产时,我们不能使用 SBT 或 Activator 运行应用程序;我们需要进行独立部署。

那么,关于标准的 Java Servlet 容器,如 Tomcat 呢?Tomcat 很棒;然而,Play 更棒。你在 Tomcat 上部署应用程序不会获得更好的性能。独立的 Play 使用 Netty,它具有优越的网络堆栈。

为了部署我们的 Jasper 报告应用程序,我们需要做一些小的修改。别担心;这些更改非常简单直接。

报告文件夹

我们需要将报告模板(JRXML 文件)从源文件夹移动到公共文件夹。我们为什么需要这样做?因为当我们生成独立部署时,它们不会被包含在应用程序 JAR 文件中。公共文件夹中的内容将被打包并部署到适当的 JAR 文件中。这就是为什么我们需要进行这个更改。

ReactiveWebStore/public/下创建一个名为reports的文件夹。然后将所有 JRXML 文件移到那里。

更改报告生成器

由于我们的模板将位于 JAR 文件中,我们需要更改加载逻辑以正确获取模板。在ReactiveWebStore/app/report/下,我们需要更改ReportBuilder.scala,编辑后应如下所示:

    package reports  
    object ReportBuilder {  
      private var reportCache:scala.collection.Map[String,Boolean] = 
      new scala.collection.mutable.HashMap[String,Boolean].empty  
      def generateCompileFileName(jrxml:String): String = 
      "/tmp/report_" + jrxml + "_.jasper"  
      def compile(jrxml:String){ 
        if(reportCache.get(jrxml).getOrElse(true)){ 
          val design:JasperDesign = JRXmlLoader.load( 
            Play.resourceAsStream("/public/reports/" + jrxml)
          (Play.current).get ) 
          JasperCompileManager.compileReportToFile(design, 
          generateCompileFileName(jrxml)) 
          reportCache += (jrxml -> false) 
        } 
      }  
      def toPdf(jrxml:String):ByteArrayInputStream = { 
        try { 
          val os:OutputStream = new ByteArrayOutputStream() 
          val reportParams:java.util.Map[String,Object] = 
          new java.util.HashMap() 
          val con:Connection =     
          DriverManager.getConnection("jdbc:mysql://localhost/RWS_DB?
            user=root&password=&useUnicode=
            true&useJDBCCompliantTimezoneShift=
            true&useLegacyDatetimeCode=false&serverTimezone=UTC") 
          compile(jrxml) 
          val jrprint:JasperPrint = 
          JasperFillManager.fillReport(generateCompileFileName(jrxml),     
          reportParams, con) 
          val exporter:JRPdfExporter = new JRPdfExporter() 
          exporter.setExporterInput(new SimpleExporterInput(jrprint)); 
          exporter.setExporterOutput(
          new SimpleOutputStreamExporterOutput(os)); 
          exporter.exportReport() 
          new ByteArrayInputStream
          ((os.asInstanceOf[ByteArrayOutputStream]).toByteArray()) 
        }catch { 
          case e:Exception => throw new RuntimeException(e) 
          null 
        }
      }
    } 

我们所做的主要更改是围绕compile函数。现在,我们使用 Jasper JRXmlLoader 从InputStream方法加载 Jasper 模板。通过Play.resourceAsStream函数提供的InputStream方法传递。正如你所见,我们传递了新的路径/public/reports/以获取模板。其余的代码基本上与之前执行的一样。

现在我们准备部署。为了做到这一点,我们需要在activator上运行一个命令,如下所示:

$ activator universal:packageZipTarball

你将看到以下结果:

更改报告生成器

任务完成后,我们的应用程序将被打包到ReactiveWebStore/target/universal/目录中,你将看到一个reactivewebstore-1.0-SNAPSHOT.tgz文件。

然后你需要提取文件,你将拥有以下目录:

  • reactivewebstore-1.0-SNAPSHOT

  • bin: 运行应用的脚本

  • conf: 所有配置文件:路由、日志、消息

  • bib: 包括第三方依赖的所有 JAR 文件

  • share: 关于应用程序的所有文档

定义秘密

在我们运行独立应用程序之前,我们需要进行一个额外的更改。我们需要更改默认的秘密。定位到reactivewebstore-1.0-SNAPSHOT/conf/application.conf文件。

application.conf文件中将秘密更改为以下内容:

play.crypto.secret = "changeme" 

你需要提供一个不同的值。它可以是什么都行,只要你不叫它changeme。如果你不更改这个,你的应用程序将无法启动。你可以在www.playframework.com/documentation/latest/ApplicationSecret获取更多信息。

如果你现在只想测试部署,我们可以称它为playworks

现在,我们已经准备好启动应用程序。

运行独立部署

为了运行应用程序,我们将使用通用任务生成的脚本。转到reactivewebstore-1.0-SNAPSHOT目录,然后运行$ bin/reactivewebstore,它看起来可能像这样:

运行独立部署

现在,你可以打开浏览器并访问http://localhost:9000/

就这样;我们的应用程序已经启动并运行。请随意测试我们构建的所有功能——它们都正常工作!

它应该看起来像这样:

运行独立部署

架构原则

可扩展性是处理更多用户、流量和数据的能力;为了实现这一点,我们需要应用一些原则和技术。我们的应用程序已经使用了最现代的技术和工具,如函数式编程、ReactiveX 编程、RxScala、Akka 框架、Play 等。然而,为了实现扩展,我们需要建立一个基础设施,以及其他能够让我们处理更多用户的系统。

一个好的应用程序架构应该围绕以下原则创建:

让我们详细看看这些原则。

服务导向(SOA/microservices)

服务导向是关于拥有更高层次的抽象,这也可以称为服务或微服务。SOA 不是关于特定技术,而是关于共享服务、灵活性和内在可操作性等原则。如果你想了解更多关于 SOA 的信息,可以查看www.soa-manifesto.org/的 SOA 宣言。微服务是 SOA 的一个特定风味,主要区别在于对粒度、自治和隔离的关注。如果你想了解更多关于微服务的信息,你还可以查看www.linkedin.com/pulse/soa-microservices-isolation-evolution-diego-pacheco以及microservices.io/

性能

正确的算法可以使你的应用程序运行顺畅,而错误的算法可能会让用户有糟糕的体验。性能是通过设计实现的——首先,选择正确的集合集和算法集以及框架集。然而,性能最终需要被测量和调整。你在应用程序中应该进行的性能实践是压力测试。Scala 生态系统中最优秀的压力测试工具是 Gatling。Gatling(gatling.io/#/)允许你使用一个非常简单但强大的 DSL 在 Scala 中编码。Gatling 专注于 HTTP 和延迟百分位数和分布,这是当今正确的做法。延迟不仅用于性能和可扩展性,而且与用户体验密切相关,因为一切都在线。

可扩展性/弹性

可扩展性是我们进行软件架构的主要原因之一,因为无法扩展的架构没有商业价值。我们将继续在本章中讨论可扩展性原则。弹性是指系统在极端不利情况下(如硬件故障或基础设施故障)能够抵抗并保持运行的能力。弹性是一个旧术语。目前,有一个新的、更现代、更准确的原理,称为反脆弱性。这个原理在 Netflix 得到了很好的发展和实际应用。反脆弱性是关于系统和架构能够适应并转移到其他系统和操作模式以保持工作状态的能力。如果你想了解更多关于反脆弱性的信息,你可以访问diego-pacheco.blogspot.com.br/2015/09/devops-is-about-anti-fragility-not-only.htmldiego-pacheco.blogspot.com.br/2015/11/anti-fragility-requires-chaos.html

可扩展性原则

围绕这些原则构建的架构使得扩展你的应用程序成为可能。然而,我们仍然需要依赖其他原则和技术来扩展它。

存在着几个关于可扩展性的原则和技术,如下所述:

  • 垂直和水平扩展(向上和向外)

  • 缓存

  • 代理

  • 负载均衡器

  • 节流

  • 数据库集群

  • 云计算/容器

  • 自动扩展

  • 反应式驱动器

垂直和水平扩展(向上和向外)

你可以添加更多资源,拥有更好的硬件,或者你可以添加更多盒子。这是两种基本的扩展方式。你总是可以改进和调整你的应用程序,以使用更少的资源并从单个盒子中获得更多。最近,在反应式编程方面有一些改进,使用更少的资源并提供了更高的吞吐量。然而,单个盒子在扩展方面总是存在限制,这就是为什么我们总是需要能够向外扩展。

缓存

数据库很棒。然而,调用传统数据库会有延迟成本。一个很好的解决方案是拥有内存缓存,你可以将其用作数据子集并从中获得快速检索的好处。Play 框架支持缓存。如果你想了解更多,请查看www.playframework.com/documentation/2.5.x/ScalaCache

在缓存方面还有其他选项。现在有很多公司使用内存作为最终的数据存储。为此,你可以考虑使用 Redis (redis.io/) 和 Memcached (memcached.org/) 等工具。然而,如果你想扩展 Redis 和 Memcached,你需要像 Netflix/Dynomite (github.com/Netflix/dynomite) 这样的东西。Dynomite 提供了一个基于 AWS Dynamo 论文的集群,对于 Redis 有以下好处:

  • 高吞吐量和低延迟

  • 多区域支持(AWS 云)

  • 令牌感知

  • 一致性哈希

  • 复制

  • 分片

  • 高可用性

注意

如果你想了解更多关于 Dynomite 的信息,请查看github.com/Netflix/dynomite/wiki

负载均衡器

负载均衡器是扩展服务器的关键工具。所以,比如说,你拥有 10 个运行我们的 Play 框架应用程序的盒子或 10 个 Docker 容器。我们将在应用程序前面需要一些东西来分发流量。有几个服务器可以完成这项工作,例如 NGINX (nginx.org/) 和 Apache HTTP 服务器 (httpd.apache.org/)。如果你想扩展你的应用程序,这是最简单的解决方案。配置和更多详细信息可以在www.playframework.com/documentation/2.5.x/HTTPServer#Setting-up-a-front-end-HTTP-server找到。

负载均衡器通常是代理服务器。您可以使用它们来支持 HTTPS。如果您愿意,您还可以在 Play 框架上启用 HTTPS (www.playframework.com/documentation/2.5.x/ConfiguringHttps)。请记住,您需要更改 swagger 嵌入式安装,因为我们所有的代码都指向 HTTP 接口。如果您在 AWS 云中进行部署,您需要更改一些配置以转发代理,您可以在www.playframework.com/documentation/2.5.x/HTTPServer#Setting-up-a-front-end-HTTP-server找到这些配置。

节流

这也被称为背压。我们在第九章“设计您的 REST API”中介绍了节流,您可以在此处获取更多详细信息。第九章。您可以在那里了解更多。然而,主要思想是限制每个用户的请求。这也是确保单个用户不会窃取所有计算资源的一种方式。从安全角度来看,这也非常重要,尤其是对于面向互联网或也称为边缘的服务。另一种保护和拥有这种能力的好方法是使用 Netflix/Zuul (github.com/Netflix/zuul)。

数据库集群

有时,问题不在于应用层面,而是在数据库中。当我们谈论可伸缩性时,我们需要能够扩展一切。我们需要为数据库拥有与中端相同的理念。对于数据库,以下工作非常重要:

  • 集群

  • 索引

  • 物化视图

  • 数据分区

对于我们的应用程序,我们使用了 MySQL 数据库。以下是一些可以帮助您扩展数据库并应用先前概念的资源:

云计算/容器

在传统数据中心中扩展应用程序总是很困难,因为我们需要硬件就位。这是通过容量规划实践完成的。容量规划很好,可以确保我们不超出预算。然而,正确完成它非常困难。软件难以预测,这是云的一个巨大优势。云只是另一层抽象。硬件和网络变得逻辑化,并且被 API 封装起来。这使得我们可以依赖云的弹性,并在需要时按需扩展我们的应用程序。然而,架构需要为这一时刻做好准备,并使用本章中描述的工具和技术。目前,有几个公共云;最佳选项如下:

今天,云不再是房间里唯一的巨象。我们还有 Linux 容器,例如 Docker(www.docker.com/)和 LXC(linuxcontainers.org/)。容器提供了另一层抽象,它们可以在云上或本地运行。这使得您的应用程序更加便携,同时也更加经济高效。容器还可以进行扩展。容器的主要优势在于速度和灵活性。与任何公共云中的虚拟化镜像相比,启动容器要快得多。它们也是便携的,可以在任何地方运行。

自动扩展

目前,这是云计算最大的资源之一。基本上,您可以定义一个基础镜像,它是一个操作系统的状态,如 Linux,云将根据需求为您创建和销毁实例。这些实例可以通过增加计算资源,如内存、CPU、网络,甚至基于自定义规则来创建。这是实现弹性的关键关注点。如果您想了解更多关于自动扩展的信息,请查看aws.amazon.com/autoscaling/

关于自动化的注意事项

为了大规模使用所有这些技术和技术,我们需要实现完全自动化(en.wikipedia.org/wiki/List_of_build_automation_software),因为手动处理这一切是不可能的。当我们使用云或容器时,没有其他选择;一切都需要自动化。有几个工具帮助我们实现这一目标,例如 Ansible(www.ansible.com/)。

不要忘记遥测

当你所有的基础设施都到位时,你还需要有监控、警报和适当的仪表板。有许多针对容器和公共云的出色工具,例如 Sensu (sensuapp.org/) 和 Prometheus (prometheus.io/)。

反应式驱动器和可发现性

反应式驱动器:我们谈论了很多,也使用 Play 框架和 RxScala 编写了很多反应式代码。然而,为了充分利用 ReactiveX 编程的优势,你需要确保一切都是非阻塞 IO 和反应式的。换句话说,我们需要让所有的驱动器都是反应式的。Slick 很棒,因为它给我们提供了与 MySQL 数据库的反应性。我们将在有驱动器或连接点的任何地方都需要应用相同的原理。现在有很多库正在变得反应式。例如,如果你想使用 Redis 进行缓存,你可以使用 Lettuce (github.com/mp911de/lettuce),这是一个反应式库。

当我们与微服务一起工作时,我们往往会有数百个微服务实例。这些微服务将在容器和/或云计算单元上运行。你不能指向特定的 IP,因为代码将不会被管理,也不会在云/容器环境中生存。云/容器基础设施是短暂的,你不知道何时一个实例会被终止。这就是为什么你需要能够在任何时候切换到另一个可用区域或地区。

有一些工具可以帮助我们在代码中应用这些更改。这些工具是 Netflix/Eureka (github.com/Netflix/eureka) 和 Consul (www.consul.io/),甚至是 Apache Zookeeper (zookeeper.apache.org/)。Eureka 有一个优势——它更容易使用,并且有围绕 JVM 生态系统的工具,这些工具已经由 Netflix 进行了实战测试。

Eureka 是一个中央注册中心,微服务在这里注册它们的 IP 和元数据。Eureka 有一个 REST API。微服务可以使用 Eureka API 查询和搜索现有应用程序。Eureka 可以在多 VPC/多区域环境中运行。还有其他 JVM 组件,例如 ribbon (github.com/Netflix/ribbon) 和 karyon (github.com/Netflix/karyon),它们可以自动注册和检索 Eureka 信息和元数据。

基于 Eureka 信息,你可以自动执行微服务负载均衡和故障转移至其他可用区域和地区。为什么我要使用 Eureka 而不是 DNS 呢?DNS 用于中间层负载均衡并不是一个好的选择,因为 DNS 不够灵活,超时时间相当长。如果你想了解更多关于可发现性的信息,请查看microservices.io/patterns/service-registry.html

反应式驱动器和可发现性

Eureka 概述 - AWS 云上的 Eureka 架构概述

如前图所示,你至少需要在三个可用区(AZs)中部署 Eureka 服务器,以确保可用性。然后,Eureka 数据将复制到每个服务器。我们的应用程序或微服务将在 Eureka 中注册,其他应用程序/微服务可以通过 REST 调用检索这些元数据,例如 IP 地址。如果你想了解更多,你可以查看github.com/Netflix/eureka/wiki/Eureka-at-a-glance

中间层负载均衡器、超时、背压和缓存

Eureka、Zookeeper 或 Consul 只是方程的一部分。我们仍然需要在客户端上使用一些软件来使用 Eureka 信息,以便进行中间层负载均衡、故障转移和缓存。Netflix 堆栈有一个组件可以做到这一点,称为 Ribbon (github.com/Netflix/ribbon)。使用 Ribbon,你可以自动从 Eureka 解析微服务 IP,进行重试,并将故障转移到其他可用区(AZ)和地区。Ribbon 有一个缓存概念;然而,它是在预加载的缓存上。

Ribbon 的理念很简单。Ribbon 的伟大之处在于一切都是响应式的,你可以使用 RxJava 和 RxScala 来与该堆栈一起工作。如果你不想使用 Ribbon,你仍然可以使用 Scala 创建一个简单的集成层,并执行相同的关注点,例如负载均衡、故障转移和缓存。

那么,背压怎么办?背压可以使用 RxJava 和 Rxscala 实现,你也将能够在客户端实现它。你可以在 Rx 的github.com/ReactiveX/RxJava/wiki/Backpressure上了解更多关于背压的信息。

所以,如果我有了客户端负载均衡、故障转移、缓存和背压功能,我就可以放心了吗?是的,你可以;然而,我们总是可以做得更好。与微服务一起工作并不容易,因为一切都是远程调用,远程调用可能会失败、挂起或超时。如果不妥善管理,这些缺点既困难又危险。还有一个解决方案可以帮助我们解决这个概念;它被称为 Hystrix (github.com/Netflix/Hystrix)。

Hystrix 是为 JVM 设计的库,用于延迟和容错保护。乍一看,Hystrix 是任何可能耗时或出错的外部代码的包装器。

Hystrix 具有线程隔离功能,并为每个资源提供专门的线程池。这很好,因为它可以防止你耗尽资源。它有一个名为断路器的执行模式。断路器将防止请求破坏整个系统。此外,它有一个仪表板,我们可以在这里可视化电路,因此,在运行时,我们可以看到正在发生什么。这种能力不仅对于监控非常有用,而且因为它易于故障排除和可视化问题所在。

可以用以下流程图进一步解释:

中继层负载均衡器、超时、背压和缓存

您想要保护的代码将围绕 Hystrix 命令。这个命令可以在同步或异步编程模型中操作。Hystrix 首先会检查电路是否关闭,这是好的,并且应该如何关闭。然后,它会检查是否有可用的线程来执行该命令,如果有可用的线程,则执行该命令。如果失败,它会尝试获取回退代码,这是在失败情况下您可以提供的第二个选项。这个回退应该是静态的;然而,您可以在后台加载数据,然后在回退时返回。另一个选项是回退到其他可用区域或地区。

下面是 Hystrix 仪表板断路器视图的工作快照:

中继层负载均衡器、超时、背压和缓存

在前面的图像中,我们可以看到 Hystrix 仪表板的示例,其中我们可以可视化关键信息,例如成功率和错误率以及电路是开启还是关闭。如果您想了解更多关于 Hystrix 仪表板的信息,请查看github.com/Netflix/Hystrix/wiki/Dashboard

使用 Akka 集群扩展微服务

我们的应用程序也使用了 Akka。为了扩展 Akka,我们需要使用 Akka 集群。Akka 集群允许我们在多台机器上集群化多个 Actor 系统。它有特殊的集群感知 Actor 路由器,我们可以使用这些 Actor 将请求路由到整个集群;更多详细信息可以在doc.akka.io/docs/akka/2.4.9/java/cluster-usage.html#Cluster_Aware_Routers找到。

Akka 集群提供了成员协议和生命周期。基本上,当新成员加入或成员离开集群时,我们可以通过集群得到通知。有了这个功能,我们可以围绕这些语义编写可扩展的解决方案。正如我们所知,当成员加入时,我们可以部署更多节点,我们也可以根据需要删除节点。

一个简单的示例是创建一个名为 frontend 的 Actor,当我们看到这个 Actor 时,我们可以在集群中部署三个后端 Actor。如果前端 Actor 离开,我们可以卸载其他 Actor。所有这些逻辑都可以使用 Akka 为我们生成的成员协议和集群事件来实现。前端 Actor 不是一个 UI 或 Web 应用程序,它只是一个接收工作的 Actor。所以,假设我们想要围绕我们的产品目录生成分析。我们可以有一个前端 Actor 接收这个请求并将工作委托给后端 Actor,这些 Actor 将在集群中部署并执行分析工作。

下图是 Akka 集群成员协议的过程视图:

使用 Akka 集群扩展微服务

如前图所示,有一组状态。首先,节点正在加入集群;然后节点可以启动。一旦节点启动,它可以离开集群。还有中间状态,例如离开和存在。

Akka 集群为我们提供了许多扩展 Actor 系统的选项。另一个有趣的选项是使用分布式 Pub/Sub 模式。如果你熟悉 JMS 主题,这几乎是一个相同的概念。对于那些不熟悉的人,你可以查看doc.akka.io/docs/akka/2.4.9/java/distributed-pub-sub.html

注意

如果你想了解更多关于 Akka 集群的信息,你可以查看doc.akka.io/docs/akka/2.4.9/common/cluster.html

使用 Docker 和 AWS 云扩展基础设施

使用 AWS 云进行扩展非常简单,因为任何时候,只需在 AWS 控制台中简单点击一下,你就可以更改硬件并使用更多的内存、CPU 或更好的网络。向外扩展并不难;然而,我们需要有良好的自动化。扩展的关键原则是拥有具有良好策略的自动扩展组。你可以在docs.aws.amazon.com/autoscaling/latest/userguide/policy_creating.html了解更多信息。

还有其他有趣的服务和组件可以帮助你扩展你的应用程序。然而,你需要记住,这可能会导致耦合。IT 行业正在向容器方向移动,因为它更快,并且易于在其他公共云中部署。

我们也可以使用 Docker 进行扩展,因为有一些集群管理器可以帮助我们扩展我们的容器。目前,有几种解决方案。在功能和成熟度方面,以下是一些更好的解决方案:

Docker Swarm:这是一个 Docker 集群。Docker Swarm 非常灵活,并且与其他 Docker 生态系统工具(如 Docker Machine、Docker Compose 和 Consul)集成良好。它可以处理数百个节点,你可以在blog.docker.com/2015/11/scale-testing-docker-swarm-30000-containers/了解更多相关信息。

Kubernetes:这是由谷歌创建的,它是一个针对开发自动化、操作和扩展 Docker 容器的完整解决方案。Kubernetes 集群有两个角色,一个是协调集群、调度应用程序并保持应用程序处于所需状态的 master 节点;还有节点,即运行应用程序的工作节点。它可以处理数百个容器并且扩展性非常好。想了解更多关于它的信息,请查看blog.kubernetes.io/2016/03/1000-nodes-and-beyond-updates-to-Kubernetes-performance-and-scalability-in-12.html

Apache Mesos:这是由 Twitter 创建的。它非常有趣,因为它可以在本地数据中心或公共云上运行裸金属。Mesos 允许你使用 Docker 容器。如果你想了解更多关于 Mesos 的信息,请查看以下论文:

mesos.berkeley.edu/mesos_tech_report.pdf

摘要

在本章中,你学习了如何将你的 Play 框架应用程序作为独立分发进行部署。此外,你还学习了几个架构原则、技术和工具,以帮助你将应用程序扩展到数千用户。

有了这些,我们也到达了这本书的结尾。希望你喜欢这次旅程。我们使用 Scala、Play Framework、Slick、REST、Akka、Jasper 和 RxScala 构建了一个不错的应用程序。感谢你抽出时间。我祝愿你在 Scala 语言编码生涯中一切顺利。

posted @ 2025-09-11 09:46  绝不原创的飞龙  阅读(40)  评论(0)    收藏  举报