GeekFactory

int128.hatenablog.com

App EngineでUnfiltered filterとScalateを使う

App EngineでUnfiltered filterとScalateを使う場合のメモです。

Unfiltered filter

クラスパスに依存関係を追加するだけでOKです。App Engine特有の考慮は必要ありませんでした。spin-upも早いので問題なさそうです。

import unfiltered.request._
import unfiltered.response._

class App extends unfiltered.filter.Plan {
  def intent = {
    case GET(Path("/something")) => ResponseString("Hello")
  }
}

Scalate

いくつか考慮が必要です。

Scalateは実行時にテンプレートをコンパイルする機能があります。App Engineでは(開発サーバも含めて)ファイルの書き込みが禁止されているため、例外が発生します。そのため、あらかじめテンプレートをコンパイルしてクラスパスに配置しておく必要があります。

  • TemplateEngineの初期化時に以下を設定する必要があります。これをやらないと実行時にコンパイルしようとして例外が発生します。
    • allowReload = falseに設定します。
    • resourceLoaderにServletResourceLoaderを設定します。
    • workingDirectoryを設定します。どこでも構いません。
    • ハマった場合はデバッガでステップ実行すると解決しやすいです。
  • あらかじめテンプレートをコンパイルしておく必要があります。
  • spin-upは比較的早いようです。

Unfilteredと組み合わせる方法はいくつかあるようですが、テンプレートの生成結果が小さいサイズであればStringで受け取る方法が最も簡単です。App Engineでは定期的にJVMが再起動するので、ヒープメモリへの影響は比較的少ないのではないかと。

import java.io.File
import org.fusesource.scalate.servlet.ServletResourceLoader
import org.fusesource.scalate.TemplateEngine
import unfiltered.request._
import unfiltered.response._

class App extends unfiltered.filter.Plan {
  def intent = {
    case GET(Path("/something")) =>
      HtmlContent ~> ResponseString(scalate.layout("/something.mustache", Map("key" -> "value")))
  }

  lazy val scalate = {
    val engine = new TemplateEngine
    engine.workingDirectory = new File("WEB-INF")
    engine.resourceLoader = new ServletResourceLoader(config.getServletContext)
    engine.allowReload = false
    engine
  }
}

(1/17追記) レスポンスのContent-Typeを設定するように修正しました。

(1/17追記) 下記のようにcase classを定義してもおk。

  def intent = {
    case GET(Path("/something")) =>
      Scalate("/something.mustache", Map("key" -> "value"))
  }

  case class Scalate(path: String, params: Map[String, Any]) extends
    ComposeResponse(HtmlContent ~> ResponseString(scalate.layout(path, params)))

Gradleでテンプレートをコンパイルする場合のビルドスクリプトも載せておきます。

dependencies {
    def scalaVersion = '2.10.3'
    compile 'org.fusesource.scalate:scalate-core_2.10:1.6.1'
    compile "org.scala-lang:scala-library:$scalaVersion"
    compile "org.scala-lang:scala-compiler:$scalaVersion"
}

task compileScalate << {
    def cl = Thread.currentThread().contextClassLoader
    configurations.runtime.each { f -> cl.addURL(f.toURI().toURL()) }
    cl.loadClass('org.fusesource.scalate.support.Precompiler').newInstance().with {
        sources_$eq(file('src/main/webapp'))
        contextClass_$eq('org.fusesource.scalate.DefaultRenderContext')
        targetDirectory_$eq(sourceSets.main.output.classesDir)
        execute()
    }
}
tasks.classes.dependsOn(tasks.compileScalate)

Unfilteredはドキュメントはいまいちですが、ルーティングのコードが分かりやすいのでいい感じ。Scalateもテンプレートの選択肢が多いのでいいですね。