Akka-CQRS(12)- akka-http for http-web-service: Routing-服务项目接口

2019-06-24 10:35:12 浏览数 (1)

上篇提到,按当前对web-service功能需要,我们需要完成数据转换marshalling,服务接口routing这两部分的调研和示范。上篇已经完成了对序列化marshalling的讨论,这篇就介绍一下routing了。akka-http提供了一套功能强大,使用又很方便的Routing DSL。Route是个类型:

代码语言:javascript复制
type Route = RequestContext ⇒ Future[RouteResult]

实际上就是个把HttpRequest转换成HttpResponse的函数。举个例子:

代码语言:javascript复制
val route: Flow[HttpRequest, HttpResponse, NotUsed]=
      get {
        pathSingleSlash {
          complete(HttpEntity(ContentTypes.`text/html(UTF-8)`,"<html><body>Hello world!</body></html>"))
        } ~
          path("ping") {
            complete("PONG!")
          } ~
          path("crash") {
            sys.error("BOOM!")
          }
      }

这个route是个handler Flow, 但Route可以用RouteResult.route2HandlerFlow转换成Flow:

代码语言:javascript复制
/**
   * Turns a `Route` into a server flow.
   *
   * This conversion is also implicitly available through [[RouteResult#route2HandlerFlow]].
   */
  def handlerFlow(route: Route)(implicit
    routingSettings: RoutingSettings,
                                parserSettings:   ParserSettings,
                                materializer:     Materializer,
                                routingLog:       RoutingLog,
                                executionContext: ExecutionContextExecutor = null,
                                rejectionHandler: RejectionHandler         = RejectionHandler.default,
                                exceptionHandler: ExceptionHandler         = null): Flow[HttpRequest, HttpResponse, NotUsed] =
    Flow[HttpRequest].mapAsync(1)(asyncHandler(route))

...

implicit def route2HandlerFlow(route: Route)(
    implicit
    routingSettings:  RoutingSettings,
    parserSettings:   ParserSettings,
    materializer:     Materializer,
    routingLog:       RoutingLog,
    executionContext: ExecutionContext = null,
    rejectionHandler: RejectionHandler = RejectionHandler.default,
    exceptionHandler: ExceptionHandler = null
  ): Flow[HttpRequest, HttpResponse, NotUsed] =
    Route.handlerFlow(route)

route是由Directive类组合而成的一个决策树decision-tree。get、path、pathSingleSlash等都是Directive, 如:

代码语言:javascript复制
  def path[L](pm: PathMatcher[L]): Directive[L] = pathPrefix(pm ~ PathEnd)

然后complete返回Route类:

代码语言:javascript复制
  def complete(m: ⇒ ToResponseMarshallable): StandardRoute =
    StandardRoute(_.complete(m))
...
abstract class StandardRoute extends Route {
  def toDirective[L: Tuple]: Directive[L] = StandardRoute.toDirective(this)
}

Directive的主要功能就是对HttpRequest的Uri进行解析,找出具体的服务接口点,已经对entity里的数据进行调取。

Route是一种可组合组件。我们可以用简单的Route组合成更多层次的Route。下面是组合Route的几种方式:

1、Route转化:对输入的request,输出的response进行转化处理后把实际运算托付给下一层内部(inner)Route

2、筛选Route:只容许符合某种条件的Route通过并拒绝其它不符合条件的Route

3、链接Route:假如一个Route被拒绝,尝试下一个Route。这个是通过 ~ 操作符号实现的

在Akka-http的routing DSL里这些Route组合操作是通过Directive实现的。Akka-http提供了大量现成的Directive,我们也可以自定义一些特殊功能的Directive,详情可以查询官方文件或者api文件。

Directive的表达形式如下:

代码语言:javascript复制
dirname(arguments) { extractions =>
  ... // 内层inner route
}

下面是Directive的一些用例:

下面的三个route效果相等:

代码语言:javascript复制
val route: Route = { ctx =>
  if (ctx.request.method == HttpMethods.GET)
    ctx.complete("Received GET")
  else
    ctx.complete("Received something else")
}

val route =
  get {
    complete("Received GET")
  } ~
  complete("Received something else")
  
val route =
  get { ctx =>
    ctx.complete("Received GET")
  } ~
  complete("Received something else")

下面列出一些Directive的组合例子:

代码语言:javascript复制
val route: Route =
  path("order" / IntNumber) { id =>
    get {
      complete {
        "Received GET request for order "   id
      }
    } ~
    put {
      complete {
        "Received PUT request for order "   id
      }
    }
  }

def innerRoute(id: Int): Route =
  get {
    complete {
      "Received GET request for order "   id
    }
  } ~
  put {
    complete {
      "Received PUT request for order "   id
    }
  }
val route: Route = path("order" / IntNumber) { id => innerRoute(id) }

val route =
  path("order" / IntNumber) { id =>
    (get | put) { ctx =>
      ctx.complete(s"Received ${ctx.request.method.name} request for order $id")
    }
  }

val route =
  path("order" / IntNumber) { id =>
    (get | put) {
      extractMethod { m =>
        complete(s"Received ${m.name} request for order $id")
      }
    }
  }

val getOrPut = get | put
val route =
  path("order" / IntNumber) { id =>
    getOrPut {
      extractMethod { m =>
        complete(s"Received ${m.name} request for order $id")
      }
    }
  }

val route =
  (path("order" / IntNumber) & getOrPut & extractMethod) { (id, m) =>
    complete(s"Received ${m.name} request for order $id")
  }

val orderGetOrPutWithMethod =
  path("order" / IntNumber) & (get | put) & extractMethod
val route =
  orderGetOrPutWithMethod { (id, m) =>
    complete(s"Received ${m.name} request for order $id")
  }

我们可以从上面这些示范例子得出结论:Directive的组合能力是routing DSL的核心。来看看Directive的组合能力是如何实现的。Directive类定义如下:

代码语言:javascript复制
//#basic
abstract class Directive[L](implicit val ev: Tuple[L]) {

  /**
   * Calls the inner route with a tuple of extracted values of type `L`.
   *
   * `tapply` is short for "tuple-apply". Usually, you will use the regular `apply` method instead,
   * which is added by an implicit conversion (see `Directive.addDirectiveApply`).
   */
  def tapply(f: L ⇒ Route): Route
  ...
}
  /**
   * Constructs a directive from a function literal.
   */
  def apply[T: Tuple](f: (T ⇒ Route) ⇒ Route): Directive[T] =
    new Directive[T] { def tapply(inner: T ⇒ Route) = f(inner) }

  /**
   * A Directive that always passes the request on to its inner route (i.e. does nothing).
   */
  val Empty: Directive0 = Directive(_(()))
...
  implicit class SingleValueModifiers[T](underlying: Directive1[T]) extends AnyRef {
    def map[R](f: T ⇒ R)(implicit tupler: Tupler[R]): Directive[tupler.Out] =
      underlying.tmap { case Tuple1(value) ⇒ f(value) }

    def flatMap[R: Tuple](f: T ⇒ Directive[R]): Directive[R] =
      underlying.tflatMap { case Tuple1(value) ⇒ f(value) }

    def require(predicate: T ⇒ Boolean, rejections: Rejection*): Directive0 =
      underlying.filter(predicate, rejections: _*).tflatMap(_ ⇒ Empty)

    def filter(predicate: T ⇒ Boolean, rejections: Rejection*): Directive1[T] =
      underlying.tfilter({ case Tuple1(value) ⇒ predicate(value) }, rejections: _*)
  }
}

注意Directive.apply参数f: (T =>Route)=>Route), 代表 dirname (args){extractions => ...} 这样的构建函数款式。还有implicit ev: Tuple[L]是给compiler的证例,它要求Tuple[L]存在于可视域。Akka-http提供了所有22个TupleXX[L]的隐形实例。再注意implicit class singleValueModifiers[T]:它提供了多层Directive的自动展平,能够实现下面的自动转换结果:

代码语言:javascript复制
Directive1[T] = Directive[Tuple1[T]]
Directive1[Tuple2[M,N]] = Directive[Tuple1[Tuple2[M,N]]] = Directive[Tuple2[M,N]]
Directive1[Tuple3[M,N,G]] = ... = Directive[Tuple3[M,N,G]]
Directive1[Tuple4[M1,M2,M3,M4]] = ... = Directive[Tuple4[M1,M2,M3,M4]]
...
Directive1[Unit] = Directive0

Directive1,Directive0:

代码语言:javascript复制
  type Directive0 = Directive[Unit]
  type Directive1[T] = Directive[Tuple1[T]]

下面是这几种Directive的使用模式:

代码语言:javascript复制
  dirname { route }                  //Directive0
  dirname[L] { L => route }          //Directive1[L]
  dirname[T] { (T1,T2...) => route}  //Directive[T]

任何类型值到Tuple的自动转换是通过Tupler类实现的:

代码语言:javascript复制
/**
 * Provides a way to convert a value into an Tuple.
 * If the value is already a Tuple then it is returned unchanged, otherwise it's wrapped in a Tuple1 instance.
 */
trait Tupler[T] {
  type Out
  def OutIsTuple: Tuple[Out]
  def apply(value: T): Out
}

object Tupler extends LowerPriorityTupler {
  implicit def forTuple[T: Tuple]: Tupler[T] { type Out = T } =
    new Tupler[T] {
      type Out = T
      def OutIsTuple = implicitly[Tuple[Out]]
      def apply(value: T) = value
    }
}

private[server] abstract class LowerPriorityTupler {
  implicit def forAnyRef[T]: Tupler[T] { type Out = Tuple1[T] } =
    new Tupler[T] {
      type Out = Tuple1[T]
      def OutIsTuple = implicitly[Tuple[Out]]
      def apply(value: T) = Tuple1(value)
    }
}

好了,还是回到具体的Uri解析上来吧。在POS例子里需要上传的指令款式如下:

代码语言:javascript复制
http://192.168.11.189:2588/pos/logon?shopid=1101&opr=1010
http://192.168.11.189:2588/pos/logoff?shopid=1101
http://192.168.11.189:2588/pos/logsales?shopid=1101&acct=001&dpt=01&code=978111&qty=3&price=1200
http://192.168.11.189:2588/pos/shopid=1101&subtotal?level=0
http://192.168.11.189:2588/pos/shopid=1101&discount?disctype=2&grouped=true&code=481&percent=20

基本上全部是Uri Path解析的工作。下面是具体的Route示范:

代码语言:javascript复制
  val route =
    (pathPrefix("pos-on-cloud") & get) {
      ((pathSuffix("logon") & parameters('shopid.as[Int], 'opr)){ (id, op) =>
          complete(s"logon: shopid=$id and opr=$op")
      }
        ~ (pathSuffix("logoff") & parameter('shopid.as[Int])){ id =>
          complete(s"logoff: shopid=$id")
        }
        ~ (pathSuffix("logsales") & parameters(
        'shopid.as[Int],
        'acct,
        'dpt,
        'code,
        'qty.as[Int],
        'price.as[Int]
        )){ (id,acct,dpt,code,qty,price) =>
        complete(s"logsales: shopid=$id,$acct,$dpt,$code,$qty,$price")
        }
        ~ (pathSuffix("subtotal") & parameters('shopid.as[Int],'level)){ (id,l) =>
        complete(s"subtotal: shopid=$id, level=$l")
        }
      )
    }

用browser来测试:

代码语言:javascript复制
http://192.168.11.189:8011/pos-on-cloud/logsales?shopid=1101&acct=001&dpt=01&code=978111&qty=3&price=1200

logsales: shopid=1101,001,01,978111,3,1200

没错,解析正确!

0 人点赞