Tomcat 上の JRuby サーブレット
2008.1/11 (鈴)
1. はじめに
- Rei: チュートリアル で JRuby から Java を使う方法は分かったけど,Java のシステムに JRuby を組み込むのはどうするんだろう…?
- Ling: どうしたの,珍しく考え込んで?
- R: JRuby でサーブレットを書こうと思って検索したら, Tomcat + JRuby でServletを書くっていうブログが見つかったんですが, なんだかとても大変そうなんです。
- L: えぇと…,そのブログのコメントに JavaUtil や JavaEmbedUtils を使えば ってあるよね。それで良くない?
本稿は, JRuby でサーブレットを記述し, Tomcat 上で動かす試みを記述する。 JRuby のバージョンは 1.0.3, Tomcat のバージョンは 6.0.14 である。 Mac OS X 10.4.11 上の Java 1.5.0_13 および Windows 2000 SP4 上の Java 1.6.0_03 で動作を確認した。
2. Hello.rb
class Hello < HttpServlet def doGet(req, res) res.content_type = 'text/html; charset="UTF-8"' wf = res.writer wf.println '<title>hello</title>' wf.println '<p>みなさん,こんにちは' end end
実現すべき目標として,上記のような JRuby によるサーブレット Hello.rb
を http://…略…/Hello.rb へのアクセスで駆動することを考える。
このサーブレットは Content-Type: text/html; charset="UTF-8"
で下記をレスポンスすることを意図している。
<title>hello</title> <p>みなさん,こんにちは
ここで基底クラス HttpServlet は Java Servlet API の
javax.servlet.http.HttpServlet である。
3. web.xml
apache-tomcat-6.0.14 を展開し,webapps フォルダの下に test フォルダ (このフォルダ名は適当に変えてよい) を作成し, その下に WEB-INF フォルダを作成する。 WEB-INF フォルダに下記の web.xml を置く。
<?xml version="1.0" encoding="ISO-8859-1"?> <web-app xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" version="2.5"> <servlet> <servlet-name>Rb</servlet-name> <servlet-class>JRubyServlet</servlet-class> </servlet> <servlet-mapping> <servlet-name>Rb</servlet-name> <url-pattern>*.rb</url-pattern> </servlet-mapping> </web-app>
これにより,初期設定のまま (apache-tomcat-6.0.14 下の bin フォルダにある
startup.sh または startup.bat で) Tomcat を起動したとき,
任意の X に対し,
http://localhost:8080/test/X.rb
へのアクセスで Java クラス JRubyServlet のサーブレットが呼び出される。
4. JRubyServlet.java
jruby-bin-1.0.3 を展開して得られる jruby-1.0.3 フォルダの直下にある lib フォルダだけを, (いましがた web.xml を置いた) WEB-INF フォルダの下に置く。
同じく WEB-INF フォルダに classes フォルダを作り, JRubyServlet.java のコンパイル結果である JRubyServlet.class を置く。 Mac OS X の場合,具体的には classes フォルダに JRubyServlet.java を置いて, 下記を行えばよい。
$ cd webapps/test/WEB-INF/classes $ javac -cp ../../../../lib/servlet-api.jar:../lib/jruby.jar JRubyServlet.java
JRubyServlet は HttpServlet の派生クラスであり,
フィールドとして JRuby 処理系オブジェクトと,JRuby による内部サーブレットを持つ。
import java.io.*; import java.util.*; import javax.servlet.*; import javax.servlet.http.*; import org.jruby.Ruby; import org.jruby.javasupport.JavaEmbedUtils; import org.jruby.runtime.builtin.IRubyObject; public class JRubyServlet extends HttpServlet { private Ruby ruby; // JRuby 処理系 private Servlet inner; // JRuby 上の内部サーブレット
init メソッドでは,まず基底クラスの init メソッドを実行する。
それから WEB-INF フォルダの実際のパス名を変数 path に取得する。
システム・プロパティ jruby.home を path の値にする。
これは通常の jruby コマンドでの環境変数 JRUBY_HOME の設定に相当する。
JavaEmbedUtils.initialize メソッドを呼び出して JRuby 処理系オブジェクト
ruby を作成する。
このメソッドは,作成する JRuby 処理系オブジェクトに対し,
jruby.home が指し示すフォルダ (この時点では WEB-INF) にある
lib 以下の適切なフォルダ群を標準モジュール検索パスとして設定する。
ruby のカレントディレクトリを path の値に設定する。 これにより,WEB-INF に置いた Ruby ファイルがモジュール検索の対象になる。
JRuby スクリプト require '_init'; InnerServlet.new を実行する。
ここでは JRuby スクリプト _init.rb がクラス InnerServlet を
javax.servlet.Servlet の実装クラスとして定義していると仮定する。
JavaEmbedUtils.rubyToJava メソッドを使って,
JRuby の InnerServlet インスタンスを
Java の javax.servlet.Servlet インスタンス inner として扱えるようにする。
inner を init メソッドで初期化する。
@Override public void init(ServletConfig config)
throws ServletException
{
super.init(config);
ServletContext context = config.getServletContext();
String path = context.getRealPath("/WEB-INF");
System.setProperty("jruby.home", path);
ruby = JavaEmbedUtils.initialize(new ArrayList<String> ());
ruby.setCurrentDirectory(path);
IRubyObject ro = ruby.evalScript("require '_init'; InnerServlet.new");
inner = (Servlet) JavaEmbedUtils.rubyToJava(ruby, ro, Servlet.class);
inner.init(config);
}
- R: 込み入っているけど,ひょっとしてここが今回の心臓部…ですか?
- L: その通り,難しいのはこの init メソッドだけです…えぇと…あと内部サーブレットの GET/POST 処理もちょっと難しいかな?
- R: ところで「内部サーブレット」って一体…?
- L: 本当に呼び出したいのは,URL に Hoge.rb があったら Hoge.rb だし, Fuga.rb があったら Fuga.rb なんだけど,Java で書くと面倒そうだから, JRuby にいったん丸投げするわけです…。
- R: それが内部サーブレット InnerServlet というわけなんですね。
サーブレット終了時には,内部サーブレットを終了させてから,
内部サーブレットの実行時処理系を終了させ,
最後に自分自身の HttpServlet オブジェクトとしての終了処理を行う。
@Override public void destroy()
{
inner.destroy();
JavaEmbedUtils.terminate(ruby);
super.destroy();
}
HTTP GET および POST に対する処理は,内部サーブレットの
javax.servlet.Servlet#service メソッドに丸投げする。
@Override protected void doGet(HttpServletRequest req,
HttpServletResponse res)
throws ServletException, IOException
{
inner.service(req, res);
}
@Override protected void doPost(HttpServletRequest req,
HttpServletResponse res)
throws ServletException, IOException
{
inner.service(req, res);
}
}
5. _init.rb
WEB-INF フォルダに _init.rb を置く。
_init.rb では,Java クラスを利用可能にするために Java を include する。
また各サーブレット・スクリプト (Hello.rb など) でいちいち完全修飾名
javax.servlet.http.HttpServlet
を書かなくてもよいようにするため,そのクラスを import しておく。
include Java import javax.servlet.http.HttpServlet # for the convenience of every servlet
内部サーブレットは GenericServlet の派生クラスとして定義する。
したがって,JRubyServlet の期待どおり Servlet の実装クラスでもある。
インスタンス変数として,サーブレット名からサーブレット・オブジェクトを検索するハッシュ表
@servlets を設ける。
class InnerServlet < javax.servlet.GenericServlet def init(config) super @servlets = {} end
-
L:
JRubyServlet から init メソッドで初期化してあるので,
GenericServlet 標準装備のログ機能が使えます。
終了時には,動作解析の便宜のため,@servlets の内容をログに記録してから,
各サーブレット・オブジェクトを終了させ,
最後に自分自身の GenericServlet としての終了処理を行う。
def destroy log("destroy: %p" % @servlets) @servlets.each_value {|s| s.destroy} super end
HTTP GET および POST に対し,service メソッドは,
HttpServletRequest#getServletPath を使ってリクエストの URL から
サーブレット名 name を取り出す。
ハッシュ表 @servlets から name をキーとしてサーブレット・オブジェクトを検索する。
もちろん,初回はハッシュ表に該当するオブジェクトがないため,nil が返される。
その場合は require name によりサーブレット名と同名の JRuby スクリプトを読み込む。
スクリプトはトップレベルでサーブレット・クラスを定義することが期待されている。
Object.const_get(name)
を使って,トップレベルに定数として定義されたサーブレット・クラスを取得する。
サーブレット・クラスに new メソッドを適用してサーブレット・インスタンス
servlet を得る。
servlet を init メソッドで初期化する。
マルチスレッドによる2重処理を避けるため,このような検索から初期化までを
self.synchronized のブロックに含める。
def service(req, res) begin log("request: %p %p from: %p" % [req.requestURI, req.query_string, req.remote_host]) servlet = nil self.synchronized { name = req.servlet_path[1..-4] # "/Poi.rb" => "Poi" servlet = @servlets[name] if servlet.nil? log("required: %p" % name) require name servlet = Object.const_get(name).new @servlets[name] = servlet servlet.init(self) end } case req.method when "GET" then servlet.doGet(req, res) when "POST" then servlet.doPost(req, res) else raise("unexpected method %p" % req.method) end rescue => ex log(ex.inspect + " at " + ex.backtrace.join("\n\tfrom ")) raise end end end
- L: 初期設定ではログは apache-tomcat-6.0.14 を展開した直下,bin や webapps と同じところにある logs 以下に作られます。
WEB-INF フォルダに Hello.rb を置く。 このとき,apache-tomcat-6.0.14 を展開した直下からみて下記のようなファイル構成になっている。
$ ls LICENSE RELEASE-NOTES bin/ lib/ temp/ work/ NOTICE RUNNING.txt conf/ logs/ webapps/ $ cd webapps/test/WEB-INF $ ls Hello.rb _init.rb classes/ lib/ web.xml
Tomcat を起動後,同一マシン上の Web ブラウザから
http://localhost:8080/test/Hello.rb
をアクセスすると,サーブレット Hello.rb が実行される。
6. Count.rb
より複雑な例として,サーブレットの開始・終了処理やログ出力等を含んだ
Count.rb を示す。
Hello.rb と同じく WEB-INF フォルダに置いて
http://localhost:8080/test/Count.rb
としてアクセスする。
アクセス回数がカウントされ,表示される。
class Count < HttpServlet def init(config) super log("hello, Count") @count = 0 end def destroy log("bye-bye, Count") super end MSG = '<title>count</title> <h2>Hello, World</h2> <p><big>みなさん,こんにちは</big> <p>これは %d 回目のあいさつですね。 ' def doGet(req, res) res.content_type = 'text/html; charset="UTF-8"' self.synchronized { @count += 1 res.writer.print MSG % @count } end end