逸言

Spray的模块结构

| Comments

Spray是基于Akka之上的轻量级Scala库,提供对客户端与服务端REST/HTTP的支持。在使用上,大概可以将其视为Java平台下的Jersey;虽然它们的特性并不尽相同。从Scala语言来看,它提供的Future与Promise可以较好地支持并发;而Akka的Actor模型无疑助推了Spray的并发处理性能。根据Spray官方网站的描述,它拥有如下的特性:

完全异步的,无阻塞的APIs
基于Actor与Future
高性能
轻量级
模块化
可测试性

之所以称其为库(library)而非框架(Framework),源于Spray的设计哲学。似乎在Spray的设计者眼中,只要沾上了“框架”这个名词,就显得不够轻量,耦合也会随之而来。虽然框架并非容器,无需管理诸多对象的生命周期,但它总会为应用添加一些框架级别的约束。框架更像是一个骨架(Skeleton),应用所要做的是将自己的内容填充到这个骨架中。应用能否运行良好,取决于框架的支撑作用。以我之浅见,大约可以如此简单分辨:框架之于应用,是一种白盒式的调用,你须得了解一些框架细节,遵循规定的约束或规范,才能更好地发挥框架的威力;而类库之于应用,则是职责向黑盒传递的委派,若要调用库,只需明晓其接口即可。

正如Spray的官方网站所说,Spray更像是基于Http搭建的一个集成层(Integration Layer)。因此你不必将自己的应用搭建在Spray之上,仅仅是当需要对Http的集成时,Spray可以成为你的一项选择。这种松散耦合,去除“供应商绑定”的方式,实在招人喜爱。Spray引人驻足的魅力,不仅在于Akka Actor模式对并发的良好支持,Scala语言本身也使其增色不少。Scala的语法较之Java更其灵活而先进,那些支持不变式、函数编程以及闭包等特性,总会让Java程序员垂涎三尺(至少在Java 8之前)。若再遥想不远将来Scala提供的async以及await(一如C# 5.0的语法特性),编写Scala程序应大有可为。当然,或也可能成亦Scala,败亦Scala,毕竟面对数量级庞大的Java程序员,Scala仍属小众。这会成为Spray的阿克琉斯之踝吗?

诸多因子促使我选择Spray作为一个研究对象。我想,通过使用Spray,并深入挖掘其源码,定能让我受益良多。当然,在剖析其源代码之前,先让我们看看Spray的模块结构吧。

既然Spray号称其具有松散耦合的组件结构,就让我们对这些模块间的依赖来一个先睹为快。要识别模块之间的依赖关系,最直观的方式莫过于绘制一个依赖图:

整洁的依赖显得清爽,斩钉截铁,绝不拖泥带水。那种如网一般深邃的依赖关系,会让人迷失。这其中的要点,仍然在于职责的分配。借助依赖关系图可以更加直观展现,何处出现多余或繁复的依赖线与环,肉眼即可观察获得。这种图甚至可以通过直觉去感受,只要绘图得当,布局合理,会有平衡之感。

如上图所示,以浅绿标明的三个模块,都处于被依赖的层次,按照“依赖沉淀”的法则,我将其放在整个图的最底层。观其名,我们似乎也可察知这几个模块更倾向于基础设施或公共设施的范畴。值得特别书写的是spray-http,因为该模块除了必须依赖的Scala之外,完全独立,没有任何依赖。它提供了基本的Http数据结构,如Request,Response以及其他公共Header。对于一个支持Http的库而言,可以视其为核心。

spray-caching提供了基本的缓存功能,是一个非常小的模块,仅仅包含cache.scala与LruCache.scala两个类。它引入了Future对象,使其能够更好地满足并发处理的缓存。此外,它还调用了google code上的一个第三方开源库concurrentlinkedhashmap(上图的紫色模块),作为存储Future对象的数据结构。spray-can是整个库的主要模块,提供了高性能的Http Server与Client。在这个模块中,主要的APIs都直接或间接继承了Akka提供的Actor。spray-servlet扮演了适配器的角色,它的主要目的是在Servlet API之上提供spray-can的服务器接口,从而使得spray能够在servlet容器中使用路由功能。因此,该模块除了依赖spray-util与spray-http之外,实则还要依赖于Java的Servlet。注意,上图的虚线依赖说明的是一种可选的关系。单从这一点来看,也部分说明了spray模块间的松散耦合度。一个示例是上图未曾列出的spray-json,就属于可选的用于处理json的模块。

无论是依赖关系,还是模块职责展现的抽象层次,spray-client与spray-routing都属于更加high-level的模块。例如spray-client可以视为对spray-can中提供的Http Client APIs的包装,属于更高层次的抽象。spray-routing则以优雅的DSL形式提供了定义REST服务的接口。

观察Spray的模块分解,非常注重职责的内聚,并以较小的粒度划分模块。由于它的设计目标为库,每个模块展现了自己的关注点,使其能够最大限度地被用户单独使用。例如当我仅需要缓存功能时,完全可以忽略Spray中支持HTTP以及REST的模块。同时,它注重调用视角的区分,为不同的调用者提供不同的抽象层次。在充分满足松耦合、高内聚的架构原则之上,适度地考虑接口的可用性与可读性。我喜欢这样一种轻量级的,尽其可能简单的架构设计。

Comments