Groovy の @Grab で Spark Framework を実行

Spark Framework - A tiny Java web framework を Groovy の @Grab を使って実行してみました。

今回のソースは http://github.com/fits/try_samples/tree/master/blog/20160801/

はじめに

以下のように @Grab を使った Spark の Groovy スクリプトを groovy コマンドで実行し、Web クライアントでアクセスしてみると、java.lang.NoSuchMethodError: javax.servlet.http.HttpServletResponse.getHeaders エラーが発生してしまいました。

now.groovy
@Grab('com.sparkjava:spark-core:2.5')
@Grab('org.slf4j:slf4j-simple:1.7.21')
import static spark.Spark.*

get('/now') { req, res -> new Date().format('yyyy/MM/dd HH:mm:ss') }

groovy コマンドで上記スクリプトを実行。

実行例
> groovy now.groovy

・・・
[Thread-1] INFO org.eclipse.jetty.server.Server - Started @3213ms

/now へアクセスすると、以下のようなエラーが発生。

エラー例(クライアント側)
$ curl http://localhost:4567/now

<html>
<head>
<meta http-equiv="Content-Type" content="text/html;charset=ISO-8859-1"/>
<title>Error 500 </title>
</head>
<body>
<h2>HTTP ERROR: 500</h2>
<p>Problem accessing /now. Reason:
<pre>    java.lang.NoSuchMethodError: javax.servlet.http.HttpServletResponse.getHeaders(Ljava/lang/String;)Ljava/util/Collection;</pre></p>
<hr /><a href="http://eclipse.org/jetty">Powered by Jetty:// 9.3.6.v20151106</a><hr/>
</body>
</html>
エラー例(サーバー側)
> groovy now.groovy

・・・
java.lang.NoSuchMethodError: javax.servlet.http.HttpServletResponse.getHeaders(Ljava/lang/String;)Ljava/util/Collection;
        at spark.utils.GzipUtils.checkAndWrap(GzipUtils.java:67)
        at spark.http.matching.Body.serializeTo(Body.java:69)
        at spark.http.matching.MatcherFilter.doFilter(MatcherFilter.java:158)
        at spark.embeddedserver.jetty.JettyHandler.doHandle(JettyHandler.java:50)
        at org.eclipse.jetty.server.session.SessionHandler.doScope(SessionHandler.java:189)
        ・・・

このエラーは、groovy コマンドの実行時に $GROOVY_HOME/lib/servlet-api-2.4.jar (Servlet 2.4) を先にロードする事が原因で発生しているようです。

HttpServletResponse.getHeadersServlet 3.0 から追加されたメソッドのため、Servlet 2.4 の API が適用されていると該当メソッドが存在せず NoSuchMethodError になります。

回避策

エラー原因は、groovy コマンドが $GROOVY_HOME/lib/servlet-api-2.4.jar をロードする事なので、Gradle 等で実行すれば上記のようなエラーは発生しません。

しかし、今回は groovy コマンドで実行する場合の回避策をいくつか検討してみました。

  • (a) -cp オプションを使用
  • (b) Groovy 設定ファイル (groovy-starter.conf) を編集
  • (c) $GROOVY_HOME/lib/servlet-api-2.4.jar を削除

(a) と (b) は Servlet 2.4 より先に Servlet 3.1 を適用させる方法で、(c) は servlet-api-2.4.jar をロードさせない方法です。

(a) -cp オプションを使用

Servlet 3.1 の JAR (下記では javax.servlet-api-3.1.0.jar)を入手し、groovy コマンドの -cp オプションでその JAR を指定して実行します。

こうする事でエラーは出なくなりました。

実行例(サーバー)
> groovy -cp lib_a/javax.servlet-api-3.1.0.jar now.groovy
・・・
実行例(クライアント)
$ curl http://localhost:4567/now
2016/07/31 20:46:42

(b) Groovy 設定ファイル (groovy-starter.conf) を編集

$GROOVY_HOME/lib/servlet-api-2.4.jar は groovy-starter.conf の設定 (load !{groovy.home}/lib/*.jar) によりロードされています。

つまり、$GROOVY_HOME/lib/*.jar よりも先に、別のディレクトリ内の JAR をロードするように groovy-starter.conf を書き換え、そのディレクトリへ Servlet 3.1 の JAR を配置すれば、(a) と同様に回避できるはずです。

groovy-starter.conf 変更例
# 以下を追加
load lib_a/*.jar

# load required libraries
load !{groovy.home}/lib/*.jar

・・・

lib_a/javax.servlet-api-3.1.0.jar を配置して実行すると、エラーは出なくなりました。

実行例(サーバー)
> groovy now.groovy
・・・
実行例(クライアント)
$ curl http://localhost:4567/now
2016/07/31 20:48:05

備考. Groovy 設定ファイル (groovy-starter.conf) の指定方法

groovy-starter.conf を直接書き換えるのはイマイチなので、任意の Groovy 設定ファイルを使いたいところです。

startGroovy スクリプトの内容を見ると GROOVY_CONF 環境変数で指定できそうです。

ただし、startGroovy.bat の方は今のところ GROOVY_CONF 環境変数を考慮しておらず、Windows 環境 (groovy.bat を使う場合) では使えません。

そこで今回は、下記 postinit.bat を用意し、Windows 環境で GROOVY_CONF に対応してみました。

%USERPROFILE%/.groovy/postinit.bat の例
if not "%GROOVY_CONF%" == "" (
    set GROOVY_OPTS=%GROOVY_OPTS% -Dgroovy.starter.conf="%GROOVY_CONF%"
    set STARTER_CONF=%GROOVY_CONF%
)

上記を配置した後、以下のように実行します。

GROOVY_CONF 環境変数の利用例 (Windows
> set GROOVY_CONF=groovy-starter_custom.conf

> groovy now.groovy
・・・

(c) $GROOVY_HOME/lib/servlet-api-2.4.jar を削除

$GROOVY_HOME/lib/servlet-api-2.4.jar を一時的に削除(拡張子を変える等)して実行します。

備考. Gradle で実行する場合

Gradle で実行する場合は、src/main/groovy/now.groovy を配置して(@Grab の箇所は削除しておく)、以下のような build.gradle を使います。

build.gradle 例
apply plugin: 'groovy'
apply plugin: 'application'

repositories {
    jcenter()
}

dependencies {
    compile 'com.sparkjava:spark-core:2.5'
    compile 'org.codehaus.groovy:groovy:2.4.7'
    runtime 'org.slf4j:slf4j-simple:1.7.21'
}

mainClassName = 'now'
実行例
> gradle run
・・・
:run
・・・
[Thread-1] INFO org.eclipse.jetty.server.Server - Started @1035ms