本文是我们名为“ 使用Scala开发现代应用程序 ”的学院课程的一部分。
在本课程中,我们提供一个框架和工具集,以便您可以开发现代的Scala应用程序。 我们涵盖了广泛的主题,从SBT构建和响应式应用程序到测试和数据库访问。 通过我们简单易懂的教程,您将能够在最短的时间内启动并运行自己的项目。 在这里查看 !
1.简介
这些天,您多久听到一次诸如“ Web API正在吞噬整个世界”之类的短语? 确实, 马克·安德森 ( Marc Andreessen)很好地总结了这一点,但是API在支持企业对企业或企业对消费者的对话中,尤其是在Web领域中,变得越来越重要。
对于许多企业而言,Web API的存在是必须具备的竞争优势,并且通常是生存的问题。
目录
-
1.简介 2.充分休息 3.从Spray到Akka Http 4.留在服务器上
-
-
4.1。 路线和指令 4.2。 编组和拆组 4.3。 深度指令 4.4。 当事情出错时 4.5。 启动/关机 4.6。 安全HTTP(HTTPS) 4.7。 测试中
5.作为客户生活 6。结论 7.接下来
2.充分休息
在当今的网络中, HTTP为王。 多年来,人们进行了许多不同的尝试来提出标准的,统一的高级协议,以在其上公开服务。 SOAP可能是第一个获得广泛普及(尤其是在企业界)的SOAP ,多年来一直用作Web服务和API开发的实际规范。 但是,它的冗长和形式主义经常成为额外复杂性的来源,这是代表状态转移 (或仅仅是REST )出现的原因之一。
如今,大多数Web服务和API都是按照REST体系结构样式开发的,因此被称为REST(ful) 。 遵循REST(ful)原理和约束,有无数种很棒的框架可用于构建Web服务和API,但是在Scala的世界中,毫无疑问是领先者: Akka HTTP 。
3.从Spray到Akka Http
你们中的许多人可能凭借其出色的前身非常流行的Spray Framework来熟悉Akka HTTP 。 它仍然在野外广泛使用,但是自去年左右以来, Spray Framework正在Akka HTTP保护伞下积极迁移,并将最终停止使用。 在撰写本文时, Akka HTTP的最新稳定版本(作为最受欢迎的Akka Toolkit的一部分分发)为2.4.11
。
Akka HTTP站在Actor Model (由Akka核心提供)和Akka Streams的肩膀上,因此完全包含了反应式编程范例。 但是,请注意,随着Spray Framework迁移到新家的进行, Akka HTTP的某些部分仍带有实验标签,并且某些合同很可能会发生变化。
4.留在服务器上
通常,当我们谈论Web服务和API时,至少涉及两个方面:服务器(提供者)和客户端(消费者)。 毫不奇怪, Akka HTTP同时支持这两种方式,因此让我们从最有趣的部分(服务器端)开始。
Akka HTTP提供了许多API层,从相当低级的请求/响应处理到漂亮的DSL 。 在本教程的这一部分中,我们将仅使用Akka HTTP服务器DSL ,因为这是在Scala中开始构建REST(ful) Web服务和API的最快(也是最漂亮)的方式。
路线和指令
Akka HTTP服务器的核心是路由 ,该路由在基于HTTP的通信中可以描述为选择最佳路径来处理传入请求的过程。 路由是使用伪指令组成和描述的: Akka HTTP服务器端DSL的构建块。
例如,让我们开发一个简单的REST(ful) Web API来管理用户,其功能在某种程度上类似于我们在“带Play框架的Web应用程序”部分中所做的。 提醒一下,这是我们的User
case类的外观(毫不奇怪,它与我们在各处使用的相同):
case class User(id: Option[Int], email: String,
firstName: Option[String], lastName: Option[String])
可能,我们可能需要Web API处理的第一条路线是返回所有用户的列表,因此以它为起点:
val route: Route = path("users") {
pathEndOrSingleSlash {
get {
complete(repository.findAll)
}
}
}
就如此容易! 每次客户端每次向/users
端点发出GET
请求时(按照path("users")
和get
指令),我们都将从底层数据存储中获取所有用户,然后将它们返回(按照complete
指令)。 实际上,路由可能非常复杂,需要提取和验证不同的参数,但幸运的是, 路由DSL具有所有内置功能,例如:
val route: Route = pathPrefix("users") {
path(IntNumber) { id =>
get {
rejectEmptyResponse {
complete(repository.find(id))
}
}
}
}
让我们仔细看看这个代码片段。 您可能已经猜到了,我们正在提供一个Web API来通过其整数标识符检索用户,该整数标识符作为URI路径参数(使用path(IntNumber)
指令)提供,例如/users/101
。 但是,具有这样的标识符的用户可能存在也可能不存在(通常来说, repository.find(id)
返回Option[User]
)。 在这种情况下,如果在数据存储区中找不到用户,则rejectEmptyResponse
指令的存在会指示我们的端点返回404
HTTP状态代码 (而不是空响应)。
看起来确实很简单,但是好奇的读者可能会想知道将使用哪种数据格式来表示用户?
编组和拆组
Akka HTTP使用编组和解组过程将对象转换为表示形式,可以通过电线进行传输。 但是实际上,在大多数时候,当涉及到REST(ful) Web API时,我们谈论的是JSON格式,而Akka HTTP对此提供了出色的支持。
import akka.http.scaladsl.marshallers.sprayjson.SprayJsonSupport._
import spray.json.DefaultJsonProtocol._
implicit val userFormat = jsonFormat4(User)
预定义jsonFormat4
函数的使用引入了对将User
案例类编组为JSON表示并将其从JSON解组回User
隐式支持,例如:
val route: Route = pathPrefix("users") {
pathEndOrSingleSlash {
post {
entity(as[User]) { user =>
complete(Created, repository.insert(user))
}
}
}
}
到目前为止看起来还不错,但是REST(ful) Web API实践者可能会认为POST
操作的语义应包括Location
标头以指出新创建的资源。 让我们解决这个问题,它需要稍微重构实现,引入onSuccess
和respondWithHeader
指令。
val route: Route = pathPrefix("users") {
pathEndOrSingleSlash {
post {
entity(as[User]) { user =>
onSuccess(repository.insert(user)) { u =>
respondWithHeader(Location(uri.withPath(uri.path / u.id.mkString))) {
complete(Created, u)
}
}
}
}
}
}
非常好,最后但并非最不重要的一点是,它特别支持以JSON格式流HTTP响应 ,特别是在使用Akka Streams时。 在“使用Slick进行数据库访问”部分中,我们已经学习了如何使用超棒的Slick库从数据存储流式传输结果,因此,这段代码看起来应该非常熟悉。
class UsersRepository(val config: DatabaseConfig[JdbcProfile]) extends Db with UsersTable {
...
def stream(implicit materializer: Materializer) = Source
.fromPublisher(db.stream(
users.result.withStatementParameters(fetchSize = 10)))
...
}
Source [T,_]可以直接从complete
指令返回,假设T
(在我们的情况下为User
)对JSON编组具有隐式支持,例如:
implicit val jsonStreamingSupport = EntityStreamingSupport.json()
val route: Route = pathPrefix("users") {
path("stream") {
get {
complete(repository.stream)
}
}
}
尽管它看起来与到目前为止我们看到的其他代码片段没有什么不同,但是在这种情况下, Akka HTTP将使用分块传输编码来传递响应。
另外,请注意, JSON支持目前处于过渡阶段,仍位于Spray Framework软件包中。 希望最终的Akka HTTP抽象将非常相似,并且迁移将只是软件包更改的问题(手指交叉)。
深度指令
Akka HTTP中的指令几乎可以执行所有带有请求或响应的指令,并且有很多令人印象深刻的预定义指令。 为了更好地理解机制,我们将再看两个示例:日志记录和安全性。
如果您需要通过检查请求和响应的完整快照来对Akka HTTP Web API进行故障排除,那么logRequestResult
指令logRequestResult
您提供很大的帮助。
val route: Route = logRequestResult("user-routes") {
...
}
这里只是对当向/users
端点发出POST
请求时,此信息在日志输出中的外观的简要说明:
[akka.actor.ActorSystemImpl(akka-http-webapi)] user-routes: Response for
Request : HttpRequest(HttpMethod(POST),http://localhost:58080/users,List(Host: localhost:58080, User-Agent: curl/7.47.1, Accept: */*, Timeout-Access: ),HttpEntity.Strict(application/json,{"email": "[email protected]"}),HttpProtocol(HTTP/1.1))
Response: Complete(HttpResponse(200 OK,List(),HttpEntity.Strict(application/json,{"id":1,"email":"[email protected]"}),HttpProtocol(HTTP/1.1)))
请注意,所有请求和响应标头以及有效负载均包括在内。 如果有敏感信息(例如密码或凭据)传递,最好在顶部实施某种过滤或屏蔽。
另一个有趣的示例与使用authenticateXxx
指令系列保护Akka HTTP Web API的安全有关。 目前,仅支持两种身份验证流程: HTTP基本身份验证和OAuth2 。 作为示例,让我们向Web API引入另一个端点以允许用户修改(通常使用PUT
请求完成)。
val route: Route = pathPrefix("users") {
path(IntNumber) { id =>
put {
authenticateBasicAsync(realm = "Users", authenticator) { user =>
rejectEmptyResponse {
entity(as[UserUpdate]) { user =>
complete(
repository.update(id, user.firstName, user.lastName) map {
case true => repository.find(id)
case _=> Future.successful(None)
}
)
}
}
}
}
}
}
该终结点使用HTTP Basic Auth保护,并将验证用户的电子邮件是否已存在于数据存储中。 为简单起见,密码始终硬编码为"password"
(请不要在实际应用中这样做)。
def authenticator(credentials: Credentials): Future[Option[User]] = {
credentials match {
case p @ Credentials.Provided(email) if p.verify("password") =>
repository.findByEmail(email)
case _ => Future.successful(None)
}
}
确实不错,但Akka HTTP设计的最佳部分可能是内置在核心中的可扩展性:万一没有预定义的指令可以满足您的需求,很容易引入您自己的指令。
当事情出错时
可以肯定的是,大多数时候您的Web API都可以正常工作,为满意的客户提供服务。 但是,不时发生坏事,最好准备好应对它们。 从本质上讲,失败的原因可能有很多:违反业务约束,数据库连接,外部依赖项不可用,垃圾回收等……在幕后,我们可以将它们分为两个不同的存储桶:异常和执行持续时间。
根据我们正在开发的Web API,特殊情况的典型示例是使用重复的电子邮件地址创建用户。 在数据存储级别上,这将导致唯一的约束冲突,需要进行特殊处理。 如您所料,有一个专用的指令handleExceptions
,例如:
val route: Route = pathPrefix("users") {
pathEndOrSingleSlash {
post {
handleExceptions(exceptionHandler) {
extractUri { uri =>
entity(as[User]) { user =>
onSuccess(repository.insert(user)) { u =>
complete(Created, u)
}
}
}
}
}
}
}
handleExceptions
指令接受ExceptionHandler
作为参数,该参数将特定的异常映射到响应。 在我们的情况下,它将SQLException映射到HTTP响应代码409
,指示冲突。
val exceptionHandler = ExceptionHandler {
case ex: SQLException => complete(Conflict, ex.getMessage)
}
很好,但是执行时间呢? 幸运的是, Akka HTTP支持各种不同的超时来保护您的Web API。 请求超时是允许限制路由返回响应所用的最大时间量之一。 如果超过了该超时时间,则将返回HTTP响应代码503
,表明该Web API目前不可用。 可以使用 application.conf
全局设置或使用timeout指令来配置所有这些超时,例如:
val route: Route = pathPrefix("users") {
pathEndOrSingleSlash {
post {
withRequestTimeout(5 seconds) {
...
}
}
}
}
使用这种细粒度控件的功能是一个非常强大的选项,因为并非所有Web API都同样重要,并且执行期望可能会有所不同。
启动/关机
Akka HTTP使用Akka的扩展机制,并提供对Http
支持的扩展实现。 有很多初始化和使用它的方法,但是最简单的方法是使用bindAndHandle
函数。
implicit val system = ActorSystem("akka-http-webapi")
implicit val materializer = ActorMaterializer()
val repository = new UsersRepository(config)
Http().bindAndHandle(new UserApi(repository).route, "0.0.0.0", 58080)
关闭过程有些棘手,但总的来说包括两个步骤:解除Http
扩展的绑定并关闭底层的actor系统,例如:
val f = Http().bindAndHandle(new UserApi(repository).route, "0.0.0.0", 58080)
f.flatMap(_.unbind) onComplete {
_ => system.terminate
}
也许您很少会遇到使用编程终止的需求,但是,尽管如此,了解它的机制还是很不错的。
安全HTTP(HTTPS)
Akka HTTP与纯HTTP一起可以用于生产部署,并且还支持通过HTTPS进行安全通信。 与我们在本教程的“带Play框架的Web应用程序”部分中学到的内容类似,我们需要一个Java密钥库,其中已导入证书。 出于开发目的,生成自签名证书是足以开始的选项:
keytool -genkeypair -v
-alias localhost
-dname "CN=localhost"
-keystore src/main/resources/akka-http-webapi.jks
-keypass changeme
-storepass changeme
-keyalg RSA
-keysize 4096
-ext KeyUsage:critical="keyCertSign"
-ext BasicConstraints:critical="ca:true"
-validity 365
但是,配置服务器端HTTPS支持需要编写大量代码,幸运的是,它已经被很好地记录了下来 。 SslSupport
特征是这种配置的示例。
trait SslSupport {
val configuration = ConfigFactory.load()
val password = configuration.getString("keystore.password").toCharArray()
val https: HttpsConnectionContext =
managed(getClass.getResourceAsStream("/akka-http-webapi.jks")).map { in =>
val keyStore = KeyStore.getInstance("JKS")
keyStore.load(in, password)
val keyManagerFactory = KeyManagerFactory.getInstance("SunX509")
keyManagerFactory.init(keyStore, password)
val tmf = TrustManagerFactory.getInstance("SunX509")
tmf.init(keyStore)
val sslContext = SSLContext.getInstance("TLS")
sslContext.init(keyManagerFactory.getKeyManagers, tmf.getTrustManagers,
new SecureRandom)
ConnectionContext.https(sslContext)
}.opt.get
}
现在,我们可以使用https
连接上下文变量通过将其作为参数传递给bindAndHandle
函数来创建HTTPS绑定,例如:
Http().bindAndHandle(new UserApi(repository).route, "0.0.0.0", 58083,
connectionContext = https)
请注意,您可能同时具有HTTP支持和/或HTTPS支持, Akka HTTP使您可以完全自由地进行这些选择。
测试中
如何进行Web API测试有很多不同的策略。 毫不奇怪,与所有其他Akka模块一样, Akka HTTP具有专用的测试支架,可与ScalaTest框架无缝集成(不幸的是,尚不支持specs2 )。
让我们从一个非常简单的示例开始,以确保/users
端点在发送GET
请求时将返回一个空用户列表。
"Users Web API" should {
"return all users" in {
Get("/users") ~> route ~> check {
responseAs[Seq[User]] shouldEqual Seq()
}
}
}
非常简单易用,测试用例看起来非常完美! 这些测试用例以惊人的速度执行,因为它们是针对路由定义运行的,而无需引导完整的HTTP服务器实例。
更为复杂的场景是开发一个用于用户修改的测试用例,该用例要求先创建用户,然后再传递HTTP基本身份验证凭据。
"Users Web API" should {
"create and update user" in {
Post("/users", User(None, "[email protected]", None, None)) ~> route ~> check {
status shouldEqual Created
header[Location] map { location =>
val credentials = BasicHttpCredentials("[email protected]", "password")
Put(location.uri, UserUpdate(Some("John"), Some("Smith"))) ~>
addCredentials(credentials) ~> route ~> check {
status shouldEqual OK
responseAs[User] should have {
'firstName (Some("John"))
'lastName (Some("Smith"))
}
}
}
}
}
}
当然,如果您正在实践TDD或派生方法(我真的相信每个人都应该这样做),那么您将享受Akka HTTP测试脚手架的健壮性和简洁性。 感觉做对了。
5.作为客户生活
我希望在这一点上,我们真正感谢Akka HTTP为REST(ful) Web服务和API开发提供的强大的服务器端支持。 但是,我们自己构建的Web API通常会成为其他外部Web服务和API的客户端。
如前所述, Akka HTTP具有出色的客户端支持,可与基于外部HTTP的Web服务和API通信。 与服务器端类似,有多种级别的客户端API可用,但是最容易使用的一种可能是请求级API 。
val response: Future[Seq[User]] =
Http()
.singleRequest(HttpRequest(uri = "http://localhost:58080/users"))
.flatMap { response => Unmarshal(response).to[Seq[User]] }
在这个简单的示例中,对我们的/users
端点只有一个请求问题,结果从JSON解组到Seq[User]
。 但是,在大多数实际应用程序中,始终要付出建立HTTP连接的代价,因此使用连接池是一种首选且有效的解决方案,这是昂贵的。 很高兴知道Akka HTTP也具有所有必要的构建基块来涵盖这些情况,例如:
val pool = Http().superPool[String]()
val response: Future[Seq[User]] =
Source.single(HttpRequest(uri = "http://localhost:58080/users") -> uuid)
.via(pool)
.runWith(Sink.head)
.flatMap {
case (Success(response), _) => Unmarshal(response).to[Seq[User]]
case _ => Future.successful(Seq[User]())
}
这两个代码段的最终结果完全相同。 但是后者使用pool并显示了更多时间, Akka HTTP始终支持Akka Streams ,并且在处理客户端HTTP通信方面非常方便。 但是,一个重要的细节是:完成后,应手动关闭连接池 ,例如:
Http().shutdownAllConnectionPools()
如果客户端和服务器的通信依赖于HTTPS协议,则Akka HTTP客户端API开箱即可支持TLS加密 。
6。结论
如果您要在Scala中构建或考虑构建REST(ful) Web服务和API,请一定尝试Akka HTTP 。 它的路由DSL是描述REST(ful)资源语义并插入实现以为其提供服务的优美而优雅的方式。 与Play Framework相比,确实有很多重叠之处,但是对于纯Web API项目而言, Akka HTTP无疑是赢家。 一些实验性模块的存在可能会立即采用Akka HTTP带来一些风险,但是每个版本都越来越接近交付稳定,简洁的合同。
7.接下来
这实在是可悲的承认,但我们的教程越来越它的尽头。 但老实说,我相信对于我们大多数人来说,这仅仅是进入Scala编程语言和生态系统世界的令人兴奋旅程的开始。 一路走来,让喜悦和成功与您同在!
完整的源代码可供下载 。
翻译自: https://www.javacodegeeks.com/2016/11/developing-modern-applications-scala-web-apis-akka-http.html