CLOVER🍀

That was when it all began.

Grapeを使ってハマったこと

Grapeを使ってちょっとハマったことについて、Twitterとかでチラチラ見かけていたので、これを機に自分も少しまとめておきます。

あ、ちなみに、別にGrapeをなにか貶めたいわけではなく、むしろ自分はよくGrapeを使っている人ですので。Grapeの便利さは、Groovyを使っている大きな理由に他なりません。

ただまあ、同じような理由でハマる方がいらっしゃった場合の、何かの参考になればと。

自分が過去にハマった(もしくは見かけた)のは、以下のケースです。

最後のは、けっこう最近に遭遇したケースですね。依存関係を一部解決できないというのは、仕事で遭遇したのですが、自宅で再度確認したら、再現しませんでした…。

まあ、とりあえず載せていってみましょう。

Groovy自身に含まれているライブラリと衝突する

以前、組み込みTomcatで遊ぼうとして、以下のようなプログラムを書きました。
embedded_tomcat.groovy

import javax.servlet.http.HttpServlet
import javax.servlet.http.HttpServletRequest
import javax.servlet.http.HttpServletResponse

@Grapes([
    @Grab('org.apache.tomcat:tomcat-catalina:7.0.42'),
    @Grab('org.apache.tomcat.embed:tomcat-embed-core:7.0.42'),
    @Grab('org.apache.tomcat:tomcat-jasper:7.0.42'),
    @GrabConfig(systemClassLoader=true, initContextClassLoader=true)
])
import org.apache.catalina.startup.Tomcat

def tomcat = new Tomcat()
tomcat.port = 8080
// tomcat.connector.useBodyEncodingForURI = true

def context = tomcat.addWebapp('/', '.')
Tomcat.addServlet(context, 'simpleServlet', new SimpleServlet())
context.addServletMapping('/simpleServlet', 'simpleServlet')

tomcat.start()
tomcat.server.await()
// tomcat.stop()

class SimpleServlet extends HttpServlet {
    // 〜省略(簡単なServletプログラム)〜
}

*過去のものをそのまま使ったので、Tomcatが古いのはご愛嬌。

動作させるためには、webappsディレクトリが必要なようなので、カレントディレクトリに

$ mkdir -p tomcat.8080/webapps

を実行して作成しておきます。

で、実行。

$ groovy embedded_tomcat.groovy

ところが、これは盛大にコケます。スタックトレースなどは端折りますが、この時はよくよく見ると、以下のようなものが含まれていました。

Caused by: java.lang.NoSuchMethodError: javax.servlet.ServletContext.getSessionCookieConfig()Ljavax/servlet/SessionCookieConfig;

NoSuchMethodError!ServletContextにServlet 3.0で追加されたメソッドが、わかっていないようですね。はて、Tomcatに含まれているServlet APIはどうしたことでしょう?
*一応、新しいバージョンのTomcatも試してみましたけど、エラーが少し変わったNoSuchMethodErrorが出ました

苦し紛れにこんなのを入れていますが、効果なし。

    @GrabConfig(systemClassLoader=true, initContextClassLoader=true)

で、問題のServletContextが誰からロードされているのか、ちょっと確認してみました。

println(javax.servlet.ServletContext.class.classLoader.getResource('javax/servlet/ServletContext.class'))

結果、こちらから。

jar:file:/[HOMEディレクトリ]/.gvm/groovy/current/lib/servlet-api-2.4.jar!/javax/servlet/ServletContext.class

つまり、Groovyに入っているServlet APIを見ていました、と。これ、Servlet API 2.4なんですよねー。

Groovyに含まれているJARをどこかに退避すれば起動するようになりますが、そういう対処もちょっとないですよね。このようにGroovyに含まれているJARと衝突してどうにもならない場合は、Gradleとかに行った方が良さそうです。

アーティファクトなどの指定を動的にはできない

Gradleで使われるように、Grabで指定するバージョンをまとめたりしようとしてGStringを使用した以下の定義を書くと

def version = '3.3.2'
@Grab("org.apache.commons:commons-lang3:$version")
import org.apache.commons.lang3.StringUtils

println(StringUtils.join(['Hello', 'World'], ', '))

以下のようにエラーになります。

The missing attribute "group" is required in @Grab annotations
 at line: 2, column: 1

これ、GStringの評価の前にAST変換がかかるので、Grabアノテーション単品で指定されておく必要があるんでしょうねぇ…。

ちなみに、こういう書き方もダメです。

def version = '3.3.2'
@Grab('org.apache.commons:commons-lang3:' + version)

さらに言うと、こういうのもダメです。

def ver = '3.3.2'
@Grab(group = 'org.apache.commons', module = 'commons-lang3', version = ver)

この場合は、

org.codehaus.groovy.control.MultipleCompilationErrorsException: startup failed:
/path/to/script.groovy: 4: Attribute "version" has value ver but should be an inline constant in @Grab annotations
 @ line 4, column 1.
   @Grab(group = 'org.apache.commons', module = 'commons-lang3', version = ver)
   ^

と怒られます。

groovycでコンパイルしてjavaコマンドで実行するのは、諦めた方がよさそう

解決できなかったネタです。

@Grabアノテーションを使ったスクリプトを、groovycでコンパイルしてjavaコマンドで実行させるのは諦めた方がよさげです。

That darned "No suitable ClassLoader found for grab" with groovyc and java -cp
http://www.randomactsofsentience.com/2012/01/that-darned-no-suitable-classloader.html

クラスローダーやGrapeの初期化まわりでエラーになるので、Grapeを使う場合はgroovyコマンドでスクリプト的に使用するケースと割り切った方が安全そうです。

Grabアノテーションで指定した依存関係を、一部解決できずに失敗する

これは、HTTPBuilderを使っていてハマったことがあります。

GroovyのHTTPBuilderで、multipart/form-dataを送信する
http://d.hatena.ne.jp/Kazuhira/20140126/1390708336

当時、HTTPBuilderを使用する時にこんな感じに書くと、

@Grab('org.codehaus.groovy.modules.http-builder:http-builder:0.6')

以下のようなエラーをもらってハマっていました。

org.codehaus.groovy.control.MultipleCompilationErrorsException

で、この時うまくいっていなかったのは、依存しているcommons-codecを取得できずに失敗していました、と。

この時は、commons-codecをExcludeしてから自分で追加すると、なんとか動きました。

@Grab('commons-codec:commons-codec:1.9')
@Grab('org.codehaus.groovy.modules.http-builder:http-builder:0.6')
@GrabExclude('commons-codec:commons-codec')

なんですけど、今の自宅環境ではうまく再現せず(もともと、会社の環境で見つけたので…)。

同じ条件でハマっていた方を見かけたことがあるので、何か原因があるんでしょうけど、この時ちゃんと追いかけて入ればよかったかなーと思っています。

なんですが、もしかしたら次に挙げるものに関連していたのかもしれませんね。

Grapeが参照するMavenリポジトリが、微妙に不完全

これは、@eiryuさんがハマっていたのを一緒に追いかけたネタですね。

Grapeの依存性解決に失敗してjcenterに問い合わせた話
http://eiryu.hatenablog.com/entry/20140430/1398874534

Failed resolving by Groovy's Grape
https://github.com/making/ltsv4j/issues/1

Grapeが参照するリポジトリのうちのひとつである、JCenterにpom.xmlはあるのにJARファイルが存在せず、Ivyが依存関係を解決できなくなりハマり続けてしまうというパターン。

もともと、Grapeの依存関係の解決順は

1. Grape's local cache
2. Maven local cache
3. Codehaus Maven repo
4. Maven Central
5. Java.net Maven repo

だったらしいのですが、Groovy 2.1.8から以下のようにJCenterが追加されたようです。

[GROOVY-6380] Improve Grapes resolution speed and stability by adding Bintray JCenter as first remote resolver in the chain
http://jira.codehaus.org/browse/GROOVY-6380

Codehausのリポジトリが遅い、有名なライブラリがあんまり入っていない、というところから追加されたリポジトリのようです。

というわけで、今の(Groovy 2.1.8〜少なくとも2.2.2)Grapeのデフォルトの依存関係の解決順は

1. Grape's local cache
2. Maven local cache
3. Bintray's JCenter
4. Codehaus Maven repo
5. Maven Central
6. Java.net Maven repo

となります。

参考)
https://github.com/groovy/groovy-core/blob/GROOVY_2_2_2/src/resources/groovy/grape/defaultGrapeConfig.xml

なんですが、先の@eiryuさんのエントリに載せてあるltsv4jは、JCenterにはpom.xmlしかなく、JARファイルが存在しない状態でした。

なので、予想としては…

  • 依存関係を解決する際の、最初のルックアップ先でIvyがpom.xmlを見つける
  • pom.xmlは見つかるものの、JARがないためそこで諦める

JARが見つからなければ、次のリポジトリへ行く…なんて動作はしないと思いますので、そこで延々とハマっていたのがこの時の事象と思われます。

こうなってしまうと、標準的にGrapeを使っているといつまでも解決できません。

ちなみに、デバッグは双方

$ grape -V install <group> <module> <version>

や

$ groovy -Dgroovy.grape.report.downloads=true -Divy.message.logger.level=4 <スクリプト名>

などでやっていました。

で、どう解決したか?

解決策は、以下あたりが考えられます。

  • 問題のあるMavenリポジトリを修正してもらう(上記の場合は、JCenter)
  • Mavenであらかじめダウンロードしておく(未検証…)
  • 問題のあるMavenリポジトリをGrapeの参照先から解除する

最初の「問題のあるMavenリポジトリを修正してもらう」が、今回の解決方法となりました。なんと、リポジトリ側に依頼して対応してもらえるとは…。

で、未検証ですがGrapeではなくてMavenであらかじめダウンロードしておけば、依存関係の解決時のルックアップ先にMavenのローカルリポジトリが入っているので、なんとかなるのではないかなーと思っています。

最後、「問題のあるMavenリポジトリをGrapeの参照先から解除する」ですが、こちらは自分が取った方法です。

以下のパスにGrapeの設定をGitHubなりからコピーしてきて、今回の場合はJCenterを削除するとMaven Centralまで流れるようになります。

$HOME/.groovy/grapeConfig.xml

Maven Centralなどにアーティファクトがある場合は、これで解決することができます。

…なんですけど、最後の2つの方法はGroovyスクリプトのポータビリティがなくなるので、あまり本質的な解決策ではありません。できれば、リポジトリ側が中途半端な状態にならないようにしていただきたいところですが。

速度アップのためにJCenterを追加したようなので、あんまりトラブルが出てしまうと、微妙ですよね…。

その他、ハマりどころを見つけたら、また追記します〜。