73
71

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Play Frameworkのソースコードリーディング Action周り

Last updated at Posted at 2014-07-23

今回のゴール

以下のコードの舞台裏を理解することが今回のゴール。

val hello = Action {
  Ok("hello")
}

def echo = Action { request =>
  Ok("Got [" + request.body + "]")
}

なお、対象のPlay Frameworkのバージョンは2.3.2。

はじめに

Actionがやることは一言でいうとHTTPリクエストを受け取ってHTTPレスポンスを返す、要はこれ。
Play Frameworkでこれを実施するにあたってHTTPリクエストとHTTPレスポンスに該当するものを先に整理しておく。

HTTPリクエスト

RequestはRequestHeaderを継承しており、両者の違いは前者がbodyなし、後者はbodyありということ。
以下のように記述した際のrequestの型はリクエストボディ部のパースが行われた後者のRequest型の方となる。

def myAction = Action { request =>

※但し、Request型といってもGET時はそもそもbodyが無いのでbody.asTextとやっても何も取れない。

HTTPレスポンス

Resultの定義は以下で、構成要素として主にレスポンスヘッダとバイト配列のボディからなることが分かる。

case class Result(header: ResponseHeader, body: Enumerator[Array[Byte]], connection: HttpConnection.Connection = HttpConnection.KeepAlive) {

Enumeratorでくるまれているのは、通常HTTPレスポンスはArray[Byte]が一発で返るというよりもむしろ複数回に分けて返されることが多いからで、その辺の仕組みが提供されている。
もちろんレスポンスだけでなく、リクエストボディも同様である。
ここについては「やさしいIteratee入門」が分かりやすいので読みましょう。
http://www.slideshare.net/TakashiKawachi/iteratee

Ok("hello")

さて、Ok("hello")の部分から見ていくことにする。

このOkはResultsトレイト内に宣言されているStatusクラスのインスタンスである。

/** Generates a ‘200 OK’ result. */
val Ok = new Status(OK)

この他にもNotFound(404)やServiceUnavailable(503)なども定義されている。

このStatusというクラスは同じくResultsトレイト内にて定義されている。

class Status(status: Int) extends Result(header = ResponseHeader(status), body = Enumerator.empty, connection = HttpConnection.KeepAlive) {

なお、new Status(OK)のOKは以下のStandardValues.scalaに宣言されている、Statusの定数。
https://github.com/playframework/playframework/blob/2.3.0/framework/src/play/src/main/scala/play/api/http/StandardValues.scala

StandardValues.scalaにはステータスコードを表すStatus以外にも

  • MimeTypes("text/html"や"application/octet-stream"など)
  • ContentTypes("text/html; charset=utf-8"など)
  • HttpVerbs("GET"や"POST"など)
  • HeaderNames("Accept-Language"や"ETag"など)
  • HttpProtocols("HTTP/1.1"など)

といった各種定数クラスが用意されている。
これらtraitはControllerにmix-inされているので、使用したい場合はAtion定義時にimportなしで使用することが出来る。覚えておくといいだろう。

trait Controller extends Results with BodyParsers with HttpProtocol with Status with HeaderNames with ContentTypes with RequestExtractors with Rendering {

で、話は戻ってOk("hello")、これはStatusインスタンスOkに("hello")とやっているわけで、要はStatus#applyを実行している。

def apply[C](content: C)(implicit writeable: Writeable[C]): Result = {
  Result(
    ResponseHeader(status, writeable.contentType.map(ct => Map(CONTENT_TYPE -> ct)).getOrElse(Map.empty)),
    Enumerator(writeable.transform(content))
  )
}

ここで実際にResultを生成して返している。
statusコード、content-type、レスポンスボディ部が定義される。

Writeableは型パラメータになっているのでコンパイル時に型に応じた適切なものが適用される。
今回は"hello"を渡しているのでcontentはString型となりWriteable[String]となる。

この他に、Writeable[JsValue]、Writeable[Xml]、Writeable[Array[Byte]]などがある。

Action

次にAction部。

Action {
  Ok("hello")
}

これはobject Actionのapplyメソッドを実行している。
Actionオブジェクトの宣言部は以下。

object Action extends ActionBuilder[Request] {
  def invokeBlock[A](request: Request[A], block: (Request[A]) => Future[Result]) = block(request)
}

ここではapplyが定義されていないので実際は継承元のActionBuilderのapplyが呼ばれている。

final def apply(block: => Result): Action[AnyContent] = apply(_ => block)

applyの引数はResult。
AnyContentなActionインスタンスを返すのがこのapply関数だということが分かる。
さらにチェーンして、

final def apply(block: R[AnyContent] => Result): Action[AnyContent] = apply(BodyParsers.parse.anyContent)(block)

冒頭の例のこっちのタイプなら上記applyから始まる。Requestを受け取ってResultを返す関数が引数。

Action{ request =>
  Ok("Got [" + request.body + "]")
}

さらにチェーンして、

final def apply[A](bodyParser: BodyParser[A])(block: R[A] => Result): Action[A] = async(bodyParser) { req: R[A] =>
    Future.successful(block(req))

bodyParserが登場。ここは最後に取り上げるが、役割はHTTPリクエストのbody部をパースしてArray[Byte]からStringやXmlを生成すること。
さらにチェーンして、

final def async[A](bodyParser: BodyParser[A])(block: R[A] => Future[Result]): Action[A] = composeAction(new Action[A] {
  def parser = composeParser(bodyParser)
  def apply(request: Request[A]) = try {
    invokeBlock(request, block)
  } catch {
    // NotImplementedError is not caught by NonFatal, wrap it
    case e: NotImplementedError => throw new RuntimeException(e)
    // LinkageError is similarly harmless in Play Framework, since automatic reloading could easily trigger it
    case e: LinkageError => throw new RuntimeException(e)
  }
  override def executionContext = ActionBuilder.this.executionContext
})

これがapplyチェーンの最後で、ここでようやくActionインスタンスが生成されて返ることになる。
なお、asyncということで戻りもこれまでのResultからFuture[Result]になっているものの、このapplyが呼ばれる前のapplyで、Future.successful(block(req))とやっていることから実行済みFutureを作成しているので(通常呼び出し時は)本スレッドと別にblockが並列実行されるわけではない事がわかる。

ActionインスタンスのinvokeBlockで定義したblockが使われているのが見て取れる。ActionインスタンスにRequestインスタンスが渡ってきた時に定義したblock部が実行される。

まとめ

一度ここでまとめると、上記でも少し触れたが以下のコードは、

def echo = Action { request =>
  Ok("Got [" + request.body + "]")
}

object ActionのapplyにRequest => Resultな関数を渡すことでActionインスタンスを生成しているということ。

実際に処理されるActionクラスの方のapplyを見てみると以下のようになっている。

def apply(rh: RequestHeader): Iteratee[Array[Byte], Result] = parser(rh).mapM {
  case Left(r) =>
    Play.logger.trace("Got direct result from the BodyParser: " + r)
    Future.successful(r)
  case Right(a) =>
    val request = Request(rh, a)
    Play.logger.trace("Invoking action with request: " + request)
    Play.maybeApplication.map { app =>
      play.utils.Threads.withContextClassLoader(app.classloader) {
        apply(request)
      }
    }.getOrElse {
      apply(request)
    }
}(executionContext)

ここを理解するにはBodyParserを理解する必要がありそうだ。

BodyParserとAnyContent

長くなったので分けました。
続きはこちら

73
71
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
73
71

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?