Akka实战:构建REST风格的微服务

Stella981
• 阅读 805

使用Akka-Http构建REST风格的微服务,服务API应尽量遵循REST语义,数据使用JSON格式交互。在有错误发生时应返回:{"errcode":409,"errmsg":"aa is invalid,the ID is expected to be bb"}类似的JSON错误消息。

代码:

代码

首先来看看代码文件结构:

├── ApiRoute.scala
├── App.scala
├── ContextProps.scala
├── book
│   ├── Book.scala
│   ├── BookContextProps.scala
│   ├── BookRoute.scala
│   └── BookService.scala
└── news
    ├── News.scala
    ├── NewsContextProps.scala
    ├── NewsRoute.scala
    └── NewsService.scala

通过名字可以看出,App.scala是启动程序,以Route结尾的是API路由定义文件,Service结尾的就是服务实现代码了。ContextProps结尾的是服务与路由交互的上下文属性部分,Service的将会在ContextProps中实例化并传给各个Route

从目录结构上看,程序是按功能模块进行划分的。book相关的路由、服务、实体都定义在book包下。相应的,与news相关的代码则写于news包。

首先来看看程序的启动文件,App.scala

  def main(args: Array[String]): Unit = {
    Files.write(Paths.get("app.pid"), Utils.getPid.getBytes(Utils.CHARSET))

    val contextProps = new ContextProps

    val bindingFuture = Http().bindAndHandle(ApiRoute(contextProps), "0.0.0.0", 3333)

    bindingFuture.onComplete {
      case Success(binding) =>
        logger.info(binding.toString)
      case Failure(e) =>
        logger.error(e.getLocalizedMessage, e)
    }
  }

定义akka-http绑定的host和port,设置ContextProps,并把它传给ApiRouteApp.scala的代码还是很简单的,接下来看看ApiRoute的实现。

  // 定义一个Health Check API,用户第3方工具(如:Nginx/Tengine)验证服务是否正常运行
  val healthCheck =
    path("health_check") {
      get { ctx =>
        logger.debug(ctx.request.toString)
        ctx.complete(HttpEntity.Empty)
      }
    }

  import me.yangbajing.akkaaction.util.JsonSupport._

  val customExceptionHandler = ExceptionHandler {
    case e: MessageException =>
      extractRequest { req =>
        val msg =
          s"""\nmethod: ${req.method}
             |uri: ${req.uri}
             |headers:
             |\t${req.headers.mkString("\n\t")}
             |$e""".stripMargin
        if (e.errcode > 500) logger.error(msg, e)
        else logger.warn(msg)

        complete(
          StatusCodes.getForKey(e.errcode) getOrElse StatusCodes.InternalServerError,
          JObject("errcode" -> JInt(e.errcode), "errmsg" -> JString(e.errmsg)))
      }
    case e: Exception =>
      extractRequest { req =>
        logger.error(req.toString, e)
        complete(
          StatusCodes.InternalServerError,
          JObject("errcode" -> JInt(500), "errmsg" -> JString(e.getLocalizedMessage)))
      }
  }

  def apply(props: ContextProps)(implicit ec: ExecutionContextExecutor, mat: Materializer) =
    handleExceptions(customExceptionHandler) {
      pathPrefix("api") {
        healthCheck ~
          NewsRoute(props) ~
          BookRoute(props)
      }
    }

代码有一点长,现在分别解说。

customExceptionHandler

自定义的异常处理器,主要用于把自定义异常和系统异常转换成JSON消息输出,并设置相对应的HTTP状态码。

apply

apply方法定义了实现的API路由,由代码可以看到newsbook两个模块的路由分别由NewsRouteBookRoute两个文件定义。把相同功能的路由、服务、实体定义在同一个逻辑上下文(包)中,个人认为是一种更好的微服务实践。

book模块详解

book
├── Book.scala
├── BookContextProps.scala
├── BookRoute.scala
└── BookService.scala
  • Book:实体
  • BookContextProps:上下文属性,服务将在此实例化。并把接口混入ContextProps中。
  • BookRoute:API路由定义
  • BookService:服务功能实现

BookRotue定义

    pathPrefix("book") {
      pathEnd {
        post {
          entity(as[Book]) { book =>
            onSuccess(props.bookService.persist(book)) { result =>
              complete(StatusCodes.Created, result)
            }
          }
        }
      } ~
        path(Segment) { bookId =>
          get {
            complete(props.bookService.findOneById(bookId))
          } ~
            put {
              entity(as[Book]) { book =>
                complete(props.bookService.updateById(bookId, book))
              }
            } ~
            delete {
              complete(props.bookService.deleteById(bookId).map(id => Map("id" -> id)))
            }
        }

Akka-Http提供了高级routing DSL,可以很自然的定义出树型结构的RESTful风格的API。由代码可见,定义了4个API。分别对应insert、select、update、delete操作,由postgetputdelete4个指令实现对应操作的HTTP方法。

pathPrefixpathEndpath3个路径定义指令的区别在于pathPrefix代表由它定义的路径还并未终结,在它下面还有子路径。而path则代表它已经是最终的路径了,pathEnd是用于在使用了pathPrefix的情况下也可以直接访问由pathPrefix指定的路径。

Segment用于把由path定义的路径抽取成一个参数(bookId)。除了Segment用于抽取一个字符串类型,还有IntNumberLongNumber用于抽取路径为Int或Long类型。

entity指令用于抽取HTTP请求的body部分,并通过as[T]方法将其自动解析为指定类型。这里使用到了akka提供的Unmarshaller特性。这里通过JsonSupport里定义的json4sUnmarshaller将用户请求提交的JSON字符串映射到Book类型。

  implicit def json4sUnmarshaller[A: Manifest](implicit mat: Materializer): FromEntityUnmarshaller[A] =
    Unmarshaller.byteStringUnmarshaller
      .forContentTypes(MediaTypes.`application/json`)
      .mapWithCharset { (data, charset) =>
        val input = if (charset == HttpCharsets.`UTF-8`) data.utf8String else data.decodeString(charset.nioCharset().name)
        jsonSerialization.read(input)
      }

  implicit def json4sMarshaller[A <: AnyRef]: ToEntityMarshaller[A] =
    Marshaller.StringMarshaller.wrap(ContentType(MediaTypes.`application/json`, HttpCharsets.`UTF-8`))(v =>
      jsonSerialization.write[A](v))

自然json4sMarshaller则是把T类型的对象映射为JSON字符串响应给请求方。

BookService

再来看看BookService服务的实现。

 def updateById(bookId: String, book: Book)(implicit ec: ExecutionContext): Future[Book] = Future {
    if (bookId != book.id)
      throw MeConflictMessage(s"${book.id} is invalid,the ID is expected to be $bookId")

    val newBooks = BookService.books.filterNot(_.id == bookId)
    if (newBooks.size == BookService.books.size)
      throw MeNotFoundMessage(s"$bookId not found")

    BookService.books ::= book
    book
  }

  def persist(book: Book)(implicit ec: ExecutionContext): Future[Book] = Future {
    if (BookService.books.exists(_.id == book.id))
      throw MeConflictMessage(s"${book.id} exsits")

    BookService.books ::= book
    book
  }

  def deleteById(bookId: String)(implicit ec: ExecutionContext): Future[String] = Future {
    val newBooks = BookService.books.filterNot(_.id == bookId)
    if (newBooks.size == BookService.books.size)
      throw MeNotFoundMessage(s"$bookId not found")

    BookService.books = newBooks
    bookId
  }

  def findOneById(bookId: String)(implicit ec: ExecutionContext): Future[Book] = Future {
    BookService.books.find(_.id == bookId).getOrElse(throw MeNotFoundMessage(s"$bookId not found"))
  }

看到每个方法的返回值都被定义成了Future[T],akka-http是一个基于akka-actorakka-stream的异步HTTP工具集,使用Future可以提供整个系统的响应。我们这里直接使用Future来模拟异步访问(数据库操作)。

在每个方法中,我们校验参数是否有效。若校验失败则直接抛出自定义异常。Future函数将捕获异常,由之前定义的customExceptionHandler自定义异常处理器来将自定义异常转换成JSON消息发送给调用方,并设置匹配的HTTP状态码。

测试

百闻不如一试,下载代码实际操作下(下载地址在文章开头)。

运行程序:

./sbt
akka-action > runMain me.yangbajing.akkaaction.restapi.App

依次执行docs/scripts/restapi目录下的测试脚本,查看各请求下REST API的返回值(需要系统安装curl)。

  • ./get-book-aa.sh:正常返回ID为aa的书
  • ./get-book-bb.sh:查找ID为bb的书返回404
  • ./post-book.sh:创建一本ID为bb的书,返回201
  • ./get-book-bb.sh:正确返回ID为bb的书
  • ./put-book.hs:正确更新ID为bb的书
  • ./put-book-invalid.sh:无效的更新ID为aa的书,返回409
  • ./delete-book-aa.sh:成功的删除ID为aa的书
  • ./get-book-aa.sh:再次查找ID为aa的书返回404
  • ./delete-book-aa.sh:再次删除ID为aa的书时返回404

总结

akka-http是一个很有意思的HTTP工具库,它完整的实现了客户端和服务端编程工具,还支持WebScoket。基于akka-actorakka-stream,提供了高并发的异步编程模型。我们可以很快捷的实现出一个响应式(Reactive)Web Service。其提供的routing DSL可方便的定义出一套树型结构的API,很自然的匹配到RESTful风格的API设计。

点赞
收藏
评论区
推荐文章
blmius blmius
3年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为
待兔 待兔
5个月前
手写Java HashMap源码
HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程22
Jacquelyn38 Jacquelyn38
3年前
2020年前端实用代码段,为你的工作保驾护航
有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )
Wesley13 Wesley13
3年前
mysql设置时区
mysql设置时区mysql\_query("SETtime\_zone'8:00'")ordie('时区设置失败,请联系管理员!');中国在东8区所以加8方法二:selectcount(user\_id)asdevice,CONVERT\_TZ(FROM\_UNIXTIME(reg\_time),'08:00','0
Wesley13 Wesley13
3年前
00:Java简单了解
浅谈Java之概述Java是SUN(StanfordUniversityNetwork),斯坦福大学网络公司)1995年推出的一门高级编程语言。Java是一种面向Internet的编程语言。随着Java技术在web方面的不断成熟,已经成为Web应用程序的首选开发语言。Java是简单易学,完全面向对象,安全可靠,与平台无关的编程语言。
Stella981 Stella981
3年前
Django中Admin中的一些参数配置
设置在列表中显示的字段,id为django模型默认的主键list_display('id','name','sex','profession','email','qq','phone','status','create_time')设置在列表可编辑字段list_editable
Wesley13 Wesley13
3年前
ES6 新增的数组的方法
给定一个数组letlist\//wu:武力zhi:智力{id:1,name:'张飞',wu:97,zhi:10},{id:2,name:'诸葛亮',wu:55,zhi:99},{id:3,name:'赵云',wu:97,zhi:66},{id:4,na
Wesley13 Wesley13
3年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
Python进阶者 Python进阶者
11个月前
Excel中这日期老是出来00:00:00,怎么用Pandas把这个去除
大家好,我是皮皮。一、前言前几天在Python白银交流群【上海新年人】问了一个Pandas数据筛选的问题。问题如下:这日期老是出来00:00:00,怎么把这个去除。二、实现过程后来【论草莓如何成为冻干莓】给了一个思路和代码如下:pd.toexcel之前把这