読者です 読者をやめる 読者になる 読者になる

WindowsのコマンドプロンプトでGrails 3開発する(エンコーディング地獄編)

どうも、ひさしぶりにはてダで書きます。

昨今、ソースコードUTF-8で書くのがデファクトだと思うんですが、WindowsでのJavaはデフォルトエンコーディングが相変わらずwindows-31jなので、何をするにも明示的にエンコードを指定しないといけません。 あと、コマンドプロンプトはデフォルトでUTF-8表示できないので、実動作はOKだけど出力が文字化けする、みたいな話もあって、結局どうしたらいいんだっけ、というのをGrails 3向きに整理してみました。

普段Windowsで開発していないので、もっといい方法があったら是非教えてください。 ちなみに、Windowsのバージョンは未だ7です。

Windows 10bashがきたら、LANGとかうまくJavaに伝わっていい感じにデフォルトエンコーディング問題は解消するんですかね。どうなんですかね。

結論 (2016/06/03時点)

仕込み

以下を設定する。前者はプロジェクトに対して1回、後者は環境に対して1回設定すればOK.

  • gradlew.bat内のjava起動行の末尾に "-Dfile.encoding=UTF-8" (ダブルクォートがポイント)を追加する。
    • この変更は、Git/SVNにコミットしてWindows開発者向けに共有する。
74: @rem Execute Gradle
75: "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% "-Dfile.encoding=UTF-8"
  • 環境変数 GRAILS_OPTS=-Dfile.encoding=UTF-8 をグローバルに設定する(コンパネ等)。

開発時

コマンドプロンプト上でアプリを実行する場合は、gradlewコマンドを使う。

gradlew.bat bootRun

テストをする場合もgradlewがよい。

gradlew.bat test

create-xxxxなどを使う場合は、grailsコマンドを使う。

grailsコマンドを使ってアプリ実行やテストをする場合は、コマンドプロンプトへの標準出力が文字化けするが気にしなければOK。 文字化けした内容が気になるのであればgradlewで再実行すれば良い。

ちなみに、chcp 65001 を使ってコマンドプロンプト自体をUTF-8表示可能なようにカスタマイズすれば、grailsコマンドonlyでうまいこといける気はするが、色々ググったり試したりしてもいまいちいい感じに設定できなかったため、ひとまず上記での運用を勧めてみる。

試行錯誤編

  • 注意事項
    • ビルド時のエンコーディングと、実行時のエンコーディングの2箇所で問題が発生するため、確認時は毎回クリーンアップすること(grails clean or gradlew clean)。そうしないと、問題が切り分けられない。
    • あと、環境変数は設定したら、次のを試す前にクリアすること。(set HOGE= みたいに値を空にすればOK)

gradlewコマンド編

(A) 普通に起動する

gradlew clean
gradlew bootRun
  • →×
    • applicaiton.ymlに日本語がある場合起動時にエラーになる
      • ※実行時エラーではあるが、ビルド時に誤ったエンコーディング(windows-31j)でビルドされていると、実行時に正しいUTF-8を指定してもNGとなる(原因は後述)。
...
Caused by: java.nio.charset.MalformedInputException: Input length = 2
        at java.nio.charset.CoderResult.throwException(CoderResult.java:281)
        at sun.nio.cs.StreamDecoder.implRead(StreamDecoder.java:339)
        at sun.nio.cs.StreamDecoder.read(StreamDecoder.java:178)
        at java.io.InputStreamReader.read(InputStreamReader.java:184)
        at org.yaml.snakeyaml.reader.UnicodeReader.read(UnicodeReader.java:123)
        at java.io.Reader.read(Reader.java:140)
        at org.yaml.snakeyaml.reader.StreamReader.update(StreamReader.java:184)
        ... 56 more
:bootRun FAILED

(B) コマンドラインエンコーディング指定する

gradlew clean
gradlew bootRun -Dfile.encoding=UTF-8
  • →○
    • ログファイル文字化けなし=アプリの動作としてOK
      • ※今回、実際アプリの挙動確認の代わりに手っ取り早くログファイルでの文字化け有無でアプリが正常にビルド&実行されているかを確認した
    • コマンドプロンプト標準出力文字化けなし=開発しやすい

(C) 環境変数エンコーディング指定する

JAVA_OPTS などでもよいが、すべてのJVM起動時にフックできてかつ Pickup.. というメッセージが出力されて効いていることがわかりやすいため、undocumentedではあるが、_JAVA_OPTIONS を使っている。(このような試行錯誤では、設定したと思っても設定方法自体が間違っていて効いていない、という何を確認しているのだかわからない残念な事故が多発するので、確実に効いて確実に確認できる方法がよい。)

gradlew clean
export _JAVA_OPTIONS=-Dfile.encoding=UTF-8
gradlew bootRun
  • →△
    • ログファイル文字化けなし=アプリの動作としてOK
    • コマンドプロンプト標準出力文字化けあり=開発しづらい

(D) gradlew.batを直接変更してエンコーディング指定する

gradlew.bat内のjava起動行の末尾に "-Dfile.encoding=UTF-8" (ダブルクォートがポイント)を追加する。

74: @rem Execute Gradle
75: "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% "-Dfile.encoding=UTF-8"
gradlew clean
gradlew bootRun
  • →○
    • ログファイル文字化けなし=アプリの動作としてOK
    • コマンドプロンプト標準出力文字化けなし=開発しやすい

gradlew編のまとめ

基本は(B)でよい。 ただし、毎回毎回打つのが大変なのでサボりたくなるのが正しい開発者の姿。 しかし、安直に(C)のようにしても、挙動としては正しいのだが、コマンドプロンプト上の出力が化けるので若干使いづらい。

結局、(D)が良さそう。

(D)におけるポイントは、ダブルクォート。 コマンドライン引数で指定した場合とまったく同じように動作させるには、このダブルクォートが必要。 一つ前のように、環境変数で直接 -Dfile.encoding=UTF-8 を評価してしまうと、Gradleプロセス自体の文字エンコーディングUTF-8になってしまう。

一般的にはそれで良いのだが、そうなるとGradleはコマンドプロンプトへの標準出力もUTF-8として出力しようとしてしまうため、結果としてコマンドプロンプト上の出力のみ化けた状態になってしまう。

ダブルクォートすることで、あえてGradleのメインプロセス自体はWindows標準のwindows-31jとして動作させつつ、Gradleプロセス内部でキックされるワーカプロセス(コンパイル用ワーカプロセス、Grailsプリプロセス等)にのみ -Dfile.encoding=UTF-8 を渡してUTF-8として動作させる。

こうすることで、実際のワーカプロセスの動作はUTF-8となりつつ、Gradleがワーカプロセスの標準入出力とコマンドプロンプトへの橋渡しをするときに適切な変換を行うことで(要出典)、コマンドプロンプト上で文字化けせずに、しかも、ANSIカラー対応の出力すら可能となっている(※)。

※動作上はこのようにみえるのですが、実装詳細については確認できていないので、あくまで想像です。間違ってたらすいません。

grailsコマンド編

(A) 普通に起動する

grails clean
grails run-app
  • →×
    • gradlew編(A)と同様に、applicaiton.ymlに日本語がある場合起動時にエラーになる

(B) コマンドラインエンコーディング指定する

grails clean
grails run-app -Dfile.encoding=UTF-8
  • →×
    • ログファイル文字化けあり=アプリの動作としてNG
    • コマンドプロンプト標準出力文字化けあり=開発しづらい

gradlewでは効果があったコマンドライン引数であるが、ビルド時には効果があるようだが(日本語含みのapplication.yml起因のエラーが出ていない)、実行時には効いていないようだ。

(C) 環境変数_JAVA_OPTIONSでエンコーディング指定する

grails clean
set _JAVA_OPTIONS=-Dfile.encoding=UTF-8
grails run-app
  • →△
    • ログファイル文字化けなし=アプリの動作としてOK
    • コマンドプロンプト標準出力文字化けあり=開発しづらい

ビルドワーカプロセスにも実行ワーカプロセスにも確実に効くように、_JAVA_OPTIONS を使ってみたところ、期待通りアプリの動作としてはOKとなった。 しかし、コマンドプロンプトの出力が文字化けするので、gradlewの(B)と比べると若干負けた気分。

(D) grails.batを直接変更してエンコーディング指定する

インストール済みのgrails.batをいじるのであまりお勧めはできないが、gradlewでの成功体験から試してみる。 $GRAILS_HOME/bin/grails.bat内のjava起動行の末尾に "-Dfile.encoding=UTF-8" (ダブルクォートがポイント)を追加する。

74: @rem Execute grails
75: "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRAILS_OPTS%  -classpath "%CLASSPATH%" org.grails.cli.GrailsCli %CMD_LINE_ARGS% "-Dfile.encoding=UTF-8"
grails clean
grails run-app
  • →×
    • ログファイル文字化けあり=アプリの動作としてNG
    • コマンドプロンプト標準出力文字化けあり=開発しづらい

まったく無力だった。

(E) 環境変数GRAILS_OPTSでエンコーディング指定する

_JAVA_OPTIONS は強すぎるので、もう少し弱い環境変数ではどうか。うまいことバランスとれたりしないか。

grails clean
set GRAILS_OPTS=-Dfile.encoding=UTF-8
grails run-app
  • →△
    • ログファイル文字化けなし=アプリの動作としてOK
    • コマンドプロンプト標準出力文字化けあり=開発しづらい

やはりこんなところか...。

grails編のまとめ

grailsコマンドを使う場合は、コマンドプロンプト上の文字化けには目をつむって、(C)か(E)を採用するぐらい。

ただし、(C)の _JAVA_OPTIONS を使ってしまうと、gradlew編の(C)の条件を満たしてしまい、gradlewを使った場合にもコマンドプロンプト出力が文字化けしてしまう。

よって、影響がgrailsコマンドに対して局所的になるように(E)の GRAILS_OPTS を使った設定をするのがバランスが良いと思う。

MalformedInputExceptionの原因を探ってみる

※gradlewでの仕込みをいったんなくしてデフォルト状態に戻しておくこと

実行時のみエンコーディング指定

gradlew clean classes
gradlew bootRun -Dfile.encoding=UTF-8
...
Caused by: java.nio.charset.MalformedInputException: Input length = 2
        at java.nio.charset.CoderResult.throwException(CoderResult.java:281)
        at sun.nio.cs.StreamDecoder.implRead(StreamDecoder.java:339)
        at sun.nio.cs.StreamDecoder.read(StreamDecoder.java:178)
        at java.io.InputStreamReader.read(InputStreamReader.java:184)
        at org.yaml.snakeyaml.reader.UnicodeReader.read(UnicodeReader.java:123)
        at java.io.Reader.read(Reader.java:140)
        at org.yaml.snakeyaml.reader.StreamReader.update(StreamReader.java:184)
        ... 56 more
:bootRun FAILED

application.ymlに日本語が含まれる場合、このようにエラーになる。

ためしに、build/resources/application.ymlの日本語コメントを削除してからbootRunすると...と試そうとして気づいた、このファイル....壊れてやがる!!!

diffをとってみると、 日本語部分が化けて制御文字含みになってしまっている のがわかる。 実行時にエラーになるのはコレが原因だったのだ!

$ diff grails-app/conf/application.yml build/resources/main/application.yml
10,12c10,12
<         name: '@info.app.name@'
<         version: '@info.app.version@'
<         grailsVersion: '@info.app.grailsVersion@'
---
>         name: 'myapp'
>         version: '1.0'
>         grailsVersion: '3.1.7'
71c71
<         # 空文字は空文字、nullはnullとして扱う
---
>         # 空▒?字▒?▒空▒?字▒?▒nullはnullとして扱▒?
73c73
<         # 前後の半角スペースのトリムはしない
---
>         # 前後▒?▒半角スペ▒?▒スのトリ▒?はしな▒?

他の差分箇所を見ればわかるが、アプリ名などをビルドによって置換されている。 ファイルをそのままコピーするのではなく、いったん内容を解釈して文字列置換をしているのだ。

つまり、ビルド時に適切なデフォルトエンコーディングを指定しない場合、実際はUTF-8であるgrails-app/conf/application.ymlをwindows-31jとして読み込んで置換処理してしまい、結果としてマルチバイト部分が壊れてしまうことになる。 実行時に正しくデフォルトエンコーディングUTF-8指定しても、すでに壊れたファイルを読み込むことになるため、当然エラーになる。

結局、実行時だけではなく、ビルド時にも適切なデフォルトエンコーディングを効かせないといけないよ、という話であり、まあ、当たり前の話なのであった。

(参考)ビルド時のみエンコーディング指定

gradlew clean classes -Dfile.encoding=UTF-8
gradlew bootRun

コマンドプロンプト上では一件正常に起動して、文字化けもせずにログ出力できているように見える。

しかし、ログファイルへの出力は文字化けしている。おそらく画面も文字化けする。

実行時デフォルトエンコーディングも当然重要ですよ、という話。

JJUG CCC 2015 SpringでGroovyについて話してきた

jjug groovy

2015/4/11のJJUG CCC 2015 Springで、Groovyについてお話ししてきました。

CFPに応募するかしばらく迷ってたんですが、ネタが思いつかなかったので先送りしていたら、〆切最終日辺りにさくらばさんや複数の人から「初心者向けでいいんですよ、むしろいいんですよ」的なメッセージを受信したのと、その近辺で今回の発表ネタのようなことを業務的にやるために割と時間をかけて資料を作り込んだのでその余波で割とどうにかなるんではないかということで応募してみたところ、有り難いことにAcceptされたのでした。

というわけで、今回は「Groovy入門+小人さんスクリプト」という構成でお話ししました。

以前にJavaOneの報告会で「Groovyの使いどころ、7つの導入パターン」というお話をさせていただいたのですが、その中の「小人さんスクリプト(House Elf)」パターンにあたります。

来ていただける方がGroovyをすでに使い込んでいることは基本的に期待できないですし、応用の話だけでは全員置いてけぼり必死と思われたので、「Javaは割と知っている人」に対して一歩ずつGroovyを理解していけるような構成にしてみました。色んな声を聞いた結果、割と成功したんではないかと思います。

というわけで、今回の資料は以下からご参照ください。

なお、これは144ページあるんですが、当日は時間の都合上絶対無理な感じだったので、50ページぐらい削りました*1。上記の公開版資料では削った分も含めて完全版にしてあるので、当日聞いていただいた方ももう一度頭から読み直す価値は結構あるかと思います。

サンプルコードはこちらからどうぞ。 nobeans/jjug-ccc-2015-spring-groovy · GitHub

それにしても今回は参加者数が多くてすごかったですね。いったいJavaに何が起こっているのか。とりあえず僕のセッションも割と満員御礼な雰囲気だったので大変有り難かったです。基調講演から自分のセッション〜懇親会まで含めて最高にハイな一日でした。とても楽しかった。スタッフの皆さん、来場された皆さん、本当にありがとうございました。

Let's enjoy Java and Groovy life!

*1:結果的に持ち時間50分ちょうどで終われたので良かった良かった

GrailsでシンプルなRESTサーバを素早く立ち上げる

grails rest

はじめに

これはWeb API Advent Calendar 2014、10日目のエントリです。

Grailsでは某RESTful DAO的なものがサクッと実装できますよ、というお話です。 だから何だ、とか、○○のFWでもできるぞこの野郎、といった苦情はご遠慮ください。

実装する

とりあえず、Grailsプロジェクトを作成して、ドメインクラス(Entityクラス)を作成します。

$ grails create-app rest-sample
$ cd rest-sample
$ grails create-domain-class Book
$ vi grails-app/domain/rest/sample/Book.groovy

Bookドメインクラスの中身を以下のように修正しましょう。 適当なtitleプロパティを追加して(制約はあってもなくても関係ない)、Resourceアノテーションを追加してるだけです。このResourceアノテーションがいわゆる魔法詠唱ポイントです。

package rest.sample

import grails.rest.Resource

// これをつけるだけで、まったくコントローラを実装しなくても指定したURIでREST APIが有効になる
@Resource(uri='/books')
class Book {
    // とりあえずタイトルだけ
    String title

    static constraints = {
        // 気の済むように制約を書く
        title maxSize: 1000, blank: false, unique: true
    }
}

とりあえず実装はこれだけでOK。 Grailsが手元にある状態からであればカップヌードルの待ち時間ぐらいでできますね。

アクセスしてみる

早速組み込みTomcatでサーバを起動して...

$ grails run-app
...
| Server running. Browse to http://localhost:8080/rest-sample

cURLでアクセスしてみましょう。

$ curl http://127.0.0.1:8080/rest-sample/books
<?xml version="1.0" encoding="UTF-8"?><list />

GETでリストを取得してみると、初期状態なので空ですね*1

本を2件登録してみます。ちまたで話題のアノ本ですね。

$ curl http://127.0.0.1:8080/rest-sample/books -X POST -d "title=プログラミングGroovy"
$ curl http://127.0.0.1:8080/rest-sample/books -X POST -d "title=Gradle徹底入門"

もう一度GETでリストを確認すると...

$ curl http://127.0.0.1:8080/rest-sample/books
<?xml version="1.0" encoding="UTF-8"?><list><book id="1"><title>プログラミングGroovy</title></book><book id="2"><title>Gradle徹底入門</title></book></list>

うまく入っていますね。 もちろんDELETEもサポートしてます。

$ curl http://127.0.0.1:8080/rest-sample/books/2 -X DELETE
$ curl http://127.0.0.1:8080/rest-sample/books
<?xml version="1.0" encoding="UTF-8"?><list><book id="1"><title>プログラミングGroovy</title></book></list>

参照専用API

Resourceアノテーションの引数にreadOnly=trueを指定すると、GETによる参照APIのみだけが提供されるようになります。 POSTしても405 Method Not Allowedになります。 情報開示系APIならreadOnlyをつけておくと良さそうです。

コンテントネゴシエーションでレスポンス形式を指定する

デフォルトではレスポンスはXML形式で返ってきますが、拡張子を使ったコンテントネゴシエーションをサポートしているので、URI末尾に.jsonをつけるだけで

$ curl http://127.0.0.1:8080/rest-sample/books.json
[{"class":"rest.sample.Book","id":1,"title":"プログラミングGroovy"}]

JSON形式でレスポンスを受け取ることができます。 ただ、classとか勝手に入っていて内部情報を暴露しすぎでアレなので、ドメインクラス→JSONへのマーシャル方法をカスタマイズした方がよいですね。 方法についてはここでは割愛します。

Content-typeの指定はAcceptヘッダでもOKです。

curl http://127.0.0.1:8080/rest-sample/books -H "Accept:application/json"
[{"class":"rest.sample.Book","id":1,"title":"プログラミングGroovy"}]

まとめ

データ構造を表現するドメインクラスはとりあえず決める必要があるので、ドメインクラスを先に実装するわけです。 で、REST APIとして提供するつもりならサクッとResourceアノテーションAPIを実装しておきつつ、徐々に、制約とかJSONのマーシャライザとかを整えたり、RESTな自前コントローラも追加して、最終的にResoruceアノテーションを削除する、というようなステップを踏めそうなので、そういう意味では初期バージョンの素早いリリースのために使うというのはアリな気がします。

なお、自前のコントローラを実装する場合も、ドメインクラスのオブジェクトとJSON/XMLなどへのコンテントネゴシエーションに基づく変換機能はもちろんそのまま活用できます。

また、Grailsは次のメジャーアップデートである3.0で*2Spring Bootベースに大きく方向転換をする予定なのですが、このようなRESTサポートは多少形を変えつつ残っていくようです。

といったところで。

関連しているようでしていない、ちょっとだけ関連している書籍

プログラミングGROOVY

プログラミングGROOVY

Gradle徹底入門 次世代ビルドツールによる自動化基盤の構築

Gradle徹底入門 次世代ビルドツールによる自動化基盤の構築

*1:プロキシ設定がある場合は、--no-proxyオプションを追加してプロキシをスルーする必要があるかも知れません。

*2:その前に2.5がワンクッション入るかも

Java8のInvokeDynamic実装一新によるGroovyのindyモードの性能改善効果が凄すぎた件

java groovy

本日、Java Day Tokyo 2014に来ています。

で、ついさっきのセッションで「JDK8ではInvokeDynamic(以下、indy)の実装を一新したのですごく速くなったよ」という話を聞いたので、Groovyのindyモードで試してみました。

Groovyは2.0(現在は2.3)でindy対応されています。 それ以前はGroovy独自機構による性能改善が行われていました。 しかし、JDK7からのホヤホヤなindyを前提にしてしまうと、JDK6で動かなくなってしまいます。 Groovyは下位互換性が重視されているので、これは問題です。 というわけで、indyが使えないJDK上でも使えるように既存の独自機構もそのまま残っているし、indy不要バージョンが標準になっていて、indyオプションを利用可能にするには$GROOVY_HOME/lib配下を$GROOVY_HOME/indy配下にあるファイルで上書き置換してあげないといけません。

と言うぐらいに色々面倒くさいのですが、じゃあそこまでやってindyを使ったところでどれだけ嬉しいのかというと、JDK7までは「全然うれしくない」が答えでした。 JDK7のindy実装がこなれてない&Groovyのindy対応もカリカリではないので、全然性能がでなかったんですね。 独自機構の方が全然速かったわけです。

さて、「JDK8ではInvokeDynamic(以下、indy)の実装を一新したのですごく速くなったよ」という話を受けて試してみたんですが、indyモード&JDK8すごくヤバいことになってました。

サンプルコード

コードとしてはフィボナッチ数の算出です。 ローカルにあったファイルを発掘したもなので、実装が云々というツッコミはこの際受け付けません。

class Fibonacci {
    def calc = { n ->
      return (n == 0) ?  0 :
             (n == 1) ?  1 :
             call(n - 1) + call(n - 2)
    }
 
    static void main(String[] args) {
        def num = args[0] as int
        println "Fib($num) = ${new Fibonacci().calc(num)}"
    }
}

これをJDKの7u55と8u05で実行してtimeコマンドで計ってみました。 厳密なベンチではないので参考ぐらいのやさしい気持ちで読んでください。 ちなみにGroovyは最新ホヤホヤの2.3.0です。

Groovy 2.3.0 (indy有効) + JDK 7u55

$ java -version
java version "1.7.0_55"
Java(TM) SE Runtime Environment (build 1.7.0_55-b13)
Java HotSpot(TM) 64-Bit Server VM (build 24.55-b03, mixed mode)

$ time groovy --indy Fibonacci.groovy 40
Fib(40) = 102334155
groovy --indy Fibonacci.groovy 40  42.03s user 0.35s system 102% cpu 41.538 total

$ time groovy --indy Fibonacci.groovy 40
Fib(40) = 102334155
groovy --indy Fibonacci.groovy 40  33.75s user 0.34s system 102% cpu 33.197 total

$ time groovy --indy Fibonacci.groovy 40
Fib(40) = 102334155
groovy --indy Fibonacci.groovy 40  41.01s user 0.35s system 102% cpu 40.512 total

Groovy 2.3.0 (indy有効) + JDK 8u05

$ java -version
java version "1.8.0_05"
Java(TM) SE Runtime Environment (build 1.8.0_05-b13)
Java HotSpot(TM) 64-Bit Server VM (build 25.5-b02, mixed mode)

$ time groovy --indy Fibonacci.groovy 40
Fib(40) = 102334155
groovy --indy Fibonacci.groovy 40  20.04s user 0.29s system 108% cpu 18.777 total

$ time groovy --indy Fibonacci.groovy 40
Fib(40) = 102334155
groovy --indy Fibonacci.groovy 40  19.73s user 0.29s system 108% cpu 18.520 total

$ time groovy --indy Fibonacci.groovy 40
Fib(40) = 102334155
groovy --indy Fibonacci.groovy 40  20.27s user 0.31s system 107% cpu 19.076 total

実に2倍。これはヤバい。

(参考)Groovy 2.3.0 (indy無効=Groovy独自機構) + JDK 8u05

参考までにindyを使わないGroovy独自機構による性能も見てみましょう。

$ java -version
java version "1.8.0_05"
Java(TM) SE Runtime Environment (build 1.8.0_05-b13)
Java HotSpot(TM) 64-Bit Server VM (build 25.5-b02, mixed mode)

$ time groovy Fibonacci.groovy 40
Fib(40) = 102334155
groovy Fibonacci.groovy 40  25.60s user 0.30s system 106% cpu 24.407 total

$ time groovy Fibonacci.groovy 40
Fib(40) = 102334155
groovy Fibonacci.groovy 40  23.40s user 0.28s system 107% cpu 22.031 total

$ time groovy Fibonacci.groovy 40
Fib(40) = 102334155
groovy Fibonacci.groovy 40  23.68s user 0.28s system 107% cpu 22.288 total

なんと、JDK7+indyにはダブルスコアで勝ってたのに、JDK8+indyに追い越されてしまいました。

indy全盛時代の幕開けを感じますね。

というようなことを、Java-ja枠のTDDセッションを聞きながらまとめてみました。

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

gradle

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に表示してもらう

grails gradle mac

はじめに

この記事は、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さんです!