Java-开发者的-Scala-指南-全-
Java 开发者的 Scala 指南(全)
原文:
zh.annas-archive.org/md5/c584bca13e5834b9dff0cc638131c885
译者:飞龙
前言
当我告诉周围的人我现在用 Scala 编程而不是 Java 时,我经常被问到,“那么,用简单的话说,使用 Scala 与 Java 相比的主要优势是什么?”我倾向于这样回答:“使用 Scala,你更接近领域,更接近普通的英语”。提高抽象级别通常是用来描述程序以便人类以更可读和自然的方式理解,而不是计算机理解的零和一。
随着在电信、制造或金融应用中遇到的计算机系统成熟和增长,不同形式的复杂性往往会出现,具体如下:
-
由于添加支持的功能而导致的复杂性,例如,在保险系统中合同替代方案的多样性,或者在我们的不断发展的社会中引入复杂的算法来解决新的挑战
-
为了弥补技术的局限性而产生的复杂性;例如,使系统分布式以处理更大的负载或提高可靠性和响应时间
-
意外复杂性,这是由于除了问题本身以外的因素引入的,例如,旧系统之间的集成以及实际上不兼容的技术,为了更快地进入消费者市场而采取的短期解决方案,或者当许多具有不同背景和风格的人在短时间内向大型代码库贡献时,对整个大型系统设计的误解
第三种复杂性显然是不受欢迎的,应该减少到最低限度,如果可能的话,应该消除,而其他两种应该保持可管理。Scala 处理所有这些,只有当系统可以用代码描述得像用写得很好的英语描述一样时,业务领域的复杂性才能得到管理。
在过去几年中,许多语言能够以比传统的面向对象方式更简洁的方式表达行为,这很大程度上归因于函数式编程(FP)的日益流行,这是一种存在了很长时间的范例,但直到最近才被认为是与所谓的命令式编程语言(如 C 或 Java)的竞争对手。Michael Feathers 在以下声明中很好地概述了两者之间的明显二分法:
"OO 通过封装移动部分使代码易于理解。FP 通过最小化移动部分使代码易于理解。"
前者侧重于将大型系统分解成更小、可重用的部分。这些部分容易推理,因为它们是根据现实生活中的对象建模的。它们之间使用接口,并旨在封装可变状态。后者强调函数的组合,这些函数理想上没有副作用。这意味着它们的结果只取决于它们的输入参数,导致在程序中最小化或消除可变状态。
由 Scala 支持的 FP(函数式编程)的声明性本质旨在编写代码来表达“要做什么”而不是“如何做”。此外,FP 方法倾向于通过组合(将函数组合在一起)使算法更加简洁,而命令式方法则倾向于引入副作用,即程序状态的变化,这会使整个算法难以以简洁的方式呈现。
本书将通过更高层次的抽象推理向 Java 开发者展示 Scala 是从 Java 的一个重要且自然的演变。这种过渡最终将导致更健壮、可维护和有趣的软件。
本书的目的并不仅仅在于探索语言的架构或深入特性,以及其详尽的语法;关于 Scala 语言的优秀书籍已经有很多,尤其是由语言创造者马丁·奥德斯基本人以及与他一起在 Typesafe 工作的人所写。
我们的目的是专注于帮助当前 Java 开发者开始使用 Scala,并让他们在使用语言时感到舒适,使他们的日常工作更加高效和有趣。
本书涵盖的内容
第一章, 在项目中交互式编程,提供了关于JVM(Java 虚拟机)的简要介绍以及一些使 Java 成功的关键特性。然后我们将开始动手实验,使用 Scala REPL(即读取-评估-打印循环),这是一个强大的交互式编程工具。我们将介绍 Scala 的一些强大构造,这些构造不仅使编程变得愉快,而且直观和高效。
第二章, 代码集成,是关于在相同的代码库下使 Scala 和 Java 代码协作。本章感兴趣的话题包括 Java 和 Scala 集合之间的互操作性,以及用 Scala 包装现有的 Java 库。此外,我们还将涉及编码风格的话题,特别是通过比较已确立的 Java 编码最佳实践与较新的 Scala 指南。
第三章, 理解 Scala 生态系统,帮助您了解 Scala 开发生态系统及其周围工具,其中大部分是从 Java 继承而来的。特别是 Java 框架如 Maven 和 IDE 如 Eclipse 不容忽视。除了开发周期的基本元素外,我们还将涵盖 Scala 特定的工具,如 SBT、Scala Worksheets 以及 Typesafe 的 Activator 及其模板的介绍。
第四章, 测试工具,是对 Scala 开发者基本工具的后续探讨,重点关注审查大多数用于单元、集成和功能测试测试数据的实用工具,以及基于属性的自动化测试。
第五章, Play 框架入门,将为您提供一个具体的 Play 框架介绍,我们将向您展示 Play 的一些酷炫特性,这些特性使得人们想要从更传统的 servlet/J2EE 模型迁移过来。
第六章, 数据库访问和 ORM 的未来,涵盖了处理关系数据库中数据持久化的方法,无论您是想重用 JPA/Hibernate 等成熟技术,还是转向更创新且前景广阔的替代方案,如SLICK(Scala 语言集成连接套件),这是基于 Scala 语言力量的一种有趣的 ORM 替代方案。此外,我们将看到如何将现有的关系数据库逆向工程为 Play CRUD 应用程序,作为迁移 Java 项目的起点。
第七章, 与集成和 Web 服务协同工作,涵盖了在当今 Java 开发中无处不在的技术。在本章中,我们将探讨如何将外部系统集成应用到 Scala 世界中以及其带来的好处。本章包含的主题与 Web 服务通过 SOAP XML、REST 和 JSON 相关。
第八章, 现代应用的基本特性 - 异步和并发,涉及可扩展应用开发的两方面。为了获得更好的性能,软件项目通常鼓励引入异步调用和并发代码。通过本章,我们将展示 Scala 的更多功能性如何使这种复杂性更加可管理和可维护。我们还将介绍 Akka 框架,这是一个用于简化并发应用程序开发的工具包。
第九章, 构建响应式 Web 应用,在上一章的基础上更进一步,介绍了市场上出现的一种新类型的应用:响应式应用。它们的特点是交互性、能够推送信息给最终用户、弹性适应负载变化的能力以及从故障中恢复的能力。本章的目标是使用本书学到的概念以及新兴技术如 WebSockets 在 Play 中构建这样的应用。
第十章, Scala 精选,以对 Web 开发未来的某些观点结束本书。例如,Java 开发者越来越多地接触到客户端的 JavaScript,无论他们是否喜欢。另一个例子是领域特定语言(DSLs)的出现,在 Java 中实现这一任务并不简单。
您需要为此书准备
由于 Scala 运行在 Java 平台JVM(Java 虚拟机)上,你可以在任何支持 Java 标准版的计算机上编写和执行书中提供的代码。要设置所需的工具,请参阅第一章,在项目中交互式编程和第三章,理解 Scala 生态系统
本书面向对象
这本书显然主要针对开发者。我们希望帮助 Java 程序员入门,并使他们能够舒适地使用该语言的语法和工具。我们将通过逐步探索 Scala 带来的某些新概念来实现这一点,特别是如何在不放弃过去十五年来围绕 Java 建立的所有成熟技术的情况下,统一面向对象和函数式编程的最佳实践。
约定
在这本书中,您将找到许多不同风格的文本,以区分不同类型的信息。以下是一些这些样式的示例及其含义的解释。
文本中的代码词汇、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 昵称如下所示:“我们可以通过使用include
指令来包含其他上下文。”
代码块如下设置:
import java.util.*;
public class ListFilteringSample {
public static void main(String[] args) {
List<Integer> elements = Arrays.asList(1, 2, 3, 4, 5);
List<Integer> filteredElements = new ArrayList<Integer>();
for (Integer element : elements)
if (element < 4) filteredElements.add(element);
System.out.println("filteredElements:" + filteredElements);
}
}
当我们希望引起您对代码块中特定部分的注意时,相关的行或项目将以粗体显示:
import java.util.*;
public class ListFilteringSample {
public static void main(String[] args) {
List<Integer> elements = Arrays.asList(1, 2, 3, 4, 5);
List<Integer> filteredElements = new ArrayList<Integer>();
for (Integer element : elements)
if (element < 4) filteredElements.add(element);
System.out.println("filteredElements:" + filteredElements);
}
}
任何命令行输入或输出都应如下编写:
> ./activator ui
新术语和重要词汇以粗体显示。屏幕上显示的词汇,例如在菜单或对话框中,在文本中如下显示:“出于好奇,您可以点击代码视图 & 在 IDE 中打开标签,然后点击运行标签。”
注意
警告或重要注意事项以如下框的形式出现。
小贴士
小贴士和技巧以如下形式出现。
读者反馈
我们始终欢迎读者的反馈。请告诉我们您对这本书的看法——您喜欢什么或可能不喜欢什么。读者反馈对我们开发您真正能从中获得最大收益的标题非常重要。
如要向我们发送一般反馈,请简单地将电子邮件发送到<feedback@packtpub.com>
,并在邮件主题中提及书名。
如果您在某个主题上具有专业知识,并且您对撰写或为书籍做出贡献感兴趣,请参阅我们的作者指南www.packtpub.com/authors。
客户支持
现在您已经是 Packt 图书的骄傲拥有者,我们有一些事情可以帮助您从您的购买中获得最大收益。
下载示例代码
您可以从www.packtpub.com
下载您购买的所有 Packt 书籍的示例代码文件。如果您在其他地方购买了这本书,您可以访问www.packtpub.com/support
并注册,以便将文件直接通过电子邮件发送给您。
勘误
尽管我们已经尽一切努力确保内容的准确性,但错误仍然可能发生。如果您在我们的某本书中发现错误——可能是文本或代码中的错误——如果您能向我们报告这一点,我们将不胜感激。通过这样做,您可以节省其他读者的挫败感,并帮助我们改进本书的后续版本。如果您发现任何勘误,请通过访问www.packtpub.com/submit-errata
,选择您的书籍,点击勘误提交表单链接,并输入您的勘误详情来报告。一旦您的勘误得到验证,您的提交将被接受,勘误将被上传到我们的网站,或添加到该标题的勘误部分下的现有勘误列表中。任何现有勘误都可以通过从www.packtpub.com/support
选择您的标题来查看。
盗版
互联网上对版权材料的盗版是一个跨所有媒体的持续问题。在 Packt,我们非常重视我们版权和许可证的保护。如果您在互联网上发现我们作品的任何非法副本,无论形式如何,请立即提供位置地址或网站名称,以便我们可以寻求补救措施。
请通过链接mailto:copyright@packtpub.com与我们联系,并提供涉嫌盗版材料的链接。
我们感谢您在保护我们作者以及为我们提供有价值内容的能力方面的帮助。
问题和建议
如果您在本书的任何方面遇到问题,请通过链接mailto:questions@packtpub.com与我们联系,我们将尽力解决。
第一章. 在您的项目中交互式编程
离开一个成熟且稳定的语言,如 Java,需要一些相当充分的理由。在尝试了解 Scala 语法之前,我们先要明确 Scala 之所以吸引人的原因。
在本章中,我们将涵盖以下主题:
-
使用 Scala 进行 Java 项目的好处
-
通过交互式会话后的快速课程熟悉语言语法,包括案例类、集合操作,以及一些有用的功能,如选项、元组、映射、模式匹配和字符串插值
使用 Scala 进行 Java 项目的好处
我们在这里提出的出现顺序和重要性仅反映我们个人的经验,因为每个项目和程序员团队在优先级方面通常都有自己的议程。
更简洁、更易于表达
您应该采用 Scala 的最终原因是可读性:类似于普通英语的代码将使任何人(包括你自己)更容易理解、维护和重构它。Scala 的独特之处在于它统一了 Java 具有的面向对象方面,以便使代码模块化,并利用函数式语言的强大功能来简洁地表达转换。为了说明如何通过在语言中引入匿名函数(也称为lambda)来实现简洁性,请看以下代码行:
List(1,2,3,4,5) filter (element => element < 4)
对于 Java 程序员来说,一开始可能会觉得这行代码看起来有些不自然,因为它不符合在类上调用方法签名的一般模式。之前代码可能的 Java 翻译如下:
import java.util.*;
public class ListFilteringSample {
public static void main(String[] args) {
List<Integer> elements = Arrays.asList(1, 2, 3, 4, 5);
List<Integer> filteredElements = new ArrayList<Integer>();
for (Integer element : elements)
if (element < 4) filteredElements.add(element);
System.out.println("filteredElements:" + filteredElements);
}
}
小贴士
下载示例代码
您可以从 Packt Publishing 的账户下载您购买的所有书籍的示例代码文件。www.packtpub.com
。如果您在其他地方购买了这本书,您可以访问www.packtpub.com/support
,并注册以直接将文件通过电子邮件发送给您。
我们首先创建一个包含五个整数的List
,然后创建一个空的List
,它将保存过滤的结果,然后遍历List
中的元素,只保留符合if
谓词(element < 4
)的元素,最后打印出结果。即使这写起来很简单,但也需要几行代码,而 Scala 的代码可以像下面这样阅读:
“从给定的List
中过滤每个元素,使得该元素小于4
”。
代码变得非常简洁且易于表达,这使得程序员能够立即理解一个复杂或冗长的算法。
提高生产力
拥有一个执行大量类型检查并充当个人助手的编译器,据我们看来,这是相对于在运行时动态检查类型的语言的一个显著优势,而 Java 作为静态类型语言,可能是它最初如此受欢迎的主要原因之一。Scala 编译器也属于这一类,并且更进一步,它能够自动找出许多类型,通常可以减轻程序员在代码中明确指定这些类型的负担。此外,你 IDE 中的编译器可以提供即时反馈,因此提高了你的生产力。
自然地从 Java 进化而来
Scala 与 Java 无缝集成,这是一个非常吸引人的特性,可以避免重新发明轮子。你今天就可以在生产环境中开始运行 Scala。像 Twitter、LinkedIn 或 Foursquare(仅举几个例子)这样的大型企业已经在过去很多年里进行了大规模部署,最近还有像 Intel 或 Amazon 这样的巨头也加入了进来。Scala 编译成 Java 字节码,这意味着性能将相当。当你执行 Scala 程序时,你运行的大多数代码可能是 Java 代码,主要区别在于程序员看到的内容以及编译代码时的先进类型检查。
更适合异步和并发代码
为了实现更好的性能和处理更多的负载,现代 Java Web 开发框架和库现在正在解决与多核架构和与不可预测的外部系统集成相关的一些难题。Scala 使用不可变数据结构和函数式编程结构以及其对并行集合的支持,更有可能成功编写出表现正确的并发代码。此外,Scala 优越的类型系统和宏支持使得可以编写出安全异步结构,例如可组合的异步和异步语言扩展。
总结来说,Scala 是唯一一个拥有所有这些特性的语言。它是静态类型的,在 JVM 上运行,并且完全兼容 Java,既面向对象又函数式,而且不啰嗦,因此提高了生产力,减少了维护,因此更有趣。
如果你现在迫不及待地想要开始尝试之前简要描述的 Scala 的诱人特性,现在是打开浏览器、访问 Typesafe 页面 URL www.typesafe.com/platform/getstarted
并下载 Typesafe Activator 的好时机。
本章剩余部分的目的是通过在交互式 shell 中输入命令,逐步介绍 Scala 的一些基本概念,并直接从编译器获得反馈。这种通过实验学习的方法应该像一股清新的空气,并且已经被证明是一种非常有效的学习语言语法和有用结构的方法。虽然 Scala 在洛桑联邦理工学院(EPFL)持续发展,但现在许多大公司和中小企业都在利用 Typesafe 平台的功能。
如同他们网站所述,Typesafe Activator 是“一个本地的网页和命令行工具,帮助开发者开始使用 Typesafe 平台”。我们将在后续的章节中更详细地介绍 Activator,但在此阶段,我们只需走最短的路来启动并运行,熟悉一些语言的语法。
现在,你应该能够将下载的 zip 压缩文件解压到你的系统中的任意目录。
在解压的存档中找到 activator 脚本,如果你正在运行 Windows,则右键单击它并选择 打开;如果你在 Linux/Mac 上,则在终端窗口中输入以下命令:
> ./activator ui
在这两种情况下,这将在浏览器窗口中启动 Activator UI。
在 Activator 的 HTML 页面的 新应用程序 部分中,点击 [基础] Hello-Scala!
模板。
注意以下截图中的 HTML 表单的 位置 字段。它指示了你的项目将被创建的位置:
目前,你不需要过多关注后台发生的事情,也不需要关注项目生成的结构。出于好奇,你可以点击 代码视图 & 在 IDE 中打开 选项卡,然后点击 运行 选项卡来执行这个 Hello World Scala 项目,它应该会打印出 "Hello, world !"。
启动一个终端窗口,并通过在命令行中输入以下命令导航到我们刚刚创建的 hello-scala 项目的根目录(假设我们的项目位于 C:\Users\Thomas\hello-scala
):
> cd C:\Users\Thomas\hello-scala
C:\Users\Thomas\hello-scala> activator console
此命令将启动 Scala 解释器,也称为 Scala REPL(读取-评估-打印-循环),这是一个简单的交互式命令行工具。
通过 REPL 学习 Scala
作为 Java 开发者,REPL 可能对你来说是新的,因为 Java 语言没有这样的东西。它曾经指的是 Lisp 语言的交互式环境,而今天,许多编程语言如 JavaScript、Clojure、Ruby 和 Scala 都有等效的工具。它由一个命令行 shell 组成,你可以输入一个或多个表达式,而不是完整的文件,并通过评估结果立即获得反馈。REPL 是一个极好的工具,它帮助我们学习所有的 Scala 语法,因为它使用编译器的全部力量编译和执行你写的每个语句。在这样的交互式环境中,你会在你写的每一行代码上获得即时反馈。
如果你刚开始接触 Scala,我们建议你仔细跟随这个 REPL 会话,因为它会给你很多关于用 Scala 编程的有用知识。
让我们深入探讨 Java 和 Scala 之间的一些最明显的差异,以便熟悉 Scala 语法。
声明 val/var 变量
在 Java 中,你会通过先放类型,然后是名称,最后是可选值来声明一个新变量:
String yourPast = "Good Java Programmer";
在 Scala 中,变量名和类型的声明顺序是颠倒的,类型出现在变量名之前。让我们将以下行输入到 REPL 中:
scala> val yourPast : String = "Good Java Programmer" [Hit Enter]
yourPast : String = "Good Java Programmer"
与 Java 相比,将变量、类型和名称的声明顺序颠倒可能看起来是一个奇怪的想法,如果你想让 Java 开发者尽可能容易地掌握 Scala 语法。然而,出于几个原因,这样做是有意义的:
-
在这种情况下,Scala 编译器能够自动推断类型。你可以(并且可能应该,为了简洁)通过输入等效但更短的代码行来省略这个类型:
scala> val yourPast = "Good Java Programmer" yourPast : String = "Good Java Programmer"
这是最基本的类型推断的示例,你会看到 Scala 编译器会尽可能尝试推断类型。如果我们省略了这个可选的类型,但遵循 Java 语法,编译器所做的解析将更难实现。
-
在我们看来,了解变量名比了解其类型更重要,以便理解程序的流程(因此将其放在前面);例如,如果你处理一个代表社会保障号码(ssn)的变量,我们认为术语 ssn 比知道它是否表示为字符串、整数或其他类型更有价值。
你可能注意到了声明前的val
变量;这意味着我们明确地将变量声明为不可变的。我们可以尝试像以下代码片段中那样修改它:
scala> yourPast = "Great Scala Programmer"
<console>:8: error: reassignment to val
yourPast = "Great Scala Programmer"
^
上述代码不仅会清楚地解释出了什么问题,还会精确地指出解析器不同意的地方(注意^
字符精确地显示了错误所在行)。
如果我们想创建一个可变变量,我们应该像以下代码片段中那样用var
声明它:
scala> var yourFuture = "Good Java Programmer"
yourFuture: String = "Good Java Programmer"
scala> yourFuture = "Great Scala Programmer"
yourFuture: String = "Great Scala Programmer"
总结来说,您不能更改yourPast
,但您可以更改yourFuture
!
在 Scala 中,行尾的分号是可选的;这是该语言的一个小而令人愉悦的特性。
让我们继续探讨一个重要的区别。在 Java 中,您有诸如int
、char
或boolean
(总共八个)这样的原始类型,以及诸如+
或>
这样的操作符来操作数据。在 Scala 中,只有类和对象,这使得 Scala 在某些方面比 Java 更“面向对象”。例如,将以下值输入到 REPL 中:
scala> 3
res1: Int = 3
默认情况下,编译器创建了一个名为res1
的不可变Int
(整数)变量(即结果 1),以防您稍后需要重用它。
现在,在 REPL 中输入以下行:
scala> 3 + 2
res2: Int = 5
上述代码类似于操作符的使用(如在 Java 中),但实际上是调用名为+
的方法,该方法在对象3
上调用,输入参数为2
,相当于稍微不太清晰的语句:
scala> (3).+(2)
res3: Int = 5
通过删除指定括号的必要性,这里添加了语法糖(即设计来使事物更容易阅读或表达的语言)。这也意味着我们现在可以在自己定义的类型上实现类似的方法,以优雅地表达代码。例如,我们可以通过简单地声明Money(10,"EUR") + Money(15,"USD")
来表达两种不同货币的Money
对象的相加(注意,Money
类型在默认的 Scala 库中不存在)。让我们在 REPL 中尝试这样做。
定义类
首先,我们可以定义一个新的名为Money
的类,它有一个名为amount
的构造函数参数,类型为Int
,如下所示:
scala> class Money(amount:Int)
defined class Money
注意
Scala 有一种特殊的语法来声明构造函数参数,这将在稍后更深入地探讨。
现在,我们可以创建一个Money
实例,如下面的代码片段所示:
scala> val notMuch = new Money(2)
notMuch : Money = Money@76eb235
您将得到一个带有其显示引用的对象。REPL 为您提供了TAB 完成功能,因此键入notMuch.
然后按Tab键:
scala> notMuch. [Tab]
asInstanceOf isInstanceOf toString
通过使用前面的自动完成,您将得到该类可用方法的建议,就像您在大多数 Java IDE 中会得到的那样。
如前所述,您可以通过调用构造函数来构建新的Money
实例,但由于它不是一个字段,您无法访问amount
变量。要将其作为Money
类的字段,您必须在它前面添加一个'val'
或'var'
声明,如下面的代码片段所示:
scala> class Money(val amount:Int)
defined class Money
这次,我们不会再次输入创建实例的行,而是将使用上箭头(显示先前表达式的快捷键:控制台的历史记录)并导航到它:
scala> val notMuch = new Money(2)
notMuch : Money = Money@73cd15da
注意
在 REPL 中,您可以在任何时候按下Tab键,并提供自动完成功能。
在这个新实例上调用自动完成将显示以下内容:
scala> notMuch. [Tab ]
amount asInstanceOf isInstanceOf toString
因此,我们可以简单地通过引用它来读取这个amount
字段的getter
值:
scala> notMuch.amount
res4: Int = 2
同样,如果我们将金额声明为var
变量而不是val
,我们也将能够访问setter
方法:
scala> class Money(var amount:Int)
defined class Money
scala> val notMuch = new Money(2)
notMuch: Money = Money@6517ff0
scala> notMuch. [ Tab ]
amount amount_= asInstanceOf isInstanceOf toString
当我们使用以下代码片段时,将调用setter
方法:
scala> notMuch.amount=3
notMuch.amount: Int = 3
解释case
类
作为 Java 开发者,我们习惯于 JavaBean 风格的领域类,这些类不仅包括具有 getter 和 setter 的字段,还包括构造函数以及hashCode
、equals
和toString
方法,如下面的代码片段所示:
public class Money {
private Integer amount;
private String currency;
public Money(Integer amount, String currency) {
this.amount = amount;
this.currency = currency;
}
public Integer getAmount() {
return amount;
}
public void setAmount(Integer amount) {
this.amount = amount;
}
public String getCurrency() {
return currency;
}
public void setCurrency(String currency) {
this.currency = currency;
}
@Override
public int hashCode() {
int hash = 5;
hash = 29 * hash + (this.amount != null ? this.amount.hashCode() : 0);
hash = 29 * hash + (this.currency != null ? this.currency.hashCode() : 0);
return hash;
}
@Override
public boolean equals(Object obj) {
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
final Money other = (Money) obj;
return true;
}
@Override
public String toString() {
return "Money{" + "amount=" + amount + ", currency=" + currency + '}';
}
}
在 Scala 中实现这一点非常简单,只需在类声明前添加case
关键字即可:
scala> case class Money(amount:Int=1, currency:String="USD")
defined class Money
我们刚刚定义了一个名为Money
的类,它有两个不可变的字段amount
和currency
,并具有默认值。
不深入探讨case
类的细节,我们可以说,除了传统 JavaBean 风格领域类的先前特性外,它们还具有强大的模式匹配机制。case
关键字与 Java 中的switch
语句类似,尽管它更加灵活,正如我们稍后将看到的。case
类包含一些附加功能,其中之一是工厂方法来创建实例(无需使用new
关键字来创建实例)。
默认情况下,Scala 类中声明的字段是公开的,与 Java 不同,Java 中的字段具有包私有作用域,定义在private
和protected
之间。我们可以将case class Money(private val amount: Int, private val currency: String)
写成私有,或者使用var
代替val
来使字段可变。
创建Money
实例的最简单方法非常直接:
scala> val defaultAmount = Money()
defaultAmount: Money = Money(1,USD)
scala> val fifteenDollars = Money(15,"USD")
fifteenDollars: Money = Money(15,USD)
scala> val fifteenDollars = Money(15)
fifteenDollars: Money = Money(15,USD)
在前面的实例声明中,由于只提供了一个参数而不是两个,编译器将其与第一个声明的字段匹配,即amount
。由于值15
与amount
(即Integer
)类型相同,编译器能够使用默认值"USD"
作为货币填充实例。
与amount
变量不同,仅使用货币参数调用Money
构造函数将失败,如下面的语句所示:
scala> val someEuros = Money("EUR")
<console>:9: error: type mismatch;
found : String("EUR")
required: Int
val someEuros = Money("EUR")
^
前面的代码无法工作,因为编译器无法猜测我们指的是哪个参数,因此尝试按声明顺序匹配它们。为了能够使用给定的"EUR"
字符串的默认值amount
,我们需要显式包含参数名称,如下面的代码片段所示:
scala> val someEuros = Money(currency="EUR")
someEuros: Money = Money(1,EUR)
因此,我们还可以显式标记所有参数,这在参数很多时是推荐的,如下面的代码片段所示:
scala> val twentyEuros = Money(amount=20,currency="EUR")
twentyEuros: Money = Money(20,EUR)
在构建实例时,还有一个非常有用的方法,即copy
方法,它从原始实例创建一个新的实例,并最终替换给定的参数:
scala> val tenEuros = twentyEuros.copy(10)
tenEuros: Money = Money(10,EUR)
我们可以使用具有显式命名参数的copy
方法,如下所示:
scala> val twentyDollars = twentyEuros.copy(currency="USD")
twentyDollars: Money = Money(20,USD)
当编写测试用例时,copy
方法非常有用,特别是在初始化具有许多相似字段的模拟实例时。
让我们继续前进,通过创建我们的 Money
类的 addition
操作来继续。为了简单起见,我们暂时假设我们只处理相同货币的金额,即默认的 USD。
在 Java 中,我们可能会用以下签名和简单内容来添加这样的方法:
public class Money {
Integer amount;
String currency;
public Money(Integer amount, String currency) {
this.amount = amount;
this.currency = currency;
}
public Money add(Money other) {
return new Money(this.amount +
other.amount, this.currency);
}
...
}
在 Scala 中,我们使用 def
关键字来定义类方法或函数。在 REPL 中,我们可以有多行表达式。以下 case
类声明,包含求和方法 +
的实现,是这种特性的一个示例:
scala> case class Money(val amount:Int=1, val currency:String="USD"){
| def +(other: Money) : Money = Money(amount + other.amount)
| }
defined class Money
注意,我们可以使用 +
作为方法名。我们还在签名声明中包含了返回类型 Money
,这虽然是可选的,因为 Scala 的类型推断会推导出它,但明确包含它是公共方法(默认情况下,如果没有指定其他作用域,方法都是公共的)的良好文档实践。此外,在 Scala 中,由于方法末尾的 return
关键字是可选的,所以总是最后一个语句返回给方法的调用者。此外,通常认为省略 return
关键字是一个好的实践,因为它不是强制的。
现在,我们可以用以下简单的表达式来编写两个 Money
实例的相加操作:
scala> Money(12) + Money(34)
res5: Money = Money(46,USD)
一旦我们开始操作对象的集合,事情就会变得有趣起来,Scala 的函数式编程部分在这方面非常有帮助。由于泛型是语言的一部分(从 Java 5 开始),Java 可以通过编写以下代码片段来遍历整数列表:
List<Integer> numbers = new ArrayList<Integer>();
numbers.add(1);
numbers.add(2);
numbers.add(5);
for(Integer n: numbers) {
System.out.println("Number "+n);
}
上述代码产生以下输出:
Number 1
Number 2
Number 5
在 Scala 中,列表的声明可以写成如下形式:
scala> val numbers = List(1,2,5)
numbers: List[Int] = List(1,2,5)
Scala 集合系统地区分不可变和可变集合,但通过默认构造不可变集合来鼓励不可变性。它们通过返回新集合而不是修改现有集合来模拟添加、更新或删除操作。
打印数字的一种方法是我们可以遵循 Java 的命令式编程风格,通过创建一个 for
循环来遍历集合:
scala> for (n <- numbers) println("Number "+n)
Number 1
Number 2
Number 5
在 Scala 中编写代码的另一种方式(以及 JVM 上的许多其他语言,如 Groovy、JRuby 或 Jython)涉及更函数式的方法,使用 lambda 表达式(有时称为闭包)。简而言之,lambda 只是你可以传递作为参数的函数。这些函数接受输入参数(在我们的例子中,是 n
整数)并返回其主体中的最后一个语句/行。它们具有以下形式:
functionName { input =>
body
}
一个典型的 lambda 示例,用于遍历我们之前定义的 numbers
列表中的元素,如下所示:
scala> numbers.foreach { n:Int =>
| println("Number "+n)
| }
Number 1
Number 2
Number 5
在这种情况下,主体只包含一个语句(println...
),因此返回 Unit
,即一个空的结果,大致相当于 Java 中的 void
,但 void
不返回任何内容。
在撰写本书时,Java 中的 lambda 表达式即将到来,并将很快作为 JDK8 发布的一部分被引入,采用类似 Scala 的风格。因此,一些函数式构造将很快对 Java 开发者可用。
应该可以以以下方式编写我们的小型示例:
numbers.forEach(n -> { System.out.println("Numbers "+n);});
如我们之前所述,Scala 集合默认是不可变的。这对于在多处理器架构中处理它们时按预期行为非常重要。与 Java 相比,Scala 集合的一个独特特性是它们包括对并行运行操作的支持。
集合操作
在本节中,我们将展示如何在 Scala 中以简洁和表达性的方式表达集合操作。
转换包含原始类型的集合
REPL 是一个伟大的工具,可以尝试我们对集合元素可以应用的有力操作。让我们回到我们的解释器提示符:
scala> val numbers = List(1,2,3,4,5,6)
numbers: List[Int] = List(1,2,3,4,5,6)
scala> val reversedList = numbers.reverse
reversedList: List[Int] = List(6,5,4,3,2,1)
scala> val onlyAFew = numbers drop 2 take 3
onlyAFew: List[Int] = List(3, 4, 5)
drop
方法表示我们移除列表的前两个元素,而take
方法表示我们只保留drop
方法得到的结果中的前三个元素。
这个最后的命令有两个有趣的原因:
-
由于每个方法调用都会被评估为一个表达式,我们可以同时链式调用多个方法(这里,
take
是在drop
的结果上调用的)。 -
如前所述,添加到 Scala 语法的语法糖使得我们可以用
numbers drop 2
来代替更传统的 Java 写法numbers.drop(2)
。
在给定列表中编写元素的另一种方式是使用::
方法,在 Scala 文档中通常被称为“cons 运算符”。这种替代语法看起来如下所示:
scala> val numbers = 1 :: 2 :: 3 :: 4 :: 5 :: 6 :: Nil
numbers: List[Int] = List(1, 2, 3, 4, 5, 6)
如果你想知道为什么这个表达式的末尾有一个Nil
值,这是因为 Scala 中有一个简单的规则,即如果一个方法的最后一个字符是:
(即冒号),则该方法应用于其右侧而不是左侧(这样的方法称为右结合)。因此,6 :: Nil
的评估在这种情况下并不等同于6.::(Nil)
,而是Nil.::(6)
。我们可以在 REPL 中展示如下:
scala> val simpleList = Nil.::(6)
simpleList: List[Int] = List(6)
5 :: 6 :: Nil
的评估是通过在之前看到的simpleList
上应用::
方法来完成的,该方法是List(6)
:
scala> val twoElementsList = List(6).::(5)
twoElementsList: List[Int] = List(5, 6)
在这种情况下,5
被添加到6
之前。重复此操作几次将得到最终的List(1,2,3,4,5,6)
。
这种方便的表达列表的方式不仅适用于简单的值,如整数,而且可以应用于任何类型。此外,我们可以通过使用:::
方法以类似的方式连接两个List
实例:
scala> val concatenatedList = simpleList ::: twoElementsList
concatenatedList: List[Int] = List(6, 5, 6)
我们甚至可以在同一个List
中混合各种类型的元素,例如整数和布尔值,如下面的代码片段所示:
scala> val things = List(0,1,true)
things: List[AnyVal] = List(0, 1, true)
然而,正如你可能注意到的,编译器在那个情况下选择的AnyVal
结果类型是整数和布尔值在它们的层次结构中遇到的第一个公共类型。例如,仅检索布尔元素(在列表中的索引为二)将返回AnyVal
类型的元素,而不是布尔值:
scala> things(2)
res6: AnyVal = true
现在,如果我们把一个String
类型的元素也放入列表中,我们将得到一个不同的公共类型:
scala> val things = List(0,1,true,"false")
things: List[Any] = List(0, 1, true, false)
这可以通过查看 Scala 类型的层次结构来直接可视化。表示原始类型(如Int
、Byte
、Boolean
或Char
)的类属于scala.AnyVal
的值类型,而String
、Vector
、List
或Set
属于scala.AnyRef
的引用类型,两者都是通用类型Any
的子类,如下面的图所示:
Scala 类型的完整层次结构可以在官方 Scala 文档中找到,网址为docs.scala-lang.org/tutorials/tour/unified-types.html
。
更复杂的对象集合
让我们操作比整数更复杂的对象。例如,我们可以创建一些我们之前创建的Money
实例的集合,并实验它们:
scala> val amounts = List(Money(10,"USD"),Money(2,"EUR"),Money(20,"GBP"),Money(75,"EUR"),Money(100,"USD"),Money(50,"USD"))
amounts: List[Money] = List(Money(10,USD), Money(2,EUR), Money(20,GBP), Money(75,EUR), Money(100,USD), Money(50,USD))
scala> val first = amounts.head
first: Money = Money(10,USD)
scala> val amountsWithoutFirst = amounts.tail
amountsWithoutFirst: List[Money] = List(Money(2,EUR), Money(20,GBP), Money(75,EUR), Money(100,USD), Money(50,USD))
过滤和分区
过滤集合中的元素是最常见的操作之一,可以写成以下形式:
scala> val euros = amounts.filter(money => money.currency=="EUR")
euros: List[Money] = List(Money(2,EUR), Money(75,EUR))
传递给filter
方法的参数是一个函数,它接受一个Money
项作为输入并返回一个Boolean
值(即谓词),这是评估money.currency=="EUR"
的结果。
filter
方法遍历集合项并对每个元素应用函数,只保留函数返回True
的元素。Lambda 表达式也被称为匿名函数,因为我们可以为输入参数赋予任何我们想要的名称,例如,用x
代替之前使用的money
,仍然得到相同的输出:
scala> val euros = amounts.filter(x => x.currency=="EUR")
euros: List[Money] = List(Money(2,EUR),Money(75,EUR))
写这个单行语句的一个稍微简短的方法是使用一个_
符号,这是在阅读 Scala 代码时经常遇到的一个字符,对于 Java 开发者来说可能一开始看起来有些不自然。它简单地意味着“那个东西”,或者“当前元素”。它可以被视为过去用来填充纸质调查或护照登记表格的空白空间或间隙。其他处理匿名函数的语言保留其他关键字,例如 Groovy 中的it
或 Python 中的self
。之前的 lambda 示例可以用简短的下划线符号重写如下:
scala> val euros = amounts.filter(_.currency=="EUR")
euros: List[Money] = List(Money(2,EUR),Money(75,EUR))
还存在一个filterNot
方法,用于保留函数评估返回False
的元素。此外,还有一个partition
方法可以将filter
和filterNot
方法组合成一个单一调用,返回两个集合,一个评估为True
,另一个为其补集,如下面的代码片段所示:
scala> val allAmounts = amounts.partition(amt =>
| amt.currency=="EUR")
allAmounts: (List[Money], List[Money]) = (List(Money(2,EUR), Money(75,EUR)),List(Money(10,USD), Money(20,GBP), Money(100,USD), Money(50,USD)))
处理元组
注意到分区结果的返回类型,(List[Money],List[Money])
。Scala 支持元组的概念。前面的括号表示法表示一个Tuple
类型,它是 Scala 标准库的一部分,并且对于同时操作多个元素非常有用,而无需创建更复杂的类型来封装它们。在我们的例子中,allAmounts
是一个包含两个Money
列表的Tuple2
对。要访问这两个集合中的任意一个,我们只需输入以下表达式:
scala> val euros = allAmounts._1
euros: List[Money] = List(Money(2,EUR),Money(75,EUR))
scala> val everythingButEuros= allAmounts._2
everythingButEuros: List[Money] = List(Money(10,USD),Money(20,GBP),Money(100,USD),Money(50,USD))
要以一行代码的形式实现这一点,并且更加简洁和自然,可以使用不引用._1
和._2
的partition
方法,如下代码片段所示:
scala> val (euros,everythingButEuros) = amounts.partition(amt =>
| amt.currency=="EUR")
euros: List[Money] = List(Money(2,EUR), Money(75,EUR))
everythingButEuros: List[Money] = List(Money(10,USD), Money(20,GBP), Money(100,USD), Money(50,USD))
这次,作为结果,我们得到了两个变量,euros
和everythingButEuros
,我们可以分别重用它们:
scala> euros
res2: List[Money] = List(Money(2,EUR), Money(75,EUR))
引入 Map
元组的另一种优雅用法与Map
集合的定义有关,Map
集合是 Scala 集合的一部分。类似于 Java,Map
集合存储键值对。在 Java 中,一个简单的HashMap
定义,用几行代码填充和检索Map
集合的元素,可以写成如下:
import java.util.HashMap;
import java.util.Map;
public class MapSample {
public static void main(String[] args) {
Map amounts = new HashMap<String,Integer>();
amounts.put("USD", 10);
amounts.put("EUR", 2);
Integer euros = (Integer)amounts.get("EUR");
Integer pounds = (Integer)amounts.get("GBP");
System.out.println("Euros: "+euros);
System.out.println("Pounds: "+pounds);
}
}
由于没有将 GBP 货币的金额插入到Map
集合中,运行此示例将为Pounds
变量返回一个null
值:
Euros: 2
Pounds: null
在 Scala 中填充Map
集合可以优雅地写成如下形式:
scala> val wallet = Map( "USD" -> 10, "EUR" -> 2 )
wallet: scala.collection.immutable.Map[String,Int] = Map(USD -> 10, EUR -> 2)
"USD" -> 10
表达式是一种方便的方式来指定键值对,在这种情况下,它与在 REPL 中直接定义的Tuple2[String,Integer]
对象等价(REPL 可以直接推断类型):
scala> val tenDollars = "USD"-> 10
tenDollars : (String, Int) = (USD,10)
scala> val tenDollars = ("USD",10)
tenDollars : (String, Int) = (USD,10)
添加和检索元素的过程非常直接:
scala> val updatedWallet = wallet + ("GBP" -> 20)
wallet: scala.collection.immutable.Map[String,Int] = Map(USD -> 10, EUR -> 2, GBP -> 20)
scala> val someEuros = wallet("EUR")
someEuros: Int = 2
然而,访问Map
集合中未包含的元素将抛出异常,如下所示:
scala> val somePounds = wallet("GBP")
java.util.NoSuchElementException: key not found: GBP (followed by a full stacktrace)
引入 Option 构造
从上一节中引入的Map
集合中安全地检索元素的一种方法是通过调用其.get()
方法,这将返回一个类型为Option
的对象,而 Java 目前还没有这个特性。基本上,Option
类型将一个值封装到一个对象中,如果该值为 null,则返回None
类型,否则返回Some(value)
。让我们在 REPL 中输入以下内容:
scala> val mayBeSomeEuros = wallet.get("EUR")
mayBeSomeEuros: Option[Int] = Some(2)
scala> val mayBeSomePounds = wallet.get("GBP")
mayBeSomePounds: Option[Int] = None
模式匹配的简要介绍
避免抛出异常使得将算法的流程作为一个评估表达式继续处理变得方便。这不仅给程序员提供了在无需检查值是否存在的情况下,对Option
值进行复杂链式操作的自由,而且还使得可以通过模式匹配来处理两种不同的情形:
scala> val status = mayBeSomeEuros match {
| case None => "Nothing of that currency"
| case Some(value) => "I have "+value+" Euros"
| }
status: String = I have 2 Euros
模式匹配是 Scala 语言的一个基本且强大的特性。我们稍后会看到更多关于它的例子。
filter
和partition
方法只是列表上所谓的“高阶函数”的两个例子,因为它们操作的是集合类型的容器(如列表、集合等),而不是类型本身。
map
方法
在集合的方法中,不能忽视的是map
方法(不要与Map
对象混淆)。基本上,它将一个函数应用于集合的每个元素,但与foreach
方法返回Unit
不同,它返回一个类似容器类型的集合(例如,一个List
将返回一个大小相同的List
),其中包含通过函数转换每个元素的结果。以下是一个简单的示例代码片段:
scala> List(1,2,3,4).map(x => x+1)
res6: List[Int] = List(2,3,4,5)
在 Scala 中,你可以如下定义独立的函数:
scala> def increment = (x:Int) => x + 1
increment: Int => Int
我们已经声明了一个increment
函数,它接受一个Int
类型的输入值(用x
表示)并返回另一个Int
类型的值(x+1
)。
之前的List
转换可以稍微以不同的方式重写,如下面的代码片段所示:
scala> List(1,2,3,4).map(increment)
res7: List[Int] = List(2,3,4,5)
通过一点语法糖,方法调用中的.
符号以及函数参数上的括号可以省略以提高可读性,这导致以下简洁的一行代码:
scala> List(1,2,3,4) map increment
res8: List[Int] = List(2, 3, 4, 5)
回到我们最初的Money
金额列表,例如,我们可以将它们转换为字符串如下:
scala> val printedAmounts =
| amounts map(m=> ""+ m.amount + " " + m.currency)
printedAmounts: List[String] = List(10 USD, 2 EUR, 20 GBP, 75 EUR, 100 USD, 50 USD)
看一下字符串插值
在 Java 中,使用+
运算符连接字符串,就像我们在上一行所做的那样,是一个非常常见的操作。在 Scala 中,处理字符串表示的一种更优雅且高效的方法是名为字符串插值的功能。自 Scala 版本 2.10 起,新的语法涉及在字符串字面量前添加一个s
字符,如下面的代码片段所示:
scala> val many = 10000.2345
many: Double = 10000.2345
scala> val amount = s"$many euros"
amount: String = 10000.2345 euros
作用域内的任何变量都可以被处理并嵌入到字符串中。使用f
插值器而不是s
可以更精确地进行格式化。在这种情况下,语法遵循与其他语言中printf
方法相同的风格,例如,%4d
表示四位数的格式化,%12.2f
表示小数点前有十二位数字,后有两位数字的浮点数表示法:
scala> val amount = f"$many%12.2f euros"
amount: String = " 10000.23 euros"
此外,字符串插值语法使我们能够嵌入表达式的完整评估,即执行计算的完整代码块。以下是一个示例,其中我们想显示many
变量的值两次:
scala> val amount = s"${many*2} euros"
amount: String = 20000.469 euros
上述代码块遵循与任何方法或函数评估相同的规则,这意味着该块中的最后一个语句是结果。尽管这里我们有一个非常简单的计算,但如果需要,也可以包含多行算法。
了解插值语法后,我们可以将之前的amounts
重写如下:
scala> val printedAmounts =
| amounts map(m=> s"${m.amount} ${m.currency}")
printedAmounts: List[String] = List(10 USD, 2 EUR, 20 GBP, 75 EUR, 100 USD, 50 USD)
groupBy
方法
另一个方便的操作是groupBy
方法,它将一个集合转换为一个Map
类型的集合:
scala> val sortedAmounts = amounts groupBy(_.currency)
sortedAmounts: scala.collection.immutable.Map[String,List[Money]] = Map(EUR -> List(Money(2,EUR), Money(75,EUR)), GBP -> List(Money(20,GBP)), USD -> List(Money(10,USD), Money(100,USD), Money(50,USD)))
foldLeft
方法
我们在这里想介绍的最后一种方法是foldLeft
方法,它将一些状态从一个元素传播到下一个元素。例如,为了对列表中的元素进行求和,你需要累积它们并跟踪从元素到元素的中间计数器:
scala> val sumOfNumbers = numbers.foldLeft(0) { (total,element) =>
| total + element
| }
sumOfNumbers: Int = 21
给定给foldLeft
的第一个参数的值0
是初始值(这意味着在应用函数的第一个List
元素时total=0
)。(total,element)
表示一个Tuple2
对。然而,请注意,对于求和,Scala API 提供了一个sum
方法,因此最后的语句可以写成如下所示:
scala> val sumOfNumbers = numbers.sum
sumOfNumbers: Int = 21
摘要
这章交互式章节介绍了对象和集合的一些常用操作,这只是为了展示 Scala 的一些表达性和强大的构造。
在下一章中,我们将越来越多地将 Scala 与现有的标准 Java Web 应用程序相结合。由于有这么多创建标准 Web 应用程序的方法,结合许多可用的框架和数据库技术,无论它们是否涉及 Spring、Hibernate、JPA、SQL 还是 NoSQL,我们将采取一些已建立的 JavaEE 教程的简单路径。
第二章. 代码集成
能够让 Java 和 Scala 在同一个代码库上协作是保证两种语言之间平稳过渡的先决条件。
在本章中,我们将快速创建一个小型的 Java Web 应用,我们将向您展示如何向其中添加 Scala 代码。然后,我们将介绍 Java 和 Scala 之间的一些最常见的集成点以及编程风格的不同之处,以便希望重构和扩展其 Java 应用的程序员可以根据一些指南进行操作。
为了避免在创建、理解和记录示例 Java 项目上花费太多时间,我们将使用 Oracle 的 NetBeans IDE 发行版中已经可用的小型数据库,并使用 IDE 的代码生成功能从中创建 JPA 持久层以及 REST API。
小贴士
下载示例 Java 项目
如果你迫不及待地想直接跳到本章的 Scala 代码集成功能,你可以跳过以下部分,并从 Packt 的网站www.packtpub.com下载现成的 maven Java 项目。
从现有数据库创建 REST API
NetBeans IDE 附带示例数据库可以从www.netbeans.org网站下载。只需点击该网站上的下载按钮,选择 IDE 的 JavaEE 版本。
一旦运行了安装向导,看到了安装成功!的消息,并且启动了 IDE(在我们的例子中是 8.0 版本),我们就可以在五分钟内创建一个功能齐全的 Web 应用了。第一次使用时,只需点击 NetBeans IDE 左上角来关闭启动屏幕,你应该能看到 IDE 左侧的三栏:项目、文件和服务。
示例数据库
我们参考的数据库可以通过点击 IDE 中的服务面板来查看。在服务标签页下的数据库菜单中,双击jdbc:derby://localhost:1527/sample [app on APP] Database Connection
链接以连接到 1527 端口的示例数据库(Derby 数据库的默认端口)以及APP
模式下的app
用户。在APP
模式下,你应该能找到包括CUSTOMER
和PRODUCT
在内的七个表。通过右键单击CUSTOMER
表并选择查看数据…,你应该能够浏览表的内容。
以下图表展示了整个数据库模式,以便你可以可视化不同表之间的依赖关系或外键:
设置 Maven 项目
为了快速设置我们的示例 Java 项目,你可以直接从下载的代码中在你的 IDE 中导入它(并跳过创建 JPA 实体和 REST Web 服务),或者在你喜欢的 NetBeans IDE 上执行以下简单步骤:
-
在 IDE 的 项目 选项卡中右键单击任何位置,选择 新建项目…,然后选择 Maven 类别和 Web 应用程序 项目类型。
-
将 项目名称 输入为
Sample
,将 组 ID 输入为com.demo
,然后点击 Next > 按钮。 -
确保选择 服务器 容器进行部署(我们使用 NetBeans 分发中的默认 GlassFish 4.0)以及 Java EE 7 Web 作为 Java EE 版本。
-
点击 完成 按钮,你应该能在 项目 选项卡下看到创建项目的结构。
创建 JPA 实体和 REST Web 服务
右键单击我们刚刚创建的 Sample 项目根目录,导航到 新建 | 从数据库创建 RESTful Web 服务…。从新打开的窗口中的下拉列表中选择 derby sample
数据库连接,应该会将数据库表显示在 可用表 部分中。仅标记 CUSTOMER
表,并选择 Add>,CUSTOMER
和 DISCOUNT_CODE
(依赖于 CUSTOMER
)应列在 已选表 中,如下截图所示:
点击 下一步 按钮,然后在下一页再次点击 下一步,最后点击 完成 将生成 Customer
和 DiscountCode
的持久化 JPA 实体以及服务外观类,CustomerFacadeREST
和 DiscountCodeFacadeREST
。请注意,自 Java EE6 以来,EntityManager
类在每个服务类中实例化,这避免了需要生成在先前版本中生成的 JPA 控制器类。
小贴士
在 NetBeans 教程的 www.netbeans.org 下有关于如何从数据库生成 RESTful Web 服务的更详细版本。
运行和测试项目
在我们开始将 Scala 代码引入我们的 Java 项目之前,我们可以在浏览器中启动我们的应用程序并测试 REST 调用。右键单击项目的 Sample 根节点,选择 运行 以部署应用程序。一旦控制台显示 GlassFish 服务器正在运行,并且浏览器中显示 Hello World! 消息以表明一切部署正确,右键单击项目根目录下的 RESTful Web Services
文件夹,并选择 测试 RESTful Web 服务。打开的对话框允许您选择是否将测试客户端作为同一项目的一部分生成或外部生成,如下截图所示:
选择 本地生成的测试客户端(适用于 Internet Explorer) 并点击 确定。
一旦部署完成,浏览器将显示一个测试页面,我们可以在这里调用customer
和discountcode
实体的 REST 方法。如果我们展开com.demo.sample.customer
文件夹,将显示更多参数。点击{id}
参数,在右侧面板中会出现一个输入字段,我们可以输入特定的客户id
值。例如,我们可以输入409
。在显示MIME类型的下拉列表中,选择application/json和GET
作为测试方法,然后点击测试,如图所示:
页面的底部现在将显示 REST 查询的响应。它包括一个状态:200(OK)消息和一个响应内容,其中原始视图选项卡将显示响应体作为 JSON,如图所示。
在 Java 中添加单元测试
最后,我们可以通过从项目面板中选择Customer.java
源文件,然后右键单击它并导航到工具 | 创建测试来为Customer
类生成一个非常简单的单元测试。只需在对话框中单击确定按钮,并在需要的情况下允许安装JUnit 4.xx。生成的文件将出现在与测试类相同的 Java 包结构下的测试包中,在我们的例子中是com.demo.sample.CustomerTest.java
,这是在 Java 中进行单元测试时的常见约定。右键单击CustomerTest
类并选择测试文件将使所有测试方法在JUnit下运行,并且默认情况下每个测试方法末尾都有一个fail
子句,因此会失败。现在,只需注释掉testGetCustomerId
的fail
语句并删除所有其他测试方法。然后,重新运行测试以在 IDE 中看到绿色的结果。或者,如果您已经在另一个 IDE 或纯文本编辑器中设置了 Maven 项目,您可以在文件系统中的项目根目录(pom.xml
文件所在位置)中,在终端窗口中输入以下 Maven 命令,这可能是您熟悉的命令:
> mvn test
-------------------------------------------------------
T E S T S
-------------------------------------------------------
Running com.demo.sample.CustomerTest
getCustomerId
Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.034 sec
Results :
Tests run: 1, Failures: 0, Errors: 0, Skipped: 0
在 Scala 中添加测试
目前,我们在小型 Maven 项目中只有 Java 代码。我们准备向相同的代码库中添加几行 Scala 代码,以展示这两种语言如何无缝交互。让我们创建一个src/test/scala
目录,位于现有的java/
目录旁边,我们将在这里放置以下新的CustomerScalaTest.scala
类,这是一个与src/test/java
下已有的测试类似的测试:
package com.demo.sample
import org.junit._
import Assert._
class CustomerScalaTest {
@Before
def setUp: Unit = {
}
@After
def tearDown: Unit = {
}
@Test
def testGetCustomerId = {
System.out.println("getCustomerId")
val instance = new Customer()
val expResult: Integer = null
val result: Integer = instance.getCustomerId()
assertEquals(expResult, result)
}
}
如果我们再次运行测试,即再次输入>mvn clean test
,该类将被忽略,因为它不是一个.java
源文件。
在 Java Maven 项目中设置 Scala
为了能够开始编写 Scala 单元测试并将 Scala 代码编译到我们的 Java 项目中,我们需要在pom.xml
文件中添加一些依赖项和 scala-maven-plugin。依赖项如下:
-
核心 scala-library 的依赖:
<dependency> <groupId>org.scala-lang</groupId> <artifactId>scala-library</artifactId> <version>2.10.0</version> </dependency>
-
scalatest(一个支持 JUnit 和其他风格的 Scala 测试框架;我们将在第四章中详细介绍)的依赖:
<dependency> <groupId>org.scalatest</groupId> <artifactId>scalatest_2.10</artifactId> <version>2.0/version> <scope>test</scope> </dependency>
-
在测试用例中使用 Java
Assert
语句的 JUnit 依赖:<dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.11</version> <scope>test</scope> </dependency>
关于scala-maven-plugin
,只需将以下 XML 块添加到pom.xml
构建文件的<plugins>
部分即可:
<plugin>
<groupId>net.alchim31.maven</groupId>
<artifactId>scala-maven-plugin</artifactId>
<executions>
<execution>
<id>scala-compile-first</id>
<phase>process-resources</phase>
<goals>
<goal>add-source</goal>
<goal>compile</goal>
</goals>
</execution>
<execution>
<id>scala-test-compile</id>
<phase>process-test-resources</phase>
<goals>
<goal>testCompile</goal>
</goals>
</execution>
</executions>
</plugin>
如果我们尝试重新运行测试,这次我们的新创建的 Scala 测试将被选中并执行,如下代码片段所示:
> mvn clean test
-------------------------------------------------------
T E S T S
-------------------------------------------------------
Running com.demo.sample.CustomerScalaTest
getCustomerId
Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.035 sec
Running com.demo.sample.CustomerTest
getCustomerId
Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.004 sec
Results :
Tests run: 2, Failures: 0, Errors: 0, Skipped: 0
关于CustomerScalaTest.scala
类,有几个观察结果值得提及。如下:
-
文件顶部的包声明与 Java 中的包声明类似。然而,与 Java 不同,Scala 中不需要在 Scala 中有一个与文件系统目录路径相对应的包声明,尽管推荐这样做。
-
Scala 中的导入语句与 Java 类似,只是
*
通配符被下划线_
替换。小贴士
你可能已经注意到,我们突然有了使用任何 Java 库的巨大能力,这意味着如果我们需要 Scala 中直接不可用的功能,我们永远都不会陷入困境,并且可以始终调用现有 Java 类中的方法。
在pom.xml
构建文件中仅添加少量内容后,我们现在已经使一个常规 Java 项目对 Scala 有了认识,这意味着我们可以自由地添加 Scala 类并在其中调用任何 Java 库。这也意味着作为 Java 开发者,我们现在能够在项目有意义的情况下迁移或重构项目的小部分,随着我们越来越熟悉 Scala 结构,逐步改进我们的代码库。
这种处理现有 Maven 项目的方法只是进行操作的一种方式。在下一章中,我们将看到一些其他方法,这些方法涉及更激进的改变,包括 Scala 的简单构建工具(SBT),它是 Maven 构建的替代方案。
Scala 和 Java 协作
回到 REPL,我们将进一步实验 Scala 和 Java 的混合使用,以探索一些常见的集成需求,特别是测试和操作我们在本章开头构建的 Java REST API。
作为对如何在第一章中介绍的hello-scala项目重启 REPL 的提醒,如果在此期间关闭了它,只需启动一个新的终端窗口,导航到hello-scala项目的根目录,并在命令提示符中输入以下命令:
> ./activator console
在集合类型之间转换
让我们先比较 Java 和 Scala 的集合类,看看我们如何从一个转换到另一个。例如,Scala 的List
(来自scala.collection.immutable
包)与java.util.List
不同,有时,从一种转换到另一种可能很有用。在 Java 中创建java.util.List
的一个方便方法是使用java.util.Arrays
实用方法asList
,其确切签名是public static<T> List<T> asList(T... a)
,其中T
指的是泛型类型。让我们按照以下方式在 REPL 中导入它:
scala> import java.util.Arrays
import java.util.Arrays
由于 JDK 类在类路径中,它们可以直接在 REPL 中访问,如下面的代码片段所示:
scala> val javaList = Arrays.asList(1,2,3,4)
javaList: java.util.List[Int] = [1, 2, 3, 4]
现在我们已经实例化了一个整数 Java 列表,我们想要将其转换为 Scala 等价物,并需要使用以下命令行导入JavaConverters
类:
scala> import scala.collection.JavaConverters._
import scala.collection.JavaConverters._
在 Scaladoc(与 Javadoc 类似,用于记录 Scala API,可在www.scala-lang.org/api/current/index.html在线查看)中查看JavaConverters
的文档,我们可以看到,例如,java.util.List
的等价物是scala.collection.mutable.Buffer
。因此,如果我们对java.util.List
调用asScala
方法,我们将得到确切的结果:
scala> val scalaList = javaList.asScala
scalaList: scala.collection.mutable.Buffer[Int] = Buffer(1, 2, 3, 4)
现在,通过在scalaList
上调用asJava
方法,我们将得到原始的java.util.List
集合:
scala> val javaListAgain = scalaList.asJava
javaListAgain: java.util.List[Int] = [1, 2, 3, 4]
为了验证在将对象转换为目标类型并再次转换回来后我们是否得到了原始对象,可以使用assert
语句,如下面的命令所示:
scala> assert( javaList eq javaListAgain)
[no output]
如果assert
没有输出,这意味着它评估为True
;否则,我们会得到一个堆栈跟踪,显示为什么它们不相等。你可能想知道这个assert
方法从哪里来;assert
是Predef
类的一个方法,这是一个 Scala 类,默认导入,包含常用类型的别名、断言(如我们使用的)以及控制台 I/O 和隐式转换的简单函数。
JavaBean 风格的属性
为了确保与 Java 框架如 Hibernate 或 JMX 的兼容性,有时你可能在类的字段上需要 Java 风格的 getter 和 setter。例如,如果我们按照以下方式在 REPL 中声明一个Company
类:
scala> class Company(var name:String)
defined class Company
我们在第一章中看到,在项目中交互式编程,Scala 访问器方法来读取和修改name
字段分别是name
和name_=
,如下所示:
scala> val sun = new Company("Sun Microsystems")
sun: Company = Company@55385db5
scala> sun.name
res33: String = Sun Microsystems
scala> sun.name_=("Oracle")
[no output is returned]
scala> sun.name
res35: String = Oracle
要有 Java 风格的 getter 和 setter 的一个直接方法是使用scala.beans.BeanProperty
注解字段,如下面的命令行所示:
scala> import scala.beans.BeanProperty
import scala.beans.BeanProperty
scala> class Company(@BeanProperty var name:String)
defined class Company
scala> val sun = new Company("Sun Microsystems")
sun: Company = Company@42540cca
scala> sun.getName()
res36: String = Sun Microsystems
scala> sun.setName("Oracle")
[no output is returned]
scala> sun.name (alternatively sun.getName)
res38: String = Oracle
Scala 和 Java 面向对象
Scala 和 Java 类之间的互操作性使得用 Scala 类替换或扩展现有的 Java 类变得非常简单。编译 Scala 类产生的字节码与 Java 产生的非常相似。例如,让我们看一下我们之前生成的 Customer
Java 类的简短版本:
public class Customer {
private Integer customerId;
private String zip;
public Customer(Integer customerId) {
this.customerId = customerId;
}
public Customer(Integer customerId, String zip) {
this.customerId = customerId;
this.zip = zip;
}
public Integer getCustomerId() {
return customerId;
}
public void setCustomerId(Integer customerId) {
this.customerId = customerId;
}
public String getZip() {
return zip;
}
public void setZip(String zip) {
this.zip = zip;
}
}
如果我们将它重构为一个具有类参数的 Scala 类并创建一个实例,在 REPL 中我们会得到以下内容:
scala> class Customer ( var customerId: Int, var zip: String) {
def getCustomerId() = customerId
def setCustomerId(cust: Int): Unit = {
customerId = cust
}
}
defined class Customer
scala> val customer = new Customer(1, "123 45")
customer: Customer = Customer@13425838
scala> customer.zip
res5: String = 123 45
然而,在这个定义中不存在只接受单个 zip
参数的构造函数:
scala> val otherCustomer = new Customer("543 21")
<console>:8: error: not enough arguments for constructor Customer: (customerId: Int, zip: String)Customer.
Unspecified value parameter zip.
val otherCustomer = new Customer("543 21")
^
为了完成我们对 Java 类的重构,我们需要一个额外的构造函数,如下所示:
scala> class Customer ( var customerId: Int, var zip: String) {
| def this( zip: String) = this(0,zip)
| def getCustomerId() = customerId
| def setCustomerId(cust: Int): Unit = {
| customerId = cust
| }
| }
defined class Customer
scala> val customer = new Customer("123 45")
customer: Customer = Customer@7944cdbd
这次,我们能够使用辅助构造函数创建一个实例,它遵循一些规则。具体如下:
-
任何辅助构造函数都必须立即调用另一个
this(…)
构造函数 -
主要构造函数必须在最后被调用,以确保所有参数都已初始化
Scala 特质作为增强的 Java 接口
软件接口是使代码通过合同与其他外部软件系统交互的有用机制,将它的行为规范与其实现隔离开来。尽管 JVM 上的 Java 类只能扩展一个单一类,但它们可以通过实现多个接口拥有多个类型。然而,Java 接口是纯抽象的,即它们只包含常量、方法签名和嵌套类型,但没有方法体;例如,请参见以下代码片段:
interface VIPCustomer {
Integer discounts();
}
相比之下,Scala 特质更强大,因为它允许部分实现方法体,因此更可重用。可以使用特质将行为混合到类中。让我们在 REPL 中举一个例子:
scala> class Customer(val name:String, val discountCode:String="N" ){
| def discounts() : List[Int] = List(5)
| override def toString() = "Applied discounts: " +
| discounts.mkString(" ","%, ","% ")
| }
defined class Customer
此类声明了两个字段,name
和 discountCode
(初始化为 "N"
表示正常),以及两个方法,discounts()
和 toString()
,其中 discounts()
将客户的折扣累加到整数列表(初始化为 5
百分比折扣)中,而 toString()
则显示它。
我们可以定义几个扩展我们刚刚创建的类的特质:
scala> trait VIPCustomer extends Customer {
| override def discounts = super.discounts ::: List(10)
| }
defined trait VIPCustomer
VIPCustomer
类是一个客户,可以获得额外的 10
百分比折扣,并将其附加到所有已提供的折扣列表中。第二个特质如下所示:
scala> trait GoldCustomer extends Customer {
| override def discounts =
| if (discountCode.equals("H"))
| super.discounts ::: List(20)
| else super.discounts ::: List(15)
}
defined trait GoldCustomer
GoldCustomer
类是一个客户,可以获得额外的 15
百分比折扣,或者如果她的评级,即 discountCode
是 "H"
(高),则可以获得 20
百分比折扣。
现在我们编写一个 Main
类来展示在实例化 Customer
类时堆叠特质时的添加。我们使用 with
关键字将这些额外的行为混合到类中,如下所示:
scala> object Main {
| def main(args: Array[String]) {
| val myDiscounts = new Customer("Thomas","H") with
| VIPCustomer with GoldCustomer
| println(myDiscounts)
| }
| }
defined module Main
我们现在可以简单地执行主方法,并得到以下预期的结果:
scala> Main.main(Array.empty)
Applied discounts: 5%, 10%, 20%
注意特质堆叠的顺序很重要。它们是从右到左相互调用的。因此,GoldCustomer
是第一个被调用的。
特性位于接口和抽象类之间。然而,你只能扩展一个抽象类,而可以扩展多个特性。
声明对象
Java 代码经常使用static
关键字来引用单例方法和常量。Scala 不支持static
标识符,而是用object
概念代替class
声明。如果你需要将 Java 代码重构为 Scala,只需使用object
声明而不是class
,你就可以得到单例实例,任务完成,额外的优势是这种 Scala 对象也可以扩展接口和特性。object
的一个简单例子是我们之前在可堆叠特性使用中展示的Main
程序声明,或者以下简单的hello world
应用程序:
scala> object Main {
| def main(args: Array[String]) {
| println("Hello Scala World !")
| }
| }
defined module Main
除了对象的概念之外,Scala 还提供了伴生对象的概念,它由一个与同一包和文件中的同名类共存的伴生对象组成。这就是为什么它被称为伴生。
介绍伴生对象
伴生对象允许存储静态方法,并且从这一点出发,你可以完全访问类的成员,包括私有成员。例如,这是一个声明静态工厂方法的好地方,并且案例类重载了apply
工厂方法,这样在创建案例类实例时就不需要使用new
关键字:
scala> case class Customer(val name:String)
defined class Customer
scala> val thomas = Customer("Thomas")
thomas: Customer = Customer(Thomas)
然而,如果你想使用,也可以使用new
关键字,如下所示:
scala> val thomas = new Customer("Thomas")
thomas: Customer = Customer(Thomas)
在底层,案例类被构建为一个常规类,其中包含其他东西,比如以下(虽然简化了)的伴生对象代码片段:
object Customer {
def apply()= new Customer("default name")
}
class Customer(name:String) {
…
}
处理异常
我们总结本节关于如何使用异常将代码从 Java 迁移到 Scala 的内容,这个概念在 Java 中无处不在。与 Java 非常相似,你可以编写try { } catch { }
块来捕获可能失败的方法调用。在 Java 中,你会编写类似于以下代码片段的内容:
package com.demo.sample;
public class ConversionSample {
static Integer parse(String numberAsString) {
Integer number = null;
try {
number = Integer.parseInt(numberAsString);
} catch (NumberFormatExceptionnfe) {
System.err.println("Wrong format for "+numberAsString);
} catch (Exception ex) {
System.err.println("An unknown Error has occurred");
}
System.out.println("Parsed Number: "+number);
return number;
}
public static void main(String[] args) {
parse("2345");
parse("23ab");
}
}
上述代码产生以下输出:
run:
Parsed Number: 2345
Wrong format for number 23ab
Parsed Number: null
BUILD SUCCESSFUL (total time: 0 seconds)
在 Scala 中,你可以直接将其翻译成等效的代码:
scala> def parse(numberAsString: String) =
try {
Integer.parseInt(numberAsString)
} catch {
case nfe: NumberFormatException =>
println("Wrong format for number "+numberAsString)
case e: Exception => println("Error when parsing number"+
numberAsString)
}
parse: (numberAsString:String)AnyVal
scala> parse("2345")
res10: AnyVal = "2345"
scala> parse("23ab")
Wrong format for number 23ab
res11: AnyVal = ()
然而,在这种情况下,编译器推断出的返回值不仅为空,而且类型错误,是AnyVal
类型,这是Int
值和异常返回值之间的通用类型。为了确保我们得到整数作为输出,我们需要从catch
块中找到的所有可能情况中返回一个Int
值:
scala> def parse(numberAsString: String) =
try {
Integer.parseInt(numberAsString)
} catch {
case nfe: NumberFormatException =>
println("Wrong format for number "+numberAsString); -1
case _: Throwable =>
println("Error when parsing number "+numberAsString)
-1
}
parse: (numberAsString:String)Int
这次我们可以从解析调用中捕获正确的返回类型,如下所示:
scala> val number = parse("23ab")
Wrong format for number 23ab
number: Int= -1
在所有情况下,我们返回一个Int
值,失败时返回-1
。这个解决方案仍然只能部分令人满意,因为调用者除非我们显示/记录它,否则并不真正知道失败的原因。更好的方法是使用例如Either
类,它表示两种可能类型之一的值,其实例要么是scala.util.Left
类型,要么是scala.util.Right
类型。在这种情况下,我们可以使用Left
部分来处理失败,使用Right
部分来处理成功结果,如下面的代码片段所示:
scala> case class Failure(val reason: String)
defined class Failure
scala> def parse(numberAsString: String) : Either[Failure,Int] =
try {
val result = Integer.parseInt(numberAsString)
Right(result)
} catch {
case _ : Throwable => Left(Failure("Error when parsing number"))
}
parse: (numberAsString:String)Either[Failure,Int]
scala> val number = parse("23ab")
number: Either[Failure,Int] = Left(Failure(Error when parsing number))
scala> val number = parse("2345")
number: Either[Failure,Int] = Right(2345)
明确写出返回类型会导致这些类型的错误在编译时出错,因此强烈推荐这样做。
最后,无需过多细节,使用scala.util.Try
类处理从Either
派生的try
和catch
块还有另一种更合适的方法。它不是将异常处理为Left
和Right
,而是返回Failure[Throwable]
或Success[T]
,其中T
是一个泛型类型。这种方法的优点是它可以在 for 推导式中使用(但我们还没有介绍它们,示例将在第五章中给出,Play 框架入门)。此外,Try
在错误处理方面的语义比Either
更好,因为它描述的是Success
或Failure
,而不是不那么有意义且更通用的术语Left
和Right
。
Java 和 Scala 代码之间的风格差异
如果你打算将 Java 代码重构或重写为 Scala 代码,有一些风格差异是有用的,值得注意。显然,编程风格在很大程度上是一个品味问题;然而,Scala 社区普遍认可的一些指导原则可以帮助 Scala 的新手编写更易于阅读和更易于维护的代码。本节致力于展示一些最常见的差异。
在 Java 中编写算法遵循命令式风格,即一系列改变程序状态的语句序列。Scala 主要关注函数式编程,采用更声明式的编程方法,其中一切都是一个表达式而不是一个语句。让我们通过一个例子来说明这一点。
在 Java 中,你通常会找到以下代码片段:
...
String customerLevel = null;
if(amountBought > 3000) {
customerLevel = "Gold";
} else {
customerLevel = "Silver";
}
...
Scala 的等效代码如下所示:
scala> val amountBought = 5000
amountBought: Int = 5000
scala> val customerLevel =
if (amountBought> 3000) "Gold" else "Silver"
customerLevel: String = Gold
注意,与 Java 语句不同,if
现在被嵌入为结果评估表达式的一部分。
通常,将所有内容作为表达式(在这里是一个不可变表达式)评估的工作方式将使代码的重用和组合变得更加容易。
能够将一个表达式的结果链接到下一个表达式,将为您提供一种简洁的方式来表达相对复杂的转换,这在 Java 中可能需要更多的代码。
调整代码布局
函数式编程的意图是尽量减少状态行为,它通常由简短的 lambda 表达式组成,这样你就可以以一种优雅和简洁的方式可视化一个相当复杂的转换,在许多情况下甚至是一行代码。因此,Scala 的一般格式建议你只使用两个空格缩进,而不是 Java 代码中普遍接受的四个空格缩进,如下面的代码片段所示:
scala> class Customer(
val firstName: String,
val lastName: String,
val age: Int,
val address: String,
val country: String,
valhasAGoodRating: Boolean
) {
override def toString() =
s" $firstName $lastName"
}
defined class Customer
如果你有很多构造函数/方法参数,按照前面所示对它们进行对齐可以使更改它们而不需要重新格式化整个缩进变得更容易。如果你想要重构具有更长名称的类,例如,将Customer
改为VeryImportantCustomer
,这将对你的版本控制系统(Git、subversion 等)产生更小、更精确的差异。
命名规范
在驼峰命名法中命名包、类、字段和方法通常遵循 Java 规范。请注意,你应该避免在变量名中使用下划线(例如first_name
或_first_name
),因为在 Scala 中下划线有特殊含义(在匿名函数中是self
或this
)。
然而,常量,很可能是用 Java 中的private static final myConstant
声明的,在 Scala 中通常使用大驼峰命名法声明,如下面的封装对象所示:
scala> object Constants {
| val MyNeverChangingAge = 20
| }
defined module Constants
在 Java 中,为变量和方法选择一个有意义的名称始终是一个优先事项,通常建议使用较长的变量名来精确描述变量或方法所代表的内容。在 Scala 中,情况略有不同;有意义的名称当然是使代码更易读的好方法。然而,我们同时还在通过使用函数和 lambda 表达式来使行为转换简洁,如果能在一段简短的代码块中捕捉到整个功能,短变量名可以是一个优势。例如,在 Scala 中递增整数列表可以简单地表示如下:
scala> val amounts = List(3,6,7,10) map ( x => x +1 )
amounts: List[Int] = List(4, 7, 8, 11)
虽然在 Java 中使用x
作为变量名通常是不被推荐的,但在这里这并不重要,因为变量没有被重用,我们可以立即捕捉到它所做的转换。有许多短或长的替代 lambda 语法会产生相同的结果。所以,应该选择哪一个?以下是一些替代方案:
scala> val amounts = List(3,6,7,10) map ( myCurrentAmount =>
myCurrentAmount +1 )
amounts: List[Int] = List(4, 7, 8, 11)
在这种情况下,一个长的变量名将一个清晰简洁的一行代码分成两行,因此,使理解变得困难。如果像以下代码片段所示,我们在多行上开始表达逻辑,有意义的名称在这里更有意义。
scala> val amounts = List(3,6,7,10) map { myCurrentAmount =>
val result = myCurrentAmount + 1
println("Result: " + result)
result
}
Result: 4
Result: 7
Result: 8
Result: 11
amounts: List[Int] = List(4, 7, 8, 11)
有时,一个较短但仍具有表达力的名称是一个很好的折衷方案,向读者表明这是一个我们在 lambda 表达式中当前正在操作的数量,如下所示:
scala> val amounts = List(3,6,7,10) map( amt => amt + 1 )
amounts: List[Int] = List(4, 7, 8, 11)
最后,对于这样一个简单的递增函数,所有这些语法中最简洁的语法,熟练的 Scala 程序员会这样写:
scala> val amounts = List(3,6,7,10) map( _ + 1 )
amounts: List[Int] = List(4, 7, 8, 11)
在 Scala 中,下划线也用于以优雅但略显笨拙的方式表达更复杂的操作,如下面的使用foldLeft
方法的求和操作(在上一章中已介绍):
scala> val sumOfAmounts = List(3,6,7,10).foldLeft(0)( _ + _ )
sumOfAmounts: Int = 26
我们不需要将0
显式地作为求和的初始值,我们可以通过使用类似于foldLeft
的reduce
方法来使这个求和表达式更加优雅。然而,我们将集合的第一个元素作为初始值(在这里,3
将是初始值),如下面的命令所示:
scala> val sumOfAmounts = List(3,6,7,10) reduce ( _ + _ )
sumOfAmounts: Int = 26
就风格而言,熟练的 Scala 程序员阅读这段代码不会有任何问题。然而,如果状态累积操作比简单的+
操作更复杂,那么按照以下命令更明确地编写它可能更明智:
scala> val sumOfAmounts =
List(3,6,7,10) reduce ( (total,element) => total + element )
sumOfAmounts: Int = 26
摘要
在本章中,我们介绍了如何开始将 Scala 代码集成到 Java 代码库中,以及如何遵循一些风格指南将一些最常用的 Java 结构重构为 Scala。如果您想了解更多,可以在docs.scala-lang.org/style/
找到一份更详尽的风格建议列表。
到目前为止,我们主要讨论了 Scala 语言和语法。在下一章中,我们将介绍补充它的工具,以及使我们的 Scala 编程既高效又愉快所必需的工具。
第三章。理解 Scala 生态系统
学习一门新语言也意味着熟悉一套新的框架和工具生态系统。好消息是,在 Scala 中,我们可以大量继承来自 Java 的非常丰富和成熟的工具和库集。在本章中,我们将介绍我们作为 Java 开发者已经熟悉的现有生态系统的重大新特性添加。
在本章中,我们将涵盖以下主题:
-
代码编辑环境——也称为 IDE
-
SBT——一个针对 Scala 的特定工具,用于构建、测试和执行代码
-
将实用工具作为插件集成到 SBT 中,以与 Java 生态系统集成
-
Scala Worksheets——一种新颖的交互式编程方法
-
使用 HTTP 和与外部基于 Web 的服务交互,包括介绍“for comprehensions”——一个有用的 Scala 构造
-
Typesafe Activator——一个方便的工具,可以快速启动项目
-
使用 Scala 进行脚本编写
继承 Java 集成开发环境(IDE)
Scala 支持所有三个主要的 Java 集成开发环境(IDE):基于 Eclipse 的(包括所有不同版本的 Eclipse,Typesafe 自带的捆绑版本称为 Scala IDE,以及更多商业 IDE,如 SpringSourceSTS),IntelliJ IDEA 和 NetBeans。这意味着你可以像以前使用 Java 一样继续工作,例如,在 IDE 中运行 Scala JUnit 测试,直接调试或远程调试。在这些平台上扩展的 Scala 支持将为你提供非常有用的自动完成功能和编译器推断的各种类型的即时反馈。在第二章中,代码集成,我们主要使用 NetBeans,因为它有一个方便、小巧且现成的数据库和嵌入式工具,可以将此数据库逆向工程为 Java 的 RESTful API。由于 Eclipse 的使用目标受众更广,也是 Typesafe 提供支持的参考 IDE,因此我们将使用它作为以下章节的主要开发环境。
您可以从 scala-ide.org 网站下载并安装 Scala IDE for Eclipse,无论是支持 Scala 的捆绑版本还是 Scala 插件,都可以通过使用更新站点(就像您在 Java 中安装任何其他 Eclipse 插件到现有环境中所做的那样)。安装捆绑或插件版本的说明在此网站上解释得非常详细,所以我们不会花太多时间在这里重复这个过程。安装 IDEA 和 NetBeans 的说明分别可在 www.jetbrains.com/
和 www.netbeans.org/
找到。
使用简单构建工具(SBT)进行构建
当处理 Scala 时,Java 生态系统的一个主要补充是 Simple Build Tool(SBT),这是一个用 Scala 编写的灵活的构建系统,它还驱动了我们在前几章中使用的 Typesafe Activator,以及我们将在本书后面部分介绍的 Play 框架。与 Java 环境中 Ant 和 Maven 使用的现有 XML 格式相比,SBT 构建定义是以 Scala 的形式编写的 领域特定语言(DSL),具有编译时检查的优势。正如我们将在本节中看到的那样,SBT 提供了许多额外的便利功能。除了基于 Ivy 的依赖管理能力以及支持 Maven 格式存储库之外,SBT 还提供增量编译和交互式外壳(即我们之前使用的 REPL)。它还支持持续测试和部署,并与许多 Scala 测试框架集成,使其成为 Scala 社区的实际构建工具。
开始使用 SBT
SBT 由一个单独的 .jar
存档以及一个非常小的启动脚本组成。因此,它可以在支持 JVM 的任何平台上安装和运行。安装说明可在 www.scala-sbt.org/
找到。
创建示例项目
一旦 SBT 被添加到你的路径中(在撰写本书时,我们使用了版本 0.13.0),在任何文件系统中创建一个名为 SampleProject
的目录,通过在终端窗口中输入以下命令:
> cd <your_filesystem_dir> (e.g. /Users/Thomas/projects/)
> mkdir SampleProject
> cd SampleProject
> sbt
[info] Set current project to sampleproject
> set name := "SampleProject"
[info] Defining *:name
[info] ...
> session save
要结束 SBT 会话,请输入以下命令:
> exit (or press CTRL-D)
这将在项目根目录下创建一个 build.sbt
文件。此文件收集有关项目的信息,即在 Java 世界中相当于 Maven 的 .pom
文件,但 build.sbt
编译成 Scala 而不是 XML。项目的整个文件结构将在稍后通过图表展示,一旦我们添加了一些库依赖项。
打开并编辑 build.sbt
以填写以下基本信息:
name := "SampleProject"
version := "1.0"
scalaVersion := "2.10.3"
注意,每个陈述之间的额外空行很重要。.sbt
文件不是 Scala 程序;它们是一系列 Scala 表达式,其中空白行是这些表达式之间的分隔符。
在我们开始编写代码之前,我们将把我们的空项目导入到我们的 IDE 中。
在 Eclipse、IntelliJ IDEA 和 NetBeans 中导入项目
sbteclipse
插件可用于将纯 SBT 项目适配为 Eclipse 项目。你只需在 project/
目录下创建一个 plugins.sbt
文件,并将以下行输入到其中以导入 sbteclipse
插件:
addSbtPlugin("com.typesafe.sbteclipse" % "sbteclipse-plugin" % "2.4.0")
之前给出的字符串是 SBT 中表达对 Maven 库依赖的一种方式;它相当于你通常会在 pom
文件中写入的内容:
<groupId>com.typesafe.sbteclipse</groupId>
<artifactId>sbteclipse-plugin</artifactId>
<version>2.4.0</version>
你看,在 SBT 中下载库及其依赖项与使用 Maven 几乎相同;它们将从 Maven 仓库(Maven central 和一些其他常用仓库在 SBT 中默认引用;这就是为什么你不需要明确写出它们)中获取。
注意,最终你应该使用不同的版本号,因为这个插件随着时间的推移而演变。当前版本与插件文档一起在 github.com/typesafehub/sbteclipse
中提供。
一旦 SampleProject/project/plugins.sbt
出现在你的项目中,你只需简单地执行以下命令即可生成一个 Eclipse 兼容的项目(仍然从项目的根目录开始):
> sbt eclipse
...
[info] Successfully created Eclipse project files for project(s):
[info] SampleProject
现在如果你还没有这样做,请启动你的 Eclipse IDE,然后选择 文件 | 导入...。导航到 通用 | 将现有项目导入工作区。浏览到你的项目根目录,就像在 Java 中做的那样,然后点击 确定。然后,点击 完成 以完成项目的导入,它将出现在 项目资源管理器 窗口中。
IntelliJ 也拥有其插件,可在 github.com/mpeltonen/sbt-idea
找到。
注意
注意,对于各种 IDE,有两个插件概念:特定 IDE 的 SBT 插件和SBT 的 IDE 插件。
sbteclipse、sbt-idea 和 nbsbt (github.com/dcaoyuan/nbscala/wiki/SbtIntegrationInNetBeans
) 插件都是需要修改你的 plugins.sbt
文件的 SBT 插件。当你运行适当的 SBT 命令时,它们会生成用于 Eclipse、IntelliJ 或 NetBeans 的项目文件。当你更新你的 SBT 文件时,你可能需要重新运行插件以更新你的 IDE 配置。
然而,如果一个 IntelliJ 用户浏览可用的 IntelliJ 插件,那么他们将在那里看到一个不同的 Scala 插件(confluence.jetbrains.com/display/SCA/Scala+Plugin+for+IntelliJ+IDEA
)。这是一个 IntelliJ 的附加组件,而不是 SBT 的附加组件。它帮助 IntelliJ 自动配置其自身以适应 SBT 项目,无需修改你的 SBT 文件或额外的命令。这种方法在 IntelliJ 社区中可能更受欢迎。
如果你使用 Maven 和 Java 世界中的 Eclipse,那么这与 m2eclipse Eclipse 插件与 eclipse Maven 插件的故事大致相同。
与 Eclipse 类似,你应该在 project/
下的 plugins.sbt
文件中编辑,并将对 sbt-idea
插件的依赖项放置如下:
addSbtPlugin("com.github.mpeltonen" % "sbt-idea" % "1.5.2")
创建 IntelliJ 兼容项目的命令如下:
> sbt gen-idea
注意
值得注意的是,截至 IntelliJ IDEA 13,IDEA Scala 插件原生支持 SBT,无需外部插件即可工作。请参阅 IntelliJ 文档了解如何将 SBT 项目导入 IDE。
有时,插件的新版本尚未出现在默认的 Maven 存储库中。在这种情况下,您必须为 SBT 添加这样一个存储库,以便能够上传插件/库。您可以通过添加以下额外的行来完成此操作:
resolvers += "Sonatype snapshots" at "http://oss.sonatype.org/content/repositories/snapshots/"
addSbtPlugin("com.github.mpeltonen" % "sbt-idea" % "1.6.0-SNAPSHOT")
自从 Scala 版本 2.10+以来,NetBeans 也有其插件:
addSbtPlugin("org.netbeans.nbsbt" % "nbsbt-plugin" % "1.0.2")
该插件本身可以从 GitHub 存储库下载和构建,如下所示:
> git clone git@github.com:dcaoyuan/nbsbt.git
> cd nbsbt
> sbt clean compile publish-local
publish-local
命令将在您的文件系统上本地部署它。然后,使用以下命令创建您项目的文件:
> sbt netbeans
我们将继续本章,采用 Eclipse 作为我们的 IDE,但大多数工具也应该在其他 IDE 下工作。此外,如果您需要与其他编辑器(如 ENSIME 和 Sublime Text)的额外集成,请浏览www.scala-sbt.org
的文档。
一旦项目被导入到 Eclipse 中,您会注意到文件结构与 Maven 项目相同;源文件有默认目录src/main/scala
和src/test/scala
,这与 Java 的结构也相同。
在 servlet 容器上运行的 Web 应用创建
在可用的 SBT 插件日益增长列表中,xsbt-web-plugin(可在github.com/JamesEarlDouglas/xsbt-web-plugin
找到)是一个有用的插件,用于创建在 servlet 容器(如 Jetty)上运行的传统的 Web 应用。至于我们之前看到的插件,安装只需在plugins.sbt
文件中添加一行,如下所示:
addSbtPlugin("com.earldouglas" % "xsbt-web-plugin" % "0.4.2")
然后,将以下行添加到build.sbt
文件中:
seq(webSettings :_*)
我们还需要将 Jetty 包含在容器类路径中,如下所示:
libraryDependencies += "org.mortbay.jetty" % "jetty" % "6.1.22" % "container"
整个最小build.sbt
文件如下总结:
name := "SampleProject"
organization := "com.samples"
version := "1.0"
scalaVersion := "2.10.3"
seq(webSettings :_*)
libraryDependencies += "org.mortbay.jetty" % "jetty" % "6.1.22" % "container"
libraryDependencies += "javax.servlet" % "servlet-api" % "2.5" % "provided"
由于我们已经更新了构建文件中的新依赖项,我们需要重新运行sbteclipse
来更新项目的 Eclipse 文件。此操作可以通过从 SBT 命令提示符重新进入来实现:
> eclipse
现在,让我们在 IDE 中用 Scala 编写一个微型的 servlet,以展示我们的简单示例逻辑,它模仿 Java 语法。在包资源管理器窗口中右键单击项目根目录,选择刷新以确保新依赖项被获取。项目的整个结构如下截图所示:
我们现在可以在src/main/scala
(在一个新的com.samples
包中)下开始编辑一个新的 Scala 文件,如下所示:
package com.samples
import scala.xml.NodeSeq
import javax.servlet.http._
class SimpleServlet extends HttpServlet {
override def doGet(request: HttpServletRequest, response: HttpServletResponse) {
response.setContentType("text/html")
response.setCharacterEncoding("UTF-8")
val responseBody: NodeSeq =
<html><body><h1>Hello, world!</h1></body></html>
response.getWriter.write(responseBody.toString)
}
}
最后,我们需要添加一个web.xml
文件,就像我们在 Java 中通常所做的那样,以配置 servlet 部署(应放在src/main/webapp/WEB-INF
目录下),如下所示:
<?xml version="1.0" encoding="UTF-8"?>
<web-app
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
version="2.5">
<servlet>
<servlet-name>simpleservlet</servlet-name>
<servlet-class>com.samples.SimpleServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>simpleservlet</servlet-name>
<url-pattern>/*</url-pattern>
</servlet-mapping>
</web-app>
从项目根目录开始,在我们的命令提示符中,我们现在可以调用 sbt
来部署和执行我们在 Jetty 容器中的小示例,如下所示:
> sbt
> container:start
2014-03-15 14:33:18.880:INFO::Logging to STDERR via org.mortbay.log.StdErrLog
[info] jetty-6.1.22
[info] NO JSP Support for /, did not find org.apache.jasper.servlet.JspServlet
[info] Started SelectChannelConnector@0.0.0.0:8080
[success] Total time: 20 s, completed Mar 15, 2014 2:33:19 PM
>
默认情况下,容器将在本地的 8080 端口上监听。
现在,你可以在网页浏览器中打开 http://localhost:8080/
并验证是否得到以下截图所示的 Hello, world! 消息:
你也可以从 SBT 运行 package
命令,这将组装一个 .war
归档并将其放置在 target/scala-2.10/sampleproject_2.10-1.0.war
下,如下所示:
> package
使用 sbt-assembly 构建单个 .jar 归档
sbt-assembly 插件可以将你的项目代码及其依赖项收集到一个单独的 .jar
文件中,该文件可以发布到仓库或部署到其他环境中。
安装插件包括将 sbt-assembly 添加为 project/assembly.sbt
中的依赖项(从 SBT 0.13 开始),如下所示:
addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "0.11.2")
要在 SBT 中运行 assembly 命令,你只需在项目根目录下创建一个 assembly.sbt
文件,如下代码片段所示:
import AssemblyKeys._ // put this at the top of the file
assemblySettings
// your assembly settings here
有关汇编设置的文档请参阅 github.com/sbt/sbt-assembly
。它们允许你修改,例如,jarName
或 outputPath
变量,以及在汇编阶段跳过测试或显式设置主类,如果你希望创建可运行的 .jar
文件的话。
使用 Scalariform 格式化代码
自动代码格式化是一个有用的功能,不仅因为它能够将相同的格式化规则应用于由不同个人编写的代码,而且还使差异在源管理工具中更加一致。
Scala IDE for Eclipse 使用 Scalariform 作为其代码格式化工具,该工具也作为 sbt-plugin 插件提供,可以添加到 plugins.sbt
文件中,如下所示:
addSbtPlugin("com.typesafe.sbt" % "sbt-scalariform" % "1.2.0")
一旦设置好,Scalariform 将在运行 SBT 中的 compile
或 test:compile
时自动格式化你的源代码。
在 Eclipse 中,格式化代码的方式与 Java 相同,即右键单击编辑器,然后导航到 源 | 格式化(或 Ctrl + Shift + F)。
尝试 Scala 工作表
在前面的章节中,我们曾将 REPL 作为交互式环境运行,以便在输入 Scala 语法时进行实验并获得即时反馈。这使我们能够非常快速地编写一些小算法,并得到正确的语法使事物工作。尽管 SBT 控制台为程序员提供了一个 :replay
命令来重新运行会话中已经编写的内容,但能够将我们的实验保存下来以供以后使用,作为我们项目的一部分,岂不是更好?这正是 Scala 工作表 的全部意义。
Scala 工作表是 Scala 对 Eclipse 支持的独特功能,它带来一个交互式环境,即在项目上下文中的 REPL。此功能现在也适用于 Scala 对 IntelliJ 的支持。
让我们去我们的 Eclipse 中的小型 servlet 示例中尝试一下。
要启动工作表,右键单击任何包或源文件,然后导航到新建 | Scala 工作表(如果不在下拉列表中,导航到其他... | Scala 向导 | Scala 工作表),如下面的截图所示:
例如,我们将选择当前的com.samples
包。点击下一步并为你的工作表输入一个名称:experiment
。
这将创建一个名为experiment.sc
的文件,它保存在源代码中,但由于它不是一个.scala
文件,因此它不会与我们的当前代码库冲突,也不会出现在部署的.jar
归档中。
默认页面看起来像以下代码片段:
packagecom.samples
object experiment {
println("Welcome to the Scala worksheet") > Welcome to the Scala worksheet
}
每个语句后面的>
符号之后的内容是评估的结果,一旦你保存 Worksheet 文件,这些结果就会被(重新)评估。你可以尝试一些语句,例如,通过替换println
语句为几行,如下所示:
object experiment {
val number = 1 + 2
List(1,2,3,3,3,4) filter (x => x < 4) distinct
case class Customer(name:String)
Customer("Helen")
new SimpleServlet()
}
一旦你保存它(Ctrl + S),样式表将在右侧显示语句评估,如下面的截图所示:
使用 HTTP 进行工作
由于 Scala 可以导入和调用 Java 类,也可以扩展它们,因此 Scala 生态系统中的许多库只是建立在强大且成熟的 Java 库之上的一个薄层,要么提供额外的功能,要么通过添加一些语法糖来简化它们的用法。
这样的例子之一是 Scala 分发库(可在dispatch.databinder.net/Dispatch.html
找到),这是一个基于 Apache 强大 HttpClient 的 HTTP 交互的有用库。让我们在 REPL 中运行一个小型的分发会话。
由于分发是一个外部库;我们首先需要将其导入到我们的 SBT 项目中,以便能够在 REPL 控制台中使用它。将分发依赖项添加到SampleProject
的build.sbt
文件中,使其看起来像以下代码片段(确保在build.sbt
中的语句之间有一个空白行):
name := "SampleProject"
…
libraryDependencies += "net.databinder.dispatch" %% "dispatch-core" % "0.11.0"
重新启动 REPL 以使库可用,并按以下方式将它们导入会话中:
scala> import dispatch._, Defaults._
import dispatch._
import Defaults._
让我们向在线地理位置服务发送一个基本请求,其中 REST API 是一个简单的GET
请求到freegeoip.net/{format}/{ip_or_hostname}
URL,如下所示:
scala> val request = url("http://freegeoip.net/xml/www.google.com")
request: dispatch.Req = Req(<function1>)
现在,我们将通过 HTTP 发送GET
请求,并将响应作为字符串接收(将 XML 包装起来,这是我们要求的服务响应格式):
scala> val result = Http( request OK as.String)
result: dispatch.Future[String] = scala.concurrent.impl.Promise$DefaultPromise@22aeb07c
注意到解释器返回的dispatch.Future[String]
的结果类型。之前版本的 dispatch 是同步的(并且仍然在dispatch-classic
库名下可用),但最新版本,如我们正在使用的版本,处理现代开发实践,即异步。我们将在第八章中学习异步 Scala 代码,现代应用程序的基本属性 – 异步和并发,但与 Java 类似,Future
充当一个不阻塞的计算占位符。这意味着我们可以在等待变量填充之前继续程序的流程,这在调用可能长时间运行的方法调用(如 REST 服务)时非常方便。然而,请注意,这里的dispatch.Future
与 Java 标准库中的java.util.concurrent.Future
是不同的实现。
要读取和显示我们的 HTTP 请求的结果,我们只需输入以下命令行:
scala> val resultAsString = result()
resultAsString: String =
"<?xml version="1.0" encoding="UTF-8"?>
<Response>
<Ip>74.125.225.114</Ip>
<CountryCode>US</CountryCode>
<CountryName>United States</CountryName>
<RegionCode>CA</RegionCode>
<RegionName>California</RegionName>
<City>Mountain View</City>
<ZipCode>94043</ZipCode>
<Latitude>37.4192</Latitude>
<Longitude>-122.0574</Longitude>
<MetroCode>807</MetroCode>
<AreaCode>650</AreaCode>
</Response>
"
在这里调用result()
实际上是调用result.apply()
方法的语法糖,这是一种在许多情况下使代码看起来优雅的便捷方式。
Dispatch 提供了许多处理请求和响应的方式,例如添加头和参数,以及将响应作为 XML 或 JSON 处理,分割成两个不同的处理器或处理流。为了展示这些行为,我们将以调用另一个在线服务为例,即Groupon服务。Groupon 是一个提供折扣券的服务,当你在各种类别中购买产品或服务(如假期、美容产品等)时,可以提供折扣券。Groupon API 可以查询以收集由城市或坐标(纬度和经度)确定的地理位置内的提供信息。
为了能够实验 API,在注册到www.groupon.com/pages/api
URL 后,你应该获得一个唯一的client_id
密钥,该密钥用于验证你,并且每次调用 API 时都必须传递。让我们在 REPL 中演示这一点:
scala> val grouponCitiesURL = url("http://api.groupon.com/v2/divisions.xml?client_id=<your own client_key>")
grouponCitiesURL: dispatch.Req = Req(<function1>)
scala> val citiesAsText = Http(grouponCitiesURL OK as.String)
citiesAsText: dispatch.Future[String] = scala.concurrent.impl.Promise$DefaultPromise@4ad28057
scala> citiesAsText()
res0: String = <response><divisions><division><id>abbotsford</id><name>Abbotsford</name><country>Canada</country><timezone>Pacific Time (US & Canada)</timezone>...
REPL 限制了输出的数量以获得更好的可读性。而不是将响应作为字符串获取,让我们将其作为 XML 处理:
scala> val citiesAsXML = Http(grouponCitiesURL OK as.xml.Elem)
citiesAsXML: dispatch.Future[scala.xml.Elem] = scala.concurrent.impl.Promise$DefaultPromise@27ac41a3
scala> citiesAsXML()
res1: scala.xml.Elem = <response><divisions><division><id>abbotsford</id><name>Abbotsford</name><country>Canada</country><timezone>Pacific Time (US & Canada)</timezone>...
这次我们的结果是更结构化的,因为它被表示为一个 XML 树。我们可以通过应用一个PrettyPrinter
对象来以更好的格式打印它,该对象将输出宽度限制在 90 个字符,缩进为 2:
scala> def printer = new scala.xml.PrettyPrinter(90, 2)
printer: scala.xml.PrettyPrinter
scala> for (xml <- citiesAsXML)
println(printer.format(xml))
scala> <response>
<divisions>
<division>
<id>abbotsford</id>
<name>Abbotsford</name>
<country>Canada</country>
<timezone>Pacific Time (US & Canada)</timezone>
<timezoneOffsetInSeconds>-25200</timezoneOffsetInSeconds>
<timezoneIdentifier>America/Los_Angeles</timezoneIdentifier>
<lat>49.0568</lat>
<lng>-122.285</lng>
...
</division>
<division>
<id>abilene</id>
<name>Abilene, TX</name>
<country>USA</country>
<timezone>Central Time (US & Canada)</timezone>...
从我们的 XML 结构中提取部分信息可以通过应用map
转换,包括 XPath 表达式来实现。XPath 表达式对于在 XML 元素间导航以保留相关部分非常有用。我们可以逐步提取 XML 片段,并将它们作为Lists
或Seqs
(序列)等集合返回,如下面的代码片段所示:
scala> val cityDivisions = citiesAsXML() map ( city => city \\ "division")
cityDivisions: scala.collection.immutable.Seq[scala.xml.NodeSeq] = List(NodeSeq(<division><id>abbotsford</id><name>Abbotsford</name><country>Canada</country>...
scala> val cityNames =
cityDivisions map ( div => (div \ "name").text)
cityNames: scala.collection.immutable.Seq[String] = List(AbbotsfordAbilene, TXAkron / CantonAlbany / Capital RegionAlbuquerqueAllentown...
这里,我们得到了一个城市名称序列,这些城市有优惠券可用。
Scala 的 for comprehension
与在 Scala 中应用连续的 map
转换以提取 XML 不同,我们可以使用一个强大的结构,称为 for comprehension
或 for expression
,它是迭代的银弹。与在 Java 中找到并用于迭代的 for
循环不同,for comprehension
返回一个结果。它们如下指定:
for (sequence) yield expression
在上述代码中,sequence
可以包含以下组件:
-
生成器:它们驱动迭代,如下所示:
element <- collection
就像 Java 循环一样,
element
代表绑定到迭代当前元素的局部变量,而collection
代表要迭代的集合。此外,第一个生成器(至少需要有一个)决定了结果类型。例如,如果输入集合是List
或Vector
,则for comprehension
将分别产生List
或Vector
。 -
过滤器:它们控制迭代,如下所示:
if expression
前面的表达式必须评估为布尔值。过滤器可以定义在生成器所在的同一行,也可以单独定义。
-
定义:它们是局部变量定义,如下所示:
variable = expression
它们是中间值,可以有助于计算结果。
使用几个具体的例子,for comprehension
结构更容易可视化:
scala> for {
elem <- List(1,2,3,4,5)
} yield "T" + elem
res3: List[String] = List(T1, T2, T3, T4, T5)
我们仅使用一个生成器就将 List[Int]
转换成了 List[String]
。使用两个生成器的示例如下所示:
scala> for {
word <- List("Hello","Scala")
char <- word
} yield char.isLower
res4: List[Boolean] = List(false, true, true, true, true, false, true, true, true, true)
我们可以在任何生成器上添加过滤器。例如,如果我们只想保留每个单词的大写字母,我们可以这样写:
scala> for {
word <- List("Hello","Scala")
char <- word if char.isUpper
} yield char
res5: List[Char] = List(H, S)
在以下示例中,我们说明了如何添加局部变量定义:
scala> for {
word <- List("Hello","Scala")
char <- word
lowerChar = char.toLower
} yield lowerChar
res6: List[Char] = List(h, e, l, l, o, s, c, a, l, a)
回到我们的 HTTP Groupon 服务,我们现在可以使用 for comprehension
提取城市名称,如下所示:
scala> def extractCityNames(xml: scala.xml.Elem) =
for {
elem <- xml \\ "division"
name <- elem \ "name"
} yield name.text
extractCityNames: (xml: scala.xml.Elem)scala.collection.immutable.Seq[String]
scala> val cityNames = extractCityNames(citiesAsXML())
cityNames: scala.collection.immutable.Seq[String] = List(Abbotsford, Abilene, TX, Akron / Canton, Albany / Capital Region, Albuquerque, Allentown / Reading, Amarillo, Anchorage...
为了能够查询 API 的第二部分以检索特定区域的特殊折扣优惠,我们还需要从查询的城市中获取经纬度信息。让我们通过返回一个包含三个元素的元组来完成这个操作,第一个元素是名称,第二个元素是纬度,第三个元素是经度:
scala> def extractCityLocations(xml: scala.xml.Elem) =
for {
elem<- xml \\ "division"
name <- elem \ "name"
latitude <- elem \ "lat"
longitude <- elem \ "lng"
} yield (name.text,latitude.text,longitude.text)
extractCityLocations: (xml: scala.xml.Elem)scala.collection.immutable.Seq[(String, String, String)]
scala> val cityLocations = extractCityLocations(citiesAsXML())
cityLocations: scala.collection.immutable.Seq[(String, String, String)] = List((Abbotsford,49.0568,-122.285), (Abilene, TX,32.4487,-99.7331), (Akron / Canton,41.0814,-81.519), (Albany / Capital Region,42.6526,-73.7562)...
在返回的城市列表中,我们可能现在只对其中一个感兴趣。让我们使用以下命令检索檀香山的地理位置:
scala> val (honolulu,lat,lng) = cityLocations find (_._1 == "Honolulu") getOrElse("Honolulu","21","-157")
honolulu: String = Honolulu
lat: String = 21.3069
lng: String = -157.858
上述代码中的 find
方法接受一个谓词作为参数。由于其返回类型是 Option
值,我们可以通过调用 getOrElse
来检索其内容,在 find
方法不返回任何匹配项的情况下,我们可以在这里写入默认值。
可以使用模式匹配来表示另一种表示形式,这在 第一章 中简要描述,即 在项目中交互式编程,如下所示:
scala> val honolulu =
cityLocations find { case( city, _, _ ) =>
city == "Honolulu"
}
honolulu: Option[(String, String, String)] = Some((Honolulu,21.3069,-157.858))
模式匹配的常规语法通常在所有 case
选项之前使用 match
关键字,所以这里是一个简化的表示,其中 match
关键字是隐式的。下划线(_
)以及 case
中给出的 city
变量在模式匹配中是通配符。我们本可以给这些下划线变量命名,但这是不必要的,因为我们没有在谓词中使用它们(即 city == "Honolulu"
)。
现在让我们创建一个请求来查询所有匹配特定地理区域的交易:
scala> val dealsByGeoArea = url("http://api.groupon.com/v2/deals.xml?client_id=<your client_id>")
dealsByGeoArea: dispatch.Req = Req(<function1>)
处理数据作为元组的一个替代方案是定义案例类以方便和可重用地封装元素。因此,我们可以定义一个 Deal
类,并重写我们之前的 for comprehension
语句,返回 Deal
实例而不是元组:
scala> case class Deal(title:String = "",dealUrl:String = "", tag:String = "")
defined class Deal
scala> def extractDeals(xml: scala.xml.Elem) =
for {
deal <- xml \\ "deal"
title = (deal \\ "title").text
dealUrl = (deal \\ "dealUrl").text
tag = (deal \\ "tag" \ "name").text
} yield Deal(title, dealUrl, tag)
extractDeals: (xml: scala.xml.Elem)scala.collection.immutable.Seq[Deal]
正如我们之前用于检索城市一样,我们现在可以通过 HTTP GET 获取交易,这次解析特定城市的 XML,比如檀香山,知道它的纬度和经度,如下所示:
scala> val dealsInHonolulu =
Http(dealsByGeoArea <<? Map("lat"->lat,"lng"->lng) OK as.xml.Elem)
dealsInHonolulu: dispatch.Future[scala.xml.Elem] = scala.concurrent.impl.Promise$DefaultPromise@a1f0cb1
<<?
操作符表示我们将 GET
方法的输入参数附加到 dealsByGeoArea
请求中。Map
对象包含这些参数。这相当于 HTTP GET 的常规表示,我们将输入参数作为键/值对放在 URL 中(即 request_url?param1=value1;param2=value2
)。这与 <<
操作符形成对比,后者会指定一个 POST
请求。从 dealsInHonolulu()
服务调用产生的原始 XML 中创建一个结构化的 Deal
实例序列可以写成如下:
scala> val deals = extractDeals(dealsInHonolulu())
deals: scala.collection.immutable.Seq[Deal] = List(Deal(Laundry Folding StylesExam with Posture Analysis and One or Three Adjustments at Cassandra Peterson Chiropractic (Up to 85% Off)One initial consultation, one exam, one posture analysis, and one adjustmentOne initial consultation, one exam, one posture analysis, and three adjustments,http://www.groupon.com/deals/cassandra-peterson-chiropractic,Beauty & Spas), Deal(Laundry Folding Styles1.5-Hour Whale-Watching Sunset Tour for an Adult or Child from Island Water Sports Hawaii (50% Off) A 1.5-hour whale watching sunset tour for one childA 1.5-hour whale watching sunset tour for one adult,http://www.groupon.com/deals/island-water-sports-hawaii-18,Arts and EntertainmentOutdoor Pursuits), Deal(Dog or Horse?$25 for Take-Home Teeth-Whit...
按类别对交易列表进行排序只是对集合应用 groupBy
方法的问题,如下所示:
scala> val sortedDeals = deals groupBy(_.tag)
sortedDeals: scala.collection.immutable.Map[String,scala.collection.immutable.Seq[Deal]] = Map("" -> List(Deal(SkeleCopSix Bottles of 3 Wine Men 2009 Merlot with Shipping Included6 Bottles of Premium Red Wine,http://www.groupon.com/deals/gg-3-wine-men-2009-merlot-package,), Deal(Famous...
注意到 groupBy
方法是应用在集合上的 MapReduce 作业的 Map
部分的一个非常方便的方法,在我们的例子中,创建一个 Map
对象,其中键是 Groupon 交易的标签或类别,值是特定类别所属的交易列表。对 Map
对象的可能的小型 Reduce
操作可以包括使用 mapValues
方法来转换这个(键,值)存储的值,从而计算每个类别的交易数量:
scala> val nbOfDealsPerTag = sortedDeals mapValues(_.size)
nbOfDealsPerTag: scala.collection.immutable.Map[String,Int] = Map("" -> 2, Arts and EntertainmentOutdoor Pursuits -> 1, Beauty & Spas ->3, Food & DrinkCandy Stores -> 1, ShoppingGifts & Giving -> 1, ShoppingFraming -> 1, EducationSpecialty Schools -> 1, Tickets -> 1, Services -> 1, TravelTravel AgenciesEurope, Asia, Africa, & Oceania -> 1)
我们刚才的例子只探索了我们可以用 HTTP 工具(如 dispatch)做什么的表面,更多内容可以在它们的文档中找到。与 REPL 的直接交互极大地增强了此类 API 的学习曲线。
处理 HTTP 交互的轻量级框架有很多优秀的替代方案,在派发的情况下,我们只看了客户端的事情。因此,可以通过 Unfiltered、Finagle、Scalatra 或 Spray 等框架构建轻量级 REST API(仅举几个例子)。Spray 目前正在重新设计,以成为 Play 框架(基于 Akka)的 HTTP 层;我们将在本书后面的章节中介绍这些技术。
利用 Typesafe Activator
为了能够在前面的章节中运行交互式编程会话,我们已经下载并安装了一个名为Typesafe Activator的工具。无论是作为命令行工具还是通过网页浏览器运行,激活器都允许我们从一个模板中创建和执行一个示例项目,在这种情况下,是一个最小的hello-scala
项目。从中,我们访问了 SBT 控制台,它充当 REPL。
Typesafe Activator 可以被视为由 SBT 驱动的轻量级 IDE。它提供了许多项目模板,程序员可以将它们作为新开发项目的起点进行重用。
基于激活器模板创建应用程序
打开一个命令终端窗口,转到你提取激活器的目录,然后输入以下命令:
> ./activator new
Enter an application name
>
你需要按照以下方式输入你新项目的名称:
> javasample
Fetching the latest list of templates...
Enter a template name, or hit tab to see a list of possible templates
> [Hit TAB]
activator-akka-cassandra activator-akka-spray
activator-play-autosource-reactivemongo activator-scalding
activator-spray-twitter akka-callcenter
akka-cluster-sharding-scalaakka-clustering
akka-distributed-workers akka-distributed-workers-java
akka-java-spring akka-sample-camel-java
akka-sample-camel-scalaakka-sample-cluster-java
akka-sample-cluster-scalaakka-sample-fsm-java-lambda
akka-sample-fsm-scalaakka-sample-main-java
akka-sample-main-scalaakka-sample-multi-node-scala
akka-sample-persistence-java akka-sample-persistence-scala
akka-sample-remote-java akka-sample-remote-scala
akka-scala-spring akka-supervision
angular-seed-play atomic-scala-examples
dart-akka-spray eventual
hello-akka hello-play
hello-play-backbone hello-play-java
hello-play-scala hello-sbt
hello-scala hello-scala-eclipse
hello-scaloid hello-slick
just-play-scalamacwire-activator
matthiasn-sse-chat-template modern-web-template
play-akka-angular-websocket play-angularjs-webapp-seed
play-cake play-example-form
play-guice play-hbase
play-java-spring play-mongo-knockout
play-scalatest-subcut play-slick
play-slick-advanced play-spring-data-jpa
play-sqlite play-with-angular-requirejs
play-yeoman play2-crud-activator
reactive-maps reactive-stocks
realtime-search scala-phantom-types
scaldi-play-example scalikejdbc-activator-template
six-minute-apps slick-android-example
slick-codegen-customization-example slick-codegen-example
slick-plainsql spray-actor-per-request
tcp-async template-template
test-patterns-scala tweetmap-workshop
我们正在使用的 1.0.13 版本已经包含了 76 个模板,这些模板结合了不同的技术和框架,以创建一些有趣的演示项目,但这个列表正在迅速增长(从 1.0.0 版本到 1.0.13 版本,这两个版本之间只相隔几个月,从 38 个增加到 76 个)。
现在,让我们看看play-java-spring
模板,这是一个 Java 项目样本,这样我们就可以熟悉其中包含的代码。因此,当提示输入要使用的模板名称时,输入其名称:
> play-java-spring
OK, application "javasample" is being created using the "play-java-spring" template.
To run "javasample" from the command-line, run:
/Users/thomas/scala/activator-1.0.13/javasample/activator run
To run the test for "javasample" from the command-line, run:
/Users/thomas/scala/activator-1.0.13/javasample/activator test
To run the Activator UI for "javasample" from the command-line, run:
/Users/thomas/scala/activator-1.0.13/javasample/activator ui
激活器创建了一个 SBT 项目,这意味着你可以编辑build.sbt
或plugins.sbt
来添加依赖项、仓库(即,解析器)以及 SBT 插件。例如,我们可以重用之前在plugins.sbt
中提到的addSbtPlugin("com.typesafe.sbteclipse" % "sbteclipse-plugin" % "2.4.0")
行,以便能够创建 Eclipse 项目文件并将项目导入我们的 Scala IDE。
首先,让我们执行程序来看看它做了什么:
> cd javasample
> ./activator run
由于示例基于 Play 框架(我们将在后面的章节中介绍),以下显示表明 Web 应用程序已部署在本地主机的 9000 端口:
--- (Running the application from SBT, auto-reloading is enabled) ---
[info] play - 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...)
在本地主机 9000 上打开浏览器,以可视化示例的基本 Web 表单,并提交一些条目以存储在小型数据库中,如下面的截图所示:
这个 Web 应用程序从简单的 HTML 表单中获取输入,并通过 JPA 将Bar
对象保存到小型数据库中。
要查看此模板中包含的代码,我们可以通过在命令窗口中首先按Ctrl + D来中断当前执行,然后输入以下命令:
> ./activator ui
几秒钟后,浏览器页面应该会在http://localhost:8888/app/javasample/
打开,显示专门针对此应用程序的激活器用户界面。点击代码视图 & 在 IDE 中打开项,通过在左侧面板上双击项目来导航到app/models/Bar.java
文件,如下面的截图所示:
浏览器显示了一个 JPA 注解的实体,正如我们通常在 Eclipse IDE 中使用彩色和格式化的语法进行工作一样。右侧的面板为教程留出了空间,这是一个快速理解代码并开始修改它的宝贵功能。顶部菜单允许你在浏览器中编译、运行或测试应用程序。你可以打开其他一些源文件来识别代码的结构,尽管我们将在稍后详细介绍 Play Web 应用程序。
总结来说,Typesafe Activator 是一种只需几分钟就能让你开始使用的方法,并且非常灵活,因为你可以直接将 activator 项目作为 SBT 项目运行,因此,如果你愿意,有生成 IDE 特定文件以在 Eclipse、IDEA 或 NetBeans 中继续工作的可能性。
REPL 作为脚本引擎
为了处理与用脚本语言编写的程序之间的互操作性,Java 社区流程定义了JSR-223,JavaTM 平台脚本,这是一个 Java 规范请求,使得在 Java 程序中执行用其他语言(如 Groovy、JavaScript、Ruby 或 Jython 等)编写的脚本成为可能。例如,我们可以编写一个嵌入基本 JavaScript 片段的 Java 程序,如下所示:
package com.demo;
import javax.script.*;
public class JSR223Sample {
public static void main(String[] args) throws Exception {
ScriptEngineManager manager = new ScriptEngineManager();
ScriptEngine engine = manager.getEngineByName("JavaScript");
// expose object as variable to script
engine.put("n", 5);
// evaluate a script string.
engine.eval("for(i=1; i<= n; i++) println(i)");
}
}
我们将在 IDE 中获得以下输出:
run:
1
2
3
4
5
BUILD SUCCESSFUL (total time: 0 seconds)
从 Scala 即将推出的 2.11 版本开始,这个非常方便的功能将允许你解释用 Scala 编写的脚本。以下是一个可以直接在 REPL 中运行的示例(摘自scala-lang.org文档):
scala> import javax.script.ScriptEngineManager
importjavax.script.ScriptEngineManager
scala> val engine =
new ScriptEngineManager().getEngineByName("scala")
engine: javax.script.ScriptEngine = scala.tools.nsc.interpreter.IMain@7debe95d
scala> engine.put("n", 5)
n: Object = 5
scala> engine.eval("1 to n.asInstanceOf[Int] foreachprintln")
1
2
3
4
5
res4: Object = null
引擎上下文可以将n
变量绑定到整数值5
,这可以在由foreach
lambda 表达式组成的单行脚本中调用。在这种情况下,脚本只是一个副作用,不返回任何有趣的价值。
摘要
在本章中,我们介绍了一些 Java 和 Scala 生态系统之间的主要差异,并注意到,除了 SBT 和 REPL(在 Java 世界中找不到)之外,我们能够重用所有 Java 库、工具和框架。我们了解到,用于在 SBT 中加载依赖项的group % artifact % version
格式与 Java 的 Maven 相同,实际上,SBT 默认情况下与大多数 Maven 仓库(例如 Maven Central)相似。因此,我们可以有信心,我们的大部分 Java 技能都是可重用的,并且至少在生态系统方面使过渡变得更容易。我们自愿省略了关于测试生态系统的讨论,因为这将是我们下一章的主要内容。
第四章. 测试工具
无论你使用哪种编程语言,都应该非常小心地进行测试,因为测试不仅以一致的方式记录了你的代码,而且对于重构和维护活动,如修复错误,也将大有裨益。
Scala 生态系统在很大程度上遵循 Java 在所有级别的测试趋势,但也有一些不同之处。在许多地方,我们会看到 Scala 正在使用 DSLs(领域特定 语言),这使得测试代码非常易于阅读和理解。实际上,测试可以是介绍 Scala 时一个很好的起点,逐步从现有的 Java 项目迁移过来。
在本章中,我们将通过一些代码示例介绍一些主要的测试工具及其用法。我们已经在 第三章 中编写了一个微型的 JUnit 风格的测试,即 理解 Scala 生态系统,因此我们将从这里开始,专注于属于 行为驱动开发(BDD)的 BDD 风格测试。BDD 对所使用的任何技术栈都是中立的,在过去的几年中,它已成为在 Gherkin 语言(它是 cucumber 框架的一部分,并在 cukes.info/gherkin.html
中解释)中编写清晰规范的一个合规选择,说明代码应该如何表现。在 Java 和许多其他语言中已经使用,这种风格的测试通常更容易理解和维护,因为它们更接近于普通英语。它们更接近于 BDD 的真正采用,旨在使业务分析师能够以结构化的方式编写测试规范,程序可以理解和实现。它们通常代表唯一的文档;因此,保持它们最新并与领域紧密相关非常重要。
Scala 主要提供了两个框架来编写测试,ScalaTest (www.scalatest.org) 和 Specs2 (etorreborre.github.io/specs2/)。由于它们彼此之间非常相似,我们只将介绍 ScalaTest,并对感兴趣的读者说明如何通过查看 Specs2 文档来了解它们之间的差异。此外,我们还将探讨使用 ScalaCheck 框架(www.scalacheck.org)进行的基于属性的自动化测试。
使用 ScalaTest 编写测试
为了能够快速开始可视化使用 ScalaTest 可以编写的某些测试,我们可以利用前一章中介绍的类型安全激活器(Typesafe Activator)中的 test-patterns-scala
模板。它包含了一系列示例,主要针对 ScalaTest 框架。
设置test-patterns-scala
激活器项目只需要你前往我们之前安装 Typesafe Activator 的目录,然后,通过> activator ui
命令启动 GUI,或者输入> activator new
来创建一个新项目,并在提示时选择适当的模板。
模板项目已经包含了sbteclipse
插件;因此,你只需在项目的根目录中通过命令提示符输入,就可以生成与 Eclipse 相关的文件,如下所示:
> activator eclipse
一旦成功创建了 Eclipse 项目,你可以通过选择文件 | 导入... | 通用 | 现有项目将其导入到你的 IDE 工作区。作为前一章的提醒,你也可以为 IntelliJ 或其他 IDE 创建项目文件,因为 Typesafe Activator 只是 SBT 的一个定制版本。
你可以查看src/test/scala
中的各种测试用例。由于一些测试使用了我们尚未覆盖的框架,如 Akka、Spray 或 Slick,我们将暂时跳过这些测试,专注于最直接的测试。
在其最简单形式中,一个ScalaTest
类(顺便说一下,它也可能测试 Java 代码,而不仅仅是 Scala 代码)可以通过扩展org.scalatest.FunSuite
来声明。每个测试都表示为一个函数值,这已在Test01.scala
类中实现,如下所示:
package scalatest
import org.scalatest.FunSuite
class Test01 extends FunSuite {
test("Very Basic") {
assert(1 == 1)
}
test("Another Very Basic") {
assert("Hello World" == "Hello World")
}
}
要仅执行这个单个测试类,你应该在命令提示符中输入以下命令:
> activator
> test-only <full name of the class to execute>
在我们的情况下,这个命令将是以下这样:
> test-only scalatest.Test01 (or scalatest.Test01.scala)
[info] Test01:
[info] - Very Basic (38 milliseconds)
[info] - Another Very Basic (0 milliseconds)
[info] ScalaTest
[info] Run completed in 912 milliseconds.
[info] Total number of tests run: 2
[info] Suites: completed 1, aborted 0
[info] Tests: succeeded 2, failed 0, canceled 0, ignored 0, pending 0
[info] All tests passed.
[info] Passed: Total 2, Failed 0, Errors 0, Passed 2
[success] Total time: 9 s, completed Nov 11, 2013 6:12:14 PM
在test-patterns-scala
项目下的src/test/scala/scalatest/Test02.scala
中给出的示例非常相似,但使用===
而不是==
会在测试失败时提供额外的信息。如下所示:
class Test02 extends FunSuite {
test("pass") {
assert("abc" === "abc")
}
test("fail and show diff") {
assert("abc" === "abcd") // provide reporting info
}
}
再次运行测试可以通过输入以下命令来完成:
> test-only scalatest.Test02
[info] Test02:
[info] - pass (15 milliseconds)
[info] - fail and show diff *** FAILED *** (6 milliseconds)
[info] "abc[]" did not equal "abc[d]" (Test02.scala:10)
[info] …
[info] *** 1 TEST FAILED ***
[error] Failed: Total 2, Failed 1, Errors 0, Passed 1
在修复失败的测试之前,这次我们可以在连续模式下执行测试,使用test-only
前的~
字符(从激活器提示符中),如下所示:
>~test-only scalatest.Test02
连续模式会在每次编辑并保存Test02
类时,让 SBT 重新运行test-only
命令。SBT 的这个特性可以通过在后台运行测试或程序而不需要显式编写命令,为你节省大量时间。在第一次执行Test02
时,你可以看到一些红色文本,指示"abc[]" 不等于 "abc[d]" (Test02.scala:10)
。
一旦你更正了abdc
字符串并保存文件,SBT 将自动在后台重新执行测试,你可以看到文本变成绿色。
连续模式也适用于其他 SBT 命令,如~run
或~test
。
Test03
展示了如何期望或捕获异常:
class Test03 extends FunSuite {
test("Exception expected, does not fire, FAIL") {
val msg = "hello"
intercept[IndexOutOfBoundsException] {
msg.charAt(0)
}
}
test("Exception expected, fires, PASS") {
val msg = "hello"
intercept[IndexOutOfBoundsException] {
msg.charAt(-1)
}
}
}
第一个场景失败,因为它期望一个IndexOutOfBoundsException
,但代码确实返回了一个有效的h
,即hello
字符串索引为 0 的字符。
为了能够将 ScalaTest 测试套件作为 JUnit 测试套件运行(例如,在 IDE 中运行或在 Maven 中构建的现有基于 JUnit 的项目中扩展,或者向构建服务器报告时),我们可以使用可用的JUnitRunner
类以及@RunWith
注解,如下面的示例所示:
import org.junit.runner.RunWith
import org.scalatest.junit.JUnitRunner
import org.scalatest.FunSuite
@RunWith(classOf[JUnitRunner])
class MyTestSuite extends FunSuite {
// ...
}
BDD 风格的测试
Test06
是另一种风格的测试示例,即 BDD。简而言之,你几乎用纯英文指定某种用户故事来描述你想要测试的场景的行为。这可以在以下代码中看到:
class Test06 extends FeatureSpec with GivenWhenThen {
feature("The user can pop an element off the top of the stack")
{
info("As a programmer")
info("I want to be able to pop items off the stack")
info("So that I can get them in last-in-first-out order")
scenario("pop is invoked on a non-empty stack") {
given("a non-empty stack")
val stack = new Stack[Int]
stack.push(1)
stack.push(2)
val oldSize = stack.size
when("when pop is invoked on the stack")
val result = stack.pop()
then("the most recently pushed element should be returned")
assert(result === 2)
and("the stack should have one less item than before")
assert(stack.size === oldSize - 1)
}
scenario("pop is invoked on an empty stack") {
given("an empty stack")
val emptyStack = new Stack[Int]
when("when pop is invoked on the stack")
then("NoSuchElementException should be thrown")
intercept[NoSuchElementException] {
emptyStack.pop()
}
and("the stack should still be empty")
assert(emptyStack.isEmpty)
}
}
}
BDD 风格的测试比 JUnit 测试具有更高的抽象层次,更适合集成和验收测试以及文档,对于了解该领域的知识的人来说。你只需要扩展FeatureSpec
类,可选地使用GivenWhenThen
特质,来描述验收需求。有关 BDD 风格测试的更多详细信息,请参阅en.wikipedia.org/wiki/Behavior-driven_development
。我们在这里只想说明,在 Scala 中可以编写 BDD 风格的测试,但我们不会进一步深入它们的细节,因为它们在 Java 和其他编程语言中已经得到了大量文档记录。
ScalaTest 提供了一个方便的 DSL,可以以接近纯英文的方式编写断言。org.scalatest.matchers.Matchers
特质包含许多可能的断言,你应该查看其 ScalaDoc 文档以查看许多使用示例。Test07.scala
表达了一个非常简单的匹配器,如下面的代码所示:
package scalatest
import org.scalatest._
import org.scalatest.Matchers
class Test07 extends FlatSpec with Matchers {
"This test" should "pass" in {
true should be === true
}
}
注意
虽然是用 ScalaTest 的 2.0 版本构建的,但 activator 项目中的原始示例使用的是现在已弃用的org.scalatest.matchers.ShouldMatchers
特质;前面的代码示例实现了相同的行为,但更加更新。
让我们使用 Scala 工作表编写一些更多的断言。右键单击包含所有之前审查过的测试文件的scalatest
包,然后选择new | Scala Worksheet。我们将把这个工作表命名为ShouldWork
。然后我们可以通过扩展带有Matchers
特质的FlatSpec
规范来编写和评估匹配器,如下面的代码所示:
package scalatest
import org.scalatest._
object ShouldWork extends FlatSpec with Matchers {
true should be === true
}
保存此工作表不会产生任何输出,因为匹配器通过了测试。然而,尝试通过将一个true
改为false
来让它失败。这在上面的代码中显示:
package scalatest
import org.scalatest._
object ShouldWork extends FlatSpec with Matchers {
true should be === false
}
这次,我们在评估过程中得到了完整的堆栈跟踪,如下面的屏幕截图所示:
我们可以开始评估更多的should
匹配器,如以下代码所示:
package scalatest
import org.scalatest._
object ShouldMatchers extends FlatSpec with Matchers {
true should be === true
List(1,2,3,4) should have length(4)
List.empty should be (Nil)
Map(1->"Value 1", 2->"Value 2") should contain key (2)
Map(1->"Java", 2->"Scala") should contain value ("Scala")
Map(1->"Java", 2->"Scala") get 1 should be (Some("Java"))
Map(1->"Java", 2->"Scala") should (contain key (2) and not contain value ("Clojure"))
3 should (be > (0) and be <= (5))
new java.io.File(".") should (exist)
}
当我们遇到测试失败时,工作表的评估就会停止。因此,我们必须修复它才能在测试中继续前进。这与我们之前使用 SBT 的test
命令运行整个测试套件是相同的,如下面的代码所示:
object ShouldMatchers extends FlatSpec with Matchers {
"Hello" should be ("Hello")
"Hello" should (equal ("Hej")
or equal ("Hell")) //> org.scalatest.exceptions.TestFailedException:
"Hello" should not be ("Hello")
}
在上一个例子中,最后一个语句(与第一个语句相反)应该失败;然而,它没有被评估。
功能测试
ScalaTest 与 Selenium(它是用于自动化浏览器测试的工具,可在www.seleniumhq.org找到)很好地集成,通过提供完整的 DSL,使得编写功能测试变得简单直接。Test08
是这种集成的明显例子:
class Test08 extends FlatSpec with Matchers with WebBrowser {
implicit val webDriver: WebDriver = new HtmlUnitDriver
go to "http://www.amazon.com"
click on "twotabsearchtextbox"
textField("twotabsearchtextbox").value = "Scala"
submit()
pageTitle should be ("Amazon.com: Scala")
pageSource should include("Scala Cookbook: Recipes")
}
让我们尝试直接在工作表中运行类似的调用。由于工作表会对每个语句评估提供反馈,因此它们非常适合直接识别问题,例如,如果链接、按钮或内容没有按预期找到。
只需在已存在的ShouldWork
工作表旁边创建另一个名为Functional的工作表。右键单击scalatest
包,然后选择New | Scala Worksheet。
工作表可以按照以下方式填写:
package scalatest
import org.scalatest._
import org.scalatest.selenium.WebBrowser
import org.openqa.selenium.htmlunit.HtmlUnitDriver
import org.openqa.selenium.firefox.FirefoxDriver
import org.openqa.selenium.WebDriver
object Functional extends FlatSpec with Matchers with WebBrowser {
implicit val webDriver: WebDriver = new HtmlUnitDriver
go to "http://www.packtpub.com/"
textField("keys").value = "Scala"
submit()
pageTitle should be ("Search | Packt Publishing")
pageSource should include("Akka")
}
在保存操作(Ctrl + S)后,工作表将被评估,并且可能为每个语句显示一些输出信息,除了最后两个带有should
匹配器的语句,因为它们应该评估为true
。
尝试将("Search | Packt Publishing")
更改为不同的值,例如Results
或仅仅是Packt Publishing
,并注意控制台输出提供了关于不匹配内容的有用信息。这在上面的屏幕截图中有展示:
这个功能测试只是触及了可能性的表面。由于我们使用的是 Java Selenium 库,在 Scala 中,你可以继承 Java 中可用的 Selenium 框架的力量。
使用 ScalaMock 进行模拟
模拟是一种可以在不需要所有依赖项都到位的情况下测试代码的技术。Java 在编写测试时提供了几个用于模拟对象的框架。最著名的是 JMock、EasyMock 和 Mockito。随着 Scala 语言引入了新元素,如特性和函数,基于 Java 的模拟框架就不够用了,这就是 ScalaMock(www.scalamock.org)发挥作用的地方。
ScalaMock 是一个本地的 Scala 模拟框架,通常用于 ScalaTest(或 Specs2)中,通过在 SBT(build.sbt
)文件中导入以下依赖项:
libraryDependencies +="org.scalamock" %% "scalamock-scalatest-support" % "3.0.1" % "test"
在 Specs2 中,需要导入以下依赖项:
libraryDependencies +=
"org.scalamock" %% "scalamock-specs2-support" % "3.0.1" % "test"
自从 Scala 版本 2.10 发布以来,ScalaMock 已经被重写,ScalaMock 版本 3.x是我们将通过模拟特例的示例简要介绍的版本。
让我们先定义我们将要测试的代码。它是一个微型的货币转换器(可在www.luketebbs.com/?p=58
找到),从欧洲中央银行获取货币汇率。检索和解析货币汇率 XML 文件只需几行代码,如下所示:
trait Currency {
lazy val rates : Map[String,BigDecimal] = {
val exchangeRates =
"http://www.ecb.europa.eu/stats/eurofxref/eurofxref-daily.xml"
for (
elem <- xml.XML.load(exchangeRates)\"Cube"\"Cube"\"Cube")
yield
(elem\"@currency").text -> BigDecimal((elem\"@rate").text)
}.toMap ++ MapString,BigDecimal
def convert(amount:BigDecimal,from:String,to:String) =
amount / rates(from) * rates(to)
}
在这个例子中,货币汇率是通过 xml.XML.load
方法从 URL 中获取的。由于 XML 是 Scala 标准库的一部分,这里不需要导入。load
方法解析并返回 XML 汇率作为一个不可变的 Elem
类型结构,Elem
是一个表示 XML 元素的案例类。这在下述代码中显示:
<gesmes:Envelope
>
<gesmes:subject>Reference rates</gesmes:subject>
<gesmes:Sender>
<gesmes:name>European Central Bank</gesmes:name>
</gesmes:Sender>
<Cube>
<Cube time="2013-11-15">
<Cube currency="USD" rate="1.3460"/>
<Cube currency="JPY" rate="134.99"/>
<Cube currency="BGN" rate="1.9558"/>
<Cube currency="CZK" rate="27.155"/>
<Cube currency="DKK" rate="7.4588"/>
<Cube currency="GBP" rate="0.83770"/>
...
...
</Cube>
</Cube>
</gesmes:Envelope>
从这个 XML 文档中访问货币汇率列表是通过 XPath 表达式在 Cube 节点内部导航完成的,因此有 xml.XML.load(exchangeRates) \ "Cube" \ "Cube" \ "Cube"
表达式。需要一个单行 for 推导(我们在上一章中引入的 for (…)
yield (…)
构造)来遍历货币汇率,并返回一个 key -> value
对的集合,在我们的情况下,键将是一个表示货币名称的字符串,而 value
将是一个表示汇率的 BigDecimal 值。注意信息是如何从 <Cube currency="USD" rate="1.3460"/>
中提取的,通过写入 (elem \ "@currency").text
来捕获货币属性,以及 (elem \ "@rate").text
来分别捕获汇率。后者将通过从给定的字符串创建一个新的 BigDecimal
值来进一步处理。
最后,我们得到一个包含所有货币及其汇率的 Map[String, BigDecimal]
。我们将 EUR(欧元)货币的映射添加到这个值中,它将代表参考汇率之一;这就是为什么我们使用 ++
运算符合并两个映射,即我们刚刚创建的映射与只包含一个 key -> value
元素的新的映射一起。
在模拟之前,让我们使用 ScalaTest 和 FlatSpec
以及 Matchers
编写一个常规测试。我们将利用我们的 Converter
特质,将其集成到以下 MoneyService
类中:
package se.chap4
class MoneyService(converter:Converter ) {
def sendMoneyToSweden(amount:BigDecimal,from:String): BigDecimal = {
val convertedAmount = converter.convert(amount,from,"SEK")
println(s" $convertedAmount SEK are on their way...")
convertedAmount
}
def sendMoneyToSwedenViaEngland(amount:BigDecimal,from:String): BigDecimal = {
val englishAmount = converter.convert(amount,from,"GBP")
println(s" $englishAmount GBP are on their way...")
val swedishAmount = converter.convert(englishAmount,"GBP","SEK")
println(s" $swedishAmount SEK are on their way...")
swedishAmount
}
}
从 MoneyService
类派生出的一个可能的测试规范如下:
package se.chap4
import org.scalatest._
import org.junit.runner.RunWith
import org.scalatest.junit.JUnitRunner
@RunWith(classOf[JUnitRunner])
class MoneyServiceTest extends FlatSpec with Matchers {
"Sending money to Sweden" should "convert into SEK" in {
val moneyService =
new MoneyService(new ECBConverter)
val amount = 200
val from = "EUR"
val result = moneyService.sendMoneyToSweden(amount, from)
result.toInt should (be > (1700) and be <= (1800))
}
"Sending money to Sweden via England" should "convert into GBP then SEK" in {
val moneyService =
new MoneyService(new ECBConverter)
val amount = 200
val from = "EUR"
val result = moneyService.sendMoneyToSwedenViaEngland(amount, from)
result.toInt should (be > (1700) and be <= (1800))
}
}
为了能够实例化 Converter
特质,我们使用在 Converter.scala
文件中定义的 ECBConverter
类,如下所示:
class ECBConverter extends Converter
如果我们从 SBT 命令提示符或直接在 Eclipse 中(作为 JUnit)执行测试,我们会得到以下输出:
> test
[info] Compiling 1 Scala source to /Users/thomas/projects/internal/HttpSamples/target/scala-2.10/test-classes...
1792.2600 SEK are on their way...
167.70000 GBP are on their way...
1792.2600 SEK are on their way...
[info] MoneyServiceTest:
[info] Sending money to Sweden
[info] - should convert into SEK
[info] Sending money to Sweden via England
[info] - should convert into GBP then SEK
[info] Passed: : Total 2, Failed 0, Errors 0, Passed 2, Skipped 0
[success] Total time: 1 s, completed
如果我们从其中检索货币汇率的 URL 并非总是可用,或者如果某一天货币汇率变动很大,导致转换后的金额不在断言 should (be > (1700) and be <= (1800))
给定的区间内,那么我们的测试可能会失败。在这种情况下,在我们的测试中对转换器进行模拟似乎是合适的,并且可以按照以下方式完成:
package se.chap4
import org.scalatest._
import org.junit.runner.RunWith
import org.scalatest.junit.JUnitRunner
import org.scalamock.scalatest.MockFactory
@RunWith(classOf[JUnitRunner])
class MockMoneyServiceTest extends FlatSpec with MockFactory with Matchers {
"Sending money to Sweden" should "convert into SEK" in {
val converter = mock[Converter]
val moneyService = new MoneyService(converter)
(converter.convert _).expects(BigDecimal("200"),"EUR","SEK").returning(BigDecimal(1750))
val amount = 200
val from = "EUR"
val result = moneyService.sendMoneyToSweden(amount, from)
result.toInt should be (1750)
}
}
expects
方法包含了当我们的代码应该调用 convert
方法时我们期望的参数,而返回方法包含了用预期输出代替实际返回结果的预期值。
ScalaMock 在如何应用模拟代码方面有许多变体,并计划在未来的版本中使用 Macros 来增强模拟语法。简而言之,Macros 是在编译期间由编译器调用的函数。这是 Scala 从 2.10 版本开始添加的一个实验性功能,它使得开发者能够访问编译器 API 并对 AST(抽象语法树)应用转换,即程序的树形表示。Macros 不在本书的范围之内,但它们在代码生成和领域特定语言(DSLs)方面非常有用。它们的用法将改进 ScalaMock 语法;例如,你可以在 inSequence {… }
或 inAnyOrder {… }
代码块中,或者在这些块的嵌套组合中应用你的模拟期望,正如它们在文档中所展示的,该文档可在 scalamock.org 上找到。ScalaMock 还支持类似于 Mockito 的风格,使用 记录-然后-验证 循环而不是我们一直使用的 期望-首先 风格。
使用 ScalaCheck 进行测试
拥有一个完整且一致的测试套件,该套件由单元测试、集成测试或功能测试组成,这对于确保软件开发的整体质量至关重要。然而,有时这样的套件是不够的。在测试特定数据结构时,常常会遇到有太多可能值需要测试的情况,这意味着有大量的模拟或测试数据生成。自动基于属性的测试是 ScalaCheck 的目标,这是一个受 Haskell 启发的 Scala 库,它允许生成(或多或少随机地)测试数据来验证你正在测试的代码的一些属性。这个库可以应用于 Scala 项目,也可以应用于 Java 项目。
要快速开始使用 ScalaCheck,你可以在 build.sbt
文件中包含适当的库,就像我们之前经常做的那样。这如下所示:
resolver += Resolver.sonatypeRepo("releases")
libraryDependencies ++= Seq(
"org.scalacheck" %% "scalacheck" % "1.11.0" % "test")
从 SBT 提示符中,你可以输入 reload
而不是退出并重新启动 SBT,以获取构建文件的新版本,然后输入 update
来获取新的依赖项。一旦完成,你也可以输入 eclipse
来更新你的项目,以便依赖项成为你的类路径的一部分,并且编辑器将识别 ScalaCheck 类。
让我们先运行由 www.scalacheck.org 上的 快速入门 页面提出的 StringSpecification
测试:
import org.scalacheck.Properties
import org.scalacheck.Prop.forAll
object StringSpecification extends Properties("String") {
property("startsWith") = forAll { (a: String, b: String) =>
(a+b).startsWith(a)
}
property("concatenate") = forAll { (a: String, b: String) =>
(a+b).length > a.length && (a+b).length > b.length
}
property("substring") = forAll { (a: String, b: String, c: String) =>
(a+b+c).substring(a.length, a.length+b.length) == b
}
}
在此代码片段中,ScalaCheck(随机)生成了一组字符串并验证了属性的正确性;第一个是直接的;它应该验证将两个字符串a
和b
相加应该产生以a
开头的字符串。这个测试听起来可能很明显会通过,无论字符串的值是什么,但第二个属性验证两个字符串连接的长度并不总是正确的;将a
和b
都喂以空值""
是一个反例,表明该属性没有被验证。我们可以通过以下方式通过 SBT 运行测试来展示这一点:
> test-only se.chap4.StringSpecification
[info] + String.startsWith: OK, passed 100 tests.
[info] ! String.concatenate: Falsified after 0 passed tests.
[info] > ARG_0: ""
[info] > ARG_1: ""
[info] + String.substring: OK, passed 100 tests.
[error] Failed: : Total 3, Failed 1, Errors 0, Passed 2, Skipped 0
[error] Failed tests:
[error] se.chap4.StringSpecification
[error] (test:test-only) sbt.TestsFailedException: Tests unsuccessful
[error] Total time: 1 s, completed Nov 19, 2013 4:30:37 PM
>
ScalaCheck 方便地输出了一个反例,ARG_0: ""
和ARG_1: ""
,这导致测试失败。
我们可以在比字符串更复杂的对象上添加更多测试。让我们在我们的测试套件中添加一个新的测试类,命名为ConverterSpecification
,以测试我们在使用 ScalaMock 进行模拟部分中创建的Converter
:
package se.chap4
import org.scalacheck._
import Arbitrary._
import Gen._
import Prop.forAll
object ConverterSpecification extends Properties("Converter") with Converter {
val currencies = Gen.oneOf("EUR","GBP","SEK","JPY")
lazy val conversions: Gen[(BigDecimal,String,String)] = for {
amt <- arbitrary[Int] suchThat {_ >= 0}
from <- currencies
to <- currencies
} yield (amt,from,to)
property("Conversion to same value") = forAll(currencies) { c:String =>
val amount = BigDecimal(200)
val convertedAmount = convert(amount,c,c)
convertedAmount == amount
}
property("Various currencies") = forAll(conversions) { c =>
val convertedAmount = convert(c._1,c._2,c._3)
convertedAmount >= 0
}
}
如果我们在 SBT 中运行测试,将显示以下输出:
> ~test-only se.chap4.ConverterSpecification
[info] + Converter.Conversion to same value: OK, passed 100 tests.
[info] + Converter.Various currencies: OK, passed 100 tests.
[info] Passed: : Total 2, Failed 0, Errors 0, Passed 2, Skipped 0
[success] Total time: 1 s, completed Nov 19, 2013 9:40:40 PM
1\. Waiting for source changes... (press enter to interrupt)
在这个规范中,我们添加了两个特定的生成器;第一个名为currencies
的生成器只能生成来自我们想要测试的有效货币列表的几个字符串,否则随机生成的字符串会产生不属于Map
的字符串。让我们将无效项"DUMMY"
添加到生成的列表中,以验证测试是否失败:
val currencies = Gen.oneOf("EUR","GBP","SEK","JPY","DUMMY")
保存后,测试会自动重新运行,因为我们指定了test-only
前的~
符号。如下所示:
[info] ! Converter.Conversion to same value: Exception raised on property evaluation.
[info] > ARG_0: "DUMMY"
[info] > Exception: java.util.NoSuchElementException: key not found: DUMMY
[info] ! Converter.Various currencies: Exception raised on property evaluation.
[info] > ARG_0: (1,,)
[info] > ARG_0_ORIGINAL: (1,DUMMY,SEK)
[info] > Exception: java.util.NoSuchElementException: key not found:
[error] Error: Total 2, Failed 0, Errors 2, Passed 0, Skipped 0
[error] Error during tests:
[error] se.chap4.ConverterSpecification
[error] (test:test-only) sbt.TestsFailedException: Tests unsuccessful
[error] Total time: 1 s, completed Nov 19, 2013 9:48:36 PM
2\. Waiting for source changes... (press enter to interrupt)
第二个名为conversions
的生成器展示了如何构建一个更复杂的生成器,该生成器利用了 for 推导式的强大功能。特别是,请注意suchThat {_ >= 0}
过滤方法确保任意选择的整数具有正值。此生成器返回一个包含所有必要值的Tuple3
三元组,用于测试Converter.convert
方法。
摘要
在本章中,我们介绍了 Scala 中可用的主要测试框架,这些框架在很大程度上继承了丰富的 Java 生态系统。此外,通过 ScalaCheck 应用属性测试,我们探索了提高测试质量的新方法。为了进一步提高软件质量,感兴趣的读者可以查看www.scala-sbt.org/
网站上列出的其他 SBT 插件,特别是scalastyle-sbt-plugin
用于检查编码风格或各种代码覆盖率插件。在下一章中,我们将深入探讨庞大的 Web 开发领域,并利用 Scala 语言的力量使门户和 Web 应用的开发变得高效且有趣。
第五章. Play 框架入门
本章开始了我们在 Scala 中进行 Web 开发的旅程。Web 开发已经成为一个选择架构和框架非常多的领域。找到合适的工具并不总是件简单的事,因为它涵盖了从传统的 Java EE 或 Spring 基础架构风格到更近期的类似 Ruby on Rails 的方法。大多数现有的解决方案仍然依赖于采用 servlet 容器模型,无论它们使用的是轻量级容器如 Jetty/Tomcat,还是支持 EJBs(企业 JavaBeans)如 JBoss、Glassfish、WebSphere 或 WebLogic。许多在线文章和会议演讲都试图比较一些替代方案,但随着这些框架的快速发展以及有时关注不同的方面(如前端与后端),编制一个公平准确的列表仍然很困难。在 Scala 世界中,创建 Web 应用的替代方案从轻量级框架如 Unfiltered、Spray 或 Scalatra 到功能齐全的解决方案如 Lift 或 Play 框架都有。
我们选择专注于 Play 框架,因为它包含了我们认为对可维护的现代软件开发至关重要的重要特性。Play 框架的一些优点包括:
-
Play 框架可扩展且稳健。它能够处理大量负载,因为它建立在完全异步模型之上,该模型基于能够处理多核架构的技术,如 Akka,这是一个用于构建并发和分布式应用的框架,我们将在第八章《现代应用的基本特性 – 异步性和并发性》中进行介绍。
-
通过提高易用性,推广DRY(即不要重复自己)原则,并利用 Scala 的表达性和简洁性,它为我们提供了增强的开发者生产力。除此之外,Play 的击中刷新工作流程,通过简单地刷新浏览器即可获得对所做更改的即时反馈,与 Java servlet 和 EJB 容器的较长的部署周期相比,这实际上提高了生产力。
-
它与基于 JVM 的现有基础设施遗产具有良好的集成。
-
它与现代客户端开发趋势具有良好的集成,这些趋势高度依赖于 JavaScript/CSS 及其周边生态系统,包括 AngularJS 或 WebJars 等框架。此外,Play 框架还支持LESS(即更简洁的 CSS)动态样式表语言以及 CoffeeScript,这是一种小型而优雅的语言,编译成 JavaScript,而无需任何额外的集成。
Play 框架版本 2.x 既有 Java 版本,也有 Scala 版本,这对于 Java 开发者来说是一个额外的优势,因为他们可能会更快地熟悉这些差异,并且在转向 Scala 之前可能已经对 Java 版本有了一些经验。
提供了几个选择,以快速开始使用 Play 框架并创建一个极简的 helloworld
项目。请注意,所有这些选择都是基于 SBT 创建项目的,正如我们在第三章理解 Scala 生态系统中简要提到的。
开始使用经典 Play 发行版
从 www.playframework.com/download
下载经典的 Play 发行版,并将 .zip
压缩包解压到您选择的目录中。将此目录添加到您的路径中(这样在文件系统上的任何位置运行 play
命令都会创建一个新的应用程序)。使用这种替代方案,您可以在终端窗口中输入以下命令:
> play new <PROJECT_NAME> (for example play new playsample)
将显示以下输出:
我们只需要按 Enter 键,因为我们已经在之前的命令中给出了项目名称。按 Enter 键后,将显示以下内容:
Which template do you want to use for this new application?
1 - Create a simple Scala application
2 - Create a simple Java application
> 1
OK, application playsample is created.
Have fun!
就这些;不到一分钟,我们已经有了一个完全工作的网络应用程序,现在我们可以执行它。由于这是一个 SBT 项目(其中 sbt
命令已被重命名为 play
),我们可以直接导航到创建项目的根目录,并开始我们的 Play 会话,就像我们在处理一个 SBT 项目一样。这可以按照以下步骤进行:
> cd playsample
> play run
[info] Loading project definition…
--- (Running the application from SBT, auto-reloading is enabled) ---
[info] play - 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...)
注意,应用程序默认在端口 9000 上启动。如果您想使用不同的端口,可以输入以下命令代替:
> play
这将带您进入 Play (SBT) 会话,然后您可以从那里选择要监听的端口。这可以按照以下步骤进行:
[playsample] $ run 9095
[info] play - Listening for HTTP on /0:0:0:0:0:0:0:0:9095
另一个选择是在终端中输入 > play "run 9095"
。
在 http://localhost:9095/
(如果您使用的是默认端口则为 9000)启动浏览器,您应该在运行的门户上看到 欢迎使用 Play 页面:
开始使用 Typesafe Activator
使用本书前面提到的基于 Activator 模板创建项目的方法,通过 Activator 开始 Play 项目的操作非常简单。只需转到 Typesafe Activator 安装目录,并输入以下命令:
> ./activator ui
这将在浏览器窗口中启动 activator。最基础的 Scala Play 项目位于 hello-play-scala
模板中。一旦您选择了模板,请注意默认位置指示了项目将被创建的位置,然后点击 创建。
让我们从激活器浏览器视图或通过导航到创建的项目根目录并在命令提示符中输入以下命令,直接运行我们的示例项目:
> ./activator run
一旦服务器在 9000 端口上监听,你可以在浏览器中打开http://localhost:9000/
URL。编译只有在访问该 URL 时才会触发,因此应用程序显示可能需要几秒钟。你浏览器中出现的界面应该类似于以下截图:
Play 应用的架构
为了更好地理解如何构建 Play 应用程序,我们首先需要了解其一些架构方面。
可视化框架栈
在我们开始探索典型样本 Play 应用程序背后的代码之前,让我们通过几个图表来可视化框架的架构。首先,展示的是由 Play 组成的技术栈的整体图如下:
在 JVM 之上运行的是 Akka 框架,这是一个基于 actor 模型来管理并发操作的平台,我们将在第八章中详细讨论,即现代应用程序的基本特性 – 异步和并发。尽管如今大多数 Web 框架仍然依赖于如 Tomcat 或 JBoss 这样的 servlet 容器,但 Play 框架的新颖之处在于通过专注于在代码可以进行热替换时使应用程序无状态,从而避免遵循这一模型,即可以在运行时替换。尽管在商业环境中广泛使用和部署,但 servlet 容器存在额外的开销,例如每个请求一个线程的问题,这可能会在处理大量负载时限制可伸缩性。对于开发者来说,每次代码更改时避免重新部署部分或完整的.ear
或.war
存档所节省的时间可能是相当可观的。
在 Akka 之上,有一个基于 Spray(一个用于构建基于 REST/HTTP 的集成层的开源工具包,现称为 Akka-Http)的 REST/HTTP 集成层,它产生并消费可嵌入的 REST 服务。这使得 Play 与现代编写 Web 应用程序的方式相关,在这种方式中,后端和前端通过 HTTP REST 服务进行通信,交换主要是 JSON/XML 消息,这些消息可以被渲染为 HTML5,因此可以充分利用前端 JavaScript 框架的全部功能。
最后,为了能够与各种其他技术集成,例如基于关系型或 NoSQL 的数据库、安全框架、社交网络、基于云或大数据解决方案,www.playmodules.net
列出了大量 Play 插件和模块。
探索请求-响应生命周期
Play 遵循着众所周知的 MVC 模式,Web 请求的生命周期可以如下所示:
为了了解这个工作流程的各个步骤,我们将探索一个作为 Play 发行版一部分的示例helloworld
应用程序。这个helloworld
应用程序比我们之前通过 Typesafe Activator 或直接使用> play new <project>
命令从头创建的项目示例要复杂一些,因此更有趣。
我们在这里考虑的helloworld
应用程序可以在<play 安装根目录>/samples/scala/helloworld
目录下找到(在撰写本文时,我们使用了 Play 2.2.1 发行版)。
对于任何已经包含sbteclipse
插件的 Play 项目,我们可以在命令提示符中直接输入以下命令来生成 Eclipse 相关的文件(在项目根目录级别):
> play eclipse
注意,由于 Play 命令只是 SBT 顶层的一个薄层,我们可以重用相同的语法,即> play eclipse
而不是> sbt eclipse
。一旦这些被导入到 IDE 中,你可以在左侧的包资源管理器面板中看到 Play 应用程序的一般源布局,如下面的截图所示:
首先,让我们使用以下命令运行应用程序,看看它的样子:
> play run
在http://localhost:9000/
打开浏览器,你应该会看到一个类似于以下截图的小型网页表单:
输入所需信息,然后点击提交以验证是否能够显示指定次数的您的名字。
请求流程的第一步出现在conf/routes
文件中,如下所示:
# Routes
# This file defines all application routes (Higher priority routes first)
# ~~~~
# Home page
GET / controllers.Application.index
# Hello action
GET /hello controllers.Application.sayHello
# Map static resources from the /public folder to the /assets URL path
GET /assets/*file controllers.Assets.at(path="/public", file)
这是我们可以定义 HTTP 请求 URL 与需要在 Play 服务器上处理请求的控制器代码之间映射的地方,如下所示:
<REQUEST_TYPE(GET, POST...)> <URL_RELATIVE_PATH> <CONTROLLER_METHOD>
例如,在浏览器中访问http://localhost:9000/hello
URL 与以下路由相匹配:
GET / controllers.Application.index
不带任何参数的index
方法将在controller.Application.scala
类上被调用。
这种将 URL 路由到控制器的方式与在 JAX-RS 或 Spring MVC 中找到的标准 Java 方式不同,在那里每个控制器都被注解。我们认为,路由文件方法给我们提供了一个清晰的概述,即 API 支持什么,也就是文档,并且它使得 Play 应用程序默认就是 RESTful 的。
即使看起来 routes
文件是一个配置文件,它实际上也是编译过的,任何拼写错误或对不存在的控制器方法的引用都会很快被识别。将 controllers.Application.index
替换为 controllers.Application.wrongmethod
,保存文件,然后在浏览器中点击重新加载按钮(Ctrl + R)。你应该会在浏览器中看到错误被很好地显示,如下面的屏幕截图所示:
注意错误消息的精确性和文件中失败行的确切指出。这种在浏览器重新加载时显示错误消息的出色方式是许多使程序员更高效的功能之一。同样,即使路由文件中没有映射错误,访问开发中的未映射 URL(例如 http://localhost:9000/hi
)也会显示错误以及 routes
文件的内容,以显示哪些 URL 可以调用。这可以在以下屏幕截图中看到:
在控制器中处理请求
接下来,让我们看看接收并处理 GET
请求的 Application
类:
object Application extends Controller {
/**
* Describes the hello form.
*/
val helloForm = Form(
tuple(
"name" -> nonEmptyText,
"repeat" -> number(min = 1, max = 100),
"color" -> optional(text)
)
)
// -- Actions
/**
* Home page
*/
def index = Action {
Ok(html.index(helloForm))
}
/**
* Handles the form submission.
*/
def sayHello = Action { implicit request =>
helloForm.bindFromRequest.fold(
formWithErrors => BadRequest(html.index(formWithErrors)),
{case (name, repeat, color) => Ok(html.hello(name, repeat.toInt, color))}
)
}
}
index
方法执行 Action
块,这是一个函数(Request[AnyContent] => Result
),它接受请求并返回一个 Result
对象。Request
类型的输入参数在这里的 index
方法中没有显示,因为它被隐式传递,并且在函数体中没有使用;如果我们想的话,可以写成 def index = Action { implicit request =>
。单行 Ok(html.index(helloForm))
表示返回的结果应该有一个 HTTP 状态码等于 200,即 Ok
,并且将 html.index
视图绑定到 helloForm
模型。
在这个小型示例中,模型由在文件中较早定义的 Form
对象组成。如下所示:
val helloForm = Form(
tuple(
"name" -> nonEmptyText,
"repeat" -> number(min = 1, max = 100),
"color" -> optional(text)
)
)
每个参数都描述为一个 key -> value
对,其中 key
是参数的名称,value
是应用于参数的函数的结果,该函数将生成一个 play.api.data.Mapping
对象。这种映射函数非常有用,可以执行对表单参数的验证。在这里,Form
参数被表示为一个元组对象,但我们可以创建更复杂的对象,例如案例类。Play 分发中的名为 forms 的示例项目包含了这种更高级处理验证方式的示例。在控制器中的 sayHello
方法中遇到的 fold
方法是一种累积验证错误的方法,以便能够一次性报告所有这些错误。让我们在填写表单时输入一些错误(例如,将 name
字段留空或在需要数字时输入字符)来验证错误是如何显示的。这可以在以下屏幕截图中看到:
渲染视图
用于渲染视图的模板位于views/index.scala.html
文件下。该模板如下所示:
@(helloForm: Form[(String,Int,Option[String])])
@import helper._
@main(title = "The 'helloworld' application") {
<h1>Configure your 'Hello world':</h1>
@form(action = routes.Application.sayHello, args = 'id -> "helloform") {
@inputText(
field = helloForm("name"),
args = '_label -> "What's your name?", 'placeholder -> "World"
)
@inputText(
field = helloForm("repeat"),
args = '_label -> "How many times?", 'size -> 3, 'placeholder -> 10
)
@select(
field = helloForm("color"),
options = options(
"" -> "Default",
"red" -> "Red",
"green" -> "Green",
"blue" -> "Blue"
),
args = '_label -> "Choose a color"
)
<p class="buttons">
<input type="submit" id="submit">
<p>
}
}
Play 模板引擎的一个优点是它基于 Scala 语言本身。这是一个好消息,因为我们不需要学习任何新的模板语法;我们可以重用 Scala 结构,无需任何额外的集成。此外,模板被编译,以便我们每次犯错时都能在编译时得到错误提示;错误将以与路由或纯 Scala 控制器代码相同的方式显示在浏览器中。这种快速的反馈与使用 Java Web 开发中更传统的JSPs(JavaServer Pages)技术相比可以节省我们大量时间。
模板顶部的声明包含将在整个模板中填充的绑定变量。模板标记可以生成任何类型的输出,如 HTML5、XML 或纯文本。模板还可以包含其他模板。
在上一个示例中,@main(title = "The 'helloworld' application'){ <block> ...}
语句指的是main.scala.html
视图文件本身,如下所示:
@(title: String)(content: Html)
<!DOCTYPE html>
<html>
<head>
<title>@title</title>
<link rel="stylesheet" media="screen" href="@routes.Assets.at("stylesheets/main.css")">
<link rel="shortcut icon" type="image/png" href="@routes.Assets.at("images/favicon.png")">
<script src="img/@routes.Assets.at("javascripts/jquery-1.6.4.min.js")" type="text/javascript"></script>
</head>
<body>
<header>
<a href="@routes.Application.index">@title</a>
</header>
<section>
@content
</section>
</body>
</html>
如您所见,此文件顶部定义的@(title: String)(content: Html)
与上一个模板中的(title = "The 'helloworld' application'){ <block of template with code> ...}
相匹配。这就是模板相互调用的方式。
@
符号表示 Scala 代码后面直接跟一个变量名或要调用的方法,或者是一个括号内给出的完整代码块,即@{ …code … }
。
在表单提交后,响应(views/hello.scala.html
)模板包含一个for
循环来显示name
字段多次。如下所示:
@(name: String, repeat: Int, color: Option[String])
@main("Here is the result:") {
<ul style="color: @color.getOrElse("inherited")">
@for(_ <- 1 to repeat) {
<li>Hello @name!</li>
}
</ul>
<p class="buttons">
<a href="@routes.Application.index">Back to the form</a>
</p>
}
与认证玩耍
在设计新的 Web 应用时,经常需要的功能之一涉及认证和授权。认证通常要求用户以用户名/密码的形式提供凭证以登录到应用程序。授权是系统确保用户只能执行其有权执行的操作的机制。在本节中,我们将通过扩展我们的helloworld
示例,添加 Play 发行版中的安全功能,以展示 Scala 中特质的用法如何为传统问题提供优雅的解决方案。
让我们定义一个新的控制器,我们将称之为Authentication
,它包含一些常用方法,如login
用于获取登录页面,authenticate
和check
用于执行认证验证,以及logout
用于返回登录页面。以下是实现方式:
object Authentication extends Controller {
val loginForm = Form(
tuple(
"email" -> text,
"password" -> text
) verifying ("Invalid email or password", result => result match {
case (email, password) => check(email, password)
})
)
def check(username: String, password: String) = {
(username == "thomas@home" && password == "1234")
}
def login = Action { implicit request =>
Ok(html.login(loginForm))
}
def authenticate = Action { implicit request =>
loginForm.bindFromRequest.fold(
formWithErrors => BadRequest(html.login(formWithErrors)),
user => Redirect(routes.Application.index).withSession(Security.username -> user._1)
)
}
def logout = Action {
Redirect(routes.Authentication.login).withNewSession.flashing(
"success" -> "You are now logged out."
)
}
}
与上一节中属于 Application
控制器的 index
方法类似,这里的 login
方法包括将一个表单(命名为 loginForm
)绑定到一个视图(命名为 html.login
,对应于文件 views/login.scala.html
)。以下是一个简单的视图模板,它包含两个文本字段来捕获电子邮件/用户名和密码:
@(form: Form[(String,String)])(implicit flash: Flash)
@main("Sign in") {
@helper.form(routes.Authentication.authenticate) {
@form.globalError.map { error =>
<p class="error">
@error.message
</p>
}
@flash.get("success").map { message =>
<p class="success">
@message
</p>
}
<p>
<input type="email" name="email" placeholder="Email" id="email" value="@form("email").value">
</p>
<p>
<input type="password" name="password" id="password" placeholder="Password">
</p>
<p>
<button type="submit" id="loginbutton">Login</button>
</p>
}
<p class="note">
Try login as <em>thomas@@home</em> with <em>1234</em> as password.
</p>
}
注意到 thomas@@home
用户名显示你可以通过输入两次来转义特殊的 @
字符。
现在我们有了处理带有待验证凭据提交的 HTML 登录页面的逻辑,但我们仍然缺少将常规方法调用封装到任何我们想要保护的控制器的缺失部分。此外,如果用户名(存储在我们的 request.session
对象中并从 cookie 中检索)不存在,此逻辑将重定向我们到登录页面。这可以用以下方式描述为特质:
trait Secured {
def username(request: RequestHeader) = request.session.get(Security.username)
def onUnauthorized(request: RequestHeader) = Results.Redirect(routes.Authentication.login)
def withAuth(f: => String => Request[AnyContent] => SimpleResult) = {
Security.Authenticated(username, onUnauthorized) { user =>
Action(request => f(user)(request))
}
}
}
我们可以将这个特质添加到同一个 Authentication.scala
控制器类中。withAuth
方法通过在它们周围应用 Security.Authenticated
方法来封装我们的 Action
调用。为了能够使用这个特质,我们只需要将其混合到我们的控制器类中,如下所示:
object Application extends Controller with Secured {
…
}
一旦特质成为我们控制器的一部分,我们可以用 withAuth
方法替换 Action
方法。例如,在调用 index
方法时,我们替换 Action
方法,如下所示:
/**
* Home page
*/
def index = withAuth { username => implicit request =>
Ok(html.index(helloForm))
}
为了能够执行我们的新功能,我们不应该忘记将 Authentication.scala
控制器的额外方法添加到路由定义中(如果省略它们,编译器会标记出来):
# Authentication
GET /login controllers.Authentication.login
POST /login controllers.Authentication.authenticate
GET /logout controllers.Authentication.logout
让我们重新运行应用程序并调用 http://localhost:9000/
页面。我们应该被路由到 login.html
页面而不是 index.html
页面。这在上面的屏幕截图中有显示:
尝试使用错误和正确的电子邮件/密码组合进行登录,以验证认证是否已正确实现。
这个基本的认证机制只是展示了你如何轻松扩展 Play 中的应用程序。它演示了使用动作组合技术,这项技术也可以应用于许多其他方面——例如,记录或修改请求——并且是拦截器的一个很好的替代方案。
当然,如果你需要通过其他服务实现认证,你可以使用与 Play 兼容的外部模块;例如,基于 OAuth、OAuth2 或 OpenID 等标准的模块。SecureSocial 模块是一个很好的例子,可在 securesocial.ws
获取。
使用 Play 的实用技巧
我们将以几条有助于 Play 每日使用的建议来结束这一章。
使用 Play 进行调试
由于函数式编程的声明性特性和编译器的强大类型检查机制,在处理 Scala 代码时,调试应该发生的频率较低。然而,如果你需要在某种情况下调试一个 Play 应用程序,你不妨像在 Java 中一样运行一个远程调试会话。为了实现这一点,只需使用额外的调试命令启动你的 Play 应用程序:
> play debug run
你应该在输出中看到一条额外的信息行,显示以下命令行:
Listening for transport dt_socket at address: 9999
从这里,你可以在你的代码中添加断点,并通过导航到名为运行 | 调试配置…的菜单在 Eclipse 中启动远程调试配置。
右键单击远程 Java 应用程序并选择新建。只需确保你在连接属性表单中输入端口:9999
,然后通过点击调试按钮开始调试。
处理版本控制
在维护代码时,可以忽略的典型文件位于以下位置,例如使用 GIT 等版本控制工具:
-
logs
-
project/project
-
project/target
-
target
-
tmp
-
dist
-
.cache
摘要
在本章中,我们介绍了 Play 框架,并涵盖了请求按照众所周知的 MVC 模式路由到控制器并通过视图渲染的典型示例。我们看到了在路由和模板的定义中使用 Scala 语法的用法给我们带来了编译时安全性的额外好处。这种帮助极大地提高了程序员的效率,并在重构时避免了拼写错误,使整个体验更加愉快。
我们还向一个helloworld
应用程序示例添加了一些基本的 HTTP 身份验证。在下一章中,我们将解决持久性/ORM 的问题,这是任何 Web 应用程序中必不可少的部分,涉及到在后端使用数据库来存储和检索数据。我们将看到如何集成 Java 中使用的现有持久性标准,如 JPA,并介绍通过 Slick 框架的持久性的一种新颖但强大的方法。
第六章. 数据库访问和 ORM 的未来
几乎任何 Web 应用程序都包含的一个基本组件是在持久化存储中存储和检索数据。无论是基于关系型还是 NoSQL,数据库通常占据最重要的位置,因为它持有应用程序数据。当一个技术栈成为遗留技术并需要重构或移植到新的技术栈时,数据库通常是起点,因为它持有领域知识。
在本章中,我们首先将研究如何集成和重用从 Java 继承的持久化框架,例如支持Java 持久化 API(JPA)的 Hibernate 和 EclipseLink 等,这些框架处理对象关系映射(ORM)。然后,我们将实验 Play 框架中默认的持久化框架 Anorm。最后,我们将介绍 Scala 的 ORM 替代方案和一种相当新颖的方法,它为更传统的基于 SQL 的查询添加了类型安全和组合,即 Slick 框架。我们将在 Play 网络开发环境中实验 Slick。我们还将涵盖从现有关系数据库生成类似 CRUD 的应用程序,这对于从遗留数据库开始时提高生产力非常有帮助。
集成现有的 ORM – Hibernate 和 JPA
如维基百科所定义:
“在计算机软件中,对象关系映射(ORM,O/RM 和 O/R 映射)是一种编程技术,用于在面向对象编程语言中转换不兼容的类型系统中的数据”。
ORM 框架在 Java 中的广泛应用,如 Hibernate,主要归功于持久化和查询数据所需编写的代码的简单性和减少。
在 Scala 中提供 JPA
虽然 Scala 有其自己的现代数据持久化标准(即我们稍后将要介绍的 Slick),但在本节中,我们将通过构建一个使用 JPA 注解 Scala 类在关系数据库中持久化数据的 SBT 项目,来介绍 Scala 世界中 JPA(Java Persistence API,可在docs.oracle.com/javaee/6/tutorial/doc/bnbpz.html
中找到)的可能的集成。它源自www.brainoverload.nl/scala/105/jpa-with-scala
上的在线示例,这对于 Java 开发者来说应该特别有趣,因为它说明了如何在 Scala 项目中同时使用 Spring 框架进行依赖注入和 bean 配置。提醒一下,由 Rod Johnson 创建的 Spring 框架于 2002 年推出,作为一种提供控制反转(即依赖注入)的方式,依赖注入的流行度增加,现在成为一个包含 Java EE 7 许多方面的功能齐全的框架。有关 Spring 的更多信息可在projects.spring.io/spring-framework/
找到。
我们将连接到我们在第二章中介绍的现有 CustomerDB 示例数据库,以展示如何读取现有数据以及创建新的实体/表以持久化数据。
如我们在第三章中看到的,理解 Scala 生态系统,创建一个空的 Scala SBT 项目只需打开命令终端,创建一个用于放置项目的目录,然后按照以下方式运行 SBT:
> mkdir sbtjpasample
> cd sbtjpasample
> sbt
> set name:="sbtjpasample"
> session save
我们可以导航到 SBT 创建的 project/
文件夹,并添加一个包含以下单行语句的 plugins.sbt
文件,以导入 sbteclipse
插件,这样我们就可以在 Eclipse IDE 下工作:
addSbtPlugin("com.typesafe.sbteclipse" % "sbteclipse-plugin" % "2.4.0")
由于我们将使用 Hibernate 和 Spring 相关的类,我们需要将这些依赖项包含到我们的 build.sbt
构建文件中(以及连接到 CustomerDB sample
数据库的 derby-client 驱动程序),使其看起来像以下代码片段:
name:="sbtjpasample"
scalaVersion:="2.10.3"
libraryDependencies ++= Seq(
"junit" % "junit" % "4.11",
"org.hibernate" % "hibernate-core" % "3.5.6-Final",
"org.hibernate" % "hibernate-entitymanager" % "3.5.6-Final",
"org.springframework" % "spring-core" % "4.0.0.RELEASE",
"org.springframework" % "spring-context" % "4.0.0.RELEASE",
"org.springframework" % "spring-beans" % "4.0.0.RELEASE",
"org.springframework" % "spring-tx" % "4.0.0.RELEASE",
"org.springframework" % "spring-jdbc" % "4.0.0.RELEASE",
"org.springframework" % "spring-orm" % "4.0.0.RELEASE",
"org.slf4j" % "slf4j-simple" % "1.6.4",
"org.apache.derby" % "derbyclient" % "10.8.1.2",
"org.scalatest" % "scalatest_2.10" % "2.0.M7"
)
为了提醒使这些依赖项在 Eclipse 中可用,我们必须再次运行 > sbt eclipse
命令并刷新 IDE 中的项目。
现在,从项目的根目录进入 > sbt eclipse
并将项目导入 IDE。
现在,让我们添加几个领域实体(在新的包 se.sfjd
下),我们希望用基于 Java 的 JPA 注解来注解。在 se.sfjd
包中定义的 Customer
实体将(至少部分地)映射到现有的 CUSTOMER
数据库表:
import javax.persistence._
import scala.reflect.BeanProperty
@Entity
@Table(name = "customer")
class Customer(n: String) {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
@Column(name = "CUSTOMER_ID")
@BeanProperty
var id: Int = _
@BeanProperty
@Column(name = "NAME")
var name: String = n
def this() = this (null)
override def toString = id + " = " + name
}
注意下划线 (_) 在声明 var id: Int = _
时代表默认值。默认值将根据变量的类型 T
设置,如 Scala 规范所定义:
-
如果
T
是Int
或其子范围类型之一,则为0
。 -
如果
T
是Long
,则为0L
。 -
如果
T
是Float
,则为0.0f
。 -
如果
T
是Double
,则为0.0d
。 -
如果
T
是Boolean
,则为false
。 -
如果
T
是Unit
,则为()
。 -
对于所有其他类型的
T
,都是null
。
Language
实体对应于我们想要持久化的新概念的添加,因此需要一个新的数据库表,如下所示:
@Entity
@Table(name = "language")
class Language(l: String) {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
@Column(name = "ID")
@BeanProperty
var id: Int = _
@BeanProperty
@Column(name = "NAME")
var name: String = l
def this() = this (null)
override def toString = id + " = " + name
}
如我们在第二章中看到的,代码集成,@BeanProperty
注解是一种生成符合 Java 的 getter 和 setter 的方法,而 this()
方法是 Hibernate 需要的无参数构造函数。
接下来,控制器类或 DAO (数据访问对象)类捕获我们想要为 Customer
实体提供的操作,例如通过接口的形式提供 save
和 find
方法的 CRUD 功能,或者在这种情况下,一个 Scala 特质:
trait CustomerDao {
def save(customer: Customer): Unit
def find(id: Int): Option[Customer]
def getAll: List[Customer]
}
CustomerDao
类的实现依赖于我们作为 Java 开发者可能熟悉的 JPA 实体管理器的各种方法:
import org.springframework.beans.factory.annotation._
import org.springframework.stereotype._
import org.springframework.transaction.annotation.{Propagation, Transactional}
import javax.persistence._
import scala.collection.JavaConversions._
@Repository("customerDao")
@Transactional(readOnly = false, propagation = Propagation.REQUIRED)
class CustomerDaoImpl extends CustomerDao {
@Autowired
var entityManager: EntityManager = _
def save(customer: Customer):Unit = customer.id match{
case 0 => entityManager.persist(customer)
case _ => entityManager.merge(customer)
}
def find(id: Int): Option[Customer] = {
Option(entityManager.find(classOf[Customer], id))
}
def getAll: List[Customer] = {
entityManager.createQuery("FROM Customer", classOf[Customer]).getResultList.toList
}
}
以类似的方式,我们可以定义一个 Language
特质及其实现,如下所示,并添加了一个 getByName
方法:
trait LanguageDao {
def save(language: Language): Unit
def find(id: Int): Option[Language]
def getAll: List[Language]
def getByName(name : String): List[Language]
}
@Repository("languageDao")
@Transactional(readOnly = false, propagation = Propagation.REQUIRED)
class LanguageDaoImpl extends LanguageDao {
@Autowired
var entityManager: EntityManager = _
def save(language: Language): Unit = language.id match {
case 0 => entityManager.persist(language)
case _ => entityManager.merge(language)
}
def find(id: Int): Option[Language] = {
Option(entityManager.find(classOf[Language], id))
}
def getAll: List[Language] = {
entityManager.createQuery("FROM Language", classOf[Language]).getResultList.toList
}
def getByName(name : String): List[Language] = {
entityManager.createQuery("FROM Language WHERE name = :name", classOf[Language]).setParameter("name", name).getResultList.toList
}
}
在我们可以执行项目之前,我们还需要遵循几个步骤:首先我们需要一个测试类,因此我们可以创建一个遵循 ScalaTest
语法(如我们之前在 第四章 中看到的)的 CustomerTest
类:
import org.junit.runner.RunWith
import org.scalatest.junit.JUnitRunner
import org.scalatest.FunSuite
import org.springframework.context.support.
lassPathXmlApplicationContext
@RunWith(classOf[JUnitRunner])
class CustomerTest extends FunSuite {
val ctx = new ClassPathXmlApplicationContext("application-context.xml")
test("There are 13 Customers in the derby DB") {
val customerDao = ctx.getBean(classOf[CustomerDao])
val customers = customerDao.getAll
assert(customers.size === 13)
println(customerDao
.find(3)
.getOrElse("No customer found with id 3"))
}
test("Persisting 3 new languages") {
val languageDao = ctx.getBean(classOf[LanguageDao])
languageDao.save(new Language("English"))
languageDao.save(new Language("French"))
languageDao.save(new Language("Swedish"))
val languages = languageDao.getAll
assert(languages.size === 3)
assert(languageDao.getByName("French").size ===1)
}
}
最后但同样重要的是,我们必须定义一些配置,包括一个 JPA 所需的 META-INF/persistence.xml
文件,我们可以将其放在 src/main/resources/
目录下,以及一个 Spring 的 application-context.xml
文件,其中所有豆类都已连接,并定义了数据库连接。persistence.xml
文件将看起来像以下这样:
<?xml version="1.0" encoding="UTF-8"?>
<persistence version="2.0"
xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd">
<persistence-unit name="JpaScala" transaction-type="RESOURCE_LOCAL">
<provider>org.hibernate.ejb.HibernatePersistence</provider>
</persistence-unit>
</persistence>
application-context.xml
文件,位于 src/main/resources/
目录下,内容较为详细,具体如下:
<?xml version="1.0" encoding="UTF-8"?>
<beans
xsi:schemaLocation="
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd
">
<tx:annotation-driven transaction-manager="transactionManager"/>
<context:component-scan base-package="se.sfjd"/>
<bean id="dataSource"
class="org.springframework.jdbc.datasource.DriverManagerDataSource"
p:driverClassName="org.apache.derby.jdbc.ClientDriver" p:url="jdbc:derby://localhost:1527/sample"
p:username="app" p:password="app"/>
<bean id="entityManagerFactory"
class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
<property name="persistenceUnitName" value="JpaScala"/>
<property name="persistenceProviderClass" value="org.hibernate.ejb.HibernatePersistence"/>
<property name="jpaDialect">
<bean class="org.springframework.orm.jpa.vendor.HibernateJpaDialect"/>
</property>
<property name="dataSource" ref="dataSource"/>
<property name="jpaPropertyMap">
<map>
<entry key="hibernate.dialect" value="org.hibernate.dialect.DerbyDialect"/>
<entry key="hibernate.connection.charSet" value="UTF-8"/>
<entry key="hibernate.hbm2ddl.auto" value="update"/>
<entry key="hibernate.show.sql" value="true"/>
</map>
</property>
</bean>
<bean id="entityManager"
class="org.springframework.orm.jpa.support.SharedEntityManagerBean">
<property name="entityManagerFactory" ref="entityManagerFactory"/>
</bean>
<bean id="transactionManager" class="org.springframework.orm.jpa.JpaTransactionManager">
<property name="entityManagerFactory" ref="entityManagerFactory"/>
<property name="dataSource" ref="dataSource"/>
</bean>
</beans>
在运行测试之前,我们需要确保数据库服务器正在运行;这已在 第二章 中解释,代码集成,当时使用 NetBeans IDE。
现在,我们可以通过右键单击 CustomerTest
类并导航到 Debug As | Scala JUnit Test 或从命令提示符中输入以下命令来执行示例:
> sbt test
3 = Nano Apple
[info] CustomerTest:
[info] - There are 13 Customers in the derby DB
[info] - Persisting 3 new languages
[info] Passed: : Total 2, Failed 0, Errors 0, Passed 2, Skipped 0
[success] Total time: 3 s
在 Play 框架中处理持久化
Play 框架可以使用任何类型的 ORM 运行,无论是基于 Java 的 JPA 还是 Scala 特定的。该框架有相关的但独立的 Java 和 Scala 版本。如 Play 文档所述,Java 版本使用 Ebean 作为其 ORM,而 Scala 替代方案不使用 ORM,而是通过 JDBC 之上的 Scala 风格抽象层 Anorm 运行。
使用 Anorm 的简单示例
为了说明 Anorm 的用法,我们将创建一个小的 Play 示例,连接到之前章节中使用的 NetBeans 分发的现有 CustomerDB
数据库,并在 第二章 中介绍,代码集成。
最直接的方法是从终端窗口创建一个默认的 Play Scala 项目,输入以下命令:
> play new anormsample
一旦创建并导入到 Eclipse 中(再次使用 > play eclipse
命令创建 Eclipse 相关文件;如需更多细节,请参阅 第五章, Play 框架入门),我们可以看到 Anorm 的依赖已经包含在 built.sbt
文件中。然而,我们需要将 derby-client
数据库驱动程序的依赖添加到该文件中,以便通过 jdbc 与数据库通信。依赖项可以按以下方式添加:
libraryDependencies ++= Seq(
jdbc,
anorm,
cache,
"org.apache.derby" % "derbyclient" % "10.8.1.2"
)
现在,我们可以定义一个 Customer
case 类,它将代表数据库中的 CUSTOMER
表,并在其伴生对象中实现一些方法形式的行为,如下所示:
package models
import play.api.db._
import play.api.Play.current
import anorm._
import anorm.SqlParser._
import scala.language.postfixOps
case class Customer(id: Pk[Int] = NotAssigned, name: String)
object Customer {
/**
* Retrieve a Customer from an id.
*/
def findById(id: Int): Option[Customer] = {
DB.withConnection { implicit connection =>
println("Connection: "+connection)
val query = SQL("SELECT * from app.customer WHERE customer_id = {custId}").on('custId -> id)
query.as(Customer.simple.singleOpt)
}
}
/**
* Parse a Customer from a ResultSet
*/
val simple = {
get[Pk[Int]]("customer.customer_id") ~
getString map {
case id~name => Customer(id, name)
}
}
}
Anorm SQL 查询符合基于字符串的 SQL 语句,其中变量绑定到值。在这里,我们将 customer_id
列绑定到 id
输入参数。由于我们希望返回一个 Option[Customer]
来处理 SQL 查询没有返回任何结果的情况,我们首先需要解析 ResultSet
对象以创建一个 Customer
实例并调用 singleOpt
方法,这将确保我们将结果包装在一个 Option
中(它可以返回 None
而不是潜在的错误)。
Application
控制器如下所示:
package controllers
import play.api._
import play.api.mvc._
import play.api.db._
import play.api.Play.current
import models._
object Application extends Controller {
def index = Action {
val inputId = 2 // Hardcoded input id for the example
val result =
DB.withConnection { implicit c =>
Customer.findById(inputId) match {
case Some(customer) => s"Found the customer: ${customer.name}"
case None => "No customer was found."
}
}
Ok(views.html.index(result))
}
}
它只是将数据库查询包围在数据库连接中,并对 Option[Customer]
实体进行一些模式匹配,以显示查询的客户 id
是否找到的不同消息。
您可能在阅读 Scala 代码时注意到了关键字 implicit
,例如在之前的代码示例中给出的 implicit c
参数。正如 Scala 文档中明确解释的那样:
"具有隐含参数的方法可以像普通方法一样应用于参数。在这种情况下,隐含标签没有效果。然而,如果这样的方法遗漏了其隐含参数的参数,这些参数将被自动提供"。
在我们之前的例子中,我们可以省略这个隐含参数,因为我们没有在方法体中进一步使用数据库连接 c
变量。
使用 inputId=2
运行应用程序可以替换为 inputId=3000;
例如,以演示没有找到客户的情况。为了避免在视图中进行任何更改,我们重用了默认 index.html
页面的欢迎信息位置;因此,您将在浏览器的绿色页眉中看到结果。
此示例仅展示了 Anorm 的基本用法;它源自 Play 框架发行版样本中的更完整的 computer-database
示例。如果您需要深入了解 Anorm 框架,可以参考它。
替换 ORM
作为 Java 开发者,我们习惯于通过使用成熟且稳定的 JPA 框架,如 Hibernate 或 EclipseLink,来处理关系型数据库的持久化。尽管这些框架使用方便,并且隐藏了跨多个表检索或更新数据的许多复杂性,但对象关系映射仍然存在 对象关系阻抗不匹配 问题;在面向对象模型中,您通过对象之间的关系遍历对象,而在关系型数据库中,您将表的数据行连接起来,有时会导致数据检索效率低下且繁琐。(这进一步在维基百科页面中解释,en.wikipedia.org/wiki/Object-relational_impedance_mismatch
。)
相反,Typesafe 栈中的 Slick
框架提出通过函数式关系映射来解决数据到关系数据库的持久化问题,力求更自然地匹配。Slick 的额外好处包括以下两个方面:
-
简洁性和类型安全:Slick 不是通过在 Java 代码中用字符串表达 SQL 查询来运行 SQL 查询,而是使用纯 Scala 代码来表达查询。在 JPA 中,Criteria API 或如 JPQL(Java Persistence Query Language)或 HQL(Hibernate Query Language)之类的语言长期以来一直试图使基于字符串的查询具有更强的类型检查,但仍然难以理解并生成冗长的代码。使用 Slick,查询通过 Scala 的
for comprehensions
功能简洁地编写。SQL 查询的类型安全在 .Net 世界中通过流行的 LINQ 框架很久以前就已经引入。 -
可组合和可重用查询:Slick 采取的函数式方法使组合成为一种自然的行为,这是当考虑将纯 SQL 作为 ORM 的替代品时缺乏的特性。
了解 Slick
让我们通过代码示例来探索 Slick 框架的行为,看看我们如何可以增强和替换更传统的 ORM 解决方案。
我们可以研究的第一个例子是我们在第四章测试工具中分析的 test-patterns-scala
activator 模板项目的一部分。项目中的 scalatest/Test012.scala
文件展示了 Slick 的典型用法如下:
package scalatest
import org.scalatest._
import scala.slick.driver.H2Driver.simple._
import Database.threadLocalSession
object Contacts extends Table(Long, String) {
def id = columnLong
def name = columnString
def gender = columnString
def * = id ~ name
}
class Test12 extends FunSuite {
val dbUrl = "jdbc:h2:mem:contacts"
val dbDriver = "org.h2.Driver"
test("Slick, H2, embedded") {
Database.forURL(dbUrl, driver = dbDriver) withSession {
Contacts.ddl.create
Contacts.insertAll(
(1, "Bob"),
(2, "Tom"),
(3, "Salley")
)
val nameQuery =
for(
c <- Contacts if c.name like "%o%"
) yield c.name
val names = nameQuery.list
names.foreach(println)
assert(names === List("Bob","Tom"))
}
}
}
代码中最有趣的部分与 SQL 查询有关。不可变变量 names
包含对数据库的查询结果;而不是将 SQL 查询表达为 String
或通过 Java Criteria API,而是通过 for comprehension
使用纯 Scala 代码,如下面的截图所示:
与基于字符串的 SQL 查询不同,任何拼写错误或对不存在表或字段的引用都会立即由编译器指出。与 JPA Criteria API 生成的冗长且难以阅读的输出代码相比,更复杂的查询将以非常自然的方式转换为可读的 for 表达式。
此示例仅包含一个表,即 Contacts
,我们通过扩展 scala.slick.driver.H2Driver.simple.Table
类来定义它。CONTACTS
数据库表包括三个列,一个作为 Long
数据类型定义的主键 id
,以及两个其他类型为 String
的属性,分别是 name
和 gender
。在 Contacts
对象中定义的 *
方法指定了一个默认投影,即我们通常感兴趣的所有列(或计算值)。表达式 id ~ name
(使用 ~
连接序列运算符)返回一个 Projection2[Long, String]
,可以将其视为 Tuple2,但用于关系表示。默认投影 (Int, String)
导致简单查询的 List[(Int, String)]
。
由于关系数据库中列的数据类型与 Scala 类型不同,因此需要映射(类似于处理 ORM 框架或纯 JDBC 访问时所需的映射)。如 Slick 文档所述,开箱即用的原始类型支持如下(根据每个数据库类型使用的数据库驱动程序,有一些限制):
-
数值类型:
Byte
,Short
,Int
,Long
,BigDecimal
,Float
,Double
-
LOB 类型:
java.sql.Blob
,java.sql.Clob
,Array[Byte]
-
日期类型:
java.sql.Date
,java.sql.Time
,java.sql.Timestamp
-
Boolean
-
String
-
Unit
-
java.util.UUID
定义领域实体之后,下一步是创建数据库,向其中插入一些测试数据,然后运行查询,就像我们使用任何其他持久化框架一样。
我们在 Test12
测试中运行的代码都被以下代码块包围:
Database.forURL(dbUrl, driver = dbDriver) withSession {
< code accessing the DB...>
}
forURL
方法指定了一个 JDBC 数据库连接,这通常包括一个对应于要使用的特定数据库的驱动程序类和一个由其 host
、port
、database name
以及可选的 username/password
定义的连接 URL。在示例中,使用了一个名为 contacts
的本地内存数据库(H2),因此连接 URL 是 jdbc:h2:mem:contacts
,这与我们在 Java 中编写的方式完全相同。请注意,Slick 的 Database
实例仅封装了创建连接的“如何做”,实际的连接仅在 withSession
调用中创建。
Contacts.ddl.create
语句将创建数据库模式,而 insertAll
方法将使用包含其主键 id
和 name
的三行数据填充 Contacts
表。
我们可以通过在 test-patterns-scala
项目的根目录下的终端窗口中输入以下命令来单独执行此测试,以验证它是否按预期运行:
> ./activator
> test-only scalatest.Test12
Bob
Tom
[info] Test12:
[info] - Slick, H2, embedded (606 milliseconds)
[info] ScalaTest
[info] Run completed in 768 milliseconds.
[info] Total number of tests run: 1
[info] Suites: completed 1, aborted 0
[info] Tests: succeeded 1, failed 0, canceled 0, ignored 0, pending 0
[info] All tests passed.
[info] Passed: Total 1, Failed 0, Errors 0, Passed 1
[success] Total time: 1 s, completed Dec 7, 2013 1:43:28 PM
目前,test-patterns-scala
项目包含对 SLF4J 日志框架的 slf4j-nop
实现的依赖,该实现禁用了所有日志。由于可视化 Scala for comprehension
语句产生的确切 SQL 语句可能很有用,让我们将 sl4j-nop
替换为 logback 实现。在你的 build.sbt
构建文件中,将 "org.slf4j" % "slf4j-nop" % "1.6.4"
这一行替换为对 logback 的引用,例如 "ch.qos.logback" % "logback-classic" % "0.9.28" % "test"
。
现在,如果你重新运行测试,你可能会看到比实际需要的更多日志信息。因此,我们可以在项目中添加一个 logback.xml
文件(在 src/test/resources/
文件夹中),如下所示:
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<root level="debug">
<appender-ref ref="STDOUT" />
</root>
<logger name="scala.slick.compiler" level="${log.qcomp:-warn}" />
<logger name="scala.slick.compiler.QueryCompiler" level="${log.qcomp.phases:-inherited}" />
…
<logger name="scala.slick.compiler.CodeGen" level="${log.qcomp.codeGen:-inherited}" />
<logger name="scala.slick.compiler.InsertCompiler" level="${log.qcomp.insertCompiler:-inherited}" />
<logger name="scala.slick.jdbc.JdbcBackend.statement" level="${log.session:-info}" />
<logger name="scala.slick.ast.Node$" level="${log.qcomp.assignTypes:-inherited}" />
<logger name="scala.slick.memory.HeapBackend$" level="${log.heap:-inherited}" />
<logger name="scala.slick.memory.QueryInterpreter" level="${log.interpreter:-inherited}" />
</configuration>
这次,如果我们只启用 "scala.slick.jdbc.JdbcBackend.statement"
日志记录器,测试的输出将显示所有 SQL 查询,类似于以下输出:
> test-only scalatest.Test12
19:00:37.470 [ScalaTest-running-Test12] DEBUG scala.slick.session.BaseSession - Preparing statement: create table "CONTACTS" ("CONTACT_ID" BIGINT NOT NULL PRIMARY KEY,"CONTACT_NAME" VARCHAR NOT NULL)
19:00:37.484 [ScalaTest-running-Test12] DEBUG scala.slick.session.BaseSession - Preparing statement: INSERT INTO "CONTACTS" ("CONTACT_ID","CONTACT_NAME") VALUES (?,?)
19:00:37.589 [ScalaTest-running-Test12] DEBUG scala.slick.session.BaseSession - Preparing statement: select x2."CONTACT_NAME" from "CONTACTS" x2 where x2."CONTACT_NAME" like '%o%'
Bob
Tom
[info] Test12:
[info] - Slick, H2, embedded (833 milliseconds)
[info] ScalaTest
[info] Run completed in 952 milliseconds.
[info] Total number of tests run: 1
[info] Suites: completed 1, aborted 0
[info] Tests: succeeded 1, failed 0, canceled 0, ignored 0, pending 0
[info] All tests passed.
[info] Passed: Total 1, Failed 0, Errors 0, Passed 1
[success] Total time: 1 s, completed Dec 10, 2013 7:00:37 PM
>
最后,为了验证是否已强制执行数据库模式验证,让我们尝试修改插入数据的一个键,以便我们有重复的键,如下面的代码行所示:
Contacts.insertAll(
(1, "Bob"),
(2, "Tom"),
(2, "Salley")
)
如果我们再次运行测试,它将失败,并显示类似于以下的消息:
[info] Test12:
[info] - Slick, H2, embedded *** FAILED *** (566 milliseconds)
[info] org.h2.jdbc.JdbcBatchUpdateException: Unique index or primary key violation: "PRIMARY_KEY_C ON PUBLIC.CONTACTS(CONTACT_ID)"; SQL statement:
[info] INSERT INTO "CONTACTS" ("CONTACT_ID","CONTACT_NAME") VALUES (?,?) [23505-166]…
搭建 Play 应用程序
在本节中,我们将通过从关系型数据库自动创建一个具有基本 CRUD 功能的完整 Play 应用程序来进一步实验 Slick 和 Play,包括模型、视图、控制器,以及测试数据和配置文件,如 Play 路由。
任何需要连接到数据库的 Web 应用程序通常至少需要在后端实现大部分 CRUD 功能。此外,能够生成默认的前端可以避免你从头开始制作。特别是,由 HTML5 视图组成的 Play 前端具有高度的复用性,因为大多数列、字段、按钮和表单的显示都可以在 HTML 编辑器中进行有限的复制粘贴重新排列。
让我们将这种逆向工程应用于我们在第二章中已经介绍过的 NetBeans 分发的示例客户数据库,代码集成。
Play 应用的生成分为两个步骤:
-
创建一个常规的 Play 项目。
-
使用名为
playcrud
的外部工具,它本身是一个 Play 应用程序,并将生成所有必需的 MVC 和配置文件,这些文件位于新的 Play 项目结构之上。
这种两步走的方法更有保证,生成的应用程序将遵循 Play 分发的最新变化,特别是关于 Play 每个新版本带来的外观和感觉的变化。
要开始使用 playcrud
工具,请在所选目录的命令行中克隆项目(假设已安装 GIT,如果没有,请访问 git-scm.com/
):
> git clone https://github.com/ThomasAlexandre/playcrud
这将创建一个名为 playcrud
的目录,项目内容是一个常规的 Play 应用程序,包括生成 Eclipse 项目的插件。因此,我们可以运行以下命令:
> cd playcrud
> play eclipse
然后,将项目导入到 Eclipse 中以更好地可视化其组成。应用程序由位于 samplecrud\app\controllers
的 Application.scala
文件中的一个控制器组成,以及位于 samplecrud\app\views
下的 index.scala.html
中的相应视图。在 samplecrud\conf
下的 routes
文件中只定义了两个路由:
# Home page
GET / controllers.Application.index
# CRUD action
GET /crud controllers.Application.generateAll
第一条路由将在浏览器中显示一个表单,我们可以输入有关数据库的信息,从而创建一个 Play 应用程序。通过查看其模板,这个表单相当容易理解:
@(dbForm: Form[(String,String,String,String)])
@import helper._
@main(title = "The 'CRUD generator' application") {
<h1>Enter Info about your existing database:</h1>
@form(action = routes.Application.generateAll, args = 'id -> "dbform") {
@select(
field = dbForm("driver"),
options = options(
"com.mysql.jdbc.Driver" -> "MySQL",
"org.postgresql.Driver" -> "PostgreSQL",
"org.h2.Driver" -> "H2",
"org.apache.derby.jdbc.ClientDriver" -> "Derby"
),
args = '_label -> "Choose a DB"
)
@inputText(
field = dbForm("dburl"),
args = '_label -> "Database url", 'placeholder -> "jdbc:mysql://localhost:3306/slick"
)
@inputText(
field = dbForm("username"),
args = '_label -> "DB username", 'size -> 10, 'placeholder -> "root"
)
@inputText(
field = dbForm("password"),
args = '_label -> "DB password", 'size -> 10, 'placeholder -> "root"
)
<p class="buttons">
<input type="submit" id="submit">
<p>
}
}
第二个是在提交表单后执行一次的 generateAll
动作,该动作将创建执行新创建的 Play 应用程序所需的所有文件。
为了能够在正确的位置生成所有文件,我们只需要编辑一个名为 baseDirectory
的配置属性,目前位于 utilities/
文件夹中的 Config.scala
文件。该属性指定了我们想要生成的 Play 应用程序的根目录。在我们编辑它之前,我们可以生成一个空白 Play 项目,baseDirectory
变量将引用它:
> cd ~/projects/internal (or any location of your choice)
> play new samplecrud
…
What is the application name? [samplecrud]
> [ENTER]
Which template do you want to use for this new application?
1 - Create a simple Scala application
2 - Create a simple Java application
> [Press 1]
Just to verify we have our blank Play application correctly created we can launch it with:
> cd samplecrud
> play run
现在,在网页浏览器中打开 http://localhost:9000
URL。
现在我们有了我们的 baseDirectory
(~/projects/internal/samplecrud
),我们可以将其添加到 Config.scala
文件中。其他关于数据库的属性只是默认值;我们在这里不需要编辑它们,因为我们将在运行 playcrud
应用程序时填写 HTML 表单时指定它们。
在一个新的终端窗口中,让我们通过输入以下命令来执行 playcrud
应用程序:
> cd <LOCATION_OF_PLAYCRUD_PROJECT_ROOT>
> play "run 9020" (or any other port than 9000)
这里,我们需要选择一个不同于 9000
的端口,因为它已被空白应用程序占用。
现在,将您的网络浏览器指向 playcrud
URL,http://localhost:9020/
。您应该会看到一个 HTML 表单,您可以在其中编辑源数据库的属性以进行 CRUD 生成,如下面的截图所示(此数据库将只进行读取):
提交表单很可能会在终端控制台中生成一些日志输出,一旦生成完成,浏览器将被重定向到端口 9000
以显示新生成的 CRUD 应用程序。由于这是我们第一次生成应用程序,它将失败,因为生成的应用程序的 build.sbt
文件已更新,需要重新加载一些新依赖项。
为了解决这个问题,通过按下 Ctrl + D 来中断当前运行的 Play 应用程序。一旦它停止,只需重新启动它:
> play run
如果一切顺利,你应该能够访问 http://localhost:9000
并看到从数据库生成的实体对应的可点击控制器列表,包括 Product
、Manufacturer
和 Purchase Order
。
让我们打开其中一个,例如制造商视图,如下截图所示:
结果屏幕显示了 CRUD 功能的READ
部分,通过显示数据库中所有制造商行的列表。分页功能默认设置为3
,这就是为什么一次只能看到 30 个制造商中的三个,但可以通过点击上一页和下一页按钮导航到其他页面。这个默认页面大小可以在每个单独的控制器中编辑(查找pageSize
val 声明),或者可以在代码生成之前修改控制器模板,以一次性更新所有控制器。此外,HTML 表格的标题是可点击的,可以根据每个特定的列对元素进行排序。
点击添加新制造商按钮将调用一个新屏幕,其中包含一个用于在数据库中创建新条目的表单。
导入测试数据
生成的应用默认使用 H2 内存数据库运行,启动时会填充一些测试数据。在生成过程中,我们使用 DBUnit 的功能将源数据库的内容导出到一个 XML 文件中,DBUnit 是一个基于 JUnit 的 Java 框架。当测试中涉及足够多的数据库数据,而你又想避免通过生成包含从真实数据库中提取的一些数据的 XML 样本文件来模拟所有内容时,DBUnit 非常有用。导出的测试数据存储在samplecrud\test\
目录下的testdata.xml
文件中。当运行生成的应用程序时,该文件将由 DBUnit 在Global.scala
的onStart
方法中加载,在创建数据库模式之后。
为了能够将数据持久化到真实的数据库中,从而避免每次重启时都导入 XML 文件,我们可以将内存中的数据替换为磁盘上的真实数据库。例如,我们可以将位于samplecrud\conf
目录下的application.conf
文件中的数据库驱动属性替换为以下几行:
db.default.driver=org.h2.Driver
db.default.url="jdbc:h2:tcp://localhost/~/customerdb"
db.default.user=sa
db.default.password=""
重启 play 应用后,新的数据库将被构建:
> play run
在浏览器中访问http://localhost:9000
URL 这次将在磁盘上创建数据库模式并填充测试数据。由于数据库在重启之间是持久化的,从现在开始我们必须在Global.scala
中注释掉ddl.create
语句以及引用testdata.xml
的 DBUnit 导入的行。
在 H2browser 中可视化数据库
Play 的一个方便功能是,你可以直接从 SBT 访问h2-browser
来在你的浏览器中可视化数据库内容。即使你使用的是除了 H2 之外的大多数数据库,这也是正确的。打开一个终端窗口并导航到生成的项目根目录:
> play
> h2-browser
通过填写以下截图所示的连接属性来连接到数据库:
假设点击显示在上一张截图中的测试连接按钮后显示测试成功,我们可以可视化并发送 SQL 查询到customerdb
数据库,如下一张截图所示:
探索应用生成的代码背后的内容
源数据库中的每个表都会生成一些工件:
-
一个
模型
,一个控制器
,以及几个视图
类 -
在
conf.routes
文件中插入了一组route
条目,如下所示为PURCHASE_ORDER
表:# PurchaseOrder # # PurchaseOrder list (look at the default values for pagination parameters) GET /purchaseorder controllers.PurchaseOrderController.list(p:Int ?= 0, s:Int ?= 2, f ?= "") # Add purchaseorder GET /purchaseorder/new controllers.PurchaseOrderController.create POST /purchaseorder controllers.PurchaseOrderController.save # Edit existing purchaseorder GET /purchaseorder/:pk controllers.PurchaseOrderController.edit(pk:Int) POST /purchaseorder/:pk controllers.PurchaseOrderController.update(pk:Int) # Delete purchaseorder POST /purchaseorder/:pk/delete controllers.PurchaseOrderController.delete(pk:Int)
模型由域实体组成,每个实体都通过 Slick 定义,结合一个表示行的 case 类和一个特定驱动程序的slick.driver.H2Driver.simple.Table
行。我们本可以避免使用 case 类,直接编写涉及列的元组,就像我们在早期的Test12
示例中看到的test-patterns-scala
激活器模板一样,但将列封装在 case 类中对于后续的模式匹配和视图中的使用来说更方便。代表PurchaseOrder
实体的模型类生成如下:
package models
case class PurchaseOrderRow(orderNum : Option[Int], customerId : Int, productId : Int, quantity : Option[Int], shippingCost : Option[Int], salesDate : Option[Date], shippingDate : Option[Date], freightCompany : Option[String])
// Definition of the PurchaseOrder table
object PurchaseOrder extends TablePurchaseOrderRow {
def orderNum = columnInt
def customerId = columnInt
def productId = columnInt
def quantity = column[Option[Int]]("QUANTITY")
def shippingCost = column[Option[Int]]("SHIPPING_COST")
def salesDate = column[Option[Date]]("SALES_DATE")
def shippingDate = column[Option[Date]]("SHIPPING_DATE")
def freightCompany = column[Option[String]]("FREIGHT_COMPANY")
def * = orderNum.? ~ customerId ~ productId ~ quantity ~ shippingCost ~ salesDate ~ shippingDate ~ freightCompany <> (PurchaseOrderRow.apply _, PurchaseOrderRow.unapply _)
def findAll(filter: String = "%") = {
for {
entity <- PurchaseOrder
// if (entity.name like ("%" + filter))
} yield entity
}
def findByPK(pk: Int) =
for (
entity <- PurchaseOrder if entity.orderNum === pk
) yield entity
...
PurchaseOrder
实体的完整代码以及相应的PurchaseOrderController
类的 CRUD 方法定义可以在 Packt Publishing 网站上下载,也可以通过在本节中解释的执行scaffolding playcrud
GitHub 项目在CustomerDB
样本数据库上重现。
最后,为特定实体生成视图的模板收集在同一个名为views.<entity_name>/
的目录下,并包括三个文件,分别是list.scala.html
、createForm.scala.html
和editForm.scala.html
,分别用于READ
、CREATE
和UPDATE
操作。它们嵌入了一种混合的纯 HTML5 标记和最小 Scala 代码,用于遍历和显示来自控制器查询的元素。注意在视图中添加了一个特定的play.api.mvc.Flash
隐式对象:Play 的这个有用特性使得在生成的视图中显示一些信息成为可能,以通知用户执行操作的结果。您可以在控制器中看到,我们通过Home.flashing {... }
语句引用它,其中根据操作的成功或失败显示各种信息。
playcrud 工具的限制
在当前实验性的playcrud
工具版本中,发现了一些限制,如下所述:
-
playcrud
项目并不总是与所有 JDBC 数据库完美兼容,特别是由于某些数据库的映射是定制的。然而,只需进行少量更改,它就足够灵活,可以适应新的映射。此外,它允许通过指定需要生成的外部文件中的表来生成部分数据库。为了启用此功能,我们只需在我们的playcrud
项目的conf/
目录下添加一个文件,命名为tables
,并写入我们想要包含的表的名称(文件中的每一行一个表名,不区分大小写)。例如,考虑一个包含以下代码的tables
文件:product purchaseorder manufacturer
此代码只为这三个表创建 MVC 类和 HTML 视图。
-
如果特定数据库数据类型的映射没有被
playcrud
处理,你将在浏览器窗口中得到一个编译错误,这很可能会提到缺少的数据类型。处理映射的playcrud
代码中的位置是utilities/DBUtil.scala
类。playcrud
的后续版本应该使这些配置对每种数据库类型更加灵活,并将它们放在外部文件中,但到目前为止,它们是嵌入在代码中的。 -
可用的代码生成是在两个已经存在的样本的基础上灵感和构建的,一个是 Play 框架分发的名为
computer-database
的样本(它展示了一个 CRUD 应用,但使用 Anorm 作为持久层,这是一个基于 SQL 的持久层框架,是 Play 的默认选项),另一个是 Typesafe 的 Slick 团队完成的 Slick 使用示例(带有Suppliers
的Coffee
数据库,展示了多对一关系)。如果你希望以不同的方式生成代码,所有模板都可以在views/
目录下找到。其中一些主要包含静态数据,例如基于build.scala.txt
模板生成build.sbt
。 -
在商业应用中,遇到具有超过 22 列的数据库表并不罕见。由于我们将这些列封装到案例类中,而 Scala 2.10 有一个限制,限制了超过 22 个元素的案例类的创建,因此目前无法生成超过该大小的 Slick 映射。希望从 Scala 2.11 开始,这个限制应该会被取消。
摘要
在本章中,我们介绍了处理关系型数据库持久化的几种方法。我们首先通过一个 Scala 与基于传统 JPA 的 ORM 持久化集成的例子进行了说明。该例子还展示了 Spring 框架与 Scala 代码库之间的集成。然后,我们介绍了 Anorm,这是 Play 框架中默认的持久化框架,它依赖于直接 SQL 查询。由于 ORM 的一些局限性,主要与可扩展性和性能相关,以及纯 SQL 查询在类型安全和可组合性方面的局限性,我们转向采用 Slick 框架,这是一种独特的持久化方法,旨在以更函数式的方式在关系型数据库中持久化数据。最后,我们考虑了通过从现有数据库生成具有基本 CRUD 功能的全功能 Play Web 应用程序,作为快速将 Slick 集成到 Play 中的方法。Slick 的未来版本从 2.0 开始增强了对代码生成的支持,并力求通过使用 Scala 宏使编写数据库查询的语法更加可读。
在下一章中,我们将探讨如何在使用 Scala 集成外部系统时使用 Scala,特别是通过 Web 服务和 REST API,支持 JSON 和 XML 等数据格式。
第七章. 使用集成和 Web 服务
由于技术堆栈不断演变,在开发商业软件时需要考虑的一个大领域是系统之间的集成。Web 的灵活性和可扩展性使得基于 HTTP 构建的服务以松散耦合的方式集成系统的数量激增。此外,为了能够通过防火墙和额外的安全机制导航到可通过这些机制访问的安全网络,HTTP 模型越来越受欢迎。在本章中,我们将介绍如何在通过 Web 服务或 REST 服务交换消息(如 XML 和 JSON 格式)时涉及 Scala。特别是,我们将考虑通过 Play 框架运行此类服务。
在本章中,我们将介绍以下主题:
-
从 XML 模式生成数据绑定,以及从它们的 WSDL 描述中生成 SOAP Web 服务类
-
在 Scala 和特别是在 Play 框架的上下文中操作 XML 和 JSON
-
从 Play 调用其他 REST Web 服务,并验证和显示其响应
在 Scala 中绑定 XML 数据
即使由于 JSON 日益流行,XML 最近从无处不在的位置有所下降,但两种格式都将继续被大量用于结构化数据。
在 Java 中,使用 JAXB 库创建能够序列化和反序列化 XML 数据以及通过 API 构建 XML 文档的类的做法很常见。
以类似的方式,Scala 可用的scalaxb
库可以生成用于处理 XML 和 Web 服务的帮助类。例如,让我们考虑一个小型的 XML 模式Bookstore.xsd
,它定义了作为书店一部分的一组书籍,如下所示:
<?xml version="1.0" encoding="UTF-8"?>
<xsd:schema
targetNamespace="http://www.books.org"
elementFormDefault="qualified">
<xsd:element name="book_store">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="book" type="book_type"
minOccurs="1" maxOccurs="unbounded"/>
</xsd:sequence>
</xsd:complexType>
</xsd:element>
<xsd:complexType name="book_type">
<xsd:sequence>
<xsd:element name="title" type="xsd:string"/>
<xsd:element name="author" type="xsd:string"/>
<xsd:element name="date" minOccurs="0" type="xsd:string"/>
<xsd:element name="publisher" type="xsd:string"/>
</xsd:sequence>
<xsd:attribute name="ISBN" type="xsd:string"/>
</xsd:complexType>
</xsd:schema>
一本书通常由其标题、作者、出版日期和 ISBN 号码定义,如下面的示例所示:
<book ISBN="9781933499185">
<title>Madame Bovary</title>
<author>Gustave Flaubert</author>
<date>1857</date>
<publisher>Fonolibro</publisher>
</book>
有几种方式可以在www.scalaxb.org网站上运行scalaxb
,要么直接作为命令行工具,通过 SBT 或 Maven 的插件,或者作为托管在heroku
上的 Web API。由于我们到目前为止基本上使用了 SBT 并且应该对它感到舒适,让我们使用 SBT 插件来创建绑定。
首先,通过在新终端窗口中运行以下命令创建一个名为wssample
的新 SBT 项目:
> mkdir wssample
> cd wssample
> sbt
> set name:="wssample"
> session save
>
现在我们需要在project/
目录下的plugins.sbt
文件中添加scalaxb
插件依赖(同时我们还将添加sbteclipse
插件,它使我们能够从 SBT 项目生成 Eclipse 项目)。生成的plugins.sbt
文件将类似于以下代码:
addSbtPlugin("com.typesafe.sbteclipse" % "sbteclipse-plugin" % "2.4.0")
addSbtPlugin("org.scalaxb" % "sbt-scalaxb" % "1.1.2")
resolvers += Resolver.sonatypeRepo("public")
此外,我们还需要稍微修改build.sbt
文件,以特别包括一个在用 SBT 编译时将生成scalaxb
XML 绑定的命令。生成的build.sbt
文件将类似于以下代码:
import ScalaxbKeys._
name:="wssample"
scalaVersion:="2.10.2"
scalaxbSettings
libraryDependencies += "net.databinder.dispatch" %% "dispatch-core" % "0.11.0"
libraryDependencies += "org.scalatest" %% "scalatest" % "2.0.M7" % "test"
sourceGenerators in Compile <+= scalaxb in Compile
packageName in scalaxb in Compile := "se.wssample"
将之前显示的 Bookstore.xsd
架构添加到在项目中创建的新 src/main/xsd
目录中。从现在起,每次你调用 SBT 命令 > compile
,scalaxb
都将在 target/scala-2.10/src_managed
目录下生成一些 Scala 类(在 build.sbt
文件中指定的包中,即 se.wssample
),除非没有进行更改。例如,在我们的小型示例中,scalaxb
生成以下案例类:
package se.wssample
case class Book_store(book: se.wssample.Book_type*)
case class Book_type(title: String,
author: String,
date: Option[String] = None,
publisher: String,
ISBN: Option[String] = None)
注意第一个案例类声明末尾的 *
,它用于指定可变参数(即不确定数量的参数,因此这里的 Book_store
构造函数可以接受多个 Book_type
实例)。一个展示如何使用生成的代码解析 XML 文档的测试类示例在 BookstoreSpec.scala
类中如下所示:
package se.wssample
import org.scalatest._
import org.scalatest.matchers.Matchers
class BookstoreSpec extends FlatSpec with Matchers {
"This bookstore" should "contain 3 books" in {
val bookstore =
<book_store >
<book ISBN="9781933499185">
<title>Madame Bovary</title>
<author>Gustave Flaubert</author>
<date>1857</date>
<publisher>Fonolibro</publisher>
</book>
<book ISBN="9782070411207">
<title>Le malade imaginaire</title>
<author>Moliere</author>
<date>1673</date>
<publisher>Gallimard</publisher>
</book>
<book ISBN="1475066511">
<title>Fables</title>
<author>Jean de La Fontaine</author>
<date>1678</date>
<publisher>CreateSpace</publisher>
</book>
</book_store>
val bookstoreInstance = scalaxb.fromXMLBook_store
println("bookstoreInstance: "+ bookstoreInstance.book)
bookstoreInstance.book.length should be === 3
}
}
当调用 > sbt test
命令时,此测试的预期输出如下:
bookstoreInstance: List(Book_type(Madame Bovary,Gustave Flaubert,Some(1857),Fonolibro,Some(9781933499185)), Book_type(Le malade imaginaire,Molière,Some(1673),Gallimard,Some(9782070411207)), Book_type(Fables,Jean de La Fontaine,Some(1678),CreateSpace,Some(1475066511)))
[info] BookstoreSpec:
[info] This bookstore
[info] - should contain 3 books
[info] Passed: : Total 1, Failed 0, Errors 0, Passed 1, Skipped 0
[success] Total time: 4 s
从 SOAP Web 服务运行 scalaxb
由于 scalaxb
支持 Web 服务描述语言(WSDL),我们还可以生成完整的 Web 服务 API,而不仅仅是与 XML 数据相关的类。为了实现此功能,我们只需将我们的 WSDL 服务描述文件复制到 src/main/wsdl
。所有具有 .wsdl
扩展名的文件将在编译时由 scalaxb
插件处理,它将创建以下三种类型的输出:
-
专门针对你应用程序的服务 API。
-
专门针对 SOAP 协议的类。
-
负责通过 HTTP 将 SOAP 消息发送到端点 URL 的类。
scalaxb
使用我们在 第三章 中介绍的调度库,即 理解 Scala 生态系统。这就是为什么我们将它添加到build.sbt
文件中的依赖项中。
让我们以一个在线 SOAP Web 服务为例,来说明如何从 WSDL 描述中使用 scalaxb
。www.webservicex.net 是一个包含各种市场细分中许多不同此类 Web 服务样本的网站。在这里,我们将关注他们的股票报价服务,该服务返回由股票符号给出的报价。API 非常简单,因为它只包含一个请求方法,getQuote
,并且返回的数据量有限。你可能想尝试任何其他可用的服务(稍后,因为你可以将多个 WSDL 文件放在同一个项目中)。它的 WSDL 描述看起来类似于以下代码:
<?xml version="1.0" encoding="utf-8"?>
<wsdl:definitions … // headers >
<wsdl:types>
<s:schema elementFormDefault="qualified" targetNamespace="http://www.webserviceX.NET/">
<s:element name="GetQuote">
<s:complexType>
<s:sequence>
<s:element minOccurs="0" maxOccurs="1" name="symbol" type="s:string" />
</s:sequence>
</s:complexType>
</s:element>
<s:element name="GetQuoteResponse">
<s:complexType>
<s:sequence>
<s:element minOccurs="0" maxOccurs="1" name="GetQuoteResult" type="s:string" />
</s:sequence>
</s:complexType>
</s:element>
<s:element name="string" nillable="true" type="s:string" />
</s:schema>
</wsdl:types>
<wsdl:message name="GetQuoteSoapIn">
<wsdl:part name="parameters" element="tns:GetQuote" />
</wsdl:message>
<wsdl:message name="GetQuoteSoapOut">
<wsdl:part name="parameters" element="tns:GetQuoteResponse" />
</wsdl:message>
...
WSDL 文件的第一部分包含 XML 模式的描述。第二部分定义了各种 Web 服务操作如下:
<wsdl:portType name="StockQuoteSoap">
<wsdl:operation name="GetQuote">
<wsdl:documentation >Get Stock quote for a company Symbol</wsdl:documentation>
<wsdl:input message="tns:GetQuoteSoapIn" />
<wsdl:output message="tns:GetQuoteSoapOut" />
</wsdl:operation>
</wsdl:portType>
<wsdl:portType name="StockQuoteHttpGet">
<wsdl:operation name="GetQuote">
<wsdl:documentation >Get Stock quote for a company Symbol</wsdl:documentation>
<wsdl:input message="tns:GetQuoteHttpGetIn" />
<wsdl:output message="tns:GetQuoteHttpGetOut" />
</wsdl:operation>
</wsdl:portType>
<wsdl:binding name="StockQuoteSoap12" type="tns:StockQuoteSoap">
<soap12:binding transport="http://schemas.xmlsoap.org/soap/http" />
<wsdl:operation name="GetQuote">
<soap12:operation soapAction="http://www.webserviceX.NET/GetQuote" style="document" />
<wsdl:input>
<soap12:body use="literal" />
</wsdl:input>
<wsdl:output>
<soap12:body use="literal" />
</wsdl:output>
</wsdl:operation>
</wsdl:binding>
...
最后,WSDL 文件的最后一部分定义了 Web 服务操作与物理 URL 之间的耦合:
<wsdl:service name="StockQuote">
…
<wsdl:port name="StockQuoteSoap12" binding="tns:StockQuoteSoap12">
<soap12:address location="http://www.webservicex.net/stockquote.asmx" />
</wsdl:port>
…
</wsdl:service>
</wsdl:definitions>
如你所见,WSDL 文件通常相当冗长,但由 scalaxb
生成的结果 Scala 合约简化为以下一个方法特质:
// Generated by <a href="http://scalaxb.org/">scalaxb</a>.
package se.wssample
trait StockQuoteSoap {
def getQuote(symbol: Option[String]): Either[scalaxb.Fault[Any], se.wssample.GetQuoteResponse]
}
注意结果类型是如何优雅地封装在Either
类中的,该类代表两种可能类型之一Left
和Right
的值,其中Right
对象对应于服务的成功调用,而Left
对象在失败情况下包含scalaxb.Fault
值,正如我们在第二章,代码集成中简要描述的那样。
由于与 SOAP 协议相关的生成类以及与 HTTP 调度相关的类并不特定于我们正在定义的服务,因此它们可以被重用,因此它们已经被生成为可堆叠的特性,包括数据类型和接口、SOAP 绑定和完整的 SOAP 客户端。以下StockQuoteSpec.scala
测试示例给出了这些特性的典型使用场景,以调用 SOAP 网络服务:
package se.wssample
import org.scalatest._
import org.scalatest.matchers.Matchers
import scala.xml.{ XML, PrettyPrinter }
class StockQuoteSpec extends FlatSpec with Matchers {
"Getting a quote for Apple" should "give appropriate data" in {
val pp = new PrettyPrinter(80, 2)
val service =
(new se.wssample.StockQuoteSoap12Bindings
with scalaxb.SoapClients
with scalaxb.DispatchHttpClients {}).service
val stockquote = service.getQuote(Some("AAPL"))
stockquote match {
case Left(err) => fail("Problem with stockquote invocation")
case Right(success) => success.GetQuoteResult match {
case None => println("No info returned for that quote")
case Some(x) => {
println("Stockquote: "+pp.format(XML.loadString(x)))
x should startWith ("<StockQuotes><Stock><Symbol>AAPL</Symbol>")
}
}
}
}
}
在这个例子中,一旦我们实例化了服务,我们只需调用 API 方法service.getQuote(Some("AAPL"))
来检索 AAPL 符号(苹果公司)的股票报价。然后我们对结果进行模式匹配,从服务返回的Either
对象中提取 XML 数据。最后,由于检索到的数据是以 XML 字符串的形式给出的,我们解析它并格式化它以便更好地阅读。我们可以使用以下代码执行测试以查看结果:
> sbt
> test-only se.wssample.StockQuoteSpec
Stockquote: <StockQuotes>
<Stock>
<Symbol>AAPL</Symbol>
<Last>553.13</Last>
<Date>1/2/2014</Date>
<Time>4:00pm</Time>
<Change>-7.89</Change>
<Open>555.68</Open>
<High>557.03</High>
<Low>552.021</Low>
<Volume>8388321</Volume>
<MktCap>497.7B</MktCap>
<PreviousClose>561.02</PreviousClose>
<PercentageChange>-1.41%</PercentageChange>
<AnnRange>385.10 - 575.14</AnnRange>
<Earns>39.75</Earns>
<P-E>14.11</P-E>
<Name>Apple Inc.</Name>
</Stock>
</StockQuotes>
[info] StockQuoteSpec:
[info] Getting a quote for Apple
[info] - should give appropriate data
[info] Passed: : Total 1, Failed 0, Errors 0, Passed 1, Skipped 0
[success] Total time: 3 s
处理 XML 和 JSON
XML 和 JSON 是结构化可以在系统部分之间交换的数据的占主导地位的格式,例如后端-前端或外部系统之间。在 Scala 中,Scala 库中提供了一些内置支持来操作这两种格式。
操作 XML
在本章以及第三章,理解 Scala 生态系统中,我们之前也简要地看到了,当使用 HTTP 时,XML 文档可以作为字面量创建,并以多种方式转换。例如,如果我们从 Play 项目根目录中键入> play console
来启动 REPL,我们就可以开始对 XML 进行实验:
scala> val books =
<Library>
<book title="Programming in Scala" quantity="15" price="30.00" />
<book title="Scala for Java Developers" quantity="10" price="25.50" />
</Library>
books: scala.xml.Elem =
<Library>
<book title="Programming in Scala" quantity="15" price="30.00"/>
<book title="Scala for Java Developers" quantity="10" price="25.50"/>
</Library>
books
变量是Elem
类型,它代表一个 XML 结构。我们不仅可以直接编写 XML 字面量,还可以使用实用方法通过解析文件或解析字符串来构建 XML Elem
,如下所示:
scala> import scala.xml._
scala> val sameBooks = XML.loadString("""
<Library>
<book title="Programming in Scala" quantity="15" price="30.00"/>
<book title="Scala for Java Developers" quantity="10" price="25.50"/>
</Library>
""")
sameBooks: scala.xml.Elem =
<Library>
<book price="30.00" quantity="15" title="Programming in Scala"/>
<book price="25.50" quantity="10" title="Scala for Java Developers"/>
</Library>
在前面的命令中使用的三引号允许我们表达一个预格式化的字符串,其中字符被转义(例如,正则字符串中的"
将被记为\"
)。
例如,处理这样的 XML 结构可能包括计算书籍的总价。这个操作可以通过 Scala 的for comprehension
实现,以下代码展示了如何实现:
scala> val total = (for {
book <- books \ "book"
price = ( book \ "@price").text.toDouble
quantity = ( book \ "@quantity").text.toInt
} yield price * quantity).sum
total: Double = 705.0
在处理与各种外部系统的集成时,检索和转换 XML 结构是经常发生的事情。通过 XPath 表达式访问我们之前所做的那样访问各种 XML 标签非常方便,并产生简洁且可读的代码。从 Excel 以 CSV 数据形式导出的信息程序化地创建 XML 也是一个常见的操作,可以按以下方式实现:
scala> val books =
<Library>
{ List("Programming in Scala,15,30.00","Scala for Java Developers,10,25.50") map { row => row split "," } map { b => <book title={b(0)} quantity={b(1)} price={b(2)} /> }}
</Library>
books: scala.xml.Elem =
<Library>
<book title="Programming in Scala" quantity="15" price="30.00"/><book title="Scala for Java Developers" quantity="10" price="25.50"/>
</Library>
操作 JSON
Scala 库支持 JSON,你只需要导入适当的库。以下是一些 REPL 用法的示例:
scala> import scala.util.parsing.json._
import scala.util.parsing.json._
scala> val result = JSON.parseFull("""
{
"Library": {
"book": [
{
"title": "Scala for Java Developers",
"quantity": 10
},
{
"title": "Programming Scala",
"quantity": 20
}
]
}
}
""")
result: Option[Any] = Some(Map(Library -> Map(book -> List(Map(title -> Scala for Java Developers, quantity -> 10.0), Map(title -> Programming Scala, quantity -> 20.0)))))
任何有效的 JSON 消息都可以转换成由 Maps
和 Lists
构成的结构。然而,通常我们希望创建有意义的类,即从 JSON 消息中表达业务领域。可在 json2caseclass.cleverapps.io
提供的在线服务正好做到这一点;它是一个方便的 JSON 到 Scala case class 转换器。例如,我们可以将我们前面的 JSON 消息复制到 Json 粘贴 文本区域,然后点击 Let's go! 按钮来尝试它,如下面的截图所示:
转换器产生以下输出:
case class Book(title:String, quantity:Double)
case class Library(book:List[Book])
case class R00tJsonObject(Library:Library)
在我们已经在 第一章 中介绍过的 case classes 的非常有趣的功能中,在项目中交互式编程 是一个用于模式匹配的分解机制。一旦 JSON 消息被反序列化为 case classes,我们就可以使用这个机制来操作它们,如下面的命令序列所示:
scala> case class Book(title:String, quantity:Double)
defined class Book
scala> val book1 = Book("Scala for Java Developers",10)
book1: Book = Book(Scala for Java Developers,10.0)
scala> val book2 = Book("Effective Java",12)
book2: Book = Book(Effective Java,12.0)
scala> val books = List(book1,book2)
books: List[Book] = List(Book(Scala for Java Developers,10.0), Book(Effective Java,12.0))
首先,我们定义了两个书籍实例并将它们放入一个列表中。
scala> def bookAboutScala(book:Book) = book match {
case Book(a,_) if a contains "Scala" => Some(book)
case _ => None
}
bookAboutScala: (book: Book)Option[Book]
之前定义的方法在 Book
构造函数上执行模式匹配,该构造函数还包含一个守卫(即 if
条件)。由于我们不使用第二个构造函数参数,所以我们用一个下划线代替了创建匿名变量。在之前定义的两个书籍实例上调用此方法将显示以下结果:
scala> bookAboutScala(book1)
res0: Option[Book] = Some(Book(Scala for Java Developers,10.0))
scala> bookAboutScala(book2)
res1: Option[Book] = None
我们可以将 case class 模式匹配与其他模式混合使用。例如,让我们定义以下正则表达式(注意三引号的使用以及使用 .r
来指定它是一个正则表达式):
scala> val regex = """(.*)(Scala|Java)(.*)""".r
regex: scala.util.matching.Regex = (.*)(Scala|Java)(.*)
此正则表达式将匹配包含 Scala 或 Java 的任何字符串。
scala> def whatIs(that:Any) = that match {
case Book(t,_) if (t contains "Scala") =>
s"${t} is a book about Scala"
case Book(_,_) => s"$that is a book "
case regex(_,word,_) => s"$that is something about ${word}"
case head::tail => s"$that is a list of books"
case _ => "You tell me !"
}
whatIs: (that: Any)String
我们现在可以在多个不同的输入上尝试这个方法并观察结果:
scala> whatIs(book1)
res2: String = Scala for Java Developers is a book about Scala
scala> whatIs(book2)
res3: String = "Book(Effective Java,12.0) is a book "
scala> whatIs(books)
res4: String = List(Book(Scala for Java Developers,10.0), Book(Effective Java,12.0)) is a list of books
scala> whatIs("Scala pattern matching")
res5: String = Scala pattern matching is something about Scala
scala> whatIs("Love")
res6: String = You tell me !
使用 Play JSON
除了 Scala 库的默认实现之外,还有许多其他库可以用来操作 JSON。除了建立在已知 Java 库(如 Jerkson,建立在 Jackson 之上)和其他已知实现(如 sjson、json4s 或 Argonaut,面向函数式编程)之上的库之外,许多 Web 框架也创建了它们自己的,包括 lift-json、spray-json 或 play-json。由于在这本书中我们主要介绍 Play 框架来构建 Web 应用程序,我们将重点关注 play-json 实现。请注意,play-json 也可以作为独立程序运行,因为它只包含一个 jar 文件,没有其他对 Play 的依赖。从 Play 项目内部运行 REPL 控制台已经包含了 play-json 依赖项,因此我们可以在控制台终端窗口中直接进行实验。
注意
如果你想在一个不同于 Play 控制台(例如,一个常规的 SBT 项目或 Typesafe activator 项目)的 REPL 中运行以下示例,那么你将不得不将以下依赖项添加到你的 build.sbt
文件中:
libraryDependencies += "com.typesafe.play" %% "play-json" % "2.2.1"
scala> import play.api.libs.json._
import play.api.libs.json._
scala> val books = Json.parse("""
{
"Library": {
"book": [
{
"title": "Scala for Java Developers",
"quantity": 10
},
{
"title": "Programming Scala",
"quantity": 20
}
]
}
}
""")
books: play.api.libs.json.JsValue = {"Library":{"book":[{"title":"Scala for Java Developers","quantity":10},{"title":"Programming Scala","quantity":20}]}}
JsValue
类型是 play-json 中包含的其他 JSON 数据类型的超类型,如下所示:
-
使用
JsNull
来表示 null 值 -
JsString
、JsBoolean
和JsNumber
分别用来描述字符串、布尔值和数字:数字包括 short、int、long、float、double 和 BigDecimal,如以下命令所示:scala> val sfjd = JsString("Scala for Java Developers") sfjd: play.api.libs.json.JsString = "Scala for Java Developers" scala> val qty = JsNumber(10) qty: play.api.libs.json.JsNumber = 10
-
JsObject
表示一组名称/值对,如下所示:scala> val book1 = JsObject(Seq("title"->sfjd,"quantity"->qty)) book1: play.api.libs.json.JsObject = {"title":"Scala for Java Developers","quantity":10} scala> val book2 = JsObject(Seq("title"->JsString("Programming in Scala"),"quantity"->JsNumber(15))) book2: play.api.libs.json.JsObject = {"title":"Programming in Scala","quantity":15}
-
JsArray
表示任何 JSON 值类型的序列(可以是异构的,即不同类型):scala> val array = JsArray(Seq(JsString("a"),JsNumber(2),JsBoolean(true))) array: play.api.libs.json.JsArray = ["a",2,true]
从程序上讲,创建一个与我们的书籍列表等价的 JSON 抽象 语法树(AST)可以表达如下:
scala> val books = JsObject(Seq(
"books" -> JsArray(Seq(book1,book2))
))
books: play.api.libs.json.JsObject = {"books":[{"title":"Scala for Java Developers","quantity":10},{"title":"Programming in Scala","quantity":15}]}
Play 最近增强以在创建我们刚刚描述的 JSON 结构时提供稍微简单的语法。构建相同 JSON 对象的替代语法如下所示:
scala> val booksAsJson = Json.obj(
"books" -> Json.arr(
Json.obj(
"title" -> "Scala for Java Developers",
"quantity" -> 10
),
Json.obj(
"title" -> "Programming in Scala",
"quantity" -> 15
)
)
)
booksAsJson: play.api.libs.json.JsObject = {"books":[{"title":"Scala for Java Developers","quantity":10},{"title":"Programming in Scala","quantity":15}]}
将 JsObject 序列化为其字符串表示形式可以通过以下语句实现:
scala> val booksAsString = Json.stringify(booksAsJson)
booksAsString: String = {"books":[{"title":"Scala for Java Developers","quantity":10},{"title":"Programming in Scala","quantity":15}]}
最后,由于 JsObject
对象代表一个树结构,你可以通过使用 XPath 表达式在树中导航,以检索各种元素,例如以下示例用于访问我们书籍的标题:
scala> val titles = booksAsJson \ "books" \\ "title"
titles: Seq[play.api.libs.json.JsValue] = ArrayBuffer("Scala for Java Developers", "Programming in Scala")
由于返回类型是 JsValue
对象的序列,将它们转换为 Scala 类型可能很有用,.as[…]
方法将方便地实现这一点:
scala> titles.toList.map(x=>x.as[String])
res8: List[String] = List(Scala for Java Developers, Programming in Scala)
使用 XML 和 JSON 处理 Play 请求
现在我们已经熟悉了 JSON 和 XML 格式,我们可以开始使用它们来处理 Play 项目中的 HTTP 请求和响应。
为了展示这些行为,我们将调用一个在线网络服务,即 iTunes 媒体库,它可在 www.apple.com/itunes/affiliates/resources/documentation/itunes-store-web-service-search-api.html
获取并得到文档说明。
它在搜索调用时返回 JSON 消息。例如,我们可以使用以下 URL 和参数调用 API:
itunes.apple.com/search?term=angry+birds&country=se&entity=software
参数过滤器会过滤掉与 愤怒的小鸟 有关的图书馆中的每一项,而实体参数仅保留软件项。我们还应用了一个额外的过滤器,以查询仅限于瑞典的应用商店。
注意
如果你还没有在 build.sbt
文件中添加,你可能需要在此处添加 dispatch 依赖项,就像我们在第三章“理解 Scala 生态系统”中处理 HTTP 时所做的那样:
libraryDependencies += "net.databinder.dispatch" %% "dispatch-core" % "0.11.0"
scala> import dispatch._
import dispatch._
scala> import Defaults._
import Defaults._
scala> val request = url("https://itunes.apple.com/search")
request: dispatch.Req = Req(<function1>)
我们 GET 方法调用中将包含的参数可以用 Scala Map
中的 (key,value)
元组来表示:
scala> val params = Map("term" -> "angry birds", "country" -> "se", "entity" -> "software")
params: scala.collection.immutable.Map[String,String] = Map(term -> angry birds, country -> se, entity -> software)
scala> val result = Http( request <<? params OK as.String).either
result: dispatch.Future[Either[Throwable,String]] = scala.concurrent.impl.Promise$DefaultPromise@7a707f7c
在这种情况下,结果类型是 Future[Either[Throwable,String]]
,这意味着我们可以通过模式匹配提取成功的调用以及失败的执行,如下所示:
scala> val response = result() match {
case Right(content)=> "Answer: "+ content
case Left(StatusCode(404))=> "404 Not Found"
case Left(x) => x.printStackTrace()
}
response: Any =
"Answer:
{
"resultCount":50,
"results": [
{"kind":"software", "features":["gameCenter"],
"supportedDevices":["iPhone5s", "iPad23G", "iPadThirdGen", "iPodTouchThirdGen", "iPadFourthGen4G", "iPhone4S", "iPad3G", "iPhone5", "iPadWifi", "iPhone5c", "iPad2Wifi", "iPadMini", "iPadThirdGen4G", "iPodTouchourthGen", "iPhone4", "iPadFourthGen", "iPhone-3GS", "iPodTouchFifthGen", "iPadMini4G"], "isGameCenterEnabled":true, "artistViewUrl":"https://itunes.apple.com/se/artist/rovio-entertainment-ltd/id298910979?uo=4", "artworkUrl60":"http://a336.phobos.apple.com/us/r30/Purple2/v4/6c/20/98/6c2098f0-f572-46bb-f7bd-e4528fe31db8/Icon.png",
"screenshotUrls":["http://a2.mzstatic.com/eu/r30/Purple/v4/c0/eb/59/c0eb597b-a3d6-c9af-32a7-f107994a595c/screen1136x1136.jpeg", "http://a4.mzst...
使用 JSON 模拟 Play 响应
当你需要将你的服务与你不拥有或直到在生产环境中部署它们才可用的外部系统集成时,测试发送和接收的消息交互可能会很麻烦。避免调用真实服务的一个有效方法是使用模拟消息来替换它,即硬编码的响应,这将缩短真实的交互,特别是如果你需要将测试作为自动化过程的一部分运行(例如,作为 Jenkins 作业的每日运行)。从 Play 控制器内部返回一个简单的 JSON 消息非常直接,如下例所示:
package controllers
import play.api.mvc._
import play.api.libs.json._
import views._
object MockMarketplaceController extends Controller {
case class AppStoreSearch(artistName: String, artistLinkUrl: String)
implicit val appStoreSearchFormat = Json.format[AppStoreSearch]
def mockSearch() = Action {
val result = List(AppStoreSearch("Van Gogh", " http://www.vangoghmuseum.nl/"), AppStoreSearch("Monet", " http://www.claudemonetgallery.org "))
Ok(Json.toJson(result))
}
}
涉及 Reads、Writes 和 Format 的 Json.format[. . .]
声明将在我们调用网络服务时在本节稍后解释,因此我们可以暂时跳过讨论这部分内容。
要尝试这个控制器,你可以创建一个新的 Play 项目,或者,就像我们在上一章第六章的最后一节“数据库访问和 ORM 的未来”中所做的那样,只需将此控制器添加到我们从现有数据库生成的应用程序中。你还需要在 conf/
目录下的 route
文件中添加一个路由,如下所示:
GET /mocksearch controllers.MockMarketplaceController.mockSearch
一旦应用程序运行,在浏览器中访问 http://localhost:9000/mocksearch
URL 将返回以下模拟 JSON 消息:
另一种方便的方法是使用在线服务来获取一个 JSON 测试消息,你可以用它来模拟响应,该服务位于 json-generator.appspot.com
。它包含一个 JSON 生成器,我们可以通过简单地点击 生成 按钮直接使用它。默认情况下,它将在浏览器窗口右侧的面板中生成一个包含随机数据的 JSON 示例,但遵循左侧面板中定义的结构,如下面的截图所示:
你可以点击 复制到 剪贴板 按钮并将生成的模拟消息直接粘贴到 Play 控制器的响应中。
从 Play 调用 Web 服务
在上一节中,为了快速实验 App Store 搜索 API,我们使用了 dispatch
库;我们已经在 第三章 中介绍了这个库,理解 Scala 生态系统。Play 提供了自己的 HTTP 库,以便与其他在线 Web 服务交互。它也是在 Java AsyncHttpClient
库(github.com/AsyncHttpClient/async-http-client
)之上构建的,就像 dispatch
一样。
在我们深入到从 Play 控制器调用 REST Web 服务之前,让我们先在 REPL 中对 Play Web 服务进行一点实验。在一个终端窗口中,要么创建一个新的 Play 项目,要么进入我们之前章节中使用的项目的根目录。一旦输入了 > play console
命令后获得 Scala 提示符,输入以下命令:
scala> import play.api.libs.ws._
import play.api.libs.ws._
scala> import scala.concurrent.Future
import scala.concurrent.Future
由于我们将异步调用 Web 服务,我们需要一个执行上下文来处理 Future
临时占位符:
scala> implicit val context = scala.concurrent.ExecutionContext.Implicits.global
context: scala.concurrent.ExecutionContextExecutor = scala.concurrent.impl.ExecutionContextImpl@44d8bd53
现在,我们可以定义一个需要调用的服务 URL。这里,我们将使用一个简单的 Web 服务,该服务根据以下签名返回作为参数的网站的地理位置:
http://freegeoip.net/{format}/{site}
格式参数可以是 json
或 xml
,而 site
将是对网站的引用:
scala> val url = "http://freegeoip.net/json/www.google.com"
url: String = http://freegeoip.net/json/www.google.com
scala> val futureResult: Future[String] = WS.url(url).get().map {
response =>
(response.json \ "region_name").as[String]
}
futureResult: scala.concurrent.Future[String] = scala.concurrent.impl.Promise$DefaultPromise@e4bc0ba
scala> futureResult.onComplete(println)
Success(California)
如我们之前在 第三章 中所见,理解 Scala 生态系统,当使用 dispatch
库时,Future
是一个包含异步计算结果的占位符,并且可以处于两种状态之一,即 完成
或 未完成
。这里,我们希望在结果可用时打印结果。
我们只从响应中提取了 region_name
项;整个 JSON 文档如下:
{
"ip":"173.194.64.106",
"country_code":"US",
"country_name":"United States",
"region_code":"CA",
"region_name":"California",
"city":"Mountain View",
"zipcode":"94043",
"latitude":37.4192,
"longitude":-122.0574,
"metro_code":"807",
"areacode":"650"
}
如果我们想封装响应的一部分,可以通过创建一个 case
类来实现,如下所示:
scala> case class Location(latitude:Double, longitude:Double, region:String, country:String)
defined class Location
play-json
库包括基于JsPath
的Reads
/Writes
/Format
组合器的支持,以便可以在运行时进行验证。如果您对使用这些组合器的所有细节感兴趣,您可能想阅读以下博客:mandubian.com/2012/09/08/unveiling-play-2-dot-1-json-api-part1-jspath-reads-combinators/
。
scala> import play.api.libs.json._
import play.api.libs.json._
scala> import play.api.libs.functional.syntax._
import play.api.libs.functional.syntax._
scala> implicit val locationReads: Reads[Location] = (
(__ \ "latitude").read[Double] and
(__ \ "longitude").read[Double] and
(__ \ "region_name").read[String] and
(__ \ "country").read[String]
)(Location.apply _)
locationReads: play.api.libs.json.Reads[Location] = play.api.libs.json.Reads$$anon$8@4a13875b
locationReads: play.api.libs.json.Reads[Location] = play.api.libs.json.Reads$$anon$8@5430c881
现在,在 JSON 响应上调用验证方法将验证我们接收到的数据是否格式正确且具有可接受值。
scala> val futureResult: Future[JsResult[Location]] = WS.url(url).get().map {
response => response.json.validate[Location]
}
futureResult: scala.concurrent.Future[play.api.libs.json.JsResult[Location]] = scala.concurrent.impl.Promise$DefaultPromise@3168c842
scala> futureResult.onComplete(println)
Success(JsError(List((/country,List(ValidationError(error.path.missing,WrappedArray()))))))
之前的JsError
对象展示了验证失败的情况;它检测到响应中未找到country
元素。实际上,正确的拼写应该是country_name
而不是country
,我们可以在locationReads
声明中更正这一点。这次验证通过了,我们得到的是我们期望的包含纬度和经度信息的JsSuccess
对象:
scala> implicit val locationReads: Reads[Location] = (
(__ \ "latitude").read[Double] and
(__ \ "longitude").read[Double] and
(__ \ "region_name").read[String] and
(__ \ "country_name").read[String]
)(Location.apply _)
locationReads: play.api.libs.json.Reads[Location] = play.api.libs.json.Reads$$anon$8@70aab9ed
scala> val futureResult: Future[JsResult[Location]] = WS.url(url).get().map {
response => response.json.validate[Location]
}
futureResult: scala.concurrent.Future[play.api.libs.json.JsResult[Location]] = scala.concurrent.impl.Promise$DefaultPromise@361c5860
scala> futureResult.onComplete(println)
scala> Success(JsSuccess(Location(37.4192,-122.0574,California,United States),))
现在,让我们创建一个示例控制器,该控制器调用网络服务从 App Store 检索一些数据:
package controllers
import play.api._
import play.api.mvc._
import play.api.libs.ws.WS
import scala.concurrent.ExecutionContext.Implicits.global
import play.api.libs.json._
import play.api.libs.functional.syntax._
import scala.concurrent.Future
import views._
import models._
object MarketplaceController extends Controller {
val pageSize = 10
val appStoreUrl = "https://itunes.apple.com/search"
def list(page: Int, orderBy: Int, filter: String = "*") = Action.async { implicit request =>
val futureWSResponse =
WS.url(appStoreUrl)
.withQueryString("term" -> filter, "country" -> "se", "entity" -> "software")
.get()
futureWSResponse map { resp =>
val json = resp.json
val jsResult = json.validate[AppResult]
jsResult.map {
case AppResult(count, res) =>
Ok(html.marketplace.list(
Page(res,
page,
offset = pageSize * page,
count),
orderBy,
filter))
}.recoverTotal {
e => BadRequest("Detected error:" + JsError.toFlatJson(e))
}
}
}
}
在这里,通过在WS
类上调用方法来展示对网络服务的调用,首先调用url
方法给出 URL,然后调用withQueryString
方法,输入参数以key->value
对的序列给出。请注意,返回类型是Future
,这意味着我们的网络服务是异步的。recoverTotal
接受一个函数,在处理错误后会返回默认值。json.validate[AppResult]
这一行使 JSON 响应与这里指定的AppResult
对象进行验证(作为app/models/
文件夹中Marketplace.scala
文件的一部分):
package models
import play.api.libs.json._
import play.api.libs.functional.syntax._
case class AppInfo(id: Long, name: String, author: String, authorUrl:String,
category: String, picture: String, formattedPrice: String, price: Double)
object AppInfo {
implicit val appInfoFormat = (
(__ \ "trackId").format[Long] and
(__ \ "trackName").format[String] and
(__ \ "artistName").format[String] and
(__ \ "artistViewUrl").format[String] and
(__ \ "primaryGenreName").format[String] and
(__ \ "artworkUrl60").format[String] and
(__ \ "formattedPrice").format[String] and
(__ \ "price").format[Double])(AppInfo.apply, unlift(AppInfo.unapply))
}
case class AppResult(resultCount: Int, results: Array[AppInfo])
object AppResult {
implicit val appResultFormat = (
(__ \ "resultCount").format[Int] and
(__ \\ "results").format[Array[AppInfo]])(AppResult.apply, unlift(AppResult.unapply))
}
AppResult
和AppInfo
案例类被创建出来,用于封装我们关心的服务元素。正如您在首次尝试 API 时可能看到的,大多数对 App Store 的搜索查询都会返回大量元素,其中大部分我们可能不需要。这就是为什么,我们可以使用一些 Scala 语法糖和组合器,在运行时验证 JSON 响应并直接提取感兴趣的元素。在尝试这个网络服务调用之前,我们只需要在conf/
目录下的routes
文件中添加所需的路由,如下面的代码所示:
GET /marketplace controllers.MarketplaceController.list(p:Int ?= 0, s:Int ?= 2, f ?= "*")
最后,在通过网络浏览器启动应用程序之前,我们还需要在MarketplaceController.scala
文件中提到的示例视图,该视图在views/marketplace/
目录下的list.scala.html
文件中创建,并在几个部分中展示如下代码:
@(currentPage: Page[AppInfo], currentOrderBy: Int, currentFilter:
String)(implicit flash: play.api.mvc.Flash)
...
@main("Welcome to Play 2.0") {
<h1>@Messages("marketplace.list.title", currentPage.total)</h1>
@flash.get("success").map { message =>
<div class="alert-message warning">
<strong>Done!</strong> @message
</div>
}
<div id="actions">
@helper.form(action=routes.MarketplaceController.list()) { <input
type="search" id="searchbox" name="f" value="@currentFilter"
placeholder="Filter by name..."> <input type="submit"
id="searchsubmit" value="Filter by name" class="btn primary">
}
</div>
...
视图的第一个部分仅包含用于导航的辅助方法,其生成方式与我们在第六章中用于 CRUD 示例生成的相同,即数据库访问和 ORM 的未来。视图的第二部分包括我们从网络服务中检索到的 JSON 元素:
...
@Option(currentPage.items).filterNot(_.isEmpty).map { entities =>
<table class="computers zebra-striped">
<thead>
<tr>
@header(2, "Picture")
@header(4, "Name")
@header(5, "Author")
@header(6, "IPO")
@header(7, "Category")
@header(8, "Price")
</tr>
</thead>
<tbody>
@entities.map{ entity =>
<tr>
<td>
<img
src="img/@entity.picture"
width="60" height="60" alt="image description" />
</td>
<td>@entity.name</td>
<td><a href="@entity.authorUrl" class="new-btn btn-back">@entity.author</a></td>
<td>@entity.category</td>
<td>@entity.formattedPrice</td>
</tr>
}
</tbody>
</table>
...
视图的第三部分和最后一部分是处理分页:
...
<div id="pagination" class="pagination">
<ul>
@currentPage.prev.map { page =>
<li class="prev"><a href="@link(page)">← Previous</a></li>
}.getOrElse {
<li class="prev disabled"><a>← Previous</a></li> }
<li class="current"><a>Displaying @(currentPage.offset + 1)
to @(currentPage.offset + entities.size) of @currentPage.total</a></li>
@currentPage.next.map { page =>
<li class="next"><a href="@link(page)">Next →</a></li>
}.getOrElse {
<li class="next disabled"><a>Next →</a></li> }
</ul>
</div>
}.getOrElse {
<div class="well">
<em>Nothing to display</em>
</div>
} }
一旦我们使用 > play run
重新启动 Play 应用程序并通过(通过网页浏览器)访问我们的本地 http://localhost:9000/marketplace?f=candy+crush
URL(该 URL 包含来自应用商店的默认搜索,其中 f
参数代表 filter
),我们将获得一个类似于以下截图的页面:
摘要
在本章中,我们看到了一些关于如何在 Scala 中操作 XML 和 JSON 格式以及如何通过 Web 服务连接到其他系统的示例。在 XML 的情况下,我们还介绍了如何从 WSDL 描述中生成 SOAP 绑定以及 Scala 类来封装 XML 模式中包含的 XML 领域。Play 框架中的 Web 服务是异步运行的,这意味着调用者在他继续进行其他有用处理(例如服务其他请求)之前不需要等待答案返回。在下一章中,我们将更精确地研究这种异步调用的概念。它基于 Future
和 Promise
的概念,这些概念也在 Java 世界中兴起,用于处理并发代码的执行。特别是,我们将通过 Akka 框架,这是一个开源工具包和运行时,它简化了并发应用程序的构建。Akka 是由 Scala 设计和编写的,包含 Scala 和 Java API,是 Play 框架基础设施的基础,使 Play 框架成为在多核架构上运行可扩展 Web 应用的理想选择。
第八章.现代应用程序的基本属性 - 异步和并发
可用性和性能是两个经常用来描述大多数商业软件背后需求的词汇。随着处理的信息量随着社交网络的兴起和在线服务的复杂性增加而不断增长,Web 服务器现在越来越多地面临沉重的负载和更高的并发请求数量。在本章中,我们将通过涵盖以下主题来探讨不同的方法,以实现更好的性能和可伸缩性:
-
Async 库,一种简化异步代码的新方法,包括 Web 服务组合的示例
-
Akka,一个基于 actor 范例简化构建并发、分布式和容错应用的工具包和运行时环境
并发的支柱
并发和异步是大多数编程语言用来增强响应时间和可伸缩性的技术,Java 也不例外。异步方法调用是一种技术,调用者对可能耗时的计算不需要等待响应,而是在计算进行的同时继续执行其他代码。一旦运行完成,调用者将被通知,无论是成功的结果还是失败的消息。
在 Java 中处理异步代码的传统方式主要是通过注册回调函数,即当任务完成时被调用的占位符。当处理异步代码时,由于执行顺序不是确定的,即执行顺序没有保证,因此复杂性往往会增加。因此,并发执行代码更难测试,因为连续调用可能不会产生相同的结果。此外,由于回调函数不可组合(这意味着它们不能以灵活的方式链式组合),将多个异步计算混合在一起以实现更高级的场景可能会变得很繁琐,从而导致在项目规模增加时出现的“回调地狱”问题(复杂性如此之高,以至于难以维护和保证代码的正确执行)。
当代码在多个核心上执行时,也会遇到并发问题。最近的硬件架构现在将多个核心嵌入到同一台机器中,作为在晶体管的最小物理尺寸达到极限时继续实现更好性能的一种方式。
处理并发代码的另一个后果是,当尝试访问相同资源时,多个执行线程可能会发生冲突。程序中的可变状态,如果没有保护共享访问,有更高的风险出现错误。确保并发代码正确执行通常需要付出增加复杂性的代价。例如,Java 线程同步机制,如使用锁,导致了难以理解和维护的解决方案。
Scala 追求不可变性的函数式方法是迈向更易并发处理的第一步。Scala 改进流程(SIP),在 Scala 中可以看作是 Java JSR 流程的等价物,已经提出了一项关于 SIP-14-Futures and Promises 的提案。这些概念并不新颖,因为它们在编写并发代码时已经被许多其他语言使用过,但新的提案试图合并 Scala 中 Futures 的各种实现。
注意
Futures 和 Promises 是通过它们可以检索异步执行结果的某些对象。要了解更多信息,请访问en.wikipedia.org/wiki/Futures_and_promises
。
如 SIP-14-Futures and Promises 所述:
Futures 提供了一种很好的方式来推理并行执行多个操作——以高效和非阻塞的方式进行。
从这个提案中,创建了一个实现,现在它是许多处理并发和异步代码的 Scala 库的基础。
Async 库 – SIP-22-Async
在第七章 使用集成和 Web 服务 中,我们简要地看到了如何调用返回Future
对象的异步 Web 服务。Async 的目标是通过提供一些强大的结构来简化异步代码,特别是处理多个异步代码块,特别是组合多个这样的块。它只包含两个结构:
-
async { <expression> }
:在这个结构中,<expression>
是异步执行的代码。 -
await { <expression returning a Future> }
:这个结构包含在一个async
块中。它暂停包含的async
块的执行,直到参数Future
完成。
async
/await
机制的一个有趣特点是它是完全非阻塞的。尽管理解async
/await
的工作原理并非必需,但两个方法async[]
和await[]
的确切签名如下供参考:
def asyncT : Future[T]
def awaitT:T
T
代表任意类型(如Int
或String
)或容器类型(如List
或Map
),这是我们描述 Scala 中泛型类型的方式。尽管我们不会过多地介绍泛型编程,这在其他书籍如Programming in Scala、Artima由Martin Odersky、Lex Spoon和Bill Venners中已经广泛描述,但了解它们存在并且是 Scala 语言核心部分是很重要的。
为了更好地理解 Async 是什么,我们将使用 REPL 中可以运行的示例。通过运行命令> play new ch8samples
创建一个新的Play
项目,并当然选择 Scala 作为项目使用的语言。一旦项目创建完成,通过在build.sbt
文件中添加一行来将 Async 库作为依赖项添加,现在文件看起来如下所示:
name := "ch8samples"
version := "1.0-SNAPSHOT"
libraryDependencies ++= Seq(
jdbc,
anorm,
cache,
"org.scala-lang.modules" %% "scala-async" % "0.9.0"
)
play.Project.playScalaSettings
我们可以在终端窗口中运行 REPL 控制台,就像往常一样,通过在项目的根目录中输入以下命令:
> play console
首先,我们需要执行一些导入,如下面的命令行所示:
scala> import scala.async.Async.{async, await}
import scala.async.Async.{async, await}
scala> import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.ExecutionContext.Implicits.global
类似地,对于线程池,需要一个执行上下文来处理异步计算应该如何以及何时执行。
第二,我们可以通过将计算封装在一个async
块中来指定异步计算:
scala> val computation = async { 3 * 2 }
computation: scala.concurrent.Future[Int] = scala.concurrent.impl.Promise$DefaultPromise@545c484c
scala> computation.value
res0: Option[scala.util.Try[Int]] = Some(Success(6))
如您所见,结果类型是Option[scala.util.Try[Int]]
,回顾一下第二章中关于Try
类的简要讨论,代码集成。我们了解到它基于一个可以取Success
或Failure
值的Either
类,分别对应于Either
类的Left
和Right
值。
在我们的例子中,计算非常迅速,并得到了成功值6
。
让我们进行一个耗时较长的计算(例如,10 秒钟),如下面的命令行所示:
scala> val longComputation = async { Thread.sleep(10000); 3*2 }
longComputation: scala.concurrent.Future[Int] = scala.concurrent.impl.Promise$DefaultPromise@7b5ab834
此外,在这 10 秒钟内,我们访问其结果值:
scala> longComputation.value
res1: Option[scala.util.Try[Int]] = None
我们将得到None
的答案,这正是我们预期的,因为计算尚未完成。
如果我们等待 10 秒钟并再次执行相同的查询,我们将得到我们的结果:
scala> longComputation.value
res2: Option[scala.util.Try[Int]] = Some(Success(6))
注意,一旦 Future 完成并赋予了一个值,它就不能被修改。
另一种获取结果的方法是在 Future 完成时得到通知或执行一些代码。我们可以在重新运行我们的计算后立即调用onComplete
方法,如下所示:
scala> val longComputation = async { Thread.sleep(10000); 3*2 }
longComputation: scala.concurrent.Future[Int] = scala.concurrent.impl.Promise$DefaultPromise@1c6b985a
scala> longComputation.onComplete(println)
scala> (no immediate result)
换句话说,在计算未完成时,我们可以继续执行其他语句:
scala> val hello = "Hello"
最终,当 10 秒钟的时间过去后,我们将在屏幕上看到值6
:
scala> Success(6)
到目前为止,我们已经看到async
方法与future
方法的表现相同,后者是scala.concurrent
包的一部分;因此,我们可以直接用future
替换async
。
优先选择的方式是结合使用async
和await
。await
方法接受一个Future
对象作为输入参数。它将async
块剩余部分包装在一个闭包中,并在等待的Future
对象(我们作为参数传递的那个)完成时将其作为回调传递。尽管await
会等待被调用的Future
对象直到其完成,但整个async
/await
执行是非阻塞的,这意味着我们可以以完全非阻塞的方式组合Future
对象。
让我们通过组合两个计算来举例说明,其中一个的计算输入依赖于另一个的计算输出。一个典型的例子是调用两个网络服务来查询天气预报服务:一个返回我们的当前地理位置,另一个需要我们的位置(坐标或城市名称)。以下命令行解释了调用过程:
scala> import play.api.libs.json._
import play.api.libs.json._
scala> import play.api.libs.ws._
import play.api.libs.ws._
scala> import play.api.libs.functional.syntax._
import play.api.libs.functional.syntax._
scala> import scala.util.{Success, Failure}
import scala.util.{Success, Failure}
scala> val locationURL = "http://freegeoip.net/xml/www.aftonbladet.se"
locationURL: String = http://freegeoip.net/xml/www.aftonbladet.se
scala> val futureLocation = WS.url(locationURL).get().map { response =>
(response.xml \ "City").text
}
futureLocation: scala.concurrent.Future[String] = scala.concurrent.impl.Promise$DefaultPromise@6039c183
等待几秒钟以确保网络服务Future
完成,然后按Enter;你会看到以下结果:
scala> val location = futureLocation.value
location: Option[scala.util.Try[String]] = Some(Success(Stockholm))
第一个服务返回我们提取的仅包含City
元素的 XML 文本。
现在,让我们尝试从openweathermap.org
网站获取第二个服务,这是一个用于测试通用网络服务代码的有用资源。以下网络服务调用返回特定位置的天气作为 JSON 消息(我们将在这里使用硬编码的Paris
城市来单独实验这个服务,而不组合两个服务):
scala> val weatherURL = "http://api.openweathermap.org/data/2.5/weather?q="
weatherURL: String = http://api.openweathermap.org/data/2.5/weather?q=
scala> val futureWeather = WS.url(weatherURL+"Paris").get().map{ response =>
response.json
}
futureWeather: scala.concurrent.Future[play.api.libs.json.JsValue] = scala.concurrent.impl.Promise$DefaultPromise@4dd5dc9f
等待几秒钟以确保网络服务Future
完成,然后输入以下语句:
scala> val weather = futureWeather.value
weather: Option[scala.util.Try[play.api.libs.json.JsValue]] = Some(Success({"coord":{"lon":2.35,"lat":48.85},"sys":{"message":0.0052,"country":"FR","sunrise":1389166933,"sunset":1389197566},"weather":[{"id":803,"main":"Clouds","description":"broken clouds","icon":"04n"}],"base":"cmc stations","main":{"temp":284.36,"pressure":1013,"temp_min":284.15,"temp_max":284.82,"humidity":86},"wind":{"speed":5.37,"deg":193},"clouds":{"all":80},"dt":1389221871,"id":2988507,"name":"Paris","cod":200}))
组合网络服务
我们现在可以使用async
/await
组合两个服务。
让我们一次性在 REPL 中复制粘贴以下行。为此,我们可以使用 REPL 的方便的:paste
命令,如下所示:
scala> :paste
// Entering paste mode (ctrl-D to finish)
val futureLocation =
WS.url(locationURL).get().map(resp => (resp.xml \ "City").text)
val futureWeather2 = async {
await(WS.url(weatherURL+await(futureLocation)).get()).body
}
futureWeather2.onComplete(println)
// once the block is copied from somewhere using ctrl-C/ctrl-D, press ctrl-D
// Exiting paste mode, now interpreting.
futureLocation: scala.concurrent.Future[String] = scala.concurrent.impl.Promise$DefaultPromise@1e111066
futureWeather2: scala.concurrent.Future[String] = scala.concurrent.impl.Promise$DefaultPromise@724ba7f5
scala> Success({"coord":{"lon":18.06,"lat":59.33},"sys":{"message":0.0251,"country":"SE","sunrise":1395808197,"sunset":1395854197},"weather":[{"id":800,"main":"Clear","description":"Sky is Clear","icon":"01d"}],"base":"cmc stations","main":{"temp":277.29,"pressure":1028,"humidity":69,"temp_min":276.15,"temp_max":278.15},"wind":{"speed":5.1,"deg":60},"rain":{"3h":0},"clouds":{"all":0},"dt":1395852600,"id":2673730,"name":"Stockholm","cod":200})
这段代码中发生的情况是,await
构造确保了位置城市对天气服务可用。
不使用await
组合服务
如果我们在定义futureWeather2
变量时没有在futureLocation
网络服务调用周围放置await
方法,我们会得到不同的答案。这是因为在这种情况下,包含位置服务答案的Future
对象在查询天气服务时还没有填充。你可以通过一次性复制粘贴以下三个语句来验证这种行为(假设locationURL
变量仍然有效,它是在介绍位置服务时创建的):
scala> :paste
// Entering paste mode (ctrl-D to finish)
val futureLocation =
WS.url(locationURL).get().map(resp => (resp.xml \ "City").text)
val futureWeather2 = async {
await(WS.url(weatherURL + futureLocation).get()).body
}
futureWeather2.onComplete(println)
// once the block is copied from somewhere using ctrl-C/ctrl-D, press ctrl-D
// Exiting paste mode, now interpreting.
futureLocation: scala.concurrent.Future[String] = scala.concurrent.impl.Promise$DefaultPromise@705a7c28
futureWeather2: scala.concurrent.Future[String] = scala.concurrent.impl.Promise$DefaultPromise@448d5fb8
scala> Success({"message":"Error: Not found city","cod":"404"}
)
这次,输出显示城市没有正确输入到天气服务中。
开始使用 Akka
Akka是一个工具包,用于简化编写并发和分布式应用程序,这些任务可能非常复杂,正如我们在本章开头所描述的。由于 Akka 既有大量书籍的详细文档,也有广泛的在线文档,我们的目标主要是进行技术实验。我们将看到如何优雅地编写 Scala 代码来解决那些如果以更传统的线程同步或其他语言(如 Java)编写可能会出错且难以理解的问题。Akka 是用 Scala 编写的,但同时也提供了 Java 和 Scala 的 API。
理解 Actor 模型
Akka 依赖于 Actor 范式来创建并发应用程序。Actor 模型早在卡尔·休伊特(Carl Hewitt)、彼得·毕晓普(Peter Bishop)和理查德·斯蒂格(Richard Steiger)撰写的原始论文《为人工智能提供一个通用的模块化 Actor 形式主义》(A Universal Modular Actor Formalism for Artificial Intelligence),发表于 1973 年的 IJCAI 会议上就已经被介绍。Erlang 是使用这种计算模型而闻名的一种语言,并且实现了非常好的可扩展性和可靠性指标(著名的九九可用性)。
不深入细节,我们可以这样说,Actor 模型是一个基于消息传递而不是方法调用的模型。每个计算单元,称为actor,封装了自己的行为,并通过异步不可变消息与其他 actor 进行通信。由于 actor 的足迹与线程相比非常小,并且状态不共享,因此非常适合编写并发和分布式应用程序。
在 Typesafe activator 模板的宝库中,有许多与 Akka 相关的项目可供选择。让我们深入探讨其中几个,以更好地理解如何使用 Akka actors 编写程序。首先,我们可以查看hello-akka
项目,以了解如何运行一个简单的 actor。
如果你还没有安装 Typesafe activator,请按照第三章中的说明,即《理解 Scala 生态系统》,来创建与hello-akka
模板相关的示例项目。一旦导入到 Eclipse 中,我们就可以开始查看Scala src
目录默认包中的主类HelloAkkaScala.scala
。
它从以下几行开始(省略了导入部分):
case object Greet
case class WhoToGreet(who: String)
case class Greeting(message: String)
class Greeter extends Actor {
var greeting = ""
def receive = {
case WhoToGreet(who) => greeting = s"hello, $who"
case Greet => sender ! Greeting(greeting)
// Send the current greeting back to the sender
}
}
如你所见,定义一个 actor 包括扩展Actor
特质,并且只需要实现抽象的receive
方法。这个方法代表了 actor 在接收到消息时的行为。它不需要处理所有类型的消息,这就是为什么它是一个部分函数。
声明的可变变量greeting
表明你可以在 actor 中安全地添加一些可变状态,因为receive
方法的处理是单线程的。
将演员之间发送的不可变消息建模为案例类很方便,Greeter
演员使用两个消息,Greet
和WhoToGreet(who:String)
。每当Greeter
演员收到WhoToGreet(who)
消息时,它只是更新其状态但不回复任何内容。相比之下,当这个相同的演员收到Greet
消息时,它使用保存的状态来回答发送消息的演员。!
方法也称为tell
(顺便说一下,这是 Akka Java API 中使用的名称)并代表向演员发送消息,其签名是actor ! message
。
此外,请注意sender
变量的存在,它是作为Actor
特质的一部分隐式提供的,因为演员回复发送者的模式很常见。然而,我们也可以在包含接收者地址的Greet
消息中添加一个ActorRef
参数,即声明一个case Greet(someone:ActorRef)
类并实现Greet
的处理,如下所示:
def receive = {
...
case Greet(someone) => someone ! Greeting(greeting)
}
HelloAkkaScala
对象定义了主程序,如下面的代码片段所示:
object HelloAkkaScala extends App {
// Create the 'helloakka' actor system
val system = ActorSystem("helloakka")
// Create the 'greeter' actor
val greeter = system.actorOf(Props[Greeter], "greeter")
// Create an "actor-in-a-box"
val inbox = Inbox.create(system)
// Tell the 'greeter' to change its 'greeting' message
greeter.tell(WhoToGreet("akka"), ActorRef.noSender)
// Ask the 'greeter for the latest 'greeting'
// Reply should go to the "actor-in-a-box"
inbox.send(greeter, Greet)
// Wait 5 seconds for the reply with the 'greeting' message
val Greeting(message1) = inbox.receive(5.seconds)
println(s"Greeting: $message1")
// Change the greeting and ask for it again
greeter.tell(WhoToGreet("typesafe"), ActorRef.noSender)
inbox.send(greeter, Greet)
val Greeting(message2) = inbox.receive(5.seconds)
println(s"Greeting: $message2")
val greetPrinter = system.actorOf(Props[GreetPrinter])
// after zero seconds, send a Greet message every second to the greeter with a sender of the greetPrinter
system.scheduler.schedule(0.seconds, 1.second, greeter, Greet)(system.dispatcher, greetPrinter)
}
运行演员的系统需要一个运行时环境;这就是system
变量所声明的。创建一个演员包括使用配置参数以及可选名称调用system.actorOf
方法。此方法返回一个ActorRef
(演员引用)对象,即演员地址,即消息可以发送的地方。ActorRef
对象是对演员的不可变和可序列化句柄,它可能位于本地主机上或同一ActorSystem
对象内。由于演员只能以异步方式通过消息进行通信,每个演员都有一个邮箱,如果演员不能像消息到达那样快速处理它们,消息可以排队。
主程序剩余部分基本上以Greet
或WhoToGreet
消息的形式向Greeter
演员发送订单。这些消息从一个也期望得到回答的Inbox
对象发送。这个Inbox
对象也被称为“actor-in-a-box”,是编写将与其他演员通信的代码的便捷方式。最后,最后一个演员greetPrinter
每秒重复向Greeter
演员发送Greet
消息(由环境安排)。
您可以通过运行命令> ./activator run
并在选择[2] HelloAkkaScala
程序来在项目中执行示例代码。您应该看到如下代码所示的内容:
Multiple main classes detected, select one to run:
[1] HelloAkkaJava
[2] HelloAkkaScala
Enter number: 2
[info] Running HelloAkkaScala
Greeting: hello, akka
Greeting: hello, typesafe
hello, typesafe
hello, typesafe
hello, typesafe
… [press CTRL-C to interrupt]
行为切换
演员在处理下一个消息之前可以切换其行为。为了说明这一点,让我们考虑一个需要为顾客预订航班座位和酒店房间的旅行代理演员的例子。旅行代理负责确保预订是事务性的,也就是说,只有当交通和住宿都预订成功时,它才是成功的,这如下面的图所示:
由于将关于演员的消息声明在其伴随对象中是一种公认的最佳实践,因此我们将以下方式表达Flight
演员:
package se.sfjd.ch8
import akka.actor.Actor
import akka.event.LoggingReceive
object Flight {
case class BookSeat(number:Int) {
require(number > 0)
}
case object Done
case object Failed
}
class Flight extends Actor {
import Flight._
var seatsLeft = 50
def receive = LoggingReceive {
case BookSeat(nb) if nb <= seatsLeft =>
seatsLeft -= nb
sender ! Done
case _ => sender ! Failed
}
}
注意在BookSeat
消息声明中找到的require
断言。这个方法属于Predef
,这是一个包含许多默认导入的有用功能的全局对象。它通过检查方法的前置和后置条件来实现一些基于契约的设计风格的规范。Flight
演员的receive
方法只处理一种类型的消息,即BookSeat(n:Int)
,这意味着只要航班还有足够的座位,就可以预订n个座位。如果还有足够的座位,Flight
演员将更新其状态,并回复一个Done
消息给发送者;否则回复Failed
。
注意围绕处理演员消息的代码块的LoggingReceive
类。它是akka.event
包的一部分,是一种方便记录到达此块的信息的方式。我们将在稍后执行示例代码时看到这些消息的样子。
以类似的方式,可以编写一个负责为n人预订房间的Hotel
演员,如下所示:
object Hotel {
case class BookRoom(number:Int) {
require(number > 0)
}
case object Done
case object Failed
}
class Hotel extends Actor {
import Hotel._
var roomsLeft = 15
def receive = LoggingReceive {
case BookRoom(nb) if nb <= roomsLeft =>
roomsLeft -= nb
sender ! Done
case _ => sender ! Failed
}
}
旅行代理演员是即将切换其行为的演员。一旦它向预订飞机座位和酒店房间的人数发送了订单,它将依次改变状态,并期待回答。由于发送给Flight
和Hotel
的消息是异步的,即非阻塞的,所以我们不知道哪个回答会先回来。此外,回答可能根本不会回来,因为此时没有保证消息已经送达或正确处理。TravelAgent
演员的代码如下所示:
object TravelAgent {
case class BookTrip(transport: ActorRef, accomodation: ActorRef, nbOfPersons: Int)
case object Done
case object Failed
}
class TravelAgent extends Actor {
import TravelAgent._
def receive = LoggingReceive {
case BookTrip(flightAgent, hotelAgent, persons) =>
flightAgent ! Flight.BookSeat(persons)
hotelAgent ! Hotel.BookRoom(persons)
context.become(awaitTransportOrAccomodation(flightAgent, hotelAgent,sender))
}
def awaitTransportOrAccomodation(transport: ActorRef, accomodation: ActorRef, customer:ActorRef): Receive = LoggingReceive {
case Flight.Done =>
context.become(awaitAccomodation(customer))
case Hotel.Done =>
context.become(awaitTransport(customer))
case Flight.Failed | Hotel.Failed =>
customer ! Failed
context.stop(self)
}
def awaitTransport(customer: ActorRef): Receive = LoggingReceive {
case Flight.Done =>
customer ! Done
context.stop(self)
case Flight.Failed =>
customer ! Failed
context.stop(self)
}
def awaitAccomodation(customer: ActorRef): Receive = LoggingReceive {
case Hotel.Done =>
customer ! Done
context.stop(self)
case Hotel.Failed =>
customer ! Failed
context.stop(self)
}
}
调用context.become(<new behavior method>)
切换演员的行为。在这个简单的旅行代理案例中,行为将切换到可以分别从Flight
和Hotel
演员接收到的预期消息,顺序不限。如果从Flight
或Hotel
演员那里收到成功的回答,TravelAgent
演员将切换其行为,只期待剩余的回答。
现在,我们只需要一个主程序来创建我们的初始演员,并与TravelAgent
演员进行通信,如下所示:
package se.sfjd.ch8
import akka.actor.Actor
import akka.actor.Props
import akka.event.LoggingReceive
class BookingMain extends Actor {
val flight = context.actorOf(Props[Flight], "Stockholm-Nassau")
val hotel = context.actorOf(Props[Hotel], "Atlantis")
val travelAgent = context.actorOf(Props[TravelAgent], "ClubMed")
travelAgent ! TravelAgent.BookTrip(flight,hotel,10)
def receive = LoggingReceive {
case TravelAgent.Done =>
println("Booking Successful")
context.stop(self)
case TravelAgent.Failed =>
println("Booking Failed")
context.stop(self)
}
}
一旦在 Eclipse 中编写了涉及用例的四个演员类,可以通过运行 Eclipse 配置来运行程序。导航到运行 | 运行配置…并编辑一个新的Java 应用程序配置窗口,知道要运行的主类是 Akka 运行时的akka.Main
类,如图下所示:
我们想要运行的实际主程序作为参数传递。为此,编辑同一窗口的参数选项卡,如图下所示:
要使 LoggingReceive
对象产生的调试消息生效,你需要添加之前截图中所指定的 VM 参数。点击 运行 按钮将在 Akka 运行时环境中执行 BookingMain
类,并显示以下消息流:
如果你想测试一个替代场景,例如,在预订酒店时看到预订失败,只需在 travelAgent ! TravelAgent.BookTrip(flight,hotel,20)
中输入更多的人数,即 20
,而不是 10
。
监督角色以处理失败
在并发运行角色的应用程序中,有时可能会抛出异常,这些异常最终会导致角色死亡。由于其他角色仍在运行,可能很难注意到部分失败。在传统的架构中,其中对象在其它对象上调用方法,调用者是接收异常的一方。由于它通常在等待响应时阻塞,因此它也是负责处理失败的一方。与角色不同,由于所有消息都是异步处理的,不知道在收到答案之前需要多长时间(如果有的话),因此关于发送消息的上下文通常不再存在以处理失败;因此,对异常的反应可能更困难。无论如何,必须对失败的角色采取一些措施,以便应用程序能够正常工作。
这就是为什么 Akka 通过提供支持来监控并最终重启一个或一组依赖角色,而拥抱“让它崩溃”的哲学。由于角色通常由其他角色创建,它们可以被组织成层次结构,其中角色的父级也是其管理者。因此,处理部分失败包括定义一些策略,根据情况同时重启部分角色层次结构。
如果我们回到我们的小型旅行预订应用程序,我们可以重构 TravelAgent
角色使其成为 Flight
和 Hotel
预订角色的管理者。因此,我们可以在 TravelAgent
类中声明以下管理者策略:
override val supervisorStrategy = OneForOneStrategy(loggingEnabled = false) {
case _: Flight.FlightBookingException =>
log.warning("Flight Service Failed. Restarting")
Restart
case _: Hotel.HotelBookingException =>
log.warning("Hotel Service Failed. Restarting")
Restart
case e =>
log.error("Unexpected failure", e.getMessage)
Stop
}
两种可能的策略是 OneForOneStrategy
和 AllForOneStrategy
。在前一种情况下,管理者的每个子角色将分别处理,而在第二种情况下,给定管理者的所有子角色将同时处理。
Flight
伴生对象现在包含了一个反映失败情况的消息,如下面的代码所示:
object Flight {
case class BookSeat(number:Int) {
require(number > 0)
}
case object Done
case object Failed
class FlightBookingException extends Exception("Unavailable Flight Booking Service")
}
为了模拟在预订航班座位时可能会失败的情况,我们可以在处理 Flight
角色的 receive
方法时引入以下方法调用,如下面的代码片段所示:
class Flight extends Actor {
import Flight._
var seatsLeft = 50
def receive = LoggingReceive {
case BookSeat(nb) if nb <= seatsLeft =>
unreliable()
seatsLeft -= nb
sender ! Done
case _ => sender ! Failed
}
private def unreliable(): Unit =
// the service is only working 75 percent of the time
if (ThreadLocalRandom.current().nextDouble() < 0.25)
throw new FlightBookingException
}
使用Run
配置重新启动预订场景将在某些时候显示失败消息,如下所示:
…
[WARN] [01/24/2014 00:23:50.098] [Main-akka.actor.default-dispatcher-3] [akka://Main/user/app/ClubMed] Flight Service Failed. Restarting
…
对于想要更深入探讨监督主题的读者,有一个完整且一致的示例,称为akka-supervision
,它是激活器模板的一部分。它演示了算术表达式的计算,以便表示总计算子部分的节点可能会失败并重新启动。
测试演员系统
由于它们的非确定性本质,与传统的单线程架构相比,在测试并发系统时需要特别注意。演员系统也不例外;消息的发送和接收是异步的,程序流程可以遵循多条路径。幸运的是,Akka 在akka-testkit
模块中提供了大量的支持,用于处理测试。
在第四章测试工具中,我们已经通过查看test-patterns-scala
激活器模板项目,涉及了scalatest
框架的许多示例。它包含了一个关于通过testkit
模块测试 Akka 演员的基本用例。您可以重新导入此模板项目到 Eclipse 中,或者如果它仍在 IDE 中,也可以直接打开它。以下测试中的GeoActor
对象的语法应该看起来很熟悉,因为它使用与我们在第三章理解 Scala 生态系统中看到的方式相同的调度库:
package scalatest
import akka.actor.ActorSystem
import akka.actor.Actor
import akka.actor.Props
import akka.testkit.TestKit
import org.scalatest.WordSpecLike
import org.scalatest.matchers.MustMatchers
import org.scalatest.BeforeAndAfterAll
import akka.testkit.ImplicitSender
//http://doc.akka.io/docs/akka/snapshot/scala/testing.html
object Setup {
class EchoActor extends Actor {
def receive = {
case x => sender ! x
}
}
case class Address(street: String,
city: String,
state: String,
zip: String)
//see https://developers.google.com/maps/documentation/geocoding/#Limits
class GeoActor extends Actor {
def receive = {
case Address(street,city,state,zip) => {
import dispatch._, Defaults._
val svc = url(s"http://maps.googleapis.com/maps/api/geocode/xml?address=${street},${city},${state},${zip}&sensor=true".replace(" ","+"))
val response = Http(svc OK as.xml.Elem)
val lat = (for {
elem <- response() \\ "geometry" \ "location" \ "lat"
} yield elem.text).head
val lng = (for {
elem <- response() \\ "geometry" \ "location" \ "lng"
} yield elem.text).head
sender ! s"${lat},${lng}"
}
case _ => sender ! "none"
}
}
}
在测试用例的主流程中,我们混合了ImplicitSender
特质,然后调用expectMsg
方法:
class Test09(asys: ActorSystem) extends TestKit(asys) with ImplicitSender with WordSpecLike with MustMatchers with BeforeAndAfterAll {
import Setup._
def this() = this(ActorSystem("Setup"))
override def afterAll {
TestKit.shutdownActorSystem(system)
}
"An Echo actor" must {
"return messages" in {
val echo = system.actorOf(Props[EchoActor])
echo ! "hello world"
expectMsg("hello world")
}
}
"Geo actor" must {
"send back lat,lon" in {
val geo = system.actorOf(Props[GeoActor])
geo ! Address("27 South Park Avenue","San Francisco","CA","94107")
expectMsg("37.7822991,-122.3930776")
}
}
}
expectMsg()
方法的作用是一个带有持续时间参数的断言,因此它不会永远等待回复。相反,如果指定的时间已经过去,它还没有收到它等待的答案,它将抛出一个异常。
进一步探索 Akka
除了演员消息和监督的有用功能外,Akka 还包括对许多其他更高级功能的支持。其中以下是一些:
-
它通过
DeathWatch
API 监控演员的生命周期。 -
它在失败后持久化演员状态以进行恢复。
-
它与演员进行远程通信,即在分布式环境中以透明的方式与演员通信。
-
它在分布式环境中进行集群处理以处理失败。集群功能的示例也作为
akka-clustering
激活器模板提供。
这些特性超出了本书的范围,但在 Akka 网站上进行了广泛记录,并可在 akka.io/docs/
获取。
摘要
在本章中,我们首先学习了如何使用 Async 工具包处理异步 Scala 代码,该工具包简化了使用 Futures 和 Promises 编写非阻塞代码。然后,我们通过介绍基于演员范式的 Akka 框架转向了并发主题。
并发和分布式系统是一个如此庞大的主题,我们只介绍了演员系统的一些基本使用场景。我们了解到,由于演员的行为和状态被封装,演员系统易于推理。此外,Akka 中的监督和集群支持使得处理故障和分布非常稳健。本章涵盖的材料只是 Akka 工具箱所能实现的一瞥;Akka 项目的非常详尽和精心编写的文档将指导你创建可扩展和分布式应用。编程异步、并发和分布式系统通常是一项复杂的任务,而演员模型使其更加易于管理。
由于 Akka 也是 Play
框架的基础,我们将在下一章继续使用它。我们将使用 Play
构建响应式网络应用,以展示如何创建需要处理数据流并将信息推送到浏览器的现代应用。
第九章:构建响应式 Web 应用程序
现代 Web 应用程序越来越需要我们从静态 Web 内容转向更动态的范式,其中大量的集成在后台进行,用户交互越来越复杂。同时,提供的在线服务需要适应不断变化的企业需求,并扩展到弹性负载,即处理高峰时段的流量。最后,除了提供的服务外,Web 应用程序现在倾向于收集有关用户交互的额外信息,以更好地理解客户行为。在本章中,我们将探讨以下主题:
-
理解使应用程序成为响应式的因素
-
介绍 Play 框架中使用 Iteratees 模式处理流
-
编写包括 Web Sockets 在内的响应式应用程序
描述响应式应用程序
Web 使用的传统拉模型,现在需要由双向通信来补充。这包括推送模型,其中用户,例如,接收异步和长时间运行服务的确认或只是接收各种类型的通知。
最近创建的响应式宣言,可在 www.reactivemanifesto.org
查阅,旨在以技术无关的方式总结表征响应式应用程序的标准:
-
响应用户事件:消息传递架构,不浪费等待资源的时间
-
响应用户负载:通过避免对共享资源的竞争来关注可伸缩性
-
响应用户失败:构建具有在所有级别恢复能力的弹性系统
-
响应用户操作:无论负载如何,都要遵守响应时间保证
在不深入探讨你被鼓励阅读的宣言细节的情况下,我们可以直接看到,之前章节中 Akka 所使用的消息驱动架构的概念,与这种响应式模型非常契合。在接下来的章节中,我们将关注在 Play 框架之上构建此类 Web 应用程序的示例。
响应式处理流
当你在 Web 应用程序中需要消费和转换数据流时,例如监控股票更新或服务上的日志活动,你需要机制来操作可以从服务器推送到浏览器的数据块,例如使用 Comet (en.wikipedia.org/wiki/Comet_(programming)
) 或 WebSocket (en.wikipedia.org/wiki/WebSocket
) 技术。Play 框架中可用的 Iteratee
模式就是这样一种机制。它最初是从 Haskell 函数式语言中借用的。
理解 Play 中的 Iteratees
Iteratee
构造旨在提供一种可组合且非阻塞的方式来处理由其对应者 Enumerator
产生的流。
让我们启动一个 Scala REPL 来更详细地探索Iteratee
/Enumerator
结构。为了创建一个新的 play 项目,就像我们之前多次做的那样,特别是在第五章,Play 框架入门中,使用以下命令:
> play new ch9samples (then choose Scala as language)
> cd ch9samples
> play console
首先,我们将提醒自己如何在类似 Java 这样的命令式语言中完成迭代。以下用 Scala 编写的语句描述了在迭代的每一步更新可变变量total
的使用:
scala> val numbers = List(1,4,7,8,10,20)
numbers: List[Int] = List(1, 4, 7, 8, 10, 20)
scala> var total = 0
total: Int = 0
scala> var iterator = numbers.iterator
iterator: Iterator[Int] = non-empty iterator
scala> while (iterator.hasNext) {
total += iterator.next
}
scala> total
res2: Int = 50
提示
如mandubian.com/2012/08/27/understanding-play2-iteratees-for-normal-humans/
博客文章中所述,在迭代时,我们需要注意以下事项:
-
迭代的状态(是否有更多元素要跟随,或者是否已完成)?
-
一个上下文(这里指总累加器)
-
一个更新上下文(即总+=iterator.next)的动作
我们在第一章,在项目中交互式编程中看到,我们可以通过使用以下方式的foldLeft
Scala 结构以简洁和更函数式的方式实现相同的操作:
scala> List(1,4,7,8,10,20).foldLeft(0){ (total,elem) =>
total + elem }
res3: Int = 50
foldLeft
结构是一个强大的结构,它应用于 Scala 集合,如Lists
。如果我们想处理其他形式的输入,如文件、网络、数据库连接或由 Akka actor 产生的流等,那么Enumerator/Iteratee
就派上用场。Enumerator
结构可以看作是数据的生产者(类似于之前的List
),而Iteratee
则是该数据的消费者,处理迭代的每个步骤。先前的涉及List
上的foldLeft
方法的例子可以用Enumerator/Iteratee
结构重写。由于iteratee
库已经在 Play 中可用,可以直接使用以下命令导入:
scala> import play.api.libs.iteratee._
import play.api.libs.iteratee._
scala> import play.api.libs.concurrent.Execution.Implicits._
import play.api.libs.concurrent.Execution.Implicits._
在导入iteratee库和全局执行上下文,以便iteratee
变量可以工作之后,我们可以定义我们的第一个Enumerator
如下:
scala> val enumerator = Enumerator(1,4,7,8,10,20)
enumerator: play.api.libs.iteratee.Enumerator[Int] = play.api.libs.iteratee.Enumerator$$anon$19@27a21c85...
以下定义的iteratee
变量表示在从enumerator
接受输入的同时要执行的计算步骤:
scala> val iteratee = Iteratee.fold(0){ (total, elem:Int) => total + elem }
iteratee: play.api.libs.iteratee.Iteratee[Int,Int] = play.api.libs.iteratee.ContIteratee@e07a406
将enumerator
结构和iteratee
结构结合起来,就是调用enumerator
的run
方法,该方法接受iteratee
作为参数:
scala> val result = enumerator.run(iteratee)
result: scala.concurrent.Future[Int] = scala.concurrent.impl.Promise$DefaultPromise@78b5282b
由于我们有一个异步计算,我们得到一个result
作为Future
,一旦完成,我们可以显示它,如下所示:
scala> result onComplete println
scala> Success(50)
之前提到的enumerator
对象是一个整数枚举器。我们可以创建多种不同类型的数据生成器,例如字符串或双精度值。以下代码展示了这一点:
scala> val stringEnumerator = Enumerator("one","two","four")
stringEnumerator: play.api.libs.iteratee.Enumerator[String] = play.api.libs.iteratee.Enumerator$$anon$19@1ca7d367
scala> val doubleEnumerator = Enumerator(1.03,2.34,4)
doubleEnumerator: play.api.libs.iteratee.Enumerator[Double] = play.api.libs.iteratee.Enumerator$$anon$19@a8e29a5
为了说明从文件创建枚举器的过程,让我们在当前项目的根目录中添加一个名为 samplefile.txt
的小文本文件,其中包含以下三行文本:
Alice
Bob
Charlie
您可以在保留原始控制台窗口中的 REPL 运行的同时,在单独的控制台窗口中创建此文件。否则,您将不得不重新运行导入语句。以下命令显示了从文件创建 Enumerator
的示例:
scala> import java.io.File
import java.io.File
scala> val fileEnumerator: Enumerator[Array[Byte]] = Enumerator.fromFile(new File("./samplefile.txt"))
fileEnumerator: play.api.libs.iteratee.Enumerator[Array[Byte]] = play.api.libs.iteratee.Enumerator$$anon$4@33500f2
Enumerator
还包含一些有用的方法。例如,每次包含当前时间的 Promise
对象超时(每 500 毫秒)时,都会生成一个事件流。
scala> val dateGenerator: Enumerator[String] = Enumerator.generateM(
play.api.libs.concurrent.Promise.timeout(
Some("current time %s".format((new java.util.Date()))),
500
))
以更通用的方式,我们可以这样说,Enumerator[E]
(读取类型 E 的枚举器)产生三种可能的类型 E 的数据块:
-
Input[E]
: 它是类型 E 的一块数据,例如,Input[LogData]
是一块LogData
-
Input.Empty
:这意味着枚举器为空,例如,一个流式传输空文件的Enumerator
-
Input.EOF
:这意味着枚举器已到达其末尾,例如,一个构建文件流并到达文件末尾的Enumerator
除了用于在 Iteratee
上运行 run
方法外,您还可以直接调用构造函数,即枚举器的 apply
方法。注意在以下两个命令中,根据您如何组合 enumerator
/iteratee
,您会得到不同的结果类型:
scala> val result = enumerator.run(iteratee)
result: scala.concurrent.Future[Int] = scala.concurrent.impl.Promise$DefaultPromise@1837220f
scala> val result2=enumerator(iteratee)
result2: scala.concurrent.Future[play.api.libs.iteratee.Iteratee[Int,Int]] = scala.concurrent.impl.Promise$DefaultPromise@5261b67f
这个最后的 Future
结果包含一个 Iteratee[Int,Int]
,即 Iteratee[数据块中包含的类型, 迭代的结果]
。
scala> val enumerator = Enumerator(1,4,7,8,10,20)
enumerator: play.api.libs.iteratee.Enumerator[Int] = play.api.libs.iteratee.Enumerator$$anon$19@7e666ce4
以下 Iteratee
消费来自 enumerator
流的所有块,并将它们作为 List
集合返回:
scala> val chunksIteratee = Iteratee.getChunks[Int]
chunksIteratee: play.api.libs.iteratee.Iteratee[Int,List[Int]] = play.api.libs.iteratee.ContIteratee@53af8d86
scala> val list = enumerator.run(chunksIteratee)
list: scala.concurrent.Future[List[Int]] = scala.concurrent.impl.Promise$DefaultPromise@66e1b41c
scala> list onComplete println
scala> Success(List(1, 4, 7, 8, 10, 20))
我们迄今为止看到的 Iteratee
示例几乎完全使用 fold
方法,就像 Scala 集合中的 foldLeft
和 foldRight
方法一样。让我们尝试构建一个更复杂的 Iteratee
:例如,从枚举器流中选择包含字母 E
的单词。这可以通过以下代码完成:
scala> def wordsWithE: Iteratee[String,List[String]] = {
def step(total:List[String])(input:Input[String]): Iteratee[String,List[String]] = input match {
case Input.EOF | Input.Empty => Done(total,Input.EOF)
case Input.El(elem) =>
if(elem.contains("E")) Cont[String,List[String]](i=> step(elem::total)(i))
else Cont[String,List[String]](i=> step(total)(i))
}
Cont[String,List[String]](i=> step(List[String]())(i))
}
wordsWithE: play.api.libs.iteratee.Iteratee[String,List[String]]
step
递归函数使用一个 total
累加变量,即在每个递归步骤中保持一些状态的上下文。这是一个包含所有我们感兴趣的结果的字符串列表。step
函数的第二个参数是每个步骤中从 enumerator
流中出现的新的数据块。这个数据块与可能的状态相匹配;如果流为空或者我们已经到达其末尾,我们以 Done
状态返回累积的结果。否则,我们处理传入的元素。如果元素验证 if
条件,那么我们将其添加到累加器中,并在递归的下一个步骤中作为 Cont
(继续)状态调用下一个步骤。否则,我们只是调用下一个步骤而不保存元素。
最后,最后一步通过在流的第一元素上调用 step
函数并使用空累加器来启动递归。将这个新定义的 Iteratee
应用到一个简单的 Enumerator
上看起来像以下命令:
scala> val output = Enumerator("ONE","TWO","THREE") run wordsWithE
output: scala.concurrent.Future[List[String]] = scala.concurrent.impl.Promise$DefaultPromise@50e0cc83
scala> output onComplete println
scala> Success(List(THREE, ONE))
对传入字符串进行的每个计算步骤要么将该字符串追加到总累加器中,要么忽略它,这取决于它是否匹配 if
条件。在这个例子中,它只是简单地检查单词中是否至少包含一个 E
。
将 Enumerator
与 Enumeratee
适配
可能会发生这样的情况,Iteratee
消费的数据与 Enumerator
产生的输入不匹配。Enumeratee
的作用是作为位于 Enumerator
和 Iteratee
之间的适配器,在将数据馈送到 Iteratee
之前对传入的数据进行转换。
作为从 Enumerator
到另一个 Enumerator
的简单转换的例子,我们可以定义一个将输入类型 String
转换为 Int
的 Enumeratee
,如下面的命令所示:
scala> val summingIteratee = Iteratee.fold(0){ (total, elem:Int) => total + elem }
summingIteratee: play.api.libs.iteratee.Iteratee[Int,Int] = play.api.libs.iteratee.ContIteratee@196fad1a
scala> Enumerator("2","5","7") through Enumeratee.map(x => x.toInt) run summingIteratee
res5: scala.concurrent.Future[Int] = scala.concurrent.impl.Promise$DefaultPromise@5ec418a8
scala> res5 onComplete println
scala> Success(14)
Enumeratee
提供的转换可以在其 map
方法中声明。
适配 Enumerator
还可能包括将输入数据转换成不同的格式,而不改变其类型。考虑到我们之前定义的 wordsWithE
,我们可以应用一个 Enumeratee
,将所有输入数据转换为大写,这样 Iteratee
消费数据流的结果就会与没有 Enumeratee
时获得的结果不同。以下代码说明了这种行为:
scala> val enumerator = Enumerator("ONE","Two","Three")
scala> enumerator run wordsWithE onComplete println
scala> Success(List(ONE))
scala> enumerator through Enumeratee.map(x=>x.toUpperCase) run wordsWithE onComplete println
scala> Success(List(THREE, ONE))
总结一下,Enumerator
是数据流的生成者,Iteratee
是该数据流的消费者,而 Enumeratee
是两者之间的适配器。Iteratee 模式已经与 Play 框架集成,作为在 Web 应用中反应式处理数据流的一种方式。在下一节中,我们将通过使用 WebSocket 在客户端和服务器之间双向通信,以这种方式构建 Web 应用。
在 Play 中实验 WebSocket 和 Iteratees
除了在查询服务时在浏览器中显示 HTML 的传统拉模型之外,大多数网络浏览器现在还支持通过 WebSocket 进行双向通信,这样服务器就可以在用户查询之前推送数据。一旦客户端和服务器之间建立了套接字,通信就可以保持开放,以便进行进一步交互,这与 HTTP 协议不同。现代 Web 应用越来越多地使用这个功能来从流中反应式地推送数据。
作为提醒,WebSocket 是一种在单个 TCP 连接上提供双向通信的协议,与 HTTP(无论是请求还是响应)的传统单向、无状态通信相反。让我们看看 Play 在这个领域提供的支持,并通过一个简短的示例演示如何建立 Play 服务器和客户端浏览器之间的 WebSocket 通信。
由于我们在本章开头已经创建了一个 ch9samples
Play 项目来在 REPL 中实验 Iteratees
,我们可以直接重用它。我们将首先打开默认可用的微小的 controllers/Application.scala
服务器端类。我们可以在其中添加一个新的 connect
方法来创建 WebSocket 交互。在一个常规的 Play 控制器中,一个方法通常会使用一个 Action
类,正如我们之前所看到的。在这个例子中,我们使用 WebSocket
类代替,如下所示:
package controllers
import play.api._
import play.api.mvc._
import play.api.libs.iteratee._
import scala.concurrent.ExecutionContext.Implicits.global
object Application extends Controller {
def index = Action {
Ok(views.html.index("Your new application is ready."))
}
def connect = WebSocket.using[String] { request =>
// Concurrent.broadcast returns (Enumerator, Concurrent.Channel)
val (out,channel) = Concurrent.broadcast[String]
// log message to stdout and send response back to client
val in = Iteratee.foreach[String] { msg =>
println(msg)
//the channel will push to the Enumerator
channel push("RESPONSE: " + msg)
}
(in,out)
}
}
在前面代码中看到的服务器端控制器中,in
变量包含处理来自客户端的消息的逻辑,并且它将产生一个 Enumerator
来组装一些将被推送到每个客户端的响应数据。
在客户端,views/main.scala.html
视图是我们将要添加 WebSocket 支持的地方,作为 JavaScript 脚本的一部分,其作用是打开一个 WebSocket 并对传入的消息做出反应。如下所示:
@(title: String)(content: Html)
<!DOCTYPE html>
<html>
<head>
<title>@title</title>
<link rel="stylesheet" media="screen" href="@routes.Assets.at("stylesheets/main.css")">
<link rel="shortcut icon" type="image/png" href="@routes.Assets.at("images/favicon.png")">
<script src="img/@routes.Assets.at("javascripts/jquery-1.9.0.min.js")" type="text/javascript"></script>
<script type="text/javascript">
function WebSocketTest() {
if ("WebSocket" in window) {
alert("WebSocket is supported by your Browser!");
// Let us open a web socket
var ws = new WebSocket("ws://localhost:9000/connect");
ws.onopen = function() {
// Web Socket is connected, send data
var msg = "Hello Websocket!"
ws.send(msg);
alert("Message is sent..."+msg);
};
ws.onmessage = function (evt) {
var received_msg = evt.data;
alert("Message is received..."+received_msg);
};
ws.onclose = function() {
// websocket is closed.
alert("Connection is closed...");
};
}
else {
// The browser doesn't support WebSocket
alert("WebSocket NOT supported by your Browser!");
}
}
</script>
</head>
<body>
<div id="sse">
<a href="javascript:WebSocketTest()">Run WebSocket</a>
</div>
</body>
</html>
现在我们已经拥有了两端,剩下的唯一步骤是为控制器的 connect
方法定义一个路由。编辑 conf/routes
文件,使其看起来如下所示:
# Routes
# This file defines all application routes (Higher priority routes first)
# ~~~~
# Home page
GET / controllers.Application.index
GET /connect controllers.Application.connect
# Map static resources from the /public folder to the /assets URL path
GET /assets/*file controllers.Assets.at(path="/public", file)
现在,我们已经准备好通过从命令提示符启动 Play 服务器来尝试演示:
> play run
在 http://localhost:9000/
(最好是支持 WebSocket 的浏览器)中打开浏览器并点击 运行 WebSocket 链接,首先应该确认浏览器确实支持 WebSocket。点击 确定 几次后,首先会显示已经发送了一条消息,然后显示通过从服务器接收消息实现了往返。你也应该在 Play 服务器提示符上看到 要发送的消息
日志消息。
从激活器模板中学习
基于迭代器的反应性应用程序列表正在不断增长,这些应用程序已经被打包并作为激活器模板部署。在撰写本书时,我们已经确定了超过五个模板,你可以查看其中的一些。它们通常将 WebSocket 与 Akka 用于通信和消息处理混合在一起,在客户端,使用 Angular.js 等 JavaScript 框架来提供简单的 HTML 渲染。
由于 Typesafe 激活器模板 HTML 页面允许你选择标签来根据某些关键词过滤项目,你可以通过选择反应性复选框来检查适当的项目。
反应性股票
这个示例是基于 Play 的 Java 版本的项目。它以图形方式演示了股票价值的实时更新(为了简单起见,这些股票价值是随机生成的)。它包含 Java 和 Scala 代码。为每个股票符号实例化一个 Akka StockActor
演员,其作用是维护一个所有关注该股票的用户列表。一些附加功能通过查询 Twitter API 来检索所有匹配特定符号的推文(例如,twitter-search-proxy.herokuapp.com/search/tweets?q=appl
)。然后,这些知识可以被处理以计算一个情感指数,这有助于决定是否购买这只股票。以下截图展示了应用程序运行后的图形界面:
响应式实时搜索
为了展示 ElasticSearch 和 Typesafe 堆栈的响应式特性之间的集成,通过 Play iteratees 和 Akka,这个示例展示了如何将日志事件推送到浏览器。提醒一下,ElasticSearch (www.elasticsearch.org
) 是一个基于 Apache Lucene (lucene.apache.org
) 全文搜索引擎的成熟技术,是一个分布式实时搜索和分析引擎。
它特别提供了一个过滤功能,当新内容与搜索条件匹配时,它会通知您的应用程序(而不是需要轮询搜索引擎以定期检查新更新)。
为了模拟内容,一个 Akka LogEntryProducerActor
演员负责在每次接收到一个 Tick
消息时生成随机的日志条目。这些消息由充当搜索协调器的 MainSearchActor
演员定期产生。最后,一个 ElasticSearchActor
演员通过与嵌入的 ElasticSearch 服务器(EmbeddedESServer
)交互来实现过滤功能,该服务器是从 Play 的 Global
类启动的。由于一旦知道了搜索条件,只需要单向通信,所以示例使用 服务器端事件(SSE)而不是通过 WebSocket 将信息推送到浏览器。
关于模板及其所有代码的更多信息可在 github.com/DrewEaster/realtime-search
找到。特别是,搜索时需要输入的查询语法被定义为 Lucene 语法,并在 lucene.apache.org/core/4_3_1/queryparser/org/apache/lucene/queryparser/classic/package-summary.html#package_description
中指定。
如果我们通过安装和运行激活器模板(从模板项目的根目录使用> activator run
命令)来执行此示例,我们可以在localhost:9000
打开浏览器,并将GET
作为搜索条件输入。几秒钟后,应该会逐渐显示一些浏览器输出,如下截图所示:
The Play-Akka-Angular-WebSocket template
作为将信息主动推送到浏览器的另一个示例,此示例通过调度一个演员来更新客户端上的时钟。这个演员的作用是通过使用 Play 的WebSocket.async[JsValue]
方法调用,通过 WebSocket 连接发送 JSON 格式的事件。在客户端使用 Angular.js JavaScript 框架,一旦开始运行,GUI 看起来如下截图所示:
展示反应式应用程序的激活器模板的数量正在增长。此外,您还可以不时查看新模板,而无需每次都升级激活器的版本。
Playing with Actor Room
在上一节中,我们看到了许多使用Enumerators
/Iteratees
来反应式地发送和接收消息的项目,它们具有不同的复杂度级别。Iteratees
功能强大,但有时使用它们可能会导致难以理解的代码片段。可在github.com/mandubian/play-actor-room
找到的 Play Actor Room 项目,通过抽象这部分内容,让程序员只关注领域逻辑(例如处理传入的消息和组装传出消息),提出减少设置Iteratees
的一些复杂性。该项目始于观察许多应用程序需要相同的功能,这可以看作是一个服务器Room
(例如,持有状态,并在分布式客户端之间充当中间人)。这个房间的角色是监听来自已连接客户端的传入消息,在处理这些消息后广播接收到的消息,或者仅向单个客户端进行单播通信。这是应用程序如何对用户/事件做出反应的一个很好的说明。因此,像多用户聊天这样的典型应用程序编写起来非常直接,它们是给出的两个示例之一。让我们实验一下 actor room 支持的最低级使用,一个名为simplest
的示例。
要在您的磁盘上的某个位置克隆项目,只需输入以下命令:
> git clone https://github.com/mandubian/play-actor-room
首先,我们可以查看应用程序运行后的样子:
> cd play-actor-room/samples/simplest
> play run
在默认的 Play 端口(http://localhost:9000/
)打开浏览器将显示一个简单的登录小部件,如下截图所示。输入您的名字进行登录,在提供的文本区域中输入消息,然后按Enter键。
在你启动 actor room 应用程序的控制台窗口中,你现在应该能看到接收来自客户端浏览器消息的 actor 打印的日志信息。信息如下所示:
[info] play - Application started (Dev)
[info] play - Starting application default Akka system.
[debug] application - Connected Member with ID:Thomas
[info] application - received Play Actor Room rocks
在打开几个浏览器窗口并使用不同的名字登录后,你可以在控制台中看到所有击中服务器房间的消息。实际上,actor room 会将接收到的消息广播回所有连接的浏览器,尽管目前视图中还没有处理这些消息的功能。
然而,你可以打开一个浏览器的控制台来查看广播消息的显示,如下面的截图所示:
此外,从第三个窗口调用http://localhost:9000/list/
URL 将返回当前连接的客户端列表。
一旦我们将项目导入到 Eclipse 中(输入> play eclipse
命令)并打开包含接收Actor
类实现的控制器,就可以观察到这个基本应用的一些有趣特性。
作为服务器角色的Receiver
角色是由一个监督Actor
创建的。它处理 JSON 格式的消息。接收Actor
的所有默认逻辑,也就是我们处理客户端消息时需要关注的唯一代码,如下所示:
class Receiver extends Actor {
def receive = {
case Received(from, js: JsValue) =>
(js \ "msg").asOpt[String] match {
case None => play.Logger.error("couldn't msg in websocket event")
case Some(s) =>
play.Logger.info(s"received $s")
context.parent ! Broadcast(from, Json.obj("msg" -> s))
}
}
}
注意,将服务器响应广播到所有客户端是由context.parent
引用的监督角色完成的。在之前的逻辑中,Broadcast
消息还包括发起者from
ActorRef
引用。
作为对默认房间行为的小修改以适应新的业务需求,例如,我们可以重用我们在第八章,现代应用程序的基本属性 – 异步和并发中创建的TravelAgent
、Flight
和Hotel
角色。我们希望为每个用户提供预订航班的能力,并且(在任何时候)监控还有多少座位可用。为此,我们可以使用一个稍微大一点的 JSON 消息作为服务器和客户端之间的交换格式。
Scala 2.10 版本附带的一个有用的增强是字符串插值的理念。我们已经在整本书中使用了这个特性,并在第一章,在项目中交互式编程中介绍了它。同样,JSON 插值作为 Play 对 JSON 支持的扩展也被创建。我们可以重用 JSON 插值,例如,进行一些优雅的模式匹配。只需将以下扩展依赖项添加到Build.scala
文件中:
val appDependencies = Seq(
"org.mandubian" %% "play-actor-room" % "0.1",
"play-json-zipper" %% "play-json-zipper" % "1.0",
"com.typesafe.play" %% "play-json" % "2.2.0"
)
一旦部署,JSON 模式匹配功能将处理来自浏览器客户端到Receiver
角色的 JSON 消息,如下所示
case Received(from, js: JsValue) =>
js match {
case json"""{
"booking":"flight",
"numberOfPersons":$v1
}""" => play.Logger.info(s"received $v1")
…
让我们添加一个Flight
actor 来跟踪可用座位数。在位于app/
源目录下的新包actors
中,我们可以添加一个类似于以下内容的Flight.scala
类:
package actors
import akka.actor.Actor
import akka.event.LoggingReceive
object Flight {
case class BookSeat(number:Int) {
require(number > 0)
}
case object GetSeatsLeft
case object Done
case object Failed
}
class Flight extends Actor {
import Flight._
def book(seats:Int):Receive = LoggingReceive {
case BookSeat(nb) if nb <= seats =>
context.become(book(seats-nb))
sender ! Done
case GetSeatsLeft => sender ! seats
case _ => sender ! Failed
}
def receive = book(50) // Initial number of available seats
}
与我们在第八章中创建可变状态变量var seatsLeft
的做法不同,现代应用程序的基本属性 – 异步和并发,我们将这种状态变化封装为每次接收BookSeat
消息时切换上下文时传递的参数。这种方式是避免持有可变变量的推荐最佳实践。我们添加了一个GetSeatsLeft
消息,以便能够查询当前状态值,在这种情况下,状态会发送回sender
actor。
在客户端,我们可以修改index.scala.html
视图来向我们的应用程序添加几个简单的小部件。特别是,我们可以添加一个占位符来显示航班剩余的座位数。这是服务器室 actor 将推送到所有连接浏览器的信息。以下是一个此类视图的示例:
@(connected: Option[String] = None)
@main(connected) {
@connected.map { id =>
<p class="pull-right">
Logged in as @id
<a href="@routes.Application.index()">Disconnect</a>
</p>
<div>Places left in flight: <input size="10" id="placesLeft"></input></div>
<div>
<select id ="booking">
<option value="flight">Flight</option>
<option value="hotel">Hotel</option>
</select>
Number of persons to book:
<textarea id ="numberOfPersons" ></textarea>
</div>
<script type="text/javascript" charset="utf-8" src="img/@routes.Application.websocketJs(id)"></script>
}.getOrElse {
<form action="@routes.Application.connect(None)" class="pull-right">
<input id="username" name="id" class="input-small" type="text" placeholder="Username">
<button class="btn" type="submit">Sign in</button>
</form>
}
}
我们还需要稍微修改处理客户端浏览器和服务器之间通过 WebSocket 通信的小型 JavaScript 片段,以便它能够处理新的 JSON 格式。修改后的websocket.scala.js
文件如下所示:
@(id: String)(implicit r: RequestHeader)
$(function() {
var WS = window['MozWebSocket'] ? MozWebSocket : WebSocket;
var wsSocket = new WS("@routes.Application.websocket(id).webSocketURL()");
var sendMessage = function() {
wsSocket.send(JSON.stringify(
{
"booking":$("#booking").val(),
"numberOfPersons":$("#numberOfPersons").val()
}
))
$("#numberOfPersons").val('');
}
var receiveEvent = function(event) {
console.log(event);
var data = JSON.parse(event.data);
// Handle errors
if(data.error) {
console.log("WS Error ", data.error);
wsSocket.close();
// TODO manage error
return;
} else {
console.log("WS received ", data);
// TODO manage display
$("#placesLeft").val(data.placesLeft);
}
}
var handleReturnKey = function(e) {
if(e.charCode == 13 || e.keyCode == 13) {
e.preventDefault();
sendMessage();
}
}
$("#numberOfPersons").keypress(handleReturnKey);
wsSocket.onmessage = receiveEvent;
})
最后,在服务器部分的Application.scala
文件中,我们可以扩展Receiver
actor 以处理传入的 JSON 消息,并联系Flight
actor 来更新和读取其当前状态值,如下所示:
[…imports from the original actor room sample…]
import play.api.libs.json._
import play.api.libs.functional.syntax._
import play.api.libs.json.extensions._
import actors._
object Receiver {
val flightBookingActor = Akka.system.actorOf(Props[Flight],"flight")
}
class Receiver extends Actor {
import Receiver.flightBookingActor
def receive = LoggingReceive {
case x:Int =>
play.Logger.info(s"Received number of seats left: $x")
val placesLeft:String = if (x<0) "Fully Booked" else x.toString
context.parent ! Broadcast("flight", Json.obj("placesLeft" -> placesLeft))
case Received(from, js: JsValue) =>
js match {
case json"""{
"booking":"flight",
"numberOfPersons":$v1
}""" =>
play.Logger.info(s"received $v1")
val nbOfPersons = v1.as[String]
flightBookingActor ! Flight.BookSeat(nbOfPersons.toInt)
val placesCount = flightBookingActor ! Flight.GetSeatsLeft
case _ => play.Logger.info(s"no match found")
}
}
}
现在我们已经准备好了所有组件,让我们在几个浏览器中运行示例。请注意,我们已经向Receiver
和Flight
actor 添加了LoggingReceive
调用,以便在执行服务器代码时获得详细的日志输出。在命令提示符中,您可以输入以下命令以启动带有激活日志输出的 Play 应用程序:
> play
> run -Dakka.loglevel=DEBUG -Dakka.actor.debug.receive=true
在 URL http://localhost/9000
上打开两个浏览器窗口(可能使用两个不同的浏览器)。完成登录步骤;例如,使用Alice和Bob作为名称,分别从两个浏览器连接到 actor 室。
从任一窗口输入您想要预订的座位数将更新两个窗口中全局剩余座位数,如下面的截图所示:
服务器的控制台输出应显示如下日志信息:
[info] play - Starting application default Akka system.
[debug] application - Connected Member with ID:Alice
[debug] application - Connected Member with ID:Bob
…
Received(Bob,{"booking":"flight","numberOfPersons":"5"})
…
Received(Alice,{"booking":"flight","numberOfPersons":"3"})
…
[info] application - Received number of seats left: 42
[DEBUG] [02/15/2014 22:51:01.226] [application-akka.actor.default-dispatcher-7] [akka://application/user/flight] received handled message GetSeatsLeft
[DEBUG] [02/15/2014 22:51:01.226] [application-akka.actor.default-dispatcher-6] [akka://application/user/$a/Alice-receiver] received handled message 42
输入的座位数大于剩余座位数时,计数器不会更新,最终会收到来自Flight
actor 的Fail
消息。
摘要
在本章中,我们尝试了 Play 框架支持的迭代器模式来处理流式数据。然后,我们结合 WebSockets 和 Akka 编写了一个小型、反应式的 Web 应用程序。
本章中我们讨论和处理的反应式 Web 应用程序的示例只是构建对事件做出反应且对失败和负载具有弹性的应用程序无限可能性的一个缩影。随着 Web 应用程序的复杂性增加,这种架构应该会越来越受欢迎和被采用。
在我们看来,能够以异步方式实时处理流是一个巨大的竞争优势,如果这个功能是以可管理和可维护的方式构建的。这正是 Play 框架与 Akka 结合所展示的目标。
在本书的下一章和最后一章中,我们将考虑一些我们认为 Scala 可以提供额外、方便帮助的领域。
第十章. Scala 小技巧
在前面的章节中,我们已经介绍了一些技术和工具包,当它们结合在一起时,为在 Scala 中构建现代和可扩展的响应式 Web 应用提供了极大的机会。Scala 现在庆祝了 10 年的存在,拥有活跃的社区和大型企业的支持,这导致了不断探索触及语言和生态系统的创新思想。
在最后一章中,我们提出探讨一些我们发现了一些令人兴奋的正在进行中的项目或技术领域,以及我们认为 Scala 可以提供优雅的解决方案,既高效又有趣。我们将涵盖以下方面:
-
通过 MongoDB 进行 NoSQL 数据库访问
-
介绍领域特定语言(DSLs)以及特别是一瞥解析组合器
-
Scala.js—在客户端将 Scala 编译成 JavaScript
探索 MongoDB
随着过去几年需要处理和存储的信息量急剧增加,许多 IT 企业一直在寻找替代传统关系型数据库来存储和查询数据的方法。不仅 SQL(NoSQL)数据库运动作为一种以更高效或更灵活的数据存储为代价来交换数据一致性和结构的方式而受到欢迎。MongoDB(www.mongodb.org)是一种数据库产品,旨在以 JSON 等格式存储文档,并且没有严格的数据库模式。除了构建用于访问和查询 MongoDB 数据库的 Java 驱动程序外,我们还将发现如何使用 Casbah Scala 工具包(github.com/mongodb/casbah
)通过 DSL 方便地访问和查询此类数据库。
进入 Casbah
要开始使用 Casbah 进行实验,唯一的要求是将它的.jar
库依赖项添加到 SBT 或 Play 项目中。在你的磁盘上的一个新目录中,从终端窗口输入> play new ch10samples
,并将 Casbah 依赖项添加到项目根目录下的build.sbt
文件中。此依赖项通过添加以下代码添加(注意,在编写此章节时,所提供的 Casbah 版本是当时可用的最新版本,但很快将可用为最终版本 2.7.0):
name := "ch10samples"
version := "1.0-SNAPSHOT"
libraryDependencies ++= Seq(
jdbc,
anorm,
cache,
"org.mongodb" %% "casbah" % "2.7.0-RC1"
)
play.Project.playScalaSettings
如果你正在使用 SBT 项目而不是 Play,你也可以添加默认的 SLF4J 日志实现,如下面的代码片段所示,否则默认使用的是无操作实现:
libraryDependencies += "org.slf4j" % "slf4j-simple" % "1.7.6"
如同往常,启动 REPL 可以通过输入> play
命令后跟> console
命令或直接> play console
来完成:
scala> import com.mongodb.casbah.Imports._
import com.mongodb.casbah.Imports._
在进行一些导入之后,我们使用以下代码连接到端口27017
上的 MongoDB 数据库:
scala> val mongoClient = MongoClient("localhost", 27017)
mongoClient: com.mongodb.casbah.MongoClient = com.mongodb.casbah.MongoClient@6fd10428
scala> val db = mongoClient("test")
db: com.mongodb.casbah.MongoDB = test
scala> val coll = db("test")
coll: com.mongodb.casbah.MongoCollection = test
到目前为止,这些语句都是在没有直接接触数据库的情况下执行的。从现在开始,在检索任何内容之前,我们需要确保有一个运行的 MongoDB 进程。如果尚未运行,请启动mongod
守护进程(下载说明可在www.mongodb.org/downloads
找到),然后输入以下命令以获取存储的集合名称:
scala> db.collectionNames
res0: scala.collection.mutable.Set[String] = Set()
我们显然得到了一个空集作为结果,因为我们还没有存储任何文档。让我们创建几个条目:
scala> val sales = MongoDBObject("title" -> "sales","amount"->50)
sales: com.mongodb.casbah.commons.Imports.DBObject = { "title" : "sales" , "amount" : 50}
scala> val sweden = MongoDBObject("country" -> "Sweden")
sweden: com.mongodb.casbah.commons.Imports.DBObject = { "country" : "Sweden"}
创建的项目尚未添加到数据库中,使用以下命令中的insert
命令:
scala> coll.insert(sales)
res1: com.mongodb.casbah.TypeImports.WriteResult = { "serverUsed" : "localhost:27017" , "n" : 0 , "connectionId" : 7 , "err" : null , "ok" : 1.0}
scala> coll.insert(sweden)
res2: com.mongodb.casbah.TypeImports.WriteResult = { "serverUsed" : "localhost:27017" , "n" : 0 , "connectionId" : 7 , "err" : null , "ok" : 1.0}
scala> coll.count()
res3: Int = 2
使用find
方法检索coll
集合的元素:
scala> val documents = coll.find() foreach println
{ "_id" : { "$oid" : "530fd91d03645ab9c17d9012"} , "title" : "sales" , "amount" : 50}
{ "_id" : { "$oid" : "530fd92703645ab9c17d9013"} , "country" : "Sweden"}
documents: Unit = ()
注意,由于我们在将文档插入集合时没有提供任何主键,每个文档都创建了一个主键。如果你确切知道你要找的对象,并提供给它作为参数,你也可以检索单个文档。为此,可以使用findOne
方法,如下面的命令行所示:
scala> val searchedCountry = MongoDBObject("country" -> "Sweden")
searchedCountry: com.mongodb.casbah.commons.Imports.DBObject = { "country" : "Sweden"}
scala> val result = coll.findOne(searchedCountry)
result: Option[coll.T] = Some({ "_id" : { "$oid" : "530fd92703645ab9c17d9013"} , "country" : "Sweden"})
由于可能并不总是存在匹配的元素,findOne
方法返回Option
,在前一个例子中结果是Some(value)
,与以下空结果形成对比:
scala> val emptyResult = coll.findOne(MongoDBObject("country" -> "France"))
emptyResult: Option[coll.T] = None
删除元素是通过remove
方法完成的,它可以像findOne
方法一样使用:
scala> val result = coll.remove(searchedCountry)
result: com.mongodb.casbah.TypeImports.WriteResult = { "serverUsed" : "localhost:27017" , "n" : 1 , "connectionId" : 9 , "err" : null , "ok" : 1.0}
scala> val countryNoMore = coll.findOne(searchedCountry)
countryNoMore: Option[coll.T] = None
最后,更新文档可以按照以下方式完成:
scala> sales
res3: com.mongodb.casbah.commons.Imports.DBObject = { "title" : "sales" , "amount" : 50}
scala> val newSales = MongoDBObject("title" -> "sales","amount"->100)
newSales: com.mongodb.casbah.commons.Imports.DBObject = { "title" : "sales" , "amount" : 100
scala> val result = coll.update(sales,newSales)
result: com.mongodb.casbah.TypeImports.WriteResult = { "serverUsed" : "localhost:27017" , "updatedExisting" : true , "n" : 1 , "connectionId" : 9 , "err" : null , "ok" : 1.0}
scala> coll.find foreach println
{ "_id" : { "$oid" : "530fd91d03645ab9c17d9012"} , "title" : "sales" , "amount" : 100}
我们可以看到,主键"530fd91d03645ab9c17d9012"仍然是我们在最初将sales
文档插入数据库时拥有的,这表明update
操作不是删除然后插入一个全新的元素。
同时更新多个文档也是支持的,我们参考mongodb.github.io/casbah/guide/index.html
中的文档以获取更多操作信息。
应用 MapReduce 转换
在 MongoDB 等文档型数据库的众多优秀特性中,还有运行 MapReduce 函数的可能性。MapReduce是一种方法,其中将查询或任务分解成更小的作业块,然后汇总这些块的结果。为了说明基于文档的方法有时与传统的关系型数据库相比可能更有用,让我们以一个小型的财务合并为例。在这个领域,为一个大公司全球范围内汇总和计算销售额可能需要处理多个正交维度。例如,销售额可以从每个子公司收集,每个子公司都有自己的地理位置、时间间隔、货币和特定类别,每个维度都遵循一些如图所示的树状结构:
地理位置可能是使用货币的决定性因素,并且应该进行转换以确保数值的一致性。在这方面,用于生成全球报告的货币通常遵循公司所有权树的根。以下是一个公司树结构的示例:
类似地,各种销售类别可能定义另一个层次结构,如下一个图所示:
报告的销售数据可能非常详细或已经累积,因此在不同层次的结构中报告。由于大型公司通常由各种所有权的较小群体组成,这些群体的所有权程度也在不断变化,因此整合工作需要根据所有这些参数聚合和计算数据。
对于许多以关系数据库表示的数据仓库解决方案,领域模型的核心可以是一个包含事实的大表,这些事实指的是前一个图中表示的各个维度。例如,本例的一些示例输入数据可以由以下销售数据(即金额)列表组成,作为 XML 行:
以下结构展示了如何在 JSON 中表示树结构:
{
"title" : "root",
"children" : [
{
"title" : "node 1",
"children" : [
{
"title" : "node 1.1",
"children" : [
...
]
},
{
"title" : "node 1.2",
"children" : [
...
]
}
]
},
{
"title" : "node 2",
"children" : [
...
]
}
]
}
以下是一个包含按地理位置划分的销售数据的 JSON 文档示例:
{
"title" : "sales",
"regions" : [
{
"title" : "nordic",
"regions" : [
{
"title" : "norway",
"amount" : 150
},
{
"title" : "sweden",
"amount" : 200
}
]
},
{
"title" : "france",
"amount" : 400
}
]
}
通过存储来自大型公司各个子公司的文档,例如 JSON,我们可以通过数据库已经支持的 MapReduce 转换来整合数据。此外,Casbah 利用 MongoDB 的聚合框架(mongodb.github.io/casbah/guide/aggregation.html
),以便能够在不使用 MapReduce 的情况下聚合值。
以 MongoDB 为例,我们只需提及 ReactiveMongo 项目(www.reactivemongo.org),该项目是一个 MongoDB 的响应式异步非阻塞驱动程序。由于它使用了我们在第九章中介绍的 Iteratee 模式,即《构建响应式 Web 应用》,结合一个流友好框架,如 Play,可以产生许多有趣且可扩展的演示,如它们网站上所列。
探索大数据的表面
在最近的数据和服务分析成就和趋势中,大数据运动占据了一席之地。特别是,Hadoop 框架建立了一种某种临时的标准“用于在计算机集群中使用简单的编程模型跨集群分布式处理大型数据集”。除了用于高吞吐量访问数据的优化分布式文件系统 HDFS 之外,Hadoop 还提供了 MapReduce 功能,用于并行处理大型数据集。由于设置和运行 Hadoop 并不总是被认为是一项简单任务,因此已经开发了一些其他框架,作为在 Hadoop 之上简化 Hadoop 作业定义的手段。在 Java 中,Cascading 框架是在 Hadoop 之上的一层,它提供了一个方便的 API,以促进 MapReduce 作业的创建。在 Scala 中,Scalding 框架已经开发出来,通过利用简洁和表达性强的 Scala 语法来进一步增强 Cascading API,正如我们可以通过查看activator-scalding
Typesafe activator 模板所观察到的。该模板提供的示例代码演示了一个单词计数应用程序,即 Hadoop MapReduce 作业的hello-world
项目。
作为 MapReduce 工作的提醒,可以考虑阅读 Google 的原始论文,可在static.googleusercontent.com/media/research.google.com/en//archive/mapreduce-osdi04.pdf
找到。
我们可以用以下两个步骤来表示计数单词的工作:
-
将文件中的行拆分为单个单词,并为每个单词创建一个键值对,其中键是
String
类型的单词,值是常数1
-
通过将具有相同键(即相同的单词)的元素分组到列表中,并通过求和值来减少列表,我们达到了我们的目标
如果你在一个终端窗口中运行> activator ui
命令,就像我们在第三章中已经多次做的那样,创建activator-scalding
模板项目,你可以验证 scalding 中单词计数指定的简洁性。不要忘记运行> activator eclipse
命令以便将项目导入到 Eclipse IDE 中:
class WordCount(args : Args) extends Job(args) {
// Tokenize into words by by splitting on non-characters. This
// is imperfect, as it splits hyphenated words, possessives
// ("John's"), etc.
val tokenizerRegex = """\W+"""
// Read the file specified by the --input argument and process
// each line by trimming the leading and trailing whitespace,
// converting to lower case,then tokenizing into words.
// The first argument list to flatMap specifies that we pass the
// 'line field to the anonymous function on each call and each
// word in the returned collection of words is given the name
// 'word. Note that TextLine automatically associates the name
// 'line with each line of text. It also tracks the line number
// and names that field 'offset. Here, we're dropping the
// offset.
TextLine(args("input"))
.read
.flatMap('line -> 'word) {
line : String => line.trim.toLowerCase.split(tokenizerRegex)
}
// At this point we have a stream of words in the pipeline. To
// count occurrences of the same word, we need to group the
// words together. The groupBy operation does this. The first
// argument list to groupBy specifies the fields to group over
// as the key. In this case, we only use the 'word field.
// The anonymous function is passed an object of type
// com.twitter.scalding.GroupBuilder. All we need to do is
// compute the size of the group and we give it an optional
// name, 'count.
.groupBy('word){ group => group.size('count) }
// In many data flows, we would need to project out just the
// 'word and 'count, since we don't care about them any more,
// but groupBy has already eliminated everything but 'word and
// 'count. Hence, we'll just write them out as tab-delimited
// values.
.write(Tsv(args("output")))
}
大部分代码确实是注释,这意味着整个算法非常接近人们会用伪代码做的描述。
如果你对大数据感兴趣,Scala 无疑填补了一个空白,并且已经有一些项目和框架正在处理大量数据流和类似 Hadoop 的工作,正在推动极限。其中,我们可以提到 Spark (spark.apache.org
) 以及 Twitter 的开源项目 SummingBird (github.com/twitter/summingbird
) 和 Algebird (github.com/twitter/algebird
)。
在 Scala 中介绍 DSLs
领域特定语言(DSL)通常通过应用于一个小而特定的领域来简化与系统的交互。它们可以通过提供简化的 API 与系统通信来针对程序员;或者它们可能涉及所谓的“业务用户”,这些用户可能对领域有足够的了解以创建一些脚本,但他们不是程序员,可能难以处理通用编程语言。一般来说,存在两种类型的领域特定语言:
-
内部领域特定语言
-
外部领域特定语言(DSL)
观察内部领域特定语言(DSL)
内部领域特定语言使用宿主语言(例如,Scala),通过添加一些语法糖、技巧和语言的特殊结构来获得简化的使用方式。Debasish Ghosh 所著的《DSLs in Action》一书通过使用语言的特征(如中缀表示法和隐式转换)说明了 Scala 内部领域特定语言的构建。
他给出了以下领域特定语言使用示例,表示用清晰英语表达的可执行程序:200 discount bonds IBM for_client NOMURA on NYSE at 72.ccy(USD)
。幕后发生了许多转换,但业务用户得到了一个非常干净的语法。
这种领域特定语言的优势在于,由于宿主语言是通用目的的(例如 Scala),你可以确信你可以用它们表达任何事物。这意味着有时你可能被迫使用不太干净的语法,但你清楚你手头有 Scala 编译器的全部功能。因此,你将始终成功产生一个实现你想要逻辑的领域特定语言脚本或程序。
然而,编译器的全部功能也可能是在许多情况下你想要避免的,在这些情况下,你希望给你的业务用户提供仅执行少数特定操作的可能性。为此,你可以实现外部领域特定语言。这包括许多额外的关注点,包括受限的语法(例如,在某些情况下你无法避免括号)和复杂的错误信息。
通过解析组合器处理外部领域特定语言
外部领域特定语言代表了一种语法完全由你决定的领域语言。这意味着你可以按照自己的意愿精确地表达事物,并且可以限制你的业务用户只能使用特定的单词或含义。这种灵活性伴随着需要付出更多的工作来实现它,因为你需要定义一个语法(通常是巴科斯-诺尔范式(BNF)),即定义所有成功解析意义或脚本的规则。在 Java 中,编写外部领域特定语言的任务可能很繁琐,通常涉及到 ANTLR 外部框架。
在 Scala 中,解析组合器是一个与 BNF 语法定义非常接近的概念,并且在编写外部领域特定语言时可以提供非常简洁和优雅的代码。
一旦你熟悉了处理语法定义的一些特定运算符,你就会发现,如果你的语言不是太复杂,编写外部领域特定语言(DSL)相当直接。关于涉及解析器组合器的所有符号和运算符的详细信息,可以在bitwalker.github.io/blog/2013/08/10/learn-by-example-scala-parser-combinators/
找到。
以下实验性代码演示了在财务合并领域的一个小型领域特定语言(DSL),其中特定的货币账户作为预定义公式的部分被操作。以下代码片段末尾给出的主方法反映了一个公式;例如,你可能将公式(3*#ACCOUNT1#
)解析为一个面向对象的结构,该结构能够计算给定账户内容乘以三的结果:
package parsercombinator
import scala.util.parsing.combinator._
import java.text.SimpleDateFormat
import java.util.Date
object FormulaCalculator {
abstract class Node
case class Transaction(amount: Int)
case class Account(name:String) extends Node {
var transactions: Iterable[Transaction] = List.empty
}
def addNewTransaction(startingBalance: Int, t: Transaction) = startingBalance + t.amount
def balance(account: Account) = account.transactions.foldLeft(0)(addNewTransaction)
case class NumberOfPeriods (value: Int) extends Node {
override def toString = value.toString
}
case class RelativePeriod (value:String) extends Node {
override def toString = value
}
case class Variable(name : String) extends Node
case class Number(value : Double) extends Node
case class UnaryOp(operator : String, arg : Node) extends Node
case class BinaryOp(operator : String, left : Node, right : Node) extends Node
case class Function(name:String,arguments:List[Node]) extends Node {
override def toString =
name+arguments.mkString("(",",",")")
}…
从公式解析中产生的对象被定义为案例类。因此,继续编写代码:
…
def evaluate(e : Node) : Double = {
e match {
case Number(x) => x
case UnaryOp("-", x) => -(evaluate(x))
case BinaryOp("+", x1, x2) => (evaluate(x1) + evaluate(x2))
case BinaryOp("-", x1, x2) => (evaluate(x1) - evaluate(x2))
case BinaryOp("*", x1, x2) => (evaluate(x1) * evaluate(x2))
case BinaryOp("/", x1, x2) => (evaluate(x1) / evaluate(x2))
}
}
object FormulaParser extends JavaTokenParsers {
val identifier: Parser[String] = ident
val relative_period: Parser[RelativePeriod] = """([N|P|\+|\-][0-9]+|CURRENT)""".r ^^ RelativePeriod
val number_of_periods: Parser[NumberOfPeriods] = """\d+""".r ^^ (i => NumberOfPeriods(i.toInt))
val account_name: Parser[String] = """[A-Za-z0-9_]+""".r
def account: Parser[Account] = "#" ~> account_name <~ "#" ^^ { Account(_) }
def function: Parser[Function] =
identifier~"("~account~","~relative_period~","~number_of_periods~")" ^^ {
case f~"("~acc~","~rp~","~nbp~")" => Function(f,List(acc,rp,nbp))
} |
identifier~"("~account~","~relative_period~")" ^^ {
case f~"("~acc~","~rp~")" => Function(f,List(acc,rp))
}
def node: Parser[Node] =
(term ~ "+" ~ term) ^^ { case lhs~plus~rhs => BinaryOp("+", lhs, rhs) } |
(term ~ "-" ~ term) ^^ { case lhs~minus~rhs => BinaryOp("-", lhs, rhs) } |
term
def term: Parser[Node] =
(factor ~ "*" ~ factor) ^^ { case lhs~times~rhs => BinaryOp("*", lhs, rhs) } |
(factor ~ "/" ~ factor) ^^ { case lhs~div~rhs => BinaryOp("/", lhs, rhs) } |
(factor ~ "^" ~ factor) ^^ { case lhs~exp~rhs => BinaryOp("^", lhs, rhs) } |
factor
def factor : Parser[Node] =
"(" ~> node <~ ")" |
floatingPointNumber ^^ {x => Number(x.toFloat) } |
account |
function
def parse(text : String) =
parseAll(node, text)
}
// Parses 3 formula that make computations on accounts
def main(args: Array[String]) {
val formulaList = List("3*#ACCOUNT1#","#ACCOUNT1#- #ACCOUNT2#","AVERAGE_UNDER_PERIOD(#ACCOUNT4#,+1,12)")
formulaList.foreach { formula =>
val unspacedFormula = formula.replaceAll("[ ]+","")
println(s"Parsing of $formula gives result:\n ${FormulaParser.parse(unspacedFormula)}")
}
}
}
如果我们通过在FormulaCalculator
类上右键单击并将鼠标移动到运行方式 | Scala 应用程序来执行这个解析器组合器代码,我们应该在 Eclipse 控制台中获得以下输出:
Parsing of 3*#ACCOUNT1# gives result:
[1.13] parsed: BinaryOp(*,Number(3.0),Account(ACCOUNT1))
Parsing of #ACCOUNT1#- #ACCOUNT2# gives result:
[1.22] parsed: BinaryOp(-,Account(ACCOUNT1),Account(ACCOUNT2))
Parsing of AVERAGE_UNDER_PERIOD(#ACCOUNT4#,+1,12) gives result:
[1.39] parsed: AVERAGE_UNDER_PERIOD(Account(ACCOUNT4),+1,12)
此输出表明,三个公式已正确解析并转换为类。最终的评估在这个练习中被省略,但可以通过在两个账户上定义一些实际交易来设置。
介绍 Scala.js
由于 Java 拥有强大的 JVM,可以在任何地方运行服务器端代码,因此 Java 成为了一个引人注目的选择。而 JavaScript 由于其灵活性、轻量级的运行时嵌入环境以及浏览器中已经可用的工具集日益增长,正逐渐成为客户端的主导选择。尽管 JavaScript 非常流行,但作为一个动态语言,它并不提供像 Java 或 Scala 这样的语言所提供的类型安全。实验性的但快速增长的 Scala.js 项目旨在将 Scala 编译成 JavaScript,在我看来,对于那些想要从 Scala 类型系统的强大功能中受益的人来说,这是一个非常好的替代方案。
设置一个展示 Scala.js 使用的项目只需几分钟,并在可用的示例“入门”项目中进行了说明,该项目的网址为github.com/sjrd/scala-js-example-app
。
示例包括一个包含一个playground <div>
元素的简单 HTML 页面,如下面的 HTML 代码所示:
<!DOCTYPE html>
<html>
<head>
<title>Example Scala.js application</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
</head>
<body>
<h1>Example Scala.js application - development version</h1>
<p>After having compiled and packaged properly the code for the application
(using `sbt packageJS`), you should see "It works" here below.
See README.md for detailed explanations.</p>
<div id="playground">
</div>
<script type="text/javascript" src="img/example-extdeps.js"></script>
<script type="text/javascript" src="img/example-intdeps.js"></script>
<script type="text/javascript" src="img/example.js"></script>
</body>
</html>
div
元素将被动态填充为:
<p>
<strong>It works!</strong>
</p>
实现这一点的 Scala 代码片段(编译成 JavaScript)在下面的main
方法中给出:
package example
import scala.scalajs.js
import js.Dynamic.{ global => g }
object ScalaJSExample {
def main(): Unit = {
val paragraph = g.document.createElement("p")
paragraph.innerHTML = "<strong>It works!</strong>"
g.document.getElementById("playground").appendChild(paragraph)
}
/** Computes the square of an integer.
* This demonstrates unit testing.
*/
def square(x: Int): Int = x*x
}
一旦我们通过js.Dynamic.global
对象获得了 HTML 页面的 DOM 访问权限,这个简单的 Scala main
方法就构建了一个新的段落节点并将其添加到现有的"playground"
节点中。
额外的 square
方法被用来展示针对 Jasmine JavaScript 测试框架编写的单元测试。
main
方法的执行是由添加到 js/startup.js
文件中的一行触发的:
ScalaJS.modules.example_ScalaJSExample().main();
Scala.js 默认生成的代码可能相当大,因为依赖于 Scala 库。Scala.js 通过 Google 的 closure compiler 提供了一种优化方法,可以减小大小并优化针对生产环境的目标 JavaScript 代码执行。
现在下一步是什么?嗯,我们可以将感兴趣的读者推荐给一些我们认为与这本书相关的更多有趣项目:
-
一个名为
play-with-scalajs-example
的项目,可在github.com/vmunier/play-with-scalajs-example
找到,它处理了 Scala.js 和 Play 框架的简单集成示例,我们在前面的章节中已经介绍过。 -
Scala.js 的一个非常有趣且更高级的使用是
TodoMVC
,这是workbench-example-app
项目的一部分,可在github.com/lihaoyi/workbench-example-app/
找到。它演示了一个用于创建待办事项列表的示例网络应用,这是一个指定用于比较在不同语言中完成的不同实现的参考应用,但它采用了创新的方法,即除了用 Scala 编译成 JavaScript 外,还具有响应式特性。到结果响应式网络应用的直接链接可在lihaoyi.github.io/workbench-example-app/todo.html
找到,并在浏览器中渲染,如下面的截图所示:
在 Scala.js 的主页 www.scala-js.org/
上已经列出了许多关于 Scala.js 的项目。由于 Scala.js 正在迅速成熟,应该很快会有更多项目可用。
最后的提示
下面的章节列出了几个你可能在使用 REPL 时觉得有用的最终提示和技巧。
在 REPL 中复制和粘贴
作为提醒,这个特性是在 第八章 中引入的,现代应用程序的基本属性 – 异步和并发,它使得在 REPL 中一次性执行整个代码片段变得容易。例如,以下命令行说明了 REPL 中的复制粘贴功能如何帮助轻松执行代码:
scala> :paste
// Entering paste mode (ctrl-D to finish)
[Copy a code block from somewhere with ctrl-C/ctrl-V]
case class Person(name:String)
val me = Person("Thomas")
val you = Person("Current Reader")
val we = List(you,me).map(_.name).reduceLeft(_+" & "+_)
val status = s"$we have fun with Scala"
[Once you are done with pasting the code block, press ctrl-D]
// Exiting paste mode, now interpreting.
defined class Person
me: Person = Person(Thomas)
you: Person = Person(Current Reader)
we: String = Current Reader & Thomas
status: String = Current Reader & Thomas have fun with Scala
在 REPL 中计时代码执行
在本书中,REPL 一直是一个非常有用的工具,用于发现和实验 Scala 的各种功能。与第三章中介绍的 Scala 工作表理解 Scala 生态系统一起,它们通过提供持续反馈来提高我们的生产力,从而使我们的开发更加敏捷。有时,测量 REPL 中执行语句或代码片段所需的时间是很方便的。这就是我们提供实现这一目标的一种方法的原因。
首先,我们定义一个名为using
的help
函数,它接受两个参数,第一个是类型为A
的param
参数,第二个是函数参数f
,它将参数的类型从A
转换为B
:
scala> def usingA <: {def close(): Unit},B(f: A=>B): B =
try { f(param) } finally { param.close() }
using: A <: AnyRef{def close(): Unit}, B(f: A => B)B
using
的作用是调用f(param)
函数,将其包裹在try {} finally{}
块中。由于这个函数背后的想法是在 I/O 资源(如FileWriter
或PrintWriter
)上应用它,我们想要保证无论发生什么情况都能关闭资源。这就是为什么你在finally
块中看到param.close
调用。这意味着param
参数不能只是任何类型A
;它必须具有额外的要求,即有一个close
方法。这正是泛型using
方法定义开始处所声明的(即[A <: {def close(): Unit}, B]
);param
参数应该是A
的子类型,它包含一个具有给定签名的函数。
通常,处理泛型类型超出了本书的范围,你不需要真正理解前面的定义就能从using
函数中受益。然而,这个例子说明了在 Scala 中使用泛型类型可以多么强大。Scala 的类型系统非常强大,编译器在编写泛型代码时会给你很大的帮助,这与 Java 中泛型的使用不同。
现在,让我们将using
函数包含到一个appendToFile
函数中,该函数将负责记录我们在 REPL 中编写的代码的评估:
scala> def appendToFile(fileName:String, textData:String) =
using (new java.io.FileWriter(fileName, true)){
fileWriter => using (new java.io.PrintWriter(fileWriter)) {
printWriter => printWriter.println(textData)
}
}
appendToFile: (fileName: String, textData: String)Unit
最后,以下timeAndLogged
函数被声明为将 REPL 中输入的代码片段包裹在日志记录和计时功能中:
scala> def timedAndLoggedT: T = {
val start = System.nanoTime
try {
val result = body
appendToFile("/tmp/repl.log",result.toString)
result
}
finally println(" "+(System.nanoTime - start) + " nanos elapsed. ")
}
timedAndLogged: TT
直到 Scala 2.10.0,你可以使用 REPL 强大模式的:wrap
方法(通过 REPL 中的> :power
命令访问)来执行所有控制台语句,而无需进一步涉及timedAndLogged
函数。:wrap
功能最近已被从 Scala 版本中移除,因此你将不得不显式地将你想要计时或记录的代码包裹在timedAndLogged
方法中,因此,你不需要涉及 REPL 的强大模式。
例如,你可以执行以下命令:
scala> timedAndLogged{ val input = 2014 ; println(input) ; Thread.sleep(2000) ; input }
2014
2004778000 nanos elapsed.
res0: Int = 2014
我们在timedAndLogged
函数中指定的/tmp/repl.log
文件当然应该包含已记录的结果,即2014
。
摘要
现在我们已经到达这本书的结尾,我们想强调在 Scala 之旅中我们接触到的许多主题和概念的关键方面。
Scala 语言的简洁和表达性语法应该使你的代码不仅更易于阅读,而且对你和其他开发者来说也更易于维护。你不必放弃 Java 生态系统中非常庞大和成熟的任何库,因为所有 API 都可以在 Scala 中直接重用。此外,你还可以从许多额外的优秀 Scala 特定库中受益。我们的建议是从你非常熟悉的领域取一段 Java 代码,也许是因为你最初就编写过它一两次。然后,尝试将其转换为 Scala 代码,并重构它以去除样板代码,使其更具函数式风格。
Play 框架不仅仅是一个 Web 框架;它打破了遵循 servlet 和 EJB 容器的传统长周期开发方法,每次重新部署都可能花费大量时间。此外,它建立在坚固和可扩展的技术之上,如 Akka,这应该让你对未来高负载和约束可用性要求感到自信。最后,我们使用它的个人经验非常愉快,因为背后的 Scala 编译器在出错时,大多数情况下都能给出非常清晰的反馈,从模板和路由规范到问题所在。由于 Play 和 Akka 都公开了 Java API,它们可以使你的过渡更容易。
我们相信,网络开发的未来是响应式的,处理大量数据流,正如它已经在许多领域发生,例如涉及内容分发和实时金融/分析服务的社交媒体网站。
我们只是触及了 Scala 所能做到的一小部分。随着你深入探索个别技术,你会发现新的特性和无限的可能性。我们的建议是一步一步地寻找可实现的目标。例如,首先熟悉 Scala 集合,特别是它们可以帮助你更好地掌握 Java 的 lambda 表达式和函数式编程,然后使用模式匹配、特质、for 推导式编写代码,然后转向更高级的主题,如隐式类型、泛型等。
最后,作为灵感,已经有大量的开源项目是用 Scala 完成的,关于我们涵盖的各个主题的许多书籍,许多由非常活跃的 Scala 社区贡献的论坛,以及来自用户组和国际会议(如 Scaladays www.scaladays.org)、Scala eXchange www.skillsmatter.com/conferences/1948-scala-exchange-2014、NEScala www.nescala.org、Jfokus www.jfokus.se、Scala.io www.scala.io、flatMap www.flatmap.no、Ping www.ping-conf.com 和 Scalapeño www.scalapeno.underscore.co.il 等会议的数年极有价值的在线视频,仅举几个例子。Scala 活动的整个日历网站可在 www.scala2014.org
查找。
有了这个想法,我希望你们对这本书的喜爱足以继续探索 Scala,编写出色的代码,并像我们一样享受乐趣!