#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|