Saturday, April 05, 2008

Tips for JRuby engine: servlet meets JRuby engine

This is the sixth post of "Tips for JRuby engine" series I've written in this blog and shows how to get started writing servlet code using JSR 223 scripting API. Scripting API are smoothly mixed into other APIs since it is a part of standard Java APIs. Using JSR 223 scripting and Servlet APIs, we can make web applications more easily and delightfully thanks to the power of dynamic languages. However, programmers should be careful to use classes an interfaces of scripting API. Scripting API considers to be run on a servlet though not perfect, while implementations of dynamic languages often have some flaws for servlet style concurrent programming. Once scripting API works togther with Servlet API, multiple threads execute a single method of a single instance concurrently. How about JRuby implementation? It is not basicaly compliant to the servlet style, concurrent invocation. For example, JRuby's runtime has only one instance of GlobalVariables which is used to share instances between Java applications and JRuby and might cause a race condition when it executes consecutive mutilple evaluations on its single runtime. Thus, I'll write about what programers should take into account to write a servlet code effectively, a couple of times in this blog.

Firstly, I'll start with a very simple servlet. It just shows simple messages after executing a hello-world script written in Ruby. Tediously simple, but important to make sure that scripting APIs work on a servlet container. Because Servlet API and its web application require a specific directory structure, and adopt their own classloading mechanism, we need to fit scripting API in to a servlet based web application. Unless, some of programmers might experience hard time for the frist servlet code in action.

Before start writing the code, we must set up two jar archives, jruby.jar and jruby-engine.jar, to the right place. It is WEB-INF/lib. Most programmers use some sort of IDE and probably know about how to set up these two archives in a web application. I'm a NetBeans user, so I clicked on Libraries > Add JAR/Folder menu of a web project, and added them.

Here are the simple servlet and Ruby script:
package mountainash;

import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;

import java.io.Reader;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;
import javax.script.ScriptException;
import javax.script.SimpleScriptContext;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

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();
SimpleScriptContext scriptContext = new SimpleScriptContext();
scriptContext.setWriter(out);

try {
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>");
ClassLoader loader = getClass().getClassLoader();
Reader reader =
new InputStreamReader(loader.getResourceAsStream("ruby/simple.rb"), "UTF-8");
engine.eval(reader, scriptContext);

out.println("</pre>");
out.println("</body>");
out.println("</html>");
} catch (ScriptException ex) {
Logger.getLogger(SimpleServlet.class.getName()).log(Level.SEVERE, null, ex);
} finally {
out.close();
}
}

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

@Override
public String getServletInfo() {
return "JRuby Engine Test: very simple";
}
}

--------------------

# simple.rb

puts "Hello World from Ruby over JRuby engine"
puts "こんにちは世界"
Lines colored red have scripting APIs mixed in to this servlet. In a init() method, the servlet gets JRuby engine's instance, which means that this servlet has only one JRuby runtime to process multiple requests rushed to this servlet all at once. Also, only one instance of JRuby engine takes care of multiple http requests concurrently. This is why this servlet doesn't use engine.put() or engine.setWriter() methods not to be suffered from a race condition. Instead, this servlet creates SimpleScriptContext type instance for each http request to share objects with Ruby script.

Where do I put Ruby scripts? This question might come up since everything should be in Java's web application directory structure. The way of getting scripts depends on where are they. We can take scripts in the servlet either specifing the file path relative to the web application's context root or reading the file as a resource by using a class loader. This servlet uses the latter one, so Ruby scripts must be in the WEB-INF/lib or WEB-INF/classes directory, or subdirectories of WEB-INF/classes, which are available for web application's class loader to load resources. Again, I'm a NetBeans user, so I created the folder whose name is ruby under the Source Packages folder and put the script there. Then, NetBeans copied it to build/web/WEB-INF/classes/ruby directory automatically, consequently, the script became loadable.
Mountainash project's directory hierarcy

+ Mountainash
+ WEB-INF
+ src
+ mountainash
- SimpleServlet.java
+ ruby
- simple.rb
+ lib
- jruby.jar
- jruby-engine.jar
+ classes -+
+ mountainash | automatically created
- SimpleServlet.class | directories and files
+ ruby |
- simple.rb -+

When this servlet gets run successfully, we get the following output:
<html>
<head>
<title>Servlet SimpleServlet</title>
</head>
<body>
<h3>Servlet SimpleServlet at /Mountainash</h3>
<pre>
Hello World from Ruby over JRuby engine
こんにちは世界
</pre>
</body>
</html>

3 comments:

reyt said...
This comment has been removed by a blog administrator.
Monojohnny said...

Excellent example Servlet - also works very well with Jython: just change:

engine = manager.getEngineByName("python");

And other directory names etc to 'python' from 'ruby'.

Domo arrigato !

Monojohnny said...

Additionally: I found that I had to add in an explicit 'reader.close()' to the Servlet; otherwise any changes to the (Python in my case) script weren't picked up.

Like this:

engine.eval(reader, scriptContext);
reader.close();

Thanks again.