#navi_header|Groovy|
Groovyを使うと超お手軽にWeb開発を始められます。・・・といっても、ちょっとしたスタブとか開発中のツール類専用です。
Jettyを組み込んでGroovyスクリプトで現在ディレクトリをさっくり公開しておしまい、な3分クッキングレベルですが、Groovyが提供しているGroovyServletとTemplateServletのお陰で、スクリプト感覚でJava/Groovy Servletプログラミングを楽しめます。
GroovyServlet, TemplateServlet共に、リクエストされる度に評価・実行されるため、書き換えたらすぐにブラウザから確認できます。コンテナの再起動など一切不要です。ただその代わり、毎回評価・実行されるためにパフォーマンスは悪いはずです。
ですので、プロダクトコード用というよりは実験や内部で使うツール類をさくっとでっち上げたい時に使う、そんな使い方になると思います。
''※Windows上で実験する人、またはGroovyServletを使うと1度目は正常に動作するのに2回目以降のアクセスで404になってしまう人は、「GroovyServlet経由で".groovy"にアクセスした時に、2回目以降で"Script not found, sending 404."とスタックトレースが発生する場合の回避策」も参照してください。(2012-10-08追記)''
#more||
#outline||
----
* 下準備
2012-09-30時点でのJetty8最新版を使います・・・が、なんかorg.eclipse.jetty.orbitの依存解決でエラーが出るので、手動で調整する必要があります。
ひとまずGrapeで手動で"org.eclipse.jetty.aggregate"から"jetty-all"をインストールします。
$ grape install org.eclipse.jetty.aggregate jetty-all 8.1.7.v20120910
・・・が、本記事を書いてる時点だと絶対に org.eclipse.jetty.orbit経由でのjavax.servlet-3.0.0.v201112011016.jarがDLできず、失敗します。
なぜか javax.servlet-3.0.0.v201112011016.''orbit'' をDLしようとするんです・・・。
ivyのxmlファイルで"ext"部分が"jar"じゃなくて"orbit"になってしまってるんですよね。というわけでこれを手で修正します。
"~/.groovy/grape/org.eclipse.jetty.orbit/javax.servlet/ivy-3.0.0.v201112011016.xml":
#pre||>
->次のようにext="jar"に修正します。
||<
これで改めて "grape install" すれば多分正常にJettyの8.1.7最新版をインストールできるはずです。
* 環境
今回の実験環境です。
Mac OS X 10.7.5
JDK 1.6.0_35
Groovy Version: 1.8.6 JVM: 1.6.0_35 Vendor: Apple Inc. OS: Mac OS X
* web.xml無し版
j1/j1.groovy:
#pre||>
import org.eclipse.jetty.server.*
import org.eclipse.jetty.servlet.*
import groovy.servlet.*
@Grab(group='org.eclipse.jetty.aggregate', module='jetty-all', version='8.1.7.v20120910')
def server = new Server(8080)
def context = new ServletContextHandler(server, '/', ServletContextHandler.SESSIONS)
context.resourceBase = '.'
context.addServlet(GroovyServlet, "*.groovy")
context.addServlet(TemplateServlet, "*.gsp")
server.start()
server.join()
||<
実行:
$ cd j1/
$ groovy j1.groovy
以下のファイルはUTF-8で保存してます。
GroovyServletの使用例:
j1/g1.groovy:
#pre||>
println """
test
hello, Jettyの組み込みテストです。
${application.getServerInfo()}
"""
||<
TemplateServletの使用例:
j1/g2.gsp:
#pre||>
TemplateServlet
TemplateServletの組み込みテストです。
<% println params %>
<% 3.times { %>
Hello World!
<% } %>
||<
"g1.groovy"は文字化けしなかったけど、"g2.gsp"の日本語部分は文字化けてしまいました。また後日調べます。
WEB-INF/のディレクトリ構成や、web.xml無しで始められるのが良い点です。
代わりに、".html"などの静的ファイルの公開も自前でaddServlet()などする必要があります。
静的ファイルやライブラリjarファイルの呼び出しが出てきたら、次に紹介するweb.xml付きのパターンにした方が楽ちんです。
* web.xml有り版
j2/j2.groovy:
#pre||>
import org.eclipse.jetty.server.*
import org.eclipse.jetty.webapp.*
import groovy.servlet.*
@Grab(group='org.eclipse.jetty.aggregate', module='jetty-all', version='8.1.7.v20120910')
def server = new Server(8080)
def webapp = new WebAppContext('.', '/')
server.handler = webapp
server.start()
server.join()
||<
j2/WEB-INF/web.xml:
#pre||>
Groovlet
groovy.servlet.GroovyServlet
Template
groovy.servlet.TemplateServlet
Groovlet
*.groovy
Template
*.gsp
||<
j1からg1.groovyやg2.gspなどをコピペしてきます。
j2/
j2.groovy
g1.groovy
g2.gsp
WEB-INF/web.xml
実行:
$ cd j2/
$ groovy j2.groovy
web.xml有り版の場合はWebアプリとしてdeployされるためか、静的コンテンツなども通常のWebアプリと同じお作法で配置し、公開できます。
* Windows環境での正体不明のトラブル
最初、Windows上で実験してたんです。
"g1.groovy"への1度目のアクセスは正常に動作します。
ところが、2回目以降のアクセスではなぜか以下の様なスタックトレースが発生し、404が返されてしまいます。
#pre||>
java.io.IOException: ファイル名、ディレクトリ名、またはボリューム ラベルの構文が間違っています。
at java.io.WinNTFileSystem.canonicalize0(Native Method)
at java.io.Win32FileSystem.canonicalize(Win32FileSystem.java:414)
at java.io.File.getCanonicalPath(File.java:589)
at org.eclipse.jetty.util.resource.FileResource.getAlias(FileResource.java:191)
at org.eclipse.jetty.server.handler.ContextHandler.getResource(ContextHandler.java:1569)
at org.eclipse.jetty.webapp.WebAppContext.getResource(WebAppContext.java:346)
at org.eclipse.jetty.webapp.WebAppContext$Context.getResource(WebAppContext.java:1248)
at groovy.servlet.AbstractHttpServlet.getResourceConnection(AbstractHttpServlet.java:182)
at groovy.util.GroovyScriptEngine.isSourceNewer(GroovyScriptEngine.java:564)
at groovy.util.GroovyScriptEngine.loadScriptByName(GroovyScriptEngine.java:496)
at groovy.util.GroovyScriptEngine.createScript(GroovyScriptEngine.java:549)
at groovy.util.GroovyScriptEngine.run(GroovyScriptEngine.java:536)
at groovy.servlet.GroovyServlet$1.call(GroovyServlet.java:120)
at org.codehaus.groovy.runtime.GroovyCategorySupport$ThreadCategoryInfo.use(GroovyCategorySupport.java:109)
at org.codehaus.groovy.runtime.GroovyCategorySupport$ThreadCategoryInfo.access$400(GroovyCategorySupport.java:65)
at org.codehaus.groovy.runtime.GroovyCategorySupport.use(GroovyCategorySupport.java:249)
at groovy.servlet.GroovyServlet.service(GroovyServlet.java:129)
at javax.servlet.http.HttpServlet.service(HttpServlet.java:802)
at org.eclipse.jetty.servlet.ServletHolder.handle(ServletHolder.java:648)
||<
TemplateServlet("g2.gsp")は影響を受けず、何度アクセスしても正常に動作してくれます。GroovyServletの場合だけこうなってしまうという謎・・・。
今回の記事ではまずした準備で書いた org.eclipse.jetty.orbit 経由の javax.servlet のインストールで1時間以上嵌ってしまい、さらにこの謎のスタックトレースで2時間ほど嵌りました。最終的に "java.io.WinNTFileSystem.canonicalize0" でWindows上でのファイル名の形式がおかしくなってるのかも・・・ならばUnix系のMacなら、とMacで試してみたらあっさりと動いてしまいました。
(上記スタックトレースの原因と回避策について以下に追記しました:2012-10-08)
** GroovyServlet経由で".groovy"にアクセスした時に、2回目以降で"Script not found, sending 404."とスタックトレースが発生する場合の回避策
非常に原因を掴みづらいトラブルですが、GroovyServlet経由で".groovy"にアクセスした時、1回目のアクセスは正常に動作するのに、2回目以降のアクセスで404発生、JettyなどServletContainer側で例外が発生しスタックトレースと"Script not found, sending 404."というエラーメッセージが出力される場合があります。
その場合、以下の様な方法で404とスタックトレースを回避することが可能です。(Groovy 1.8.8 + Jetty 8, Win7SP1, JDK1.7で確認)
*** 回避策:groovy.servlet.GroovyServletを継承した独自のServletクラスでgetResourceConnection()をカスタマイズする。
詳細は後述しますが、groovy.servlet.AbstractHttpServletで実装されているgetResourceConnection()に、1行だけ追加したServletクラスを追加します。
+ 手っ取り早くgroovy.servlet.GroovySerlvetを継承しただけの空っぽのクラスをJavaでもGroovyでも良いので作成。
+ groovy.servlet.AbstractHttpServletのgetResourceConnection()メソッドのシグネチャをコピペしてくる。
+ getResourceConnection()では最初に次の1行を実行したら、すぐに親クラス(super)のgetResourceConnection()に処理を委譲する。: if (name.startsWith("file:")) name = name.replaceFirst("file:", "");
+ web.xmlでgroovy.servlet.GroovyServletではなく、今回作成したServletクラスで".groovy"を処理させる。
今回は以下の様な構成にしました。
#pre||>
WEB-INF/
web.xml
classes/
groovy/
servlet/
MyGroovyServlet.groovy
||<
MyGroovyServlet.groovy:
#pre||>
package groovy.servlet
import groovy.servlet.*
public class MyGroovyServlet extends GroovyServlet {
public URLConnection getResourceConnection(String name) throws ResourceException {
if (name.startsWith("file:")) name = name.replaceFirst("file:", "")
return super.getResourceConnection(name)
}
}
||<
groovycでコンパイルしておきます。
$ cd WEB-INF/classes/
$ groovyc groovy/servlet/MyGroovyServlet.groovy
WEB-INF/web.xml:
#pre||>
Groovlet
groovy.servlet.MyGroovyServlet
Groovlet
*.groovy
||<
これで、2回目以降のアクセスでも正常に動作するGroovyServletでお手軽プログラミングが楽しめるようになります。
*** 2回目以降のアクセスでトラブルになる原因
GroovyScriptEngineのキャッシュ更新チェック時に、すでに"file:"スキーマのURL展開されたものに対してServletContext.getResource()を呼んでしまっていることが原因です。
まず今回のトラブルに関連する箇所に絞って、GroovyServletが".groovy"ファイルを実行する仕組みを簡単に解説します。
GroovyServletはGroovyScriptEngineで指定されたgroovyファイルを実行しています。この時、リクエストの度に毎回GroovyScriptEngineを初期化するのではなく、Servletのinit()中に初期化したGroovyScriptEngineを使いまわしています。
GroovyScriptEngineでは、一度実行したスクリプトについては内部でキャッシュしています。同じスクリプト名を実行するとき、GroovyScriptEngineは指定されたスクリプト名の"リソース"の最終更新日を調べ、再コンパイルが必要か判断しています。
GroovyScriptEngineではgroovy.util.ResourceConnectorのgetResourceConnection()を介して「スクリプト名」から実際のスクリプトを取り出しています。この時、実際のスクリプトはURLConnectionの形で取り出され、スクリプトのキャッシュはURLConnection.getURL().toExternalForm()の戻り値をキーとしてキャッシュされる・・・ようです。(すみません、ここちょっと不確かで、GroovyScriptEngine中でのscriptCache.get()メソッドの呼び出しは全てURL.toExternalForm()の値を引数にしているんですが、scriptCache.put()のキー値がどういう表現になるのか確認しきれていません)
ファイルシステム上のスクリプト名を取り出す場合は、「スクリプト名」ほぼイコール「ファイル名」でほぼそのままURLに変換できます。
しかしServletContainer上で指定された場合、「スクリプト名」は "/foo/bar/baz.groovy" のようなURLパス名で指定されてきます。これを、ServletContextのgetResource()を使って、実際に展開されているファイル名に変換する必要があります。
この変換処理を実装しているのが、groovy.servlet.AbstractHttpServletで実装されているgetResourceConnection()メソッドになります(AbstractHttpServletはResourceConnectorを実装しています)。
実際にローカルファイルシステム上でのファイルパスをURLで取り出しtoExternalForm()に変換すると、以下のようになります。
Windows: file:C:¥foo¥bar¥baz.groovy
Mac: file:/foo/bar/baz.groovy
ここまでを踏まえて、デフォルトの実装のGroovyServletがURLで指定された".groovy"をどう処理するか1回目と2回目で見ていきます。
1回目:
+ URLで指定されたURLをGroovyScriptEngineに渡す。
+ GroovyScriptEngineはResourceConnectorのgetResourceConnection()を呼び出す。
+ この時呼び出されるResourceConnectorはGroovyServlet、ひいてはAbstractHttpServletのインスタンスになる。(GroovyServletがinit()中でGroovyServletEngineのコンストラクタで、ResourceConnectorとして自分自身のthisを渡している)
+ AbstractHttpServletのgetResourceConnection()により、URLからServletContext.getResource()により実際のファイルパスのURLに変換される。
+ GroovyScriptEngine内でisSourceNewer()によりキャッシュが無いかチェックされるが、1回目では当然キャッシュされていないので、そのままキャッシュにputされる。
2回目:
+ isSourceNewer()までは同じ。
+ isSourceNewer()内でスクリプトのキャッシュについて最終更新日を確認するが、この時キャッシュのスクリプト名について再度ResourceConnectorのgetResourceConnection()を呼び出す。
この2回目以降のisSourceNewer()内のgetResourceConnection()の呼び出しが問題で、Macの場合は2回目以降では
"/foo/bar/baz.groovy"
がgetResourceConnection()に渡されてきます。この形式であれば、AbstraceHttpServlet内ではServletContextのgetRealPath("/")で親ディレクトリを取り出し、"/baz.groovy"まで戻した上で再度 ServletContext.getResource("/baz.groovy")で正しいファイルパスのURLConnectionを返すことが出来ます。つまりAbstractHttpServletのデフォルト実装で正常に期待通りの動作をしてくれます。
ところがWindowsの場合、なぜか2回目以降では
"file:C:¥foo¥bar¥baz.groovy"
というのが渡されてきてしまいます。これでは、AbstractHttpServletのデフォルト実装では元の"/baz.groovy"に戻せません。よって、これがそのまま ServletContext.getResource("file:C:¥foo¥bar¥baz.groovy")の呼び出しになってしまい、不正なファイルパスとして例外が発生し、以下のようなスタックトレースになってしまったわけです。
java.io.IOException: ファイル名、ディレクトリ名、またはボリューム ラベルの構文が間違っています。
at java.io.WinNTFileSystem.canonicalize0(Native Method)
...
at org.eclipse.jetty.webapp.WebAppContext$Context.getResource(WebAppContext.java:1248)
at groovy.servlet.AbstractHttpServlet.getResourceConnection(AbstractHttpServlet.java:182)
(ここでちょうど、isSourceNewerからAbstractHttpServletのgetResourceConnection()が呼ばれてます)
at groovy.util.GroovyScriptEngine.isSourceNewer(GroovyScriptEngine.java:564)
at groovy.util.GroovyScriptEngine.loadScriptByName(GroovyScriptEngine.java:496)
at groovy.util.GroovyScriptEngine.createScript(GroovyScriptEngine.java:549)
at groovy.util.GroovyScriptEngine.run(GroovyScriptEngine.java:536)
...
*** 回避策の解説
ここまで判明し、さてどうやってこの問題を回避するか、となるわけですが、たかがお手軽WebプログラミングをしたいがためだけにGroovyScriptEngineまで手を出す余裕はありません。
「その場限りの」アドホック対応として、getResourceConnection()に"file:"始まりの文字列が渡されてきたら先頭の"file:"を空文字列に置換してから処理するように修正します。これが回避策で示したMyGroovyServletのソースコードで、先頭の"file:"を空にしてから親の(=AbstractHttpServletの)本来のgetResourceConnection()の実装に渡しています。
回避策自体の本質はたったの1行なのですが、なぜその1行でOKなのか、という部分がServlet処理と言うよりはGroovyのScript処理の内部実装の話になってしまい、調査に時間を取られてしまいました。
* 参考:
- Groovy - Grape
-- http://groovy.codehaus.org/Grape
--- 一番下に、Jettyサーバを組み込んでTemplateServletを提供する例が載ってます。
- GroovyServlet (groovy 2.0.4 API)
-- http://groovy.codehaus.org/gapi/
- Groovy - Groovy Templates
-- http://groovy.codehaus.org/Groovy+Templates
--- 最後の方にweb.xmlで動かす場合のサンプルが載ってます。
- Practically Groovy: Go server-side up, with Groovy
-- http://www.ibm.com/developerworks/java/library/j-pg03155/
Jetty組み込み関連:
- Jetty/Tutorial/Embedding Jetty - Eclipsepedia
-- http://wiki.eclipse.org/Jetty/Tutorial/Embedding_Jetty
- Jetty/Reference/Dependencies - Eclipsepedia
-- http://wiki.eclipse.org/Jetty/Reference/Dependencies
- Jetty/Tutorial/Jetty HelloWorld - Eclipsepedia
-- http://wiki.eclipse.org/Jetty/Tutorial/Jetty_HelloWorld
意外に苦労した点としては、GroovyやJetty組み込み関連のWikiページ資料などで使われているJettyのバージョンが古く(Jetty6や7)、Jetty8だとクラス名やパッケージ階層が(今回のような超基本的かつ単純なクラスでさえも)変更されてしまっていたため、いちいちJetty8のJavaDoc上で探し直して、引数なども再確認が必要だったのが手間でした。
#navi_footer|Groovy|