上篇提到,按当前对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
没错,解析正确!