Skip to main content

Running JRuby "servlets" on Phobos

Posted by robc on March 23, 2007 at 3:54 PM PDT

Tim Bray wrote about running servlet-like JRuby scripts inside a web application and I left a comment saying "Phobos can do that". This got me thinking on how to make it easy to get people started in this model.

As always, the best way is to use a running application as a starting point. Once the basic application layout and configuration are in place, all you need is a well-defined place for your scripts and your static files. The starting point is this war file (4.9MB).

To produce it, I started off with the latest Phobos release (0.5.8), created an empty application in NetBeans and generated a web application. The result is a vanilla war file that will run on any servlet container.

In the interest of making the war file more manageable, I then removed all the jars that are not essential, e.g. XMLBeans, JDOM and Rome. They can all be added back easily by dropping them into the WEB-INF/lib directory. I also deleted all the jMaki libraries from the top-level resource directory, since they are unlikely to be used in this scenario. Subsequently, I took the latest JRuby interpreter and its associated JSR-223 scripting engine and dropped them into WEB-INF/lib.

At this point, I got a web application that can run JRuby scripts. If you save a JRuby script, e.g. test.rb, in the WEB-INF/application/script directory, it will be invoked when a request for the /test.rb URL comes in. The script can access the servlet request and response objects using the $request and $response global variables. Here's the inevitable "hello, world!" example:

$response.contentType = 'text/plain'
writer = $response.writer
writer.println 'HelloHello, world!'
writer.flush

This is far from earth-shattering, of course. What's interesting though is that I did this without writing any code, thanks to the scripting engine pluggability offered by JSR-223. Phobos itself doesn't have any knowledge of JRuby (it finds the right scripting engine to use by looking at the file extension), so I could just as easily have used Groovy by adding the necessary jars to WEB-INF/lib.

Although it all works, I took a few more steps to improve usability. First of all, when accessing the top page of my application, I'd like to run a JRuby script. This is not the default in Phobos, but it's easy to fix: just add one line to WEB-INF/application/startup.js, the Phobos startup script for the application. Here's the line:

application.mapping.rules.push({ url: "/", script: "index.rb" });

This JavaScript snippet adds one more URL mapping rule to Phobos, telling it to dispatch to the index.rb script when a request for the / URL comes in. By default, scripts live inside WEB-INF/application/script, so that's where Phobos will look for the index.rb file.

Finally, it'd be nice, while writing Ruby code, to have access to all the Ruby 1.8 libraries, so I decided to copy them over from my Ruby installation and, to keep things simple, I put them under WEB-INF/classes/ruby/1.8. Once again, I tried to save a few kilobytes by removing obviously unnecessary libraries, like the Tk ones.

Somehow we have to tell JRuby where to look for libraries. I took the easy way out by adding another line to the startup script to set the system property that the JRuby JSR-223 engine uses to configure the JRuby load path. This is not going to scale very well but it makes things simpler. And since I was setting the load path, I also added another entry to it, to allow user libraries to be saved under WEB-INF/classes/rubylib. This way, malicious or overly curious clients won't be able to access them directly.

Here's the resulting code:

java.lang.System.setProperty("com.sun.script.jruby.loadpath", "ruby/1.8" + java.io.File.pathSeparator + "rubylib");

Relative paths are resolved by the application classloader, which includes the WEB-INF/classes component, so I didn't have to repeat it here. From what I know about the way JRuby looks for libraries, it should be possible to add to WEB-INF/lib a single jar file containing all Ruby 1.8 libraries. Even better, it could contain pre-parsed versions of the source files, identified by the .rb.ast.ser extension, resulting in faster library loading.

Now the basic application is ready. I ran the jar tool and created a war file, which I put online here. In order to use it, download and expand it, then deploy it in expanded form onto GlassFish or any other servlet container. (It's going to run as a war too, but then you won't be able to modify it while it's running.) Any JRuby scripts you want to expose to your clients go under WEB-INF/application/script, any JRuby scripts you plan on loading into other scripts using require go under WEB-INF/classes/rubylib and all static files (HTML, CSS, images...) go in the root directory of the web application.

The default URL mapping is very simple, but you can customize it by defining new rules in startup.js (in JavaScript only, alas, until somebody writes a functional JRuby to JavaScript bridge). It's not hard to do things like mapping URLs matching arbitrary regular expressions to a certain JRuby script, or running scripts without requiring the .rb suffix to be part of the request URL. I'll try to add more examples in the future.

Related Topics >>