JSR 223 API のJRuby script engineを使うサンプルコード その3

昨年末にJRuby script engineの出力先を変更できないというバグを修正してCVSにコミットしました。CVSにある最新版を使うと、サーブレットの中で実行したRubyスクリプトのputsなどをブラウザに出力できるようになります。例えば、このサーブレットを実行すると

//SimpleServlet.java

package arches;

import java.io.*;
import java.net.*;
import javax.script.ScriptContext;
import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;
import javax.script.ScriptException;
import javax.script.SimpleScriptContext;
import javax.servlet.*;
import javax.servlet.http.*;

public class SimpleServlet extends HttpServlet {
    private ScriptEngine engine;

    @Override
    public void init() {
        ScriptEngineManager manager = new ScriptEngineManager();
        engine = manager.getEngineByName("jruby");
    }

    protected void processRequest(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        response.setContentType("text/html;charset=UTF-8");
        PrintWriter out = response.getWriter();
        try {
            ScriptContext context = new SimpleScriptContext();
            context.setWriter(out);
            context.setAttribute("sessionid",
                    request.getSession().getId(), ScriptContext.ENGINE_SCOPE);
            context.setAttribute("transfered",
                    "できたかな??", ScriptContext.ENGINE_SCOPE);
            out.println("<html>");
            out.println("<head>");
            out.println("<title>Servlet SimpleServlet</title>");
            out.println("</head>");
            out.println("<body>");
            out.println("<h3>Servlet SimpleServlet at " + request.getContextPath() + "</h3>");
            out.println("<pre>");
            engine.eval("puts 'JRubyから、こんにちは世界'", context);
            engine.eval("puts $sessionid", context);
            String path = getServletContext().getRealPath("/") + "WEB-INF/classes/ruby/";
            String filename = path + "test.rb";
            engine.eval(new FileReader(filename), context);
            out.println("</pre>");
            out.println("</body>");
            out.println("</html>");
        } catch (ScriptException ex) {
            throw new ServletException(ex);
        } finally {
            out.close();
        }
    }

    protected void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        processRequest(request, response);
    }

    protected void doPost(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        processRequest(request, response);
    }

    public String getServletInfo() {
        return "Short description";
    }
}
# test.rb

require 'java'
include_class 'java.lang.System'

message = "おためしJRuby"
puts "#{message}, #{$transfered}"
System.out.println message + "!!"

at_exit { puts "ここでExit" }

ブラウザにこのように表示されます。
http://www.servletgarden.com/images/output_from_simpleservlet.png
(Rubyの中でSystem.out.printlnで出力しているところはブラウザには出力されません。)
このサーブレット

ScriptContext context = new SimpleScriptContext();
context.setWriter(out);
context.setAttribute("sessionid",
                     request.getSession().getId(), ScriptContext.ENGINE_SCOPE);
context.setAttribute("transfered",
                     "できたかな??", ScriptContext.ENGINE_SCOPE);

のように、自分でScriptContextのインスタンスを作って、ここに必要なインスタンスをkey,valueペアでセットしておいて、evalメソッドの引数で渡していますが、ここが肝心なところです。
JSR223 APIではengine.put(key, value)のようにしてスクリプト実行時に必要なインスタンスを渡す方法もありますが、これはJRubyScriptEngineクラスの親クラスであるjavax.script.AbstractScriptEngine内のcontextに直接セットされるので、サーブレットのようなマルチスレッド環境下では使えません。誰かのクレジットカード番号が自分のブラウザに表示されてしまうようなトラブルにつながります。サーブレットの場合は必ず、自分でcontextを持ちましょう。
ところが、javax.script.Invocable#invokeFunction(), javax.script.Invocable#invokeMeothd()はcontextを渡せません。これらは親クラスのcontextを参照しているだけです。これはAPIの設計ミスです。サーブレットで使う場合は、値をセットするところからinvokeFunction/Method()を実行して、値をリセットするところまでsynchronizedブロックで囲むしかないでしょう。

さて、JSR223 APIではあらかじめコンパイル(parseのこと。スクリプトコンパイルして.classにするのではない)しておいて、parse無しのevalだけを後で実行できるようになっています。この機能を利用すると、サーブレットのinit()でparseしてしまって、毎回のリクエスト実行時にはevalだけをすればいいという使い方もありです。開発が終わってしまったときにはこちらの方が明らかに早いです。この方法を使う場合には、たとえばこんなサーブレットを作ります。

// EvalTestServlet.java

package arches;

import java.io.*;
import java.net.*;

import javax.script.Compilable;
import javax.script.CompiledScript;
import javax.script.ScriptContext;
import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;
import javax.script.ScriptException;
import javax.script.SimpleScriptContext;
import javax.servlet.*;
import javax.servlet.http.*;

public class EvalTestServlet extends HttpServlet {
    private ScriptEngine engine;
    private CompiledScript script1, script2;
    
    @Override
    public void init() throws ServletException {
        try {
            ScriptEngineManager manager = new ScriptEngineManager();
            engine = manager.getEngineByName("jruby");
            String path = getServletContext().getRealPath("/") + "WEB-INF/classes/ruby/";
            String filename = path + "testJava.rb";
            script1 = ( (Compilable) engine).compile(new FileReader(filename));
            filename = path +"arrayTest.rb";
            script2 = ( (Compilable) engine).compile(new FileReader(filename));
        } catch (ScriptException ex) {
            throw new ServletException(ex);
        } catch (FileNotFoundException ex) {
            throw new ServletException(ex);
        }
    }

    protected void processRequest(HttpServletRequest request, HttpServletResponse response)
    throws ServletException, IOException {
        response.setContentType("text/html;charset=UTF-8");
        PrintWriter out = response.getWriter();
        try {
            ScriptContext context = new SimpleScriptContext();
            context.setWriter(out);
            out.println("<html>");
            out.println("<head>");
            out.println("<title>Servlet EvalTestServlet</title>");  
            out.println("</head>");
            out.println("<body>");
            out.println("<h3>Servlet EvalTestServlet at " + request.getContextPath () + "</h3>");
            out.println("<pre>");
            script1.eval(context);
            script2.eval(context);
            out.println("</pre>");
            out.println("</body>");
            out.println("</html>");
        } catch (ScriptException ex) {
            throw new ServletException(ex);
        } finally { 
            out.close();
        }
    }
(略)
}
# testJava.rb
# from http://jruby.codehaus.org/The+JRuby+Tutorial+Part+1+-+Getting+Started
 
require 'java'
include_class 'java.util.TreeSet'
set = TreeSet.new
set.add "foo"
set.add "Bar"
set.add "baz"
set.each do |v|
  puts "value: #{v}"
end
# arrayTest.rb 
# from http://pine.fm/LearnToProgram/?Chapter=07
 
languages = ['English', 'German', 'Ruby']

languages.each do |lang|
  puts 'I love ' + lang + '!'
  puts 'Don\'t you?'
end

puts 'And let\'s hear it for C++!'
puts '...'

init()メソッド内で行っているように、CompileScript#compile()メソッドを実行しておけば、あとはコンパイル(parse)されたスクリプトをscript1.eval(context)やscript2.eval(context)のように実行するだけです。実行すると、このように表示されます。
http://www.servletgarden.com/images/output_from_evaltestservlet.png
なお、Rubyスクリプトは全てサーブレットソースコードと同じディレクトリにrubyという名前のディレクトリを作って、ここに置きました。