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 をそのまま呼んでいるだけです。
templateEngine.layout("index.ssp", Map("foo" -> "uno", "bar" -> "dos"))
結果として String 型が返ってきて、それが ScalatraKernel#renderResponseString でそのまま出力されることになります。