seratch's weblog in Japanese

About Scala, Java and Ruby programming in Japaense. If you need English information, go to http://blog.seratch.net/

Scalatra 2.0.3 ソースコードリーディング

Scalatra 2.0.3 を読みました

この記事は Scalatra 2.0.3 を対象に書かれています。

ScalatraServlet と ScalatraFilter

まず Scalatra のコントローラの基底 class/trait には ScalatraServlet と ScalatraFilter の二つがありますが、どのように使い分けるのでしょうか。

ScalatraServlet
abstract class ScalatraServlet
  extends javax.servlet.http.HttpServlet
  with org.scalatra.ScalatraKernel
  with org.scalatra.Initializable

Scalatra DSL の javax.servlet.http.HttpServlet を継承した実装です。
ほとんどの Scalatra アプリケーションではこの基底クラスを使うことが推奨されます。

以下のような場合は ScalatraServlet を使いましょう。

  • Scalatra のルーティングをあなたのアプリケーションの subcontext (コンテキストパスが ROOT でない)で実行したい場合
  • routes にマッチしなかったリクエストを完全にコントロールしたい場合
  • 単に静的コンテンツの serve を Filter でやりたいと考えた場合(それは ScalatraServlet でもできます)
  • ScalatraServlet と ScalatraFilter の違いがよくわかっていない場合

上記は scaladoc の抄訳です。ここで書かれている内容は実際に web.xml のサンプルを見ると分かりやすいと思います。

https://github.com/scalatra/scalatra/blob/master/example/src/main/webapp/WEB-INF/web.xml

ScalatraFilter
trait ScalatraFilter 
  extends javax.servlet.Filter 
  with org.scalatra.ScalatraKernel
  with org.scalatra.Initializable

Scalatra DSL の javax.servlet.Filter を implements した実装です。
以下のような場合には ScalatraServlet よりも ScalatraFilter を使う方がよいかもしれません。

  • URL 空間を他の servlet や filter と共有していて routes にマッチしなかったリクエストの処理を他に委譲したい場合(これはレガシーなアプリケーションとの統合にとても便利です)

ScalatraServlet と違って routes にマッチしなかった場合に HTTP ステータス 404 や 405 をレスポンスしません。
代わりに FilterChain で処理を委譲します。ScaltraFilter に半信半疑な場合は ScalatraServlet を使いましょう。

ScalatraKernel

ScalatraServlet も ScalatraFilter も ScalatraKernel のサブ型になっていて、この trait が Scalatra のコア部分です。

この trait の実装が実際にどのようになっているか、追ってみます。

trait ScalatraKernel 
  extends org.scalatra.Handler 
  with org.scalatra.CoreDsl 
  with org.scalatra.Initializable 
  with org.scalatra.servlet.ServletApiImplicits

ルーティングの仕組み (Scalatra 2.0.3)

org.scalatra.ScalatraKernel の中身をみると「get("/foo")」が「GET /foo」にどのようにルーティングされるかがわかります。

class App extends ScalatraServlet {
  get("/foo") { ... }
}

「"/foo"」が暗黙の型変換によって RouteMatcher に変換されて

protected implicit def string2RouteMatcher(path: String): RouteMatcher = new SinatraRouteMatcher(path, requestPath)

実際には、この get が呼ばれます。addRoute では Route を生成して ScalatraKernel 内に保持している routes(RouteRegistry) へ登録します。

def get(routeMatchers: RouteMatcher*)(action: => Any) = addRoute(Get, routeMatchers, action)


この処理を単純化したサンプルを書いてみました。get(path: String) というメソッドは定義されていませんが正常に動作することが分かります。

case class Path(path: String)
trait RoutingSupport {
  protected implicit def string2Path(path: String) = Path(path)
  def get(path: Path)(action: => Any) = {
    println("added new route: (" + path + " -> " + action + ")")
  }
}

class Application extends RoutingSupport {
  get("/foo") {
    "200 ok"
  }
}
val app = new Application
// "added new route: (Path(/foo) -> 200 ok)" と出力される


また、String 型だけでなく Boolean 型の名前渡し、 PathPattern 型、 Regex 型にも暗黙の型変換が定義されています。

protected implicit def booleanBlock2RouteMatcher(block: => Boolean): RouteMatcher = new BooleanBlockRouteMatcher(block)

Boolean 型の名前渡しは、以下のような例です。

get("/foo", request.getRemoteHost == "127.0.0.1", request.getRemoteUser == "admin") {
  // "/foo" への localhost からのリクエスト、かつユーザが admin の場合のみ
}

ルーティングの仕組み (Scalatra 2.1.0-SNAPSHOT)

リリースまでにまだ変更があるかもしれませんが、少しリファクタリングされています。

  • org.scalatra.ScalatraKernel
  • org.scalatra.ScalatraKernel.Routing

ScalatraKernel.Routing に暗黙の型変換と get post のような routing の DSL インタフェースが分離されています。

また「"/foo"」が暗黙の型変換によって RouteMatcher でなく RouteTransformer に変換されるようになりました。

protected implicit def string2RouteTransformer(path: String): RouteTransformer = Route.appendMatcher(path)

RouteTransformer は org.scalatra の package object に、Route を受け取って Route を返す関数への型エイリアスとして定義されています。

type RouteTransformer = (Route => Route)

get の引数型も RouteMatcher* から RouteTransformer* に変更になっていますが

def get(transformers: RouteTransformer*)(action: => Any): Route = addRoute(Get, transformers, action)

RouteMatcher も RouteTransformer に型変換することで互換性が保たれています。

protected implicit def routeMatcher2RouteTransformer(matcher: RouteMatcher): RouteTransformer = Route.appendMatcher(matcher)

リクエストを受けてからレスポンスするまでの流れ

ScalatraServlet でも ScalatraFilter でも ScalatraKernel に定義されたリクエストの処理は以下のメソッドが呼び出されるようになっています。

def handle(request: HttpServletRequest, response: HttpServletResponse)

内部で executeRoutes を呼び出して、さらに routes から実行する action を探し出して実行する runRoutes を呼んでいます。

protected def executeRoutes(): Any = {
  val result = try {
    runFilters(routes.beforeFilters) // before の処理を適用
    val actionResult = runRoutes(routes(request.method)).headOption // action の実行
    actionResult orElse matchOtherMethods() getOrElse doNotFound() // matchOtherMethods は 405、doNotFound は 404 を返す
  } catch {
    case e: HaltException => renderHaltException(e)
    case e => errorHandler(e) // 例外のハンドリング
  } finally {
    runFilters(routes.afterFilters) // after の処理を適用
  }
  renderResponse(result) // レスポンスボディのレンダリング
}

runRoutes の中では HTTP メソッドで絞り込まれた routes の中からマッチするものを探して invoke します。

protected def runRoutes(routes: Traversable[Route]) =
  for {
    route <- routes.toStream // toStream makes it lazy so we stop after match
    matchedRoute <- route()
    actionResult <- invoke(matchedRoute)
  } yield actionResult

「toStream makes it lazy so we after match」はこんな感じで、汎用的に応用できそうなテクニックです。

scala> for ( i <- Seq(1,2,3).toStream; j <- Seq(i*i) if i == 2 ) yield j
res0: scala.collection.immutable.Stream[Int] = Stream(4, ?)

このメソッドの結果は Stream になっているので executeRoutes ではそこから headOption で先頭を Option[Any] として取り出しています。

val actionResult = runRoutes(routes(request.method)).headOption

runRoutes の中の invoke では

protected def invoke(matchedRoute: MatchedRoute) =
  withRouteMultiParams(Some(matchedRoute)) {
    liftAction(matchedRoute.action)
  }

まず request(RichRequest*1 )に withRouteMultiParams で URL に埋まっているパラメータを追加して

protected def withRouteMultiParams[S](matchedRoute: Option[MatchedRoute])(thunk: => S): S = {
  val originalParams = multiParams
  request(MultiParamsKey) = originalParams ++ matchedRoute.map(_.multiParams).getOrElse(Map.empty)
  try { thunk } finally { request(MultiParamsKey) = originalParams }
}

liftAction で MatchedRoute から取り出した action: => Any を実行し結果を Optio[Any] で受け取ります。

private def liftAction(action: Action): Option[Any] = try { Some(action()) } catch { case e: PassException => None }


再び executeRoutes に戻って、最後のレスポンスボディのレンダリング部分です。

protected def executeRoutes(): Any = {
  val result = try { ... }
  // ...
  renderResponse(result)
}

renderResponse は actionResult を renderResponseBody に渡して

protected def renderResponse(actionResult: Any) {
  if (contentType == null) contentTypeInferrer.lift(actionResult) foreach { contentType = _ }
  renderResponseBody(actionResult)
}

renderResponseBody は loop に Unit 型が渡されるまで renderPipeline.lift(a) を引数に loop を再帰呼び出しします。

protected def renderResponseBody(actionResult: Any) {
  @tailrec def loop(ar: Any): Any = ar match {
    case r: Unit =>
    case a => loop(renderPipeline.lift(a) getOrElse ())
  }
  loop(actionResult)
}

renderPipeline の取得は以下の通りです。この RenderPipeline 型とは何者かというと PartialFunction[Any, Any] 型のエイリアスです*2

protected def renderPipeline: RenderPipeline = {
  case bytes: Array[Byte] => response.getOutputStream.write(bytes)
  case file: File => using(new FileInputStream(file)) { in => zeroCopy(in, response.getOutputStream) }
  case _: Unit => // If an action returns Unit, it assumes responsibility for the response
  case x: Any  => response.getWriter.print(x.toString)
}

renderResponseBody の renderPipeline.lift(a) は PartialFunction#lift(Any) の呼び出しです。

PartialFunction の apply はマッチしなかったら MatchError を throw しますが、lift はマッチすれば Some を返し、そうでなければ None を返します。

scala> val pf: PartialFunction[Any, Any] = { case s: String => s }
pf: PartialFunction[Any,Any] = <function1>

scala> pf.lift(123)
res5: Option[Any] = None

scala> pf.lift("abc")
res6: Option[Any] = Some(abc)

http://www.scala-lang.org/api/current/index.html#scala.PartialFunction


たとえば renderPipeline 内の「case x: Any => response.getWriter.print(x.toString)」にマッチすると Option にくるまれた Unit が返ります*3

scala> val pf: PartialFunction[Any, Any] = { case a: Any => println(a) }
pf: PartialFunction[Any,Any] = <function1>

scala> pf.lift("aaa")
aaa
res2: Option[Any] = Some(())

loop には Unit 型が渡ることになるので、ここが renderResponseBody での loop の再帰呼び出しの底になります。

2.0.3 の実装でいうと actionResult が Array[Byte] 型、 File 型以外はすべて #toString の結果がそのまま出力されるということになります。

templateEngine の仕組み(ScalateSupport)

Scalate を使う場合 ScalateSupport という trait を mixin します。この trait は ScalatraKernel のサブ型になっています*4

以下のフィールドが定義されているので、これを呼び出して使います。

protected[scalatra] var templateEngine: TemplateEngine = _

createTemplateEngine というメソッドによって初期化されます。

ServletTemplate は Scalate のクラスで ScalatraTemplateEngine は ScalateSupport 内に定義されている拡張部分です。

abstract override def initialize(config: Config) {
  super.initialize(config)
  templateEngine = createTemplateEngine(config)
}

protected def createTemplateEngine(config: Config): TemplateEngine =
  config match {
    case servletConfig: ServletConfig => new ServletTemplateEngine(servletConfig) with ScalatraTemplateEngine
    case filterConfig: FilterConfig => new ServletTemplateEngine(filterConfig) with ScalatraTemplateEngine
    case _ =>
      // Don't know how to convert your Config to something that
      // ServletTemplateEngine can accept, so fall back to a TemplateEngine
      new TemplateEngine with ScalatraTemplateEngine
    }

Scalatra の action 内で以下のように Scalate を呼び出すことができますが

layoutTemplate("index.ssp", Map("foo" -> "uno", "bar" -> "dos"))
ssp("index.ssp", Map("foo" -> "uno", "bar" -> "dos"))

いずれも中では Scalate の TemplateEngine#layout をそのまま呼んでいるだけです。

http://scalate.fusesource.org/maven/1.5.3/scalate-core/scaladocs/org/fusesource/scalate/servlet/ServletTemplateEngine.html

templateEngine.layout("index.ssp", Map("foo" -> "uno", "bar" -> "dos"))

結果として String 型が返ってきて、それが ScalatraKernel#renderResponseString でそのまま出力されることになります。

*1:HttpServletRequest の拡張でそれ自身が mutable な Map になっています

*2:org.scalatra の package object に定義されています

*3:override しなかった場合 Array[Byte] や File の場合も Unit を返しているので loop は一階しか再帰しませんが

*4:self-type で ScalatraKernel 型をとる trait に書き換えることができそうです