Reading LiftFilter Bootup
さて、始めてみる。どのようにLiftがどのようにリクエストをさばくのかを読むため、通常、web.xmlで唯一宣言されている「LiftFilter」辺りから読んでいく。幸い、http://www.assembla.com/spaces/liftweb/wiki/HTTP_Pipelineにヒントがある。
Bootup 起動
- 妥当であれば、サーブレットコンテキストをセットする
- サーブレットフィルタ設定から、LiftブートローダのFQCN(Fully Qualified Class Name、完全修飾クラス名)をチェックする。
- LiftRulesをタッチし、クラスパスサービスをステートレスディスパッチテーブルに追加する。これにより、レスキューサーバが動けるようになる。
- 2.で取得したFQCNからリフレクトし、Liftを起動する。デフォルトではbootstrap.liftweb.Boot。
- Liftが起動したら、内部リソースバンドルを読み込む。LiftRules.liftCoreResourceNameで定義された…ところから。
- productionモードかどうかチェックし、そうであれば、LiftRules.templateCacheを通じて、メモリ内にテンプレートキャッシュが設定される。500まで。
- ここまで終われば、doneBootフラグをtrueにする
- LiftServletにサーブレットコンテキストを渡し、インスタンスを生成する。
http://www.assembla.com/spaces/liftweb/wiki/HTTP_Pipeline (をなんとなく和訳)
予備知識として、サーブレットフィルタとはリクエストをサーブレットに渡す手前で通るフィルタで、initメソッドで初期化、doFilterメソッドでリクエストをフィルタ、destoryメソッドで終了処理をするらしい*1。ということで、Boot処理はLiftFilterクラスのinitメソッドでされるようだ。ここから読む。
LiftFilter...
import provider.servlet._ class LiftFilter extends ServletFilterProvider
def init(config: FilterConfig) { ctx = new HTTPServletContext(config.getServletContext) LiftRules.setContext(ctx) bootLift(Box.legacyNullTest(config.getInitParameter("bootloader"))) }
- インスタンスメンバであるctxに値をセット
- HTTPContextはメソッドの抽象的定義のみ
- HTTPServletContextはほぼラップしてるだけ
- LiftRulesのメンバとしてctxをセット
- bootLiftを呼ぶ。引数として、"bootloader"パラメータの値が指定されていればその内容をBoxとしてラップ*2、指定されていなければEmpty
- FilterConfig#getInitParameter("bootloader")で、パラメータが指定されていればその値、なければnull
- Box#legacyNullTestは、引数がnullならEmpty、値があればFullでラップする
たとえば、bootLiftメソッドは誰が持っているか、HTTPContextはどのパッケージかなど、わかりにくい点も多い。
bootLiftメソッドの持ち主は、継承したクラス/トレイトのどこかか、オブジェクトの「._」でimportしたもの(今の場合はimport Helpers._。)のどこかにある。今回はtrait HTTPProviderで見つかった。
クラス名は気合いで予想する。*3
Eclipse Scala IDEの設定が上手く行っていれば、Ctrl+左クリックで参照できたりする(F3は効かなかった)。ただ、Scala IDEはあまりにも遅かったので、今はEmacs + Ensimeで頑張っているが、Ensimeだと上手く参照できない…かもしれない。まだよくわからない。
/** * Executes Lift's Boot and makes necessary initializations */ protected def bootLift(loader: Box[String]): Unit = { try { val b: Bootable = loader.map(b => Class.forName(b).newInstance.asInstanceOf[Bootable]) openOr DefaultBootstrap preBoot b.boot } catch { case e => logger.error("Failed to Boot! Your application may not run properly", e); } finally { postBoot actualServlet = new LiftServlet(context) actualServlet.init } }
先の説明からみれば、ここにおおむねの流れがある様子だ。
val b: Bootable = loader.map(b => Class.forName(b).newInstance.asInstanceOf[Bootable]) openOr DefaultBootstrap
- Box#mapは、Boxの中身を操作した結果をBoxでラップしなおして返す
- Box#openOrは、Boxの中身か、Emptyであれば第一引数(今の場合はDefaultBootstrap)を返す
- 引数が一つだけのメソッドは、「.」と引数の括弧を省略できることになっている
ので、指定されたbootloader値のクラスか、指定されていなければDefaultBootstrapをb:Bootableとしている。
preBoot
private def preBoot() { // do this stateless LiftRules.statelessDispatchTable.prepend(NamedPF("Classpath service") { case r@Req(mainPath :: subPath, suffx, _) if (mainPath == LiftRules.resourceServerPath) => ResourceServer.findResourceInClasspath(r, r.path.wholePath.drop(1)) }) }
- LiftRules.statelessDispatchTableは、DispatchPF型を取るRulesSeqオブジェクトで、prependメソッドによって、起動中に限り、引数の関数を自身内部に追加している
- 引数はNamedPF、名前付きのPartialFunction。"Classpath service"という名前で、{}の部分関数を渡している。たぶん、あとで必要なときに実行されることになる。
- case r@Req(mainPath :: subPath, suffx, _)は、mainPathとsubPathからなるなんらかのList(たぶん)と、suffxと、もう一つ任意をとるReqオブジェクトである場合、そのReqオブジェクトをrとして、=>以下をごりごりする、という意味
- なはずだが、Reqのapplyをみてもそれっぽいモノはない。ないんだよなー。なんとなくは読めるんだが…。
- かつ、mainPathがLiftRules.resourceServerPath : String、通常は"classpath"と同値であれば:
- ResourceServer.findResourceInClasspathからレスポンスを返しているらしい
- ResourceServerでは、Lift組み込みのcssやjsを返しているらしいのはわかっているので、深追いはやめよう
- ResourceServer.findResourceInClasspathからレスポンスを返しているらしい
- case r@Req(mainPath :: subPath, suffx, _)は、mainPathとsubPathからなるなんらかのList(たぶん)と、suffxと、もう一つ任意をとるReqオブジェクトである場合、そのReqオブジェクトをrとして、=>以下をごりごりする、という意味
- 引数はNamedPF、名前付きのPartialFunction。"Classpath service"という名前で、{}の部分関数を渡している。たぶん、あとで必要なときに実行されることになる。
つまり、組み込みcssやjsを返すdispatcher(割り振り屋さん)を定義している。例えばリクエストされたアドレスが「http://example.com/classpath/some.css」であれば、このディスパッチャがResourceServerで管理するファイルを返している、のような感じ。
b.boot
とりあえず、無指定の場合に使われるDefaultBootstrapを追う。
private[http] case object DefaultBootstrap extends Bootable { def boot(): Unit = { val f = createInvoker("boot", Class.forName("bootstrap.liftweb.Boot").newInstance.asInstanceOf[AnyRef]) f.map {f => f()} } }
createInvokerはClassHelpersにあり、第二引数にあるインスタンスが持つ第一引数の名前のメソッドを起動させる関数をBoxでラップしている。ややこしい。
で、f.map{f => f()}でそれが起動され、結果は捨てられる(Unitだから、そう考えていいよね?)。
つまり、bootstrap.liftweb.Boot#bootが起動されますよってことだ。問題なければ。
catch句はとりあえず飛ばそう。次。
postBoot
private def postBoot { try { ResourceBundle getBundle (LiftRules.liftCoreResourceName) if (Props.productionMode && LiftRules.templateCache.isEmpty) { // Since we're in productin mode and user did not explicitely set any template caching, we're setting it LiftRules.templateCache = Full(InMemoryCache(500)) } } catch { case _ => logger.error("LiftWeb core resource bundle for locale " + Locale.getDefault() + ", was not found ! ") } finally { LiftRules.bootFinished() } }
ResourceBundleはjavaのソレ。LiftRules.liftCoreResourceNameは、標準では "i18n.lift-core" となっていて、つまり src/main/resources/i18n.lift-core_ja_JP.properties などを作れば、日本語メッセージファイルとしてあつかわれる。
Props.productionModeは、つまるところ System.getProperty("run.mode") の値から判定している。で、かつtemplateCacheがboxのEmptyであれば、InMemoryCacheを設定するわけだ。InMemoryCasheはいわゆるLRUキャッシュを使っているらしい。
で、finallyブロックでの LiftRules.bootFinished() により、内部の_doneBootフラグをtrueにする。ブート処理完了。
actualServlet = new LiftServlet(context)
private var servletContext: HTTPContext = null def this(ctx: HTTPContext) = { this () this.servletContext = ctx }
コンテキストを与えて、LiftServletは内部に保持するのみ。
なぜval扱いにしないのだろか?引数なしコンストラクタも用意しているの?だとしたらその意味は?
actualServlet.init
def init = { LiftRules.ending = false }
終了処理フラグ(って書くと古きよきSI業界っぽいけど)を折っておく。
まとめ
Bootupは以上です。
dispatcherの設定とか、のちのち関わるだろう部分があるので、その辺は読み進めていく中で色々わかるだろーということで、何となくそんなモノがある程度の理解で進めていけばいいと思います(上から目線)。