Gradleで特定のJarをtestRuntimeで使いたいけどWarからは除外したい

Twitterで@ruimoさんがつぶやかれていたので色々試してみた結果をまとめておきます。 (試行錯誤で徐々に追記しています。)

経緯

解決案

案1 標準APIで一応あるはずのexclude/excludes (効かない)

warタスクでもexcludesによるパターン除外が効けば良いんですけど、どうもできない感じですね...。パターン指定の仕方が悪いのかしら。Antパターン指定とかで試してみたんですが、何にも変化なしな感じ。

war {
    excludes = ["**/hoge*"]
}

案2 warタスクで自前でフィルタリング

結局「warに入らなくしたいだけ」なので、余計なruntimeなどをいじるのではなくwarに入れる処理のことろで自前フィルタリングするのが良さげです。

war {
    // 自前でフィルタリングすると一応除外できた。
    // この場合、推移的依存関係は使えないのですべての除外Jarを指定しなければならない。
    def myExcludes = ["hogehoge", "foofoo"]
    classpath = classpath.collect { file ->
        // 除外リスト内のパタンと部分一致に入っているものはnullにしておいて後でfindAllで除外する。
        myExcludes.any { file =~ it } ? null : file
    }.findAll { it } // null除外
}

どうでもいいけど、Groovyコレクション操作でfindAllの反対が欲しいなぁ。removeAllはJava標準APIに引きずられて破壊的なのが駄目な感じ。

で、この方式でWarに入るJarは除外できます。ただし、これだと推移的依存関係は辿れなくて、ファイル名マッチングをするしかないので、すべての除外Jarを指定しなければならないのが面倒です。上記でfoofooが実はbarbarにも依存してたりすると、Warの中にbarbarだけが含まれてしまいます。

案3 warタスクで自前でフィルタリング(推移的除外関係の自動解決版) (ただし、中途半端)

で、ちょっとトリッキーだけど解決策としてこんなのを考えてみました。

configurations {
    excludeFromWar
}

dependencies {
    //...

    // warタスクでの自前フィルタリングで、推移的依存関係を全て列挙したくないので、
    // このように除外目的のconfigurationsを作ってしまうと楽な気がする。
    excludeFromWar 'hoge:hogehoge:1.0'
    excludeFromWar 'foo:foofoo:1.2'
}

war {
    // 推移的依存関係を自動的に辿れるように除外用のconfigurationsを作ると便利。
    classpath = classpath.collect { file ->
        // 除外リスト内のパタンと部分一致に入っているものはnullにしておいて後でfindAllで除外する。
        configurations.excludeFromWar.contains(file) ? null : file
    }.findAll { it } // null除外
}

こうすると、たとえfoofooが実はbarbarに依存していても、foofooの除外指定からたどられて除外対象に含まれるのでbarbarも除外できます。

でも、これもまだ半端な実装になっていて、Warに含めたいbazbazがbarbarに依存していると、foofooの推移的除外関係から単純にbarbarを除外しようとするので、Warの中にbarbarが含まれず、bazbazがまともに動かなくなってしまいます。もうちょっとその辺まじめに実装すればよいかもしれませんが、特定プロジェクトの都合で確実に除外指定したいブツが分かってるなら、案2あたりで止めておく方がよさげ。

案4 providedRuntimeを使う方法 (ボツ)

Warプラグインで用意されているprovidedRutimeを使えば、もしかしてこれだけで良かったりする?

War プラグインは providedCompile と providedRuntime の2つの依存構成を追加します。 これらの構成は WAR アーカイブには追加されないという点を除けば、それぞれ compile、runtime と同じスコープを持ちます。 provided 構成が推移的に機能することは特筆すべき点です。 http://gradle.monochromeroad.com/docs/userguide/war_plugin.html

configurations {
    // まずはwarで不要なものをruntimeから除外してしまう。
    runtime.exclude module: 'hogehoge'
    runtime.exclude module: 'foofoo'
}

dependencies {
    // warには不要であるがruntime/testRuntimeで必要なJarをあらためてprovidedRuntimeで宣言しなおす。
    providedRuntime 'hoge:hogehoge:1.0'
    providedRuntime 'foo:foofoo:1.2'
}

ruimoさんのユースケースだとこれで良いような気がしてます(動作確認してない)。

案5 providedRuntimeを使う方法(DRY版) (ボツ)

だいぶシンプルになりましたが、除外設定とprovidedRuntime指定の2箇所でJarの情報が分散してるのがDRYではありませんね。でも、そこは心配ありません。Groovyなのでこんな書き方ができます。

// こうして一元管理もできる。そう、Groovyならね。
def excludesFromWar = [
    [group: 'hoge', module: 'hogehoge', version: '1.0'],
    [group: 'foo', module: 'foofoo', version: '1.2'],
]

configurations {
    // まずはwarで不要なものをruntimeから除外してしまう。
    excludesFromWar.each { runtime.exclude it }
}

dependencies {
    // warには不要であるがruntime/testRuntimeで必要なJarをあらためてprovidedRuntimeで宣言しなおす。
    excludesFromWar.each { providedRuntime "${it.group}:${it.module}:${it.version}" }
}

案6 直接testRuntime/testCompileに再設定する方法 (ボツ) [4/17 21:00頃追記]

providedRuntimeはテスト実行時(testRuntime)には入ってこないようです。 と、よく考えたらテストで使えれば良いのだから直接testRuntime、コンパイル時も必要ならtestCompileに設定すればいいじゃないか、と気がつきました。なお、testCompileにいれておけばtestRuntimeにも含まれます。

ということで、たぶんこれでFAな気がするんですがどうでしょうかね。

// こうして一元管理もできる。そう、Groovyならね。
def excludesFromWar = [
    [group: 'org.codehaus.groovy', module: 'groovy-all', version: '2.1.6'],
    [group: 'org.spockframework', module: 'spock-core', version: '0.7-groovy-2.0'],
]

configurations {
    // まずはwarで不要なものをruntimeから除外してしまう。
    excludesFromWar.each { runtime.exclude it }
}

dependencies {
    compile project(':subproject-a')

    // warには不要であるがtestCompileで必要なJarをあらためて追加する。
    excludesFromWar.each { testCompile "${it.group}:${it.module}:${it.version}" }
}

できあがるWarファイルの中身: (groovy-allもspock-coreも含まれていない)

  Length     Date   Time    Name
 --------    ----   ----    ----
        0  04-17-14 21:36   META-INF/
       25  04-17-14 21:36   META-INF/MANIFEST.MF
        0  04-17-14 21:36   WEB-INF/
        0  04-17-14 21:36   WEB-INF/lib/
     2329  04-17-14 21:35   WEB-INF/lib/subproject-a.jar
 --------                   -------
     2354                   5 files

Gradle上の依存関係:

$ gradle dep
:dependencies

------------------------------------------------------------
Root project
------------------------------------------------------------

archives - Configuration for archive artifacts.
No dependencies

compile - Compile classpath for source set 'main'.
\--- project :subproject-a
     \--- org.codehaus.groovy:groovy-all:2.1.6

default - Configuration for default artifacts.
\--- project :subproject-a

groovy - The Groovy libraries to be used for this Groovy project. (Deprecated)
No dependencies

providedCompile - Additional compile classpath for libraries that should not be part of the WAR archive.
No dependencies

providedRuntime - Additional runtime classpath for libraries that should not be part of the WAR archive.
No dependencies

runtime - Runtime classpath for source set 'main'.
\--- project :subproject-a

testCompile - Compile classpath for source set 'test'.
+--- project :subproject-a
|    \--- org.codehaus.groovy:groovy-all:2.1.6
+--- org.codehaus.groovy:groovy-all:2.1.6
\--- org.spockframework:spock-core:0.7-groovy-2.0
     +--- junit:junit-dep:4.10
     |    \--- org.hamcrest:hamcrest-core:1.1 -> 1.3
     +--- org.codehaus.groovy:groovy-all:2.0.5 -> 2.1.6
     \--- org.hamcrest:hamcrest-core:1.3

testRuntime - Runtime classpath for source set 'test'.
\--- project :subproject-a

案7 直接testRuntime/testCompileに再設定する方法(正常動作版) [4/18 16:30 追記]

案6は実は駄目ですね。うまく動いてると勘違いしたのはサンプルコードがまずかったです。依存先のsrc/mainのクラスとして何かに外部モジュールBに更に依存していて、そのBに対する制御をメイン側のPJでやるというサンプルじゃないと駄目なのでした。

何が問題かというと、上の出力を見てわかるようにtestRuntimeに追加したはずの依存モジュールが入っていない(testRuntimeはtestCompileを引き継ぐので本来はtestCompileと同じだけの品揃えがあるべき)。 実は、configurationsでごっそりruntimeからexcludeしてしまうと、メインPJのruntime系の依存性もすべてexcludeされてしまうのでした...。 runtimeから除外してtestRuntimeに追加してるのに、excludeの評価が「runtime系全体」にかかるのはおそらく内部実装の都合上な印象を受けます。が、そうなっているものは仕方がない。

依存先モジュール/プロジェクトごとに個別に推移的依存性をexcludeすればOKなので、こうやれば意図通りに動きました。

// こうして一元管理もできる。そう、Groovyならね。
def shouldExcludesFromWar = [
    [group: 'org.apache.commons', module: 'commons-lang3', version: '3.3.1'],
]

def excludesFromWar = {
    // configurationsの代わりに個別にexcludeすれば、
    // このプロジェクト自体のtestCompile/testRuntimeへの追加は意図通りに動作する。
    shouldExcludesFromWar.each { exclude module: it.module }
}

dependencies {
    compile project(':subproject-a'), excludesFromWar
    compile 'org.codehaus.groovy:groovy-all:2.1.6'
    testCompile 'org.spockframework:spock-core:0.7-groovy-2.0'

    // warには不要であるがtestCompile/testRuntimeで必要なJarをあらためて追加する。
    shouldExcludesFromWar.each { 
        // test配下の実行時だけ必要な場合
        testRuntime "${it.group}:${it.module}:${it.version}"

        // test配下のコンパイル時も必要な場合
        //testCompile "${it.group}:${it.module}:${it.version}"
    }
}

案8 直接testRuntime/testCompileに再設定する方法(超シンプル版) [4/18 16:30 追記]

http://stackoverflow.com/a/23131137/1257166 のOpal氏の2案目をみて気づきましたが、依存モジュールごとの推移的依存先について選択的ではなくごっそりすべてexcludeして良いなら、これだけでOKです。

dependencies {
    compile project(':subproject-a'), { transitive = false }
    compile 'org.codehaus.groovy:groovy-all:2.1.6'
    testCompile 'org.spockframework:spock-core:0.7-groovy-2.0'

    // warには不要であるがtestCompile/testRuntimeで必要なJarをあらためて追加する。
    testRuntime 'org.apache.commons:commons-lang3:3.3.1'
}

ぐるっと一周してすごくシンプルになりましたね。

参考コード

実際にテストまで動くサンプルは https://github.com/nobeans/gradle-war-excludes-jar-sample/ においてあります。

Groovy2.3.0-beta-1と噂のTraitを試してみた

Groovy2.3.0-beta-1がリリースされましたね。

目玉としてはやはりtraitでしょう。

今までのMetaClassによるメソッドの動的探索パスをいじる系の機能で似たようなことはできましたが、何かと限界(複数スレッドにおける動作の保証とかかなり泥臭い感じだったり、一度動的メソッドを追加すると取り除くのが大変だったり)がありましたが、それらを超えるために静的ソリューションとしてtraitが実装されました*1

ドキュメントがすごくわかりやすくて、サンプルコードを上から順に読むだけで少なくともどのように使えるかは難なく理解できるでしょう。

最初の方を読んでるとJava8のインタフェース+デフォルトメソッドと同じかな?と誤解しますが、Groovyのtraitはステートフルなのです。つまりプロパティを定義して状態が持てる。もう完全に普通の多重継承です。とうぜんダイアモンド継承などのコンフリクト問題はありますが、ドキュメントにあるとおり、「自前実装最強」「それ以外は登場順の後勝ち」的なルールで明確に決定されますし、特定の実装を優先したい場合も簡単です。まあ、衝突が気になるような名前は避ければいいわけですし。詳しくはドキュメントのこの辺を読みましょう。

GroovyConsoleで軽く試してみましたが、非常にいい感じです。

gist10127510

いわゆるアカデミックな本家の"trait"や、"Scalaのtrait"とどの辺が違うのかあたりは気になるところです。

2014/4/9追記

ASTはこんな感じ。細かい点はおいといてなるほどねーな感じです。

@groovy.transform.Trait
abstract interface public class Flyable extends java.lang.Object { 

    @org.codehaus.groovy.transform.trait.Traits$Implemented
    abstract public java.lang.String fly() {
    }

    @org.codehaus.groovy.transform.trait.Traits$Implemented
    abstract public java.lang.String getName() {
    }

    @org.codehaus.groovy.transform.trait.Traits$Implemented
    abstract public void setName(java.lang.String value) {
    }

}
public class Bird implements Flyable, Flyable$Trait$FieldHelper, groovy.lang.GroovyObject extends java.lang.Object { 

    private java.lang.String Flyable__name 
    private static org.codehaus.groovy.reflection.ClassInfo $staticClassInfo 
    public static transient boolean __$stMC 
    private transient groovy.lang.MetaClass metaClass 
    public static long __timeStamp 
    public static long __timeStamp__239_neverHappen1397005006100 

    public Bird() {
        metaClass = /*BytecodeExpression*/
        Flyable$Trait$Helper.$init$(this)
    }

    public void setName(java.lang.String arg1) {
        Flyable$Trait$Helper.setName(this, arg1)
    }

    public java.lang.String fly() {
        return Flyable$Trait$Helper.fly(this)
    }

    public java.lang.String getName() {
        return Flyable$Trait$Helper.getName(this)
    }

    static { 
        __timeStamp__239_neverHappen1397005006100 = 0
        __timeStamp = 1397005006100
        Flyable$Trait$Helper.$static$init$(this)
    }

    @groovy.transform.CompileStatic
    public java.lang.String Flyable__name$get() {
        return Flyable__name 
    }

    @groovy.transform.CompileStatic
    public void Flyable__name$set(java.lang.String val) {
        Flyable__name = val 
    }

    public java.lang.Object this$dist$invoke$1(java.lang.String name, java.lang.Object args) {
        return this."$name"(* args )
    }

    public void this$dist$set$1(java.lang.String name, java.lang.Object value) {
        this ."$name" = value 
    }

    public java.lang.Object this$dist$get$1(java.lang.String name) {
        return this ."$name"
    }
    //....
}
public class UFO implements groovy.lang.GroovyObject extends java.lang.Object { 

    private static org.codehaus.groovy.reflection.ClassInfo $staticClassInfo 
    public static transient boolean __$stMC 
    private transient groovy.lang.MetaClass metaClass 
    public static long __timeStamp 
    public static long __timeStamp__239_neverHappen1397005006102 

    public UFO() {
        metaClass = /*BytecodeExpression*/
    }

    public java.lang.Object this$dist$invoke$1(java.lang.String name, java.lang.Object args) {
        return this."$name"(* args )
    }

    public void this$dist$set$1(java.lang.String name, java.lang.Object value) {
        this ."$name" = value 
    }

    public java.lang.Object this$dist$get$1(java.lang.String name) {
        return this ."$name"
    }

    //...
}
abstract public static class Flyable$Trait$Helper implements groovy.lang.GroovyObject extends java.lang.Object { 

    private static org.codehaus.groovy.reflection.ClassInfo $staticClassInfo 
    public static transient boolean __$stMC 
    private transient groovy.lang.MetaClass metaClass 

    public Flyable$Trait$Helper() {
        metaClass = /*BytecodeExpression*/
    }

    public static void $init$(Flyable $self) {
        (( $self ) as Flyable$Trait$FieldHelper).Flyable__name$set('trait')
    }

    public static void $static$init$(java.lang.Class<Flyable> $static$self) {
    }

    public static java.lang.String fly(Flyable $self) {
        return "I'm $name and flying!"
    }

    public static java.lang.String getName(Flyable $self) {
        return (( $self ) as Flyable$Trait$FieldHelper).Flyable__name$get()
    }

    public static void setName(Flyable $self, java.lang.String value) {
        (( $self ) as Flyable$Trait$FieldHelper).Flyable__name$set(value)
    }

    public java.lang.Object this$dist$invoke$1(java.lang.String name, java.lang.Object args) {
        return this."$name"(* args )
    }

    public void this$dist$set$1(java.lang.String name, java.lang.Object value) {
        this ."$name" = value 
    }

    public java.lang.Object this$dist$get$1(java.lang.String name) {
        return this ."$name"
    }

    public java.lang.Object methodMissing(java.lang.String name, java.lang.Object args) {
        return Flyable."$name"(* args )
    }

    public void propertyMissing(java.lang.String name, java.lang.Object val) {
        Flyable."$name" = val 
    }

    public java.lang.Object propertyMissing(java.lang.String name) {
        return Flyable."$name"
    }

    //...
}
abstract interface public static class Flyable$Trait$FieldHelper extends java.lang.Object { 

    final public static java.lang.String Flyable__name 

    abstract public void Flyable__name$set(java.lang.String val) {
    }

    abstract public java.lang.String Flyable__name$get() {
    }

}

*1:実際の経緯がこのとおりかは自信なし

Grails/Gradleの「さっきのテストレポート」をAlfredに表示してもらう

はじめに

この記事は、G*(Groovy, Grails ..) Advent Calendar 2013の15日目として書かれたものです。 14日目は @grimroseさんでした。

最近某自作GrailsアプリにてGrails上でVert.xを利用してWebSocketのプッシュを実装したりしてたので、今回はその話を書こうかなと思っていたのですがやっぱりやめて、以前からそのうち書こうと思って寝かせていた開発Tipsネタの方がちょうど良い感じに熟してきたのでそちらを紹介することにします。

Grailsによる通知からのテストレポート表示(前回までのあらすじ)

以前、Grailsのアプリ起動完了やテストの結果をGrowlで通知するEvents.groovyが便利な件という記事を書きました。

Grailsのビルドイベントを、Grailsのイベント機構を利用して拾って、Growlで通知するというものです。 通知するだけではなくて、その通知ペインをクリックすると、そのテストレポートがブラウザで表示されるので、ユニットテストが結構捗ります。

f:id:nobeans:20131215011240p:plain

また、Mac OS X LionでOS標準の通知機構ができたので、それを利用した方法もこちらで紹介されています。自分も今はこれをベースに使っています。

terminal-notifierというMac OS標準のNotificationに通知するためのコマンドがあるので、growlnotifyコマンドの代わりにそれを使う感じです。 openオプションを使うことで通知ペインのクリック時にURLを表示させることができます。 openオプションにテストレポートHTMLファイルのURLfile://.../index.htmlを指定することで、クリック時のブラウザ連携を実現することができるわけです。 他にもexecuteオプションで任意のコマンドを実行させたりもできます。 詳しくは↓あたりをどうぞ。

Gradleによる通知からのテストレポート表示

さて、GradleもGrailsとは違う方式ながら、イベントリスナ機構を持っています。

Gradle標準(?)の実験的プラグインで通知アプリへの連携方法が提供されていて、それを使ってビルドイベントの前後で通知する方法がMr.Hakiのブログでまとめられています。

自分の場合は、取り回し重視で以下のシンプルなコードを改造して、直接terminal-notifierコマンドを実行するようにして使っています。

f:id:nobeans:20131215011244p:plain

サンプルコードは最後にまとめて紹介するとして、先を急ぎます。

Alfredとは?

ここで突然ですが、AlfredというMac用のコマンドライン型ランチャがあります。ホットキーで表示される入力ペインにアプリケーション名を入力すると、インクリメンタルサーチで候補が絞り込まれていき、対象が見つかったところで選択してENTERで実行します。QuickSilverも有名ですが、自分はAlfredを愛用しています。

単なるアプリの起動だけではなく、Spotlightのようにファイル内容の検索もできますし、「カスタム検索(Custom Searches)」を独自定義すればシームレスに色々な情報にたどり着く基盤を築くことができます。

さっきのテストレポートをみたいんだけど...

さて、さきほど紹介したGrails/Gradleの通知→レポート表示の場合、通知が表示されている間にクリックしないとレポートにたどり着けません。 履歴を表示してクリックすることもできますが一手間増えますし、だからといって、Stickyに通知が残るようにすると逆にウザいことが多いのでいやな感じです。 また、通知からしばらくたって、コードを修正している最中にテストレポートを再確認したくなることもあります。 「さっきのテストレポートがみたいなぁ」と思うわけです。

みなさんは、IDEでコードを書いていて「あのソースファイルを修正しよう」と思ったときどうやってそのファイルを開いていますか? まさかエクスプローラ風のビューアからディレクトリ階層をたどってファイルを見つけてダブルクリックしてないですよね? EclipseでもIntelliJでも、リソース名/型名によるインクリメンタルサーチでファイルを開く機能があります。 「あのソースファイル」と思った時点でその名前が念頭にあるんですから、それを使って対象を開くのがもっとも頭にも手にもやさしい自然な方法な訳です。 VimmerならUniteプラグインの出番ですね。

さて、テストレポートの表示も同じです。 頭では「さっきのテストレポートがみたいなぁ」と思っています。 その欲求をできるだけストレートにアクションに結びつけるにはどうしたらよいか。 ここでAlfred(または類似のランチャ)が役立つわけです。

  • 「あのアプリが起動したいなぁ」→ Alfredでアプリ名を指定して起動する
  • 「さっきのテストレポートがみたいなぁ」→ Alfredで開けないかな?

というごく自然な流れですね。

Alfredで「さっきのテストレポート」を表示するには

Alfredのカスタム検索を使って、これを実現しましょう。

しかし、彼はGrails/Gradleの文脈なんて知らないので、「さっきのテストレポート」をAlfredに見つけさせるのは難しいです。 そこで、通知と同じようにビルドイベントのフックで仕込みをしてしまうことにしましょう。

  1. ビルド実行後のフックで「テストレポートのファイルURI」を固定パスの隠しファイルに書き込んでおく
  2. 1.で書き込んだ固定パスのファイルを開くようなカスタム検索をAlfredに定義する

これだけです。単純な戦略ですがこれで十分です。

あとは、Grailsの場合なら「さっき起動したアプリのホーム画面」もさくっと表示できると捗りますね。

というわけで、早速やってみましょう。

実際のコードサンプル

Grails

以下のコードを~/.grails/scripts/_Events.groovyとして保存します。

Gradle

以下のコードを~/.gradle/init.gradleとして保存します。

Alfred

以下の文字列をコピーして、Alfredの入力欄に貼り付けるだけでカスタム検索が登録できます。 ユーザのホームディレクトリを使うので、以下の__YOUR_ACCOUNT__の部分を自分のアカウント名に書き換えてから、Alfredの入力欄に貼り付けてください。

  • AlfredからのGradleテストレポート表示
alfredapp://customsearch/Gradle%20Open%20Latest%20Test-Report/gradletestreport/ascii/url=file:///Users/__YOUR_ACCOUNT__/.gradle/latestTestReport.html
  • AlfredからのGrailsテストレポート表示
alfredapp://customsearch/Grails%20Open%20Latest%20Test-Report/grailstestreport/ascii/url=file:///Users/__YOUR_ACCOUNT__/.grails/latestTestReport.html
  • AlfredからのGrailsローカル起動アプリのホーム画面表示
alfredapp://customsearch/Grails%20Open%20Latest%20Run-App/grailsrunapp/ascii/url=file:///Users/__YOUR_ACCOUNT__/.grails/latestRunApp.html

これでOKです。 あとは、↓の辺りからGrailsやGradleのアイコンを探して、"Drop icon"ゾーンにドラッグ&ドロップすれば、更に見た目がクールになります。

試してみる

まずはGradleプロジェクトをビルドしてテストも実行しましょう。 無事に以下のような通知が表示されたでしょうか?

f:id:nobeans:20131215011244p:plain

表示されていれば裏できっと「さっきのテストレポート」情報も記録されているはず。 Alfredの入力ペインを表示させて「gradle」と入力してみると...

f:id:nobeans:20131215011249p:plain

「Gradle Open Latest Test-Report」を選択すると、期待通りブラウザ上にテストレポートが表示されましたね。

f:id:nobeans:20131215011255p:plain

Grailsプロジェクトがある人はそっちも是非ためしてみてください。

  • grails test-appを実行→「Grails Open Latest Test-Report」
  • grails run-appを実行→「Grails Open Latest Run-App」

おわりに

他のOSやコマンドラインランチャでもこの方法が使えます。 Mac/Alfred使い以外の人も是非お試しを。

さて、明日の16日目は @nobusueさんです!

GroovyServとGradle Daemonとの違い

10/23 9:50 訂正&追記: GradleがデフォルトでDaemon起動すると書いてた件 (thanks to きょんくん)

元記事: http://dev.classmethod.jp/server-side/gradle/gradle-first-contact/

経緯的な

id:daisuke-mが昼過ぎに技術を語るいつものシリーズにGradleを追加した旨のツイートをしてました。 生粋のGradlerとしては喜び勇んでチェックしてみたんですが、「Gradleのインストール」であるべきところを「Groovyのインストール」と書いてたので、親切心でコメントしたわけです。

そこまでは良かったのですが、ちょっと調子に乗ってしまいまして、

と書いたところ、記事に晒し上げされてしまいました。 なんということでしょう。

f:id:nobeans:20131022224044p:plain

そのままフェードアウトしてくれれば終わればまだ良かったのですが、自称怖くないモンスターの人に見付かって問い詰められているid:daisuke-mが忍びなさMAXでつい反応してしまったら、気がついたら私がGroovyServとGradle Daemonの違いについてなんか書かなければならない流れになっておりました。

ああ、恐ろしきかな因果の流れ。

GroovyServとは

Groovyコマンドを実行可能なJVMプロセスをサーバとして常駐させておいて、ネイティブコマンドを使ってTCP経由でコマンド引数をサーバに送り込み、評価した結果を再びTCP経由でもらって出力する、という拙作のツールです。

Groovyに限らずJVM系言語でスクリプトを書いた場合、JVMプロセスを起動して色々なクラスローディングする準備処理が遅くて、1秒ぐらいの待ち時間が発生します。

$ time groovy -e "println 'Hello'"
Hello

real    0m1.347s
user    0m1.481s
sys     0m0.168s

GroovyServをインストールして、クライアントコマンド経由で実行すると

$ time groovyclient -e "println 'Hello'"
Hello

real    0m0.057s
user    0m0.002s
sys     0m0.005s

すごく...はやいです...。

といっても、初回はサーバプロセスを起動するための待ち時間が数秒かかるんですけども。 一度起動したらそれ以降は上のような感じです。 はいはい、爆速爆速。

なお、mainクラスのエントリポイントを指定できればいいので、JythonとかClojureも同じように高速化できますScalaのコンパイルも頑張れば早くなるようですVim+QuickRunでgroovyConsole風にさくっと実行できるのにフル機能エディタ!みたいなこともできますし、SublimeText2からも似たようなことができます

一応このネタで2013/5にGr8conf EU 2013というデンマーク コペンハーゲンのGroovy系カンファレンスで発表してきたりしました。

詳しくは、ちょっと雑多な感じですがユーザガイド(日本語)をどうぞ。

Gradle Daemonとは

Gradleも同様にJVM上で動作するツールなので、起動の待ち時間が大変遅いわけです。 毎回JVMプロセスを一から起動してると、さくさくビルドやテストを試せません。 GroovyServみたいな仕組みが欲しいですよね。

ということで、Gradle本体が同じような仕組みを用意したのがGradle Daemonです。 歴史的に言うと、GroovyServの方が若干早いはず。 パクられた!とは言いませんが、時期的なものを考えると多少は参考にされたんじゃないかと自意識過剰気味にアピールしておきます。どうかな。違うかも。

最近のGradleはデフォルトでDaemonを利用します。 GradleがデフォルトでDaemon起動すると書いたな、アレは噓だ。 --daemonオプションが必要です。 以下はしれっと書き換えてあります。 すいません。

--daemonオプションはまだデーモンがいなければ起動して使い、すでに居ればそれを使うというオプションです。 というわけで、適当なGradleプロジェクトで、2回gradle --daemon tasksを実行してみると、2回目が劇的に早くなってるんじゃないかと思います。

たとえば、自宅のかなり古いMacBook mid 2008で試してみると、

$ gradle --daemon tasks
...
Total time: 12.02 secs

$ gradle --daemon tasks
...
Total time: 2.52 secs

とこんな感じです。すごいですね。 jpsしてみると

$ jps
9376 Jps
4136 GradleDaemon

とデーモンプロセスが常駐してるのが分かるはず。

あと--daemonオプションが付けられていようが、--no-daemonというオプションを追加すれば、デーモンは無視して自前プロセスで実行できたりします。 デーモンプロセスありきでalias設定してあっても、一時的にはずせるわけですね。

また、デーモンプロセスが要らなくなったら、普通にkillコマンドでもいいですが--stopオプションを使えばgradleコマンド経由でコントロールできます。

個人的にはGradle Wrapperを優先しつつDaemonは必須で使いたいので、以下のようなaliasをシェルで設定して使っています。

gradle='if [ -x ./gradlew ]; then GRADLE_BIN=./gradlew; else GRADLE_BIN=\gradle; fi; $GRADLE_BIN --daemon'

こちらも詳しくはユーザガイド(日本語)をどうぞ。

ちなみにGroovyServもビルドにGradleを使っていて、Daemon様におかれましては大変お世話になっております。

GroovyServとGradle Daemonの違い

Gradle Daemonは、Gradleタスクの起動処理をショートカットする
GroovyServは、Groovyスクリプトの起動処理をショートカットする
Gradle Daemonは、gradleコマンドのオプションですべて操作する
GroovyServは、groovyclientコマンドのオプションで操作するほかに、サーバプロセス起動用のgroovyserverコマンドがある
Gradle Daemonは、ユーザの気付かないうちにデーモンプロセスが常駐してる
GroovyServは、サーバプロセスの起動時にコンソール出力してアピールする

気の利いた大喜利風にオチがつけばいいのですが、実力的に無理でした。ごめんなさい。

結局、コンセプト的なものは一緒ですけど、用途が違うので細かいところで色々と違いますね。 あんまりまじめに比べるモノじゃないと思われます。 むしろGroovyServと比べるべきはNailgunとかDripあたり。

おまけ: NailgunとGroovyServの違い

Nailgunはよく知られたJVMプロセス常駐系アーキテクチャのツールです。 歴史的にはGroovyServよりも古いです。 古すぎて2005年以降はまったく更新されていません。 と思ったら、2012/11月にv0.9.1がリリースされててちょっと驚きました。

NailgunはピュアJava実装なのでビルトイン目的によく使われます。 というかAPI的にもそれっぽい感じ。 JRubyもNailgunを使ってますね。

GroovyServはGroovyで実装してるのでビルトイン目的には向いてません。 GroovyServは今更ピュアJava路線に変更するのは不可能ではないにしろ面倒つらい感じです。 ビルトインの代わりに、単にクライアントの引数でmainクラスのエントリポイントを指定するだけで色々なJVM言語の起動時間短縮を計れます。物は言いようです。

機能的な違いでいえば、下のような感じです。

  • GroovyServのみサポート
    • アクセス元アドレスと認証トークンを使ったアクセスコントロール
      • 許可されたアドレス以外のマシンからのコマンド実行はエラー
      • 同一マシンでも認証トークンが一致しないとコマンド実行はエラー
    • クライアント側のCLASSPATH環境変数をサーバ側に一時的に反映する
    • クライアント側の任意の環境変数をサーバ側に永続的に反映する(一時的反映がベストであるが現状は永続的)
    • クライアント側のカレントディレクトリをサーバ側に一時的に反映する
    • クライアント側のCtrl-Cでサーバ側の処理を中断する
  • 両方ともサポート
    • System#exit()が実行されてもJVMは落ちない

ほらこうやってみるとGroovyServ結構がんばってるでしょ? Nailgun側のメリットはビルトイン以外に思いつきませんが、他に何かあるかもしれません。 フェアじゃないのは単に知識不足です。すいません。

おまけ: DripとGroovyServの違い

Dripはシンプルで割り切ったアーキテクチャで中々興味深いところはありますが、アーキテクチャやサポート機能が違いすぎてGroovyServとはあまり競合しないふいんき。

簡単に言うと、

http://b.hatena.ne.jp/nobeans/20130904#bookmark-109556574

GroovyServのようなJVMプロセス常駐系/興味深いアプローチ/単一プロセスを使い回すとdirtyになっていくのでJVM引数,クラスパス,起動クラスのハッシュごとに異なるJVMデーモンを使う/コードベースが超小さい/bash限定は楽そう

という感じです。

Dripは、bashを前提にしているので色々実装がシンプルです。うらやましいです。

GroovyServは、Mac/Linux/Windowsで動作します。
現状ではWindowsサポートが若干手薄な気もしますが、次期バージョンではできるだけ環境ごとの機能差がないように頑張っています。
Dripは、カレントディレクトリ毎に別プロセスを起動します。
よって、複数プロセスが常駐します(一定時間でタイムアウトして消える)。
既存JVMプロセスに対するネイティブレベルでのCWD制御とかを不要にしたりしてます。割り切り方が素敵です。

GroovyServは、ポート毎に1つのサーバプロセスを用意しています。
普通はデフォルトポートで十分なので、サーバプロセスは1つだと思えばOKです。ある意味シンプル。
CWDなどは毎回力業で反映しています。

こんな感じでしょうか。

測定マシンについて一言

なお、上記記事中の性能的なものはすべて自宅の超古いMacbook 2008 Midで測定してます。 そうとう遅いです。 苦痛です。 今晩のAppleのイベントで新MBPでたら速攻で買い換えたい所存です。

まとめ

というわけで、GVMを使ってGradleをインストールしたら、ついでに

$ gvm install groovy
$ gvm install groovyserv

すると良いと思います。 更に手が滑って

$ gvm install gaiden

Gaidenのインストールまで進んだあなたはファッションリーダとしての自覚と風格が感じられて頼もしい限りです。

標準バンドルのVisualVMのウィンドウが空っぽになってしまったらキャッシュを削除すべし

経緯的な

9月にTwitterにぼやいた件のその後。

事象

事象としては、jvisualvmを起動するとこんな画面になって何もできないというもの。

f:id:nobeans:20131017135555p:plain

Netbeansプラットフォームアプリケーションの素のウィンドウだけ表示してみました的な感じで、メニューバーも本来のVisualVMとは違ってまっさらな感じ。 かろうじてVisualVM臭がするのはAboutダイアログぐらいです。 ホントになにもできません。

http://visualvm.java.net/download.html からダウンロードしたApp版のVisualVMは普通に動きます。

ちなみに、Macです。他のOSで発生するかは知りません。

解決策

VisualVMのキャッシュが壊れていたようです。 /Users/me/Library/Caches/VisualVM/7u14を削除してから起動したら、正常に動作しました。

ちなみに、7u40や7u45にバンドルされてるjvisualvmを使っても、ディレクトリ名は7u14でした。 なんででしょうね。

本家Grailsユーザガイドを開くと自動的に日本語翻訳にリダイレクトするChrome拡張

Grailsユーザガイド日本語翻訳版とブックマークレットの紹介 - 豆無日記で、紹介したブックマークレットを毎回実行するのがダルすぎるので、Chrome拡張にしてみました。

以下のURLからChrome拡張をインストールしてください。

セキュリティの都合から野良サイトからの直接インストールはできないようなので*1、いったんダウンロードしてから拡張の管理画面にドラッグ&ドロップするとよいでしょう。

これで、http://grails.org/doc/latest/*なサイトを開くと自動的にhttp://grails.jp/doc/latest/*にリダイレクトされるようになります。 これでGoogle検索結果からのリンクでもさくっと日本語版が表示できますね。

コードという程のモノではない何かはGitHubにサラしてあります。

*1:https://support.google.com/chrome_webstore/answer/2664769?p=crx_warning&rd=1

Grailsユーザガイド日本語翻訳版とブックマークレットの紹介

有志によるGrailsユーザガイドの日本語翻訳作業がずっと行われていたのですが、実は8月の頭にちょうど一区切り良い感じになりました。 そろそろリリースされそうなGrails 2.3向けに追記された部分などまだちょっと英語部分も残っていますが、基本機能についてはほぼすべて翻訳完了な感じです。

残念なことに検索しても出てくるのは本家の英語版だけだったりすることが多いです。

URLのドメイン部分がgrails.orggrails.jpと違うだけなので、ブラウザのアドレスバーを手作業で書き換えて日本語版を表示させたりして過ごしていますが、さすがに面倒になってきたのでブックマークレットを書いてみました。

  • タイトル例
    • Grailsユーザガイドを日本語化する
  • URL
    • javascript:location.href=location.href.replace(/grails.org/,'grails.jp')

そのまんまですが、わりと便利な感じなのでよかったらどうぞ。