Play 2.0 コードリーディング - リクエストを受ける(前)

一人Play 2.0 コードリーディングの第二弾です。

今回はリクエストを受けとった時にどのような処理をするかを追ってみました。
そもそも発端はPlay2.0ではリクエストをどのように処理させているのか気になり、
意図的に例外を発生させてみました。

すると、出力されたスタックトレースは以下のようなものだったのです。

play.core.ActionInvoker$$anonfun$receive$1$$anon$1: Execution exception [[Exception: null]]
        at play.core.ActionInvoker$$anonfun$receive$1.apply( ...
        at play.core.ActionInvoker$$anonfun$receive$1.apply( ...
        at akka.actor.Actor$class.apply( ...
        at play.core.ActionInvoker.apply( ...
        at akka.actor.ActorCell.invoke( ...
        at akka.dispatch.Mailbox.processMailbox( ...
Caused by: java.lang.Exception: null
        at controllers.Application$$anonfun$index$1.apply( ...
        at controllers.Application$$anonfun$index$1.apply( ...
        at play.api.mvc.Action$$anonfun$apply$4.apply( ...
        at play.api.mvc.Action$$anonfun$apply$4.apply( ...
        at play.api.mvc.Action$$anon$1.apply( ...
        at play.core.ActionInvoker$$anonfun$receive$1$$anonfun$4.ap ...

大本がakka.dispatch.Mailbox.processMailboxになっており、
Play2.0本体のスタックがほとんど出力されていません。
こりゃどういうことだ?と気になってソースを追ったのがこのエントリーです。

結論的を先に書いてしまうと、
Play2.0は以下の流れでリクエストを処理していました。

  1. リクエストを受けとるとNettyの仕組みを使って play.core.server.netty.PlayDefaultUpstreamHandlerがハンドリングする
  2. play.core.Serverを経由してplay.core.Router.RoutesがActionを決定する
  3. AkkaにActionを実行する為のメッセージが登録される
  4. AkkaからActionを実行する

これらの処理を順を追って説明したいと思います。
大半は自分のためのメモのため、ダラダラと長くなってしまったので前後編に分けます。


さて、Play2.0は内部的にはNettyを使用して動いています。
Play2.0自体はNettyのラッパーと言っても過言ではありません。
そのため、リクエストを受けとるとorg.jboss.netty.channel.SimpleChannelUpstreamHandlerのサブクラスでハンドリングします。

つまりそれは、play.core.server.netty.PlayDefaultUpstreamHandlerのことです。
リクエストを受けとるとPlayDefaultUpstreamHandlerのmessageReceivedメソッドが実行されます。

private[server] class PlayDefaultUpstreamHandler(server: Server, allChannels: DefaultChannelGroup) 
    extends SimpleChannelUpstreamHandler with Helpers 
    with WebSocketHandler with RequestBodyHandler {
  // Play2.0本体で、リクエストを受けとった時に一番最初に実行されるメソッド
  override def messageReceived(
      ctx: ChannelHandlerContext,
      e: MessageEvent) {
    e.getMessage match {
      case nettyHttpRequest: HttpRequest => {
        val keepAlive = isKeepAlive(nettyHttpRequest)
        val websocketableRequest = websocketable(nettyHttpRequest)
        var version = nettyHttpRequest.getProtocolVersion
        val nettyUri = new QueryStringDecoder(nettyHttpRequest.getUri)
        val parameters = Map.empty[String, Seq[String]] 
              ++ nettyUri.getParameters.asScala.mapValues(_.asScala)

        ...

        // Actionの取得
        val handler = server.getHandlerFor(requestHeader)

messageReceivedではリクエストヘッダーの解析等を行なった後、
server.getHandlerForでActionを取得する処理が行なわれます。

serverのクラスはplay.core.server.NettyServerクラスで、
getHandlerForの返り値の型はEither[Result, (Handler, Application)]です。
つまり、正常にActionが決定した場合rightでHandlerとApplicationのタプルを、
エラーが発生した場合はleftでResultを返すということです。
HandlerはActionがmix-inしているトレイトでメンバーを一切もたない、
マーカーの為の型です。


続いてgetHandleForの中身も見てみます。getHandleForの定義はplay.core.server.Serverトレイトにあります。

  def getHandlerFor(request: RequestHeader): Either[Result, (Handler, Application)] = {

    def sendHandler: Either[Throwable, (Handler, Application)] = {
      applicationProvider.get.right.map { application =>
        val maybeAction = application.global.onRouteRequest(request)

        // Actionが見つからなかった場合はNotFound用のHandlerを返す。
        (
          maybeAction.getOrElse(
            Action(BodyParsers.parse.empty)
              (_ => application.global.onHandlerNotFound(request))), 
            application
        )
    }
    def logExceptionAndGetResult(e: Throwable) = {
      // エラー発生時のハンドリング
      DefaultGlobal.onError(request, e)
    }

    sendHandler.left.map(logExceptionAndGetResult)
  }

getHandlerForのポイントは以下の三つです。

  1. play.core.ApplicationProviderからApplicationを取得しています。
  2. Actionの取得処理をapplication.global.onRouteRequest()に移譲しています。
  3. エラーのハンドリングをgetHandlerForで行なっています。Actionが見付からなかった場合はNotFoundを返すHandlerを返し、途中で例外が発生した場合はDefaultGlobal.onErrorを使ってInternalServerErrorのResultを返します。

applicationProviderの実際の型はPlayを実行しているコンテキストから、以下の三つのサブクラスの中から一つが選ばれます。

  1. StaticApplication - 本番稼動などで使用するApplicationProvider
  2. TestApplication - テスト用のApplicationProvider
  3. ReloadableApplication - Dev Mode用のリロードが出来るApplicationProvider

application.global.onRouteRequestではconfig/routesをもとに、
実際にActionを取得する処理を行なっていますが、続きは後編で解説します。