快速了解 Scala 技术栈

阅读数:22799 2014 年 9 月 8 日

话题:Scala语言 & 开发架构

我无可救药地成为了 Scala 的超级粉丝。在我使用 Scala 开发项目以及编写框架后,它就仿佛凝聚成为一个巨大的黑洞,吸引力使我不得不飞向它,以至于开始背离 Java。固然 Java 8 为 Java 阵营增添了一丝亮色,却是望眼欲穿,千呼万唤始出来。而 Scala 程序员,却早就在享受 lambda、高阶函数、trait、隐式转换等带来的福利了。

Java 像是一头史前巨兽,它在 OO 的方向上几乎走到了极致,硬将它拉入 FP 阵营,确乎有些强人所难了。而 Scala 则不,因为它的诞生就是 OO 与 FP 的混血儿——完美的基因融合。

“Object-Oriented Meets Functional”,这是 Scala 语言官方网站上飘扬的旗帜。这也是 Scala 的野心,当然,也是 Martin Odersky 的雄心。

Scala 社区的发展

然而,一门语言并不能孤立地存在,必须提供依附的平台,以及围绕它建立的生态圈。不如此,语言则不足以壮大。Ruby 很优秀,但如果没有 Ruby On Rails 的推动,也很难发展到今天这个地步。Scala 同样如此。反过来,当我们在使用一门语言时,也要选择符合这门语言的技术栈,在整个生态圈中找到适合具体场景的框架或工具。

当然,我们在使用 Scala 进行软件开发时,亦可以寻求庞大的 Java 社区支持;可是,如果选择调用 Java 开发的库,就会牺牲掉 Scala 给我们带来的福利。幸运的是,在如今,多数情况你已不必如此。伴随着 Scala 语言逐渐形成的 Scala 社区,已经开始慢慢形成相对完整的 Scala 技术栈。无论是企业开发、自动化测试或者大数据领域,这些框架或工具已经非常完整地呈现了 Scala 开发的生态系统。

快速了解 Scala 技术栈

若要了解 Scala 技术栈,并快速学习这些框架,一个好的方法是下载 typesafe 推出的 Activator。它提供了相对富足的基于 Scala 以及 Scala 主流框架的开发模板,这其中实则还隐含了 typesafe 为 Scala 开发提供的最佳实践与指导。下图是 Activator 模板的截图:

那么,是否有渠道可以整体地获知 Scala 技术栈到底包括哪些框架或工具,以及它们的特性与使用场景呢?感谢 Lauris Dzilums 以及其他在 Github 的 Contributors。在 Lauris Dzilums 的 Github 上,他建立了名为 awesome-scala 的Repository,搜罗了当下主要的基于 Scala 开发的框架与工具,涉及到的领域包括:

  • Database
  • Web Frameworks
  • i18n
  • Authentication
  • Testing
  • JSON Manipulation
  • Serialization
  • Science and Data Analysis
  • Big Data
  • Functional Reactive Programming
  • Modularization and Dependency Injection
  • Distributed Systems
  • Extensions
  • Android
  • HTTP
  • Semantic Web
  • Metrics and Monitoring
  • Sbt plugins

是否有“乱花渐欲迷人眼”的感觉?不是太少,而是太多!那就让我删繁就简,就我的经验介绍一些框架或工具,从持久化、分布式系统、HTTP、Web 框架、大数据、测试这六方面入手,作一次蜻蜓点水般的俯瞰。

持久化

归根结底,对数据的持久化主要还是通过 JDBC 访问数据库。但是,我们需要更好的 API 接口,能更好地与 Scala 契合,又或者更自然的 ORM。如果希望执行 SQL 语句来操作数据库,那么运用相对广泛的是框架 ScalikeJDBC,它提供了非常简单的 API 接口,甚至提供了 SQL 的 DSL 语法。例如:

  val alice: Option[Member] = withSQL {
    select.from(Member as m).where.eq(m.name, name)
  }.map(rs => Member(rs)).single.apply()

如果希望使用 ORM 框架,Squeryl 应该是很好的选择。我的同事杨云在项目中使用过该框架,体验不错。该框架目前的版本为 0.9.5,已经比较成熟了。Squeryl 支持按惯例映射对象与关系表,相当于定义一个 POSO(Plain Old Scala Object),从而减少框架的侵入。若映射违背了惯例,则可以利用框架定义的 annotation 如 @Column 定义映射。框架提供了 org.squeryl.Table[T] 来完成这种映射关系。

因为可以运用 Scala 的高阶函数、偏函数等特性,使得 Squeryl 的语法非常自然,例如根据条件对表进行更新:

update(songs)(s =>
  where(s.title === "Watermelon Man")
  set(s.title := "The Watermelon Man",
      s.year  := s.year.~ + 1)
)

分布式系统

我放弃介绍诸如模块化管理以及依赖注入,是因为它们在 Scala 社区的价值不如 Java 社区大。例如,我们可以灵活地运用 trait 结合 cake pattern 就可以实现依赖注入的特性。因此,我直接跳过这些内容,来介绍影响更大的支持分布式系统的框架。

Finagle 的血统高贵,来自过去的寒门,现在的高门大族 Twitter。Twitter 是较早使用 Scala 作为服务端开发的互联网公司,因而积累了非常多的 Scala 经验,并基于这些经验推出了一些颇有影响力的框架。由于 Twitter 对可伸缩性、性能、并发的高要求,这些框架也极为关注这些质量属性。Finagle 就是其中之一。它是一个扩展的 RPC 系统,以支持高并发服务器的搭建。我并没有真正在项目中使用过 Finagle,大家可以到它的官方网站获得更多消息。

对于分布式的支持,绝对绕不开的框架还是 AKKA。它产生的影响力如此之大,甚至使得 Scala 语言从 2.10 开始,就放弃了自己的 Actor 模型,转而将 AKKA Actor 收编为 2.10 版本的语言特性。许多框架在分布式处理方面也选择了使用 AKKA,例如 Spark、Spray。AKKA 的 Actor 模型参考了 Erlang 语言,为每个 Actor 提供了一个专有的 Mailbox,并将消息处理的实现细节做了良好的封装,使得并发编程变得更加容易。AKKA 很好地统一了本地 Actor 与远程 Actor,提供了几乎一致的 API 接口。AKKA 也能够很好地支持消息的容错,除了提供一套完整的 Monitoring 机制外,还提供了对 Dead Letter 的处理。

AKKA 天生支持 EDA(Event-Driven Architecture)。当我们针对领域建模时,可以考虑针对事件进行建模。在 AKKA 中,这些事件模型可以被定义为 Scala 的 case class,并作为消息传递给 Actor。借用 Vaughn Vernon 在《实现领域驱动设计》中的例子,针对如下的事件流:

我们可以利用 Akka 简单地实现:

case class AllPhoneNumberListed(phoneNumbers: List[Int])
case class PhoneNumberMatched(phoneNumbers: List[Int])
case class AllPhoneNumberRead(fileName: String)

class PhoneNumbersPublisher(actor: ActorRef) extends ActorRef {
	def receive = {
		case ReadPhoneNumbers =>
		//list phone numbers

		actor ! AllPhoneNumberListed(List(1110, ))
	}
}

class PhoneNumberFinder(actor: ActorRef) extends ActorRef {
	def receive = {
		case AllPhoneNumberListed(numbers) => 
			//match

			actor ! PhoneNumberMatched()
	}
}

val finder = system.actorOf(Prop(new PhoneNumberFinder(...)))
val publisher = system.actorOf(Prop(new PhoneNumbersPublisher(finder)))

publisher ! ReadPhoneNumbers("callinfo.txt")

若需要处理的电话号码数据量大,我们可以很容易地将诸如 PhoneNumbersPublisher、PhoneNumberFinder 等 Actors 部署为 Remote Actor。此时,仅仅需要更改客户端获得 Actor 的方式即可。

Twitter 实现的 Finagle 是针对 RPC 通信,Akka 则提供了内部的消息队列(MailBox),而由 LinkedIn 主持开发的 Kafka 则提供了支持高吞吐量的分布式消息队列中间件。这个顶着文学家帽子的消息队列,能够支持高效的 Publisher-Subscriber 模式进行消息处理,并以快速、稳定、可伸缩的特性很快引起了开发者的关注,并在一些框架中被列入候选的消息队列而提供支持,例如,Spark Streaming 就支持 Kafka 作为流数据的 Input Source。

HTTP

严格意义上讲,Spray 并非单纯的 HTTP 框架,它还支持 REST、JSON、Caching、Routing、IO 等功能。Spray 的模块及其之间的关系如下图所示:

我在项目中主要将 Spray 作为 REST 框架来使用,并结合 AKKA 来处理领域逻辑。Spray 处理 HTTP 请求的架构如下图所示:

Spray 提供了一套 DSL 风格的 path 语法,能够非常容易地编写支持各种 HTTP 动词的请求,例如:

trait HttpServiceBase extends Directives with Json4sSupport {
     implicit val system: ActorSystem
     implicit def json4sFormats: Formats = DefaultFormats
     def route: Route
}

trait CustomerService extends HttpServiceBase {
     val route = 
          path("customer" / "groups") {
               get {
                    parameters('groupids.?) {
                         (groupids) =>
                              complete {
                                   groupids match {
                                        case Some(groupIds) => 
                    ViewUserGroup.queryUserGroup(groupIds.split(",").toList)
                                        case None => ViewUserGroup.queryUserGroup()
                                   }
                              }
                    }
               }
          } ~
          path("customers" / "vip" / "failureinfo") {
               post {
                    entity(as[FailureVipCustomerRequest]) {
                         request => 
                              complete {
                                   VipCustomer.failureInfo(request) 
                              }
                    }
               }
          }
}

我个人认为,在进行 Web 开发时,完全可以放弃 Web 框架,直接选择 AngularJS 结合 Spray 和 AKKA,同样能够很好地满足 Web 开发需要。

Spray 支持 REST,且 Spray 自身提供了服务容器 spray-can,因而允许 Standalone 的部署(当然也支持部署到 Jetty 和 tomcat 等应用服务器)。Spray 对 HTTP 请求的内部处理机制实则是基于 Akka-IO,通过 IO 这个 Actor 发出对 HTTP 的 bind 消息。例如:

 IO(Http) ! Http.Bind(service, interface = "0.0.0.0", port = 8889)

我们可以编写不同的 Boot 对象去绑定不同的主机 Host 以及端口。这些特性都使得 Spray 能够很好地支持当下较为流行的 Micro Service 架构风格。

Web 框架

正如前面所说,当我们选择 Spray 作为 REST 框架时,完全可以选择诸如 AngularJS 或者 Backbone 之类的 JavaScript 框架开发 Web 客户端。客户端能够处理自己的逻辑,然后再以 JSON 格式发送请求给 REST 服务端。这时,我们将模型视为资源(Resource),视图完全在客户端。JS 的控制器负责控制客户端的界面逻辑,服务端的控制器则负责处理业务逻辑,于是传统的 MVC 就变化为 VC+R+C 模式。这里的 R 指的是 Resource,而服务端与客户端则通过 JSON 格式的 Resource 进行通信。

若硬要使用专有的 Web 框架,在 Scala 技术栈下,最为流行的就是 Play Framework,这是一个标准的 MVC 框架。另外一个相对小众的 Web 框架是 Lift。它与大多数 Web 框架如 RoR、Struts、Django 以及 Spring MVC、Play 不同,采用的并非 MVC 模式,而是使用了所谓的 View First。它驱动开发者对内容生成与内容展现(Markup)形成“关注点分离”。

Lift 将关注点重点放在 View 上,这是因为在一些 Web 应用中,可能存在多个页面对同一种 Model 的 Action。倘若采用 MVC 中的 Controller,会使得控制变得非常复杂。Lift 提出了一种所谓 view-snippet-model(简称为 VSM)的模式。

View 主要为响应页面请求的 HTML 内容,分为 template views 和 generated views。Snippet 的职责则用于生成动态内容,并在模型发生更改时,对 Model 和 View 进行协调。

大数据

大数据框架最耀眼的新星非 Spark 莫属。与许多专有的大数据处理平台不同,Spark 建立在统一抽象的 RDD 之上,使得它可以以基本一致的方式应对不同的大数据处理场景,包括 MapReduce,Streaming,SQL,Machine Learning 以及 Graph 等。这即 Matei Zaharia 所谓的“设计一个通用的编程抽象(Unified Programming Abstraction)。

由于 Spark 具有先进的 DAG 执行引擎,支持 cyclic data flow 和内存计算。因此相比较 Hadoop 而言,性能更优。在内存中它的运行速度是 Hadoop MapReduce 的 100 倍,在磁盘中是 10 倍。

由于使用了 Scala 语言,通过高效利用 Scala 的语言特性,使得 Spark 的总代码量出奇地少,性能却在多数方面都具备一定的优势(只有在 Streaming 方面,逊色于 Storm)。下图是针对 Spark 0.9 版本的 BenchMark:

由于使用了 Scala,使得语言的函数式特性得到了最棒的利用。事实上,函数式语言的诸多特性包括不变性、无副作用、组合子等,天生与数据处理匹配。于是,针对 WordCount,我们可以如此简易地实现:

file = spark.textFile("hdfs://...")

file.flatMap(line => line.split(" "))
    .map(word => (word, 1))
    .reduceByKey(_ + _)

要是使用 Hadoop,就没有这么方便了。幸运的是,Twitter 的一个开源框架 scalding 提供了对 Hadoop MapReduce 的抽象与包装。它使得我们可以按照 Scala 的方式执行 MapReduce 的 Job:

class WordCountJob(args : Args) extends Job(args) {
  TextLine( args("input") )
    .flatMap('line -> 'word) { line : String => tokenize(line) }
    .groupBy('word) { _.size }
    .write( Tsv( args("output") ) )

  // Split a piece of text into individual words.
  def tokenize(text : String) : Array[String] = {
    // Lowercase each word and remove punctuation.
    text.toLowerCase.replaceAll("[^a-zA-Z0-9\\s]", "").split("\\s+")
  }
}

测试

虽然我们可以使用诸如 JUnit、TestNG 为 Scala 项目开发编写单元测试,使用 Cocumber 之类的 BDD 框架编写验收测试。但在多数情况下,我们更倾向于选择使用 ScalaTest 或者 Specs2。在一些 Java 开发项目中,我们也开始尝试使用 ScalaTest 来编写验收测试,乃至于单元测试。

若要我选择 ScalaTest 或 Specs2,我更倾向于 ScalaTest,这是因为 ScalaTest 支持的风格更具备多样性,可以满足各种不同的需求,例如传统的 JUnit 风格、函数式风格以及 Spec 方式。我的一篇博客《ScalaTest 的测试风格》详细介绍了各自的语法。

一个被广泛使用的测试工具是 Gatling,它是基于 Scala、AKKA 以及 Netty 开发的性能测试与压力测试工具。我的同事刘冉在 InfoQ 发表的文章《新一代服务器性能测试工具 Gatling》对 Gatling 进行了详细深入的介绍。

ScalaMeter 也是一款很不错的性能测试工具。我们可以像编写 ScalaTest 测试那样的风格来编写 ScalaMeter 性能测试用例,并能够快捷地生成性能测试数据。这些功能都非常有助于我们针对代码或软件产品进行 BenchMark 测试。我们曾经用 ScalaMeter 来编写针对 Scala 集合的性能测试,例如比较 Vector、ArrayBuffer、ListBuffer 以及 List 等集合的相关操作,以便于我们更好地使用 Scala 集合。以下代码展示了如何使用 ScalaMeter 编写性能测试:

import org.scalameter.api._

object RangeBenchmark
extends PerformanceTest.Microbenchmark {
  val ranges = for {
    size <- Gen.range("size")(300000, 1500000, 300000)
  } yield 0 until size

  measure method "map" in {
    using(ranges) curve("Range") in {
      _.map(_ + 1)
    }
  }
}

根据场景选择框架或工具

比起 Java 庞大的社区,以及它提供的浩如烟海般的技术栈,Scala 技术栈差不多可以说是沧海一粟。然而,麻雀虽小却五脏俱全,何况 Scala 以及 Scala 技术栈仍然走在迈向成熟的道路上。对于 Scala 程序员而言,因为项目的不同,未必能涉猎所有技术栈,而且针对不同的方面,也有多个选择。在选择这些框架或工具时,应根据实际的场景做出判断。为稳妥起见,最好能运用技术矩阵地方式对多个方案进行设计权衡与决策。

我们也不能固步自封,视 Java 社区而不顾。毕竟那些 Java 框架已经经历了千锤百炼,并有许多成功的案例作为佐证。关注 Scala 技术栈,却又不局限自己的视野,量力而为,选择合适的技术方案,才是设计与开发的正道。

作者简介

张逸,现为 ThoughtWorks Lead Consultant。作为一名咨询师,主要为客户提供组织的敏捷转型、过程改进、企业系统架构、领域驱动设计、大数据、代码质量提升、测试驱动开发等咨询与培训工作。