JRuby 1.1 と ERB サーブレット

2008.1/21 (鈴)



1. はじめに

本稿では,前回「Tomcat 上の JRuby サーブレット」の続編として, Tomcat 上の JRuby サーブレット・システムの JRuby 1.1 対応と ERB によるテンプレート機能の追加等を試みる。 自由なソフトウェアとしての JRuby の発展にわずかながらでも寄与するため, 下記の構成要素を GNU LGPL の下におく。

新設した ERB サーブレットを除き,ファイルの構成は前回と同様である。 対象とする JRuby のバージョンは 1.1RC1,Tomcat のバージョンは 6.0.14 である。 Mac OS X 10.4.11 上の Java 1.5.0_13 および Windows 2000 SP4 上の Java 1.6.0_04 で動作を確認した。

2. JRubyServlet の JRuby 1.1 対応

前回の JRuby 1.0.3 からJRuby 1.1 に移行するには, まず,webapps/test/WEB-INF/ 直下の lib フォルダを, jruby-bin-1.1RC1 を展開して得られる jruby-1.1RC1/ 直下の lib フォルダに置き換える。

$ cd webapps/test/WEB-INF
$ rm -rf lib
$ gzcat ~/Downloads/jruby-bin-1.1RC1.tar.gz | tar xf -
$ mv jruby-1.1RC1/lib .
$ rm -rf jruby-1.1RC1

JRubyServlet.java には下記の変更が必要である。

<         IRubyObject ro = ruby.evalScript("require '_init'; InnerServlet.new");
---
>         IRubyObject ro =
>             ruby.evalScriptlet("require '_init'; InnerServlet.new");

コンパイル方法は前回と変わらない。

$ cd classes
$ javac -cp ../../../../lib/servlet-api.jar:../lib/jruby.jar JRubyServlet.java

以下,専ら JRuby 1.1RC1 を用いる。

3. _init.rb と ERB サーブレット

実用上,サーブレット・システムには Java の JSP に相当する機能があることが好ましい。 Ruby には ERB と呼ばれる一種のテンプレート・エンジンが標準で用意されており,JRuby でも利用可能である。 ここでは ERB による JSP 類似機能の実現を試みる。

まず webapps/test/WEB-INF/web.xml に下記のように *.erb に対する対応規則を追加する。

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

   <servlet-mapping>
     <servlet-name>Rb</servlet-name>
     <url-pattern>*.erb</url-pattern>
   </servlet-mapping>
</web-app>

次に webapps/test/WEB-INF/_init.rb に下記の改変を行う (改変後はこのようになる)。

  1. require 'erb' を追加する。
    require 'erb'
    
  2. 内部サーブレットの service(req, res) メソッドでの servlet 取得処理を次のように変更する。
          servlet = nil
          self.synchronized {
            spath = req.servlet_path
            servlet = @servlets[spath]
            if servlet.nil?
              log("load: %p" % spath)
              case spath[-3..-1]
              when ".rb"
                path = "." + spath  # "/Poi.rb" => "./Poi.rb"
                script = open(path) {|rf| rf.read}
                mod = Module.new
                mod.module_eval(script, path)
                name = spath[1..-4] # "/Poi.rb" => "Poi"
                servlet = mod.const_get(name).new
              when "erb"
                path = ".." + spath # "/Poi.erb" => "../Poi.erb"
                script = open(path) {|rf| rf.read}
                erb = ERB.new(script, nil, "%")
                servlet = erb.def_class(ErbServlet, "expand(req, res)").new
                servlet.inner_servlet = self
              else
                raise("unexpected servlet path %p" % spath)
              end
              @servlets[spath] = servlet
              servlet.init(self)
            end
          }
    
    URL のパターン *.erb に対応する処理は when "erb" の節にある。 URL に対応するテキスト・ファイルの内容 script から, erb = ERB.new(script, nil, "%") でテンプレート・オブジェクトを作成する。 servlet = erb.def_class(ErbServlet, "expand(req, res)").new で,無名の ErbServlet 派生クラスのインスタンスとして,目的の servlet を作成する。

  3. ErbServlet クラスの定義を下記のように追加する。
    class ErbServlet < HttpServlet
      include ERB::Util
      attr_accessor :inner_servlet
    
      def doGet(req, res)
        s = expand(req, res); res.writer.print(s)
      end
    
      def doPost(req, res)
        s = expand(req, res); res.writer.print(s)
      end
    end
    
    ここで s = expand(req, res)erb.def_class(ErbServlet, "expand(req, res)") が作成した無名クラスの expand(req, res) メソッドを呼出し,テンプレートから生成した文字列 s を返す。

この後,Tomcat を起動すると, webapps/test/ に置いた *.erb ファイルが ERB による サーブレットの定義として働く。以下,こうして定義されるサーブレットを ERB サーブレットと呼ぶ。 ERB サーブレットの例として webapps/test/Count.erb を示す。

% res.content_type = "text/html; charset=UTF-8"
% if @count.nil? then @count = 1
%                else @count += 1 end
% s = "<それでは>"
% 
<title>count</title>
<h2>Hello, World</h2>
<p><big>みなさん,こんにちは</big>
<p><%= @count %> 回目のあいさつですね。
<p><%=h s %>

この例では,未設定のインスタンス変数の参照に nil が返されることを利用して @count の初期設定をしている。

ERB サーブレットは HTML 以外のデータを返すこともできる。 「Tomcat + JRuby でServletを書く」 にある param.rb を ERB サーブレットで表現し直した webapps/test/param.erb を示す。

% res.content_type = "text/xml; charset=UTF-8"
<parameters>
% req.parameter_names.each {|name|
  <param name="<%=h name %>"><%=h req.get_parameter(name) %></param>
% }
</parameters>

もちろん, webapps/test/WEB-INF/Hello.rb や webapps/test/WEB-INF/Count.rb などの普通の JRuby サーブレットを使うこともできる。

4. manage.erb

現在,インスタンスが作成されている JRuby サーブレットを一覧し, 選択的に消去するためのユーティリティであり, ERB サーブレットの実例でもある webapps/test/manage.erb を下記に示す。 Mac 上の Camino ブラウザでの動作例を 本稿冒頭の図に示す。

<%
# manage.erb -- H20.1/21
#
# Copyright (c) 2008 OKI Software Co., Ltd.
#
# This is free software:  you can redistribute it and/or modify it under
# the terms of the GNU Lesser General Public License as published by the
# Free Software Foundation, either version 2.1 of the License, or (at
# your option) any later version.
#
# This is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
# FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public
# License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this.  If not, see <http://www.gnu.org/licenses/>.

res.content_type = "text/html; charset=UTF-8"
d = (req.get_parameter_values("d") or [])
%>
<html>
  <head>
    <title>manage</title>
  </head>
  <body>
   <h2>サーブレット管理サーブレット</h2>
%   self.synchronized {
%     d.each {|name|
%       s = inner_servlet.servlets.delete(name)
%       if s.nil?
          <p><%=h name %> は消去済みのようです。
%       else
%         log("manage.erb destory: %p => %p" % [name, s.class])
%         s.destroy
          <p><%=h name %> を消去しました。
%        end
%     }
%   }
%   if inner_servlet.servlets.empty?
      <p>
      <a href="manage.erb">manage.erb を再開します</a>
%   else
      <p>消去したいサーブレットを選択してください。
      <form action="manage.erb" method="POST">
%       inner_servlet.servlets.each_key {|servlet_name|
          <input type="CHECKBOX" name="d" value="<%=h servlet_name %>">
          <%=h servlet_name %><br>
%       }
        <br>
        <input type="SUBMIT" value="消去">
      </form>
%   end
  </body>
</html>

このサーブレットには現在のところ保護はない。 アクセスできる任意のブラウザから任意の JRuby サーブレットを消去できる。 運用時には何らかの認証機構等が必要である。

5. おわりに

今回の試みでは,前回の Tomcat 上の JRuby サーブレット・システムを JRuby 1.1 に対応させ, ERB によるテンプレート機能を追加し,各サーブレットの名前空間の分離を実現した。 また,このようなサーブレットによる初歩的なシステム管理機能を実現した。

サンプルを動作させてすぐ気付くことは, 各 JRuby サーブレットが初回アクセス時こそレスポンスが遅いが,2回目からは軽快に動作することである。 これを実現するため,内部サーブレットではインスタンス変数 @servlets に各サーブレット・オブジェクトをキャッシュしている。 これは ERB サーブレットについても同様である。
Torture による予備的な調査では,Java サーブレットに比べ, 初回アクセスを除いた速度の差は数倍以内におさまっている。

JRuby によるサーブレット・システムの実用化に対する主要な障壁は,今回の試みにより 基本的には取り払われたと言えるかもしれない。


次回へつづく


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