IDEA+sbt-appengineで流れるような開発
前回の投稿で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がシンボリックリンクになっているはず。
使い方
- sbtで "symlink" を実行。
- IDEAでデバッガ実行(=sbtも起動。前回の投稿参照)
- sbtで "~ compile" を実行
簡単に仕組みを説明。 symlinkを実行することによって、appserverの管理下にクラスファイルを出力するようになります。単にシンボリックリンクを張るだけなので初回だけ。sbt cleanしたときは再度実行します。 続いて、IDEAでデバッガを起動し、sbtの "~ compile" を実行してソースファイルを監視します。ソースコードに修正があった場合はsbtの監視によってコンパイルされ、クラスファイルが生成されます(生成先はsbt symlinkによってappsever管理下になっている)。 ブラウザからアクセスすると、JRebelによってクラスの再読込が行われ、sbtには "JRebel: Reloading class 'package.path.to.ClassName'." と出力されます。
これで、IDEAからsbtを起動し、ブラウザで確認しつつコードを書いていくことが出来る♪