scala akka_使用Scala开发现代应用程序:使用Akka HTTP的Web API

scala akka

本文是我们名为“ 使用Scala开发现代应用程序 ”的学院课程的一部分。

在本课程中,我们提供一个框架和工具集,以便您可以开发现代的Scala应用程序。 我们涵盖了广泛的主题,从SBT构建和响应式应用程序到测试和数据库访问。 通过我们简单易懂的教程,您将能够在最短的时间内启动并运行自己的项目。 在这里查看

1.简介

这些天,您多久听到一次诸如“ Web API正在吞噬世界”之类的短语? 确实, 马克·安德森Marc Andreessen)很好地总结了这一点,但是API在支持企业对企业企业对消费者的对话中,尤其是在Web领域中,变得越来越重要。

对于许多企业而言,Web API的存在是必须具备的竞争优势,并且通常是生存的问题。

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的肩膀上,因此完全包含了React式编程范例。 但是,请注意,随着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标头,以指出新创建的资源。 让我们解决这个问题,它需要稍微重构实现,引入onSuccessrespondWithHeader指令。

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": "a@b.com"}),HttpProtocol(HTTP/1.1))
  Response: Complete(HttpResponse(200 OK,List(),HttpEntity.Strict(application/json,{"id":1,"email":"a@b.com"}),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)

与纯HTTP一起Akka 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, "a@b.com", None, None)) ~> route ~> check {
      status shouldEqual Created
      header[Location] map { location =>
        val credentials = BasicHttpCredentials("a@b.com", "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 HTTPREST(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

scala akka

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值