IDEA+sbt-appengineで流れるような開発

Ken published on
6 min, 1049 words

Categories: Programming

前回の投稿でIDEAとsbt-appengineが連携してくれたのは嬉しいけど、ソースを修正するたびにappserverの再起動は面倒!! 出来ることなら、IDEAでコーディング→ブラウザ確認→コーディング→...と繰り返したい。

こちらも実現できたので、方法を紹介。 やったことはこれだけ。

  • JRebelでクラスリローディング
  • クラスファイルの出力先をwebapp配下にする
  • sbtでソースコード監視

環境を簡単に書いておくと、IDEA+sbt(appengine plugin)という組み合わせ。 MyProjectがあって、その下にSubProject1, SubProject2がある構成。このMyProjectとかSubProjectはsbtの管理下にある状態。

まずはJRebelの導入から。 JRebel自体は有償だけど、Scala開発者にはライセンスを提供してくれてるのでこれを活用。 ■ ZeroTurnaround http://sales.zeroturnaround.com/ jrebel.jarを適当な場所に置く。ライセンスファイルもjarと同じところに置いておく。

JRebelでクラスリローディング

クラスリローディング対象とするディレクトリを指定するために、rebel.xmlを作成する。 MyProject/src/test/resources/rebel.xml
<?xml version="1.0"?>
<application>
    <classpath>
        <dir name="/Users/ken/workspace/MyProject/SubProject1/target/scala_2.8.1/classes" />
        <dir name="/Users/ken/workspace/MyProject/SubProject2/target/scala_2.8.1/classes" />
    </classpath>
</application>

続いて、sbtでJRebelを有効にする。 sbt-appengine-pluginではJREBEL_JAR_PATHを指定するだけでJRebelが有効になるはずなんだけど、IDEAのsbtコンソールでは環境変数を見てくれないらしいので、直接指定します。 MyProject/project/build/Project.scala

override def scanDirectories = Nil
override val devAppserverJvmOptions = List("-Xdebug", "-Xrunjdwp:transport=dt_socket,server=y,address=2011,suspend=y") ++ List("-noverify", "-javaagent:/Users/ken/dev/jrebel/jrebel.jar") ++ super.devAppserverJvmOptions

JRebelに関係するのは

List("-noverify", "-javaagent:/Users/ken/dev/jrebel/jrebel.jar")

の部分。 ちなみに、XdebugとかはIDEAで接続するための記述。

クラスファイルの出力先をwebapp配下にする

JRebelの設定も終わったので、あとはIDEAからsbtを実行すればいいんだけど、sbtの出力先がappengineの出力先(webapp)とは異なっているためsbtの "~ compile" で監視をしても意味が無い。

そこで、SubProjectの出力先をwebapp配下にします。具体的には、MyProject/SubProject1/target/scala_2.8.1/classesを削除して、MyProject/target/scala_2.8.1/webapp/WEB-INF/classesにシンボリックリンクを張る。 手動でシンボリックリンクを張ればいいんだけど、sbt cleanを実行すると再度やり直さなければならないので、symlinkアクションを作った。 MyProject/project/build/Project.scala

import java.lang.Runtime
lazy val symlinkInstance = new Symlink
lazy val symlink = symlinkAction
def symlinkAction = task { args => task { symlinkInstance(args); None } }
class Symlink() extends Runnable {
    def run() = {
        def exec(command:String) = Runtime.getRuntime.exec(command)
        val subs = Set("SubProject1", "SubProject2")
        val webapp = outputPath / "webapp" / "WEB-INF" / "classes"

        log.info("Creating directory %s".format(webapp))
        exec("mkdir -p %s".format(webapp))

        subs.foreach { sub =>
            log.info("Processing for %s.".format(sub))
            val path = "%s/%s".format(sub, mainCompilePath)
            val command = "ln -s ../../../%s %s".format(webapp, path)

            log.info("Removing directory %s".format(path))
            exec("rm -fr %s".format(path))

            log.info("Creating symlink %s -> %s".format(path, webapp))
            exec(command)
            log.info("")
        }
        None
    }  

    def apply(args:Seq[String]):Option[String] = {
        new Thread(this).start()
        None
    }

}

コード中の val subs = Set("SubProject1", "SubProject2")" を各自の環境に合わせてください。

sbt reload後に "symlink" アクションが追加されているので、実行してパスを確認して下さい。サブプロジェクトのclassesがシンボリックリンクになっているはず。

使い方

  1. sbtで "symlink" を実行。
  2. IDEAでデバッガ実行(=sbtも起動。前回の投稿参照)
  3. sbtで "~ compile" を実行

簡単に仕組みを説明。 symlinkを実行することによって、appserverの管理下にクラスファイルを出力するようになります。単にシンボリックリンクを張るだけなので初回だけ。sbt cleanしたときは再度実行します。 続いて、IDEAでデバッガを起動し、sbtの "~ compile" を実行してソースファイルを監視します。ソースコードに修正があった場合はsbtの監視によってコンパイルされ、クラスファイルが生成されます(生成先はsbt symlinkによってappsever管理下になっている)。 ブラウザからアクセスすると、JRebelによってクラスの再読込が行われ、sbtには "JRebel: Reloading class 'package.path.to.ClassName'." と出力されます。

これで、IDEAからsbtを起動し、ブラウザで確認しつつコードを書いていくことが出来る♪