SiteMap も JRebel(JavaRebel) で HotDeploy されるようにする

Liftを使用しての開発では mvn scala:cc と mvn:jetty:run をしてファイル保存のタイミングで変更が反映されるようにしていました。
scala ファイルを変更すると scala:cc により自動でコンパイルが実行され、class ファイルが変更されたことにより Jetty がクラスをリロードするという流れです。

ただ、ときどき Jetty がリロードしてくれなかったりと、ちょっと動作がいまいちな時があったので、JRebel を使用して HotDeploy する方法に変えました。
JRebel の設定などは JavaRebelとscala:ccで快適Lift開発 - スコトプリゴニエフスク通信 を参考にしました。
また、JRebel は scala を応援しているらしく、scala から生成したクラスファイルに限り無償でライセンスを得ることができます。scala 開発者用ライセンスは http://www.zeroturnaround.com/scala-license/ の各項目に必要事項を入力後、メールの添付ファイルで送られてきます*1。ライセンスファイルは JRebel の jar ファイルと同じディレクトリに配置しておけば OK です。

さて、さくさく HotDeploy がきいてくれて、IDE ようのプラグインもあったりとなかなか良い JRebel なのですが、Lift で使用した場合に SiteMap の設定変更が反映されません。これは JRebel が問題というよりも、SiteMap を設定している Boot.boot メソッドは起動時にしか呼び出されていないためのようです。
そこで、リクエスト毎に SiteMap を再設定するようにして、SiteMap の設定変更も HotDeploy されるようにしました。

まずは、SiteMap の設定部分を Boot から別クラスに移動します。

package bootstrap.liftweb

import _root_.net.liftweb.util._
import _root_.net.liftweb.http._
import _root_.net.liftweb.sitemap._
import _root_.net.liftweb.sitemap.Loc._
import Helpers._

/**
  * A class that's instantiated early and run.  It allows the application
  * to modify lift's environment
  */
class Boot {
  def boot {
    // where to search snippet
    LiftRules.addToPackages("sandbox")

    // Build SiteMap
    LiftRules.setSiteMap((new SiteMapHolder).siteMap) // (1)
  }
}

class SiteMapHolder { // (2)
  val siteMap = SiteMap(Menu(Loc("Home", List("index"), "Home")))
}

(1) の部分にあった SiteMap の設定を (2) の SiteMapHolder クラスに移動しています。SiteMapHolder は class にしてあるのですが、これは object にすると JRebel がうまく HotDeploy してくれないような動きをしたためです。

つぎに SiteMap を再設定するサーブレットフィルタを作成します。

package bootstrap.liftweb

import _root_.javax.servlet.{Filter, FilterChain, FilterConfig, ServletRequest, ServletResponse}
import _root_.net.liftweb.http.LiftRules

class ReloadSiteMapFilter extends Filter {
  def doFilter(req: ServletRequest, res: ServletResponse, chain: FilterChain): Unit = {
    LiftRules.setSiteMap((new SiteMapHolder).siteMap) // (3)
    chain.doFilter(req, res)
  }

  def init(config: FilterConfig): Unit = ()

  def destroy(): Unit = ()
}

(3) の部分で LiftRules に SiteMap を再設定しています。おそらくこのタイミングで JRebel の HotDeploy がきき、変更後の SiteMapHolder が使用されます。

最後に web.xml を修正して、サーブレットフィルタを追加します。

<?xml version="1.0" encoding="ISO-8859-1"?>

<!DOCTYPE web-app
	  PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
	  "http://java.sun.com/dtd/web-app_2_3.dtd">

<web-app>
  <filter>
    <filter-name>ReloadSiteMapFilter</filter-name>
    <filter-class>bootstrap.liftweb.ReloadSiteMapFilter</filter-class>
  </filter>
  
  <filter>
    <filter-name>LiftFilter</filter-name>
    <display-name>Lift Filter</display-name>
    <description>The Filter that intercepts lift calls</description>
    <filter-class>net.liftweb.http.LiftFilter</filter-class>
  </filter>
  
  <filter-mapping>
    <filter-name>ReloadSiteMapFilter</filter-name>
    <url-pattern>/*</url-pattern>
  </filter-mapping>
  
  <filter-mapping>
    <filter-name>LiftFilter</filter-name>
    <url-pattern>/*</url-pattern>
  </filter-mapping>

</web-app>

あとは JRebel を有効にして、Jetty を起動。


c:\develop\workspaces\lift-sandbox>set MAVEN_OPTS=-noverify -javaagent:C:\development\libraries\JRebel.jar %MAVEN_OPTS%

c:\develop\workspaces\lift-sandbox>mvn jetty:run

ついでに、scala ファイル変更時にコンパイルが実行されるように、scala:cc を別のコマンドプロンプトで実行。

c:\develop\workspaces\lift-sandbox>mvn scala:cc

これで、SiteMap に Menu を追加したり、Loc.Hidden 等の設定を追加した場合でも、HotDeploy が効くようになります。ヨカッタヨカッタ。

*1:ちなみにこのフォーム、入力に不備がなく、送信に成功した場合でも同じ画面に遷移するため、送信できたのかいまいち分かりづらかったりします(汗