Tomcat 上の JRuby サーブレット

2008.1/11 (鈴)


1. はじめに

本稿は, 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.rbhttp://…略…/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

JRubyServletHttpServlet の派生クラスであり, フィールドとして 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 がクラス InnerServletjavax.servlet.Servlet の実装クラスとして定義していると仮定する。 JavaEmbedUtils.rubyToJava メソッドを使って, JRuby の InnerServlet インスタンスを Java の javax.servlet.Servlet インスタンス inner として扱えるようにする。

innerinit メソッドで初期化する。

    @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);
    }

サーブレット終了時には,内部サーブレットを終了させてから, 内部サーブレットの実行時処理系を終了させ, 最後に自分自身の 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 クラスを利用可能にするために Javainclude する。 また各サーブレット・スクリプト (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

終了時には,動作解析の便宜のため,@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 を得る。 servletinit メソッドで初期化する。

マルチスレッドによる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

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

Windows 2000 SP4 上での実行例を本稿冒頭の図に示す。

7. おわりに

本稿の試みは Tomcat に限らず Java Servlet Specification に従った任意のコンテナに適用できると期待される。 あくまで実験用コードであるが,終了時の資源解放,ログ出力,排他制御など,一通りの処理を実現している。 したがって,産業界の競争で鍛えられた Java Web サーバ環境を JRuby で利用する実用レベルのシステムのための良い基礎になると考えられる。


次回へつづく


Copyright (c) 2008 OKI Software Co., Ltd.