Reading LiftFilter Request

さて。起動部分は大雑把に見たので、実際のリクエストに対する処理を見てみる。

サーブレットフィルタはリクエストを捉え、LiftFilter#doFilterを通してリクエストを処理させる。

ということで、LiftFilterの親クラス、 net.liftweb.http.provider.servlet.ServletFilterProviderのdoFilter から見てみることにする。

ServletFilterProvider#doFilter

  /**
   * Executes the Lift filter component.
   */
  def doFilter(req: ServletRequest, res: ServletResponse, chain: FilterChain) = {
    if (LiftRules.ending) chain.doFilter(req, res)
    else {
      LiftRules.reqCnt.incrementAndGet()
      try {
        TransientRequestVarHandler(Empty,
                                   RequestVarHandler(Empty,
                                                     (req, res) match {
              case (httpReq: HttpServletRequest, httpRes: HttpServletResponse) =>
                val httpRequest = new HTTPRequestServlet(httpReq, this)
                val httpResponse = new HTTPResponseServlet(httpRes)

                service(httpRequest, httpResponse) {
                  chain.doFilter(req, res)
                }
              case _ => chain.doFilter(req, res)
            }))
      } finally {LiftRules.reqCnt.decrementAndGet()}
    }
  }

net/liftweb/http/provider/servlet/ServletFilterProvider.scala

LiftRules.ending
  @volatile private[http] var ending = false

/net/liftweb/http/LiftRules.scala

正確にはこの先の話になるけど、よーするにサーブレットのdestory中ではないことを確認している(と思われる)フラグ。

true、つまり終了処理中の場合、chan.doFilter(req, res)で、他のサーブレットフィルタに渡すだけにして、else以下の本体的部分にわたらないようにしている。

で、elseの場合:

LiftRules.reqCnt.incrementAndGet(); try{...} finally {LiftRules.reqCnt.decrementAndGet()}
  private[http] val reqCnt = new AtomicInteger(0)

/net/liftweb/http/LiftRules.scala

java.util.concurrent.atomic
クラス AtomicInteger

原子的な更新が可能な int 値です。原子変数のプロパティーの詳細は、java.util.concurrent.atomic パッケージ仕様を参照してください。AtomicInteger は、原子的に増分されるカウンタなどのアプリケーションで使用されます。

(...)

現在の値を 1 だけ原子的に増分します。

戻り値:
更新された値

http://java.sun.com/javase/ja/6/docs/ja/api/java/util/concurrent/atomic/AtomicInteger.html

はい、リクエストを受け付けた回数をカウントしているわけですね。で、最後にカウンタを減らしている。

コレもサーブレットの終了処理中にいきなり終わらないように、処理中のカウンタを用意してるわけですね。たぶん。

さて、次はtryの中身。

        TransientRequestVarHandler(Empty,
                                   RequestVarHandler(Empty,
                                                     (req, res) match {
              case (httpReq: HttpServletRequest, httpRes: HttpServletResponse) =>
                val httpRequest = new HTTPRequestServlet(httpReq, this)
                val httpResponse = new HTTPResponseServlet(httpRes)

                service(httpRequest, httpResponse) {
                  chain.doFilter(req, res)
                }
              case _ => chain.doFilter(req, res)
            }))

net/liftweb/http/provider/servlet/ServletFilterProvider.scala

ややこしくなってきた。

TransientRequestVarHandlerには、引数としてEmptyとRequestVarHandlerが何かのオブジェクトを作って渡して、全体としてオブジェクトを作っている。ちなみに、こういうときはobject TransientRequestVarHanderのapply関数が呼ばれることになっている。

RequestVarHanderには、引数としてEmptyと(req, res)のmatch式の結果を与えている。

(req, res)のmatch式内部では、reqがHttpServletRequest、かつresがHttpServletResponseであるときに、service関数に両者をラップしただろうものを渡しつつ、{...}で関数ブロックを渡している。

service関数は親クラス、net.liftweb.http.provider.HTTPProvider内にあるようだ。

さて、どこから読もうか、serviceが本体っぽいので、ここから読もう。リクエストのラッパーはその中で出てくるだろー。

HTTPProvider#service

  /**
   * Call this function in order for Lift to process this request
   * @param req - the request object
   * @param resp - the response object
   * @param chain - function to be executed in case this request is supposed to not be processed by Lift
   */
  protected def service(req: HTTPRequest, resp: HTTPResponse)(chain: => Unit) = {
    tryo {
      LiftRules.early.toList.foreach(_(req))
    }

    val newReq = Req(req, LiftRules.statelessRewrite.toList,
                     LiftRules.statelessTest.toList, System.nanoTime)

    CurrentReq.doWith(newReq) {
      URLRewriter.doWith (
	url =>
          NamedPF.applyBox(
	    resp.encodeUrl(url),
            LiftRules.urlDecorate.toList
	  ) openOr resp.encodeUrl(url)
      ) {
	if (
	  !(isLiftRequest_?(newReq) && 
	    actualServlet.service(newReq, resp))
	) {
	  chain
	}
      }
    }
  }

net/liftweb/http/provider/HTTPProvider.scala

さて、いよいよという感じがあるが、よく見ると「actualServlet.service」なんてモノがあるので、まだここは準備でしかなさそう。

tryo {...}
  /**
   * Wraps a "try" block around the function f
   * @param f - the block of code to evaluate
   * @return <ul>
   *   <li>Full(result of the evaluation of f) if f doesn't throw any exception
   *   <li>a Failure if f throws an exception
   *   </ul>
   */
  def tryo[T](f: => T): Box[T] = tryo(Nil, Empty)(f)

net/liftweb/util/ControlHelpers.scala

(あとでかく)

Reading LiftFilter Bootup

さて、始めてみる。どのようにLiftがどのようにリクエストをさばくのかを読むため、通常、web.xmlで唯一宣言されている「LiftFilter」辺りから読んでいく。幸い、http://www.assembla.com/spaces/liftweb/wiki/HTTP_Pipelineにヒントがある。

Bootup 起動

  1. 妥当であれば、サーブレットコンテキストをセットする
  2. サーブレットフィルタ設定から、LiftブートローダFQCN(Fully Qualified Class Name、完全修飾クラス名)をチェックする。
  3. LiftRulesをタッチし、クラスパスサービスをステートレスディスパッチテーブルに追加する。これにより、レスキューサーバが動けるようになる。
  4. 2.で取得したFQCNからリフレクトし、Liftを起動する。デフォルトではbootstrap.liftweb.Boot。
  5. Liftが起動したら、内部リソースバンドルを読み込む。LiftRules.liftCoreResourceNameで定義された…ところから。
  6. productionモードかどうかチェックし、そうであれば、LiftRules.templateCacheを通じて、メモリ内にテンプレートキャッシュが設定される。500まで。
  7. ここまで終われば、doneBootフラグをtrueにする
  8. 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

https://github.com/lift/framework/blob/2.3/web/webkit/src/main/scala/net/liftweb/http/LiftServlet.scala

  def init(config: FilterConfig) {
    ctx = new HTTPServletContext(config.getServletContext)

    LiftRules.setContext(ctx)

    bootLift(Box.legacyNullTest(config.getInitParameter("bootloader")))

  }

https://github.com/lift/framework/blob/2.3/web/webkit/src/main/scala/net/liftweb/http/provider/servlet/ServletFilterProvider.scala

  1. インスタンスメンバであるctxに値をセット
  2. LiftRulesのメンバとしてctxをセット
  3. 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
      }
    }

https://github.com/lift/framework/blob/2.3/web/webkit/src/main/scala/net/liftweb/http/provider/HTTPProvider.scala

先の説明からみれば、ここにおおむねの流れがある様子だ。

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))
    })
  }

https://github.com/lift/framework/blob/2.3/web/webkit/src/main/scala/net/liftweb/http/provider/HTTPProvider.scala

  • 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を返しているらしいのはわかっているので、深追いはやめよう

つまり、組み込み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()
    }
  }

https://github.com/lift/framework/blob/2.3/web/webkit/src/main/scala/net/liftweb/http/provider/HTTPProvider.scala

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
  }

https://github.com/lift/framework/blob/master/web/webkit/src/main/scala/net/liftweb/http/LiftServlet.scala

コンテキストを与えて、LiftServletは内部に保持するのみ。

なぜval扱いにしないのだろか?引数なしコンストラクタも用意しているの?だとしたらその意味は?

actualServlet.init
  def init = {
    LiftRules.ending = false
  }

https://github.com/lift/framework/blob/master/web/webkit/src/main/scala/net/liftweb/http/LiftServlet.scala

終了処理フラグ(って書くと古きよきSI業界っぽいけど)を折っておく。

まとめ

Bootupは以上です。
dispatcherの設定とか、のちのち関わるだろう部分があるので、その辺は読み進めていく中で色々わかるだろーということで、何となくそんなモノがある程度の理解で進めていけばいいと思います(上から目線)。

*1:see http://www.javaroad.jp/servletjsp/sj_servlet10.htm

*2:という表現が妥当かはわからないが

*3:途中からgrepした。

Redmine、最近は使ってないです

nekokennekoken 2011/06/03 12:01

とても参考になりました。
うちもSEの会社です。
仕事のスタイルはがちがちのウォーターフォールExcelで進捗やTODO管理してるので、Redmineとかのチケット駆動開発やツールにはみな懐疑的です。

最近も使われているようでしたら、その後の事例などご紹介いただければ幸いです。
楽しみに待っております!
http://d.hatena.ne.jp/katzchang/20090924/p1#c

というコメントを頂いたので、せっかくなのでこちらで返答します。

Redmine、最近は使ってないです。

丈夫そうな段ボールをタスクボードとして、付箋をチケットとして管理するのが、一番負担が少ないです。で、もうすこし大きな単位、例えばストーリ単位でExcel表にまとめておいて、自分だけが結果を記入していく感じにしてます。

Redmineとかのサービスは、多人数で情報共有する場合にはいいんでしょうけど、最近はそこまでは必要ないので、あまり使わなくなりました。

愛が故に求めるもの

まぁ、こんなところに書き散らしたところで愚痴でしかないんでしょうけど、どういう歴史的事情でこうなってるのかは気になるかも。もしかして対応方法があれば教えてください。つまり、Excelの話です。

  • 印刷時にフォント幅が崩れるのは致命的
  • 複数ブックを開くときに親ウィンドウ内に展開するのは勘弁してほしい
  • 異なるパスの同名ファイルが開けるようにしてほしい
  • 結合セルを含むコピペで結合が解けてしまうことがある
  • "/"を入力しようとするとAlt押したような状態になる?

たぶん他にもある。

アジャイルジャパン富山サテライト地方セッション予告:「その一方で、ウォーターフォールの現場にて開発者が出来ること」

4/15(金)に予定しているアジャイルジャパン富山サテライトで、地方セッションということで一つお話をさせて頂くことになりました。

情報・参加申し込みはアジャイルジャパン2011サテライトin富山 : ATNDからどうぞ。

内容予告

タイトル

「その一方で、ウォーターフォールの現場にて開発者が出来ること」

概要

アジャイルアジャイル言ったところで来週の自分の現場がどうなるものか!!と思わないでもない昨今が続いていますが、皆様いかがお過ごしでしょうか?
それにしてもキラキラ輝いて見えるアジャイル文化/アジャイルラクティスを参考に、目の前にそびえ立つ(もしくは切り立つ)ウォーターフォールの現場をどうにか出来るか出来ないかについて探っていきます。奮闘してきた経験を交えつつ。

スピーカー情報

katzchang
いわゆる”二次請け”業務系SEとして、要求定義から運用支援まで、ユーザ対応からプログラミングまで広く浅くやってます。77世代、子育てクラスタ、ときどきお料理ブロガー。
http://d.hatena.ne.jp/katzchang
http://twitter.com/katzchang

アジャイルジャパン富山サテライト セッション内容作戦会議

  • 「変えていくには信頼されること、信頼されるには実績を見せること」
  • 「入社半年の社員に『え?テスト書かないで、どうやって開発していくんですか?』っていわれた」
  • 「テスト書いて役にたったと思わないと、テストなんて書かないよね」
  • イノベーションについて語ってください」「それ無理」
  • 「講演はストーリ重視で、お題と数個のテーマを提示して、膨らませていく感じですすめるといいらしい」
  • 「パブリックスピーカーの告白、いいらしいよ」

パブリックスピーカーの告白 ―効果的な講演、プレゼンテーション、講義への心構えと話し方

パブリックスピーカーの告白 ―効果的な講演、プレゼンテーション、講義への心構えと話し方

  • ペアプロ?最近やってないわ。新人教育とか、難しいときとかだけだね」
  • 「ワールドカフェでふりかえれば?」
  • 「ursm の後ろには獣道ができる」

その他、思い出したら追記していくかも。