Akka-CQRS(12)- akka-http for http-web-service: Routing-服務專案介面

雪川大蟲發表於2019-06-20

   上篇提到,按當前對web-service功能需要,我們需要完成資料轉換marshalling,服務介面routing這兩部分的調研和示範。上篇已經完成了對序列化marshalling的討論,這篇就介紹一下routing了。akka-http提供了一套功能強大,使用又很方便的Routing DSL。Route是個型別:

type Route = RequestContext ⇒ Future[RouteResult]

實際上就是個把HttpRequest轉換成HttpResponse的函式。舉個例子: 

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:

  
/**
   * 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, 如:

  def path[L](pm: PathMatcher[L]): Directive[L] = pathPrefix(pm ~ PathEnd)

然後complete返回Route類:

  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的表達形式如下: 

dirname(arguments) { extractions =>
  ... // 內層inner route
}

下面是Directive的一些用例: 

下面的三個route效果相等:

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的組合例子:

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類定義如下:

//#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的自動展平,能夠實現下面的自動轉換結果:

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:

  type Directive0 = Directive[Unit]
  type Directive1[T] = Directive[Tuple1[T]]

下面是這幾種Directive的使用模式:

  dirname { route }                  //Directive0
  dirname[L] { L => route }          //Directive1[L]
  dirname[T] { (T1,T2...) => route}  //Directive[T]

任何型別值到Tuple的自動轉換是通過Tupler類實現的:

/**
 * 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例子裡需要上傳的指令款式如下:

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示範:

  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來測試:

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

沒錯,解析正確!

相關文章