Author Archive

JRuby & Sinatra web service as an executable jar

Posted by tomdz on September 20, 2011 – 12:15 PM

Recently I was working on a web service that uses Sinatra with JRuby. Now JRuby running on the JVM and all, I was thinking it would be nice to neatly bundle everything up in one jar and give that to java to run. No JRuby would need to be installed, no need for rvm or anything, only java required. Some quick googling found this article from Yoko Harada which, while not exactly what I needed, gave a lot of good hints. The most relevant difference is that in my case, the application is already using Maven to build instead of rake, and I didn’t want to introduce a second build tool. If you are already using rake, then you can use tools that make this bundling in a jar quite easy, e.g. Warbler or Rawr.

For Maven on the other hand a little bit more manual work is required, which I’m going to show you with a simple hello world example.

Let’s get started with this simple Sinatra Hello World app:

require 'rubygems'
require 'sinatra'

get "/" do
"Hello World"
end

Now one of the goals was to run this without requiring that JRuby is installed. Let’s apply the same constraint to the project setup. Instead of a local JRuby installation, we’ll instead use jruby-complete.jar directly. First, to install it, we’ll use Maven:

<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.tomdz</groupId>
<artifactId>sinatra-test</artifactId>
<version>1.0.0-SNAPSHOT</version>
<packaging>pom</packaging>
<name>sinatra-test</name>
<dependencies>
<dependency>
<groupId>org.jruby</groupId>
<artifactId>jruby-complete</artifactId>
<version>1.6.4</version>
</dependency>
</dependencies>
</project>

Running Maven with this pom.xml

$ mvn install

will give us the jruby-complete.jar in the local maven repositiory, usually at ~/.m2/repository/org/jruby/jruby-complete/1.6.4/jruby-complete-1.6.4.jar.

Next, we need to initialize rubygems and also install Bundler to keep things simple. A normal Ruby (1.8) installation has libraries and gems in a directory structure like this:

lib
+- jruby
   +- 1.8
      +- bin
      +- cache
      +- doc
      +- gems
      +- specifications

We are going to keep this structure, but in the local project folder instead of wherever (J)Ruby is installed. To install Bundler (and OpenSSL), we can now use the local jruby-complete.jar file like so:

$ java -jar ~/.m2/repository/org/jruby/jruby-complete/1.6.4/jruby-complete-1.6.4.jar \
       -S gem install bundler jruby-openssl \
              --no-ri --no-rdoc \
              -i lib/jruby/1.8/

Fetching: bundler-1.0.18.gem (100%)
Successfully installed bundler-1.0.18
Fetching: bouncy-castle-java-1.5.0146.1.gem (100%)
Fetching: jruby-openssl-0.7.4.gem (100%)
Successfully installed bouncy-castle-java-1.5.0146.1
Successfully installed jruby-openssl-0.7.4
3 gems installed

This is actually cheating a little bit because JRuby will find the gem script in the current path, not in the jar. Unfortunately there doesn’t seem to be a way to make JRuby look for it in the jar at the moment, so we have to rely on the system’s Ruby installation for now. Since the gem script is a simple wrapper around the GemRunner class which will be loaded from the jar, this usually works out ok. This only becomes a problem if there is no Ruby installed on the machine where this is executed, or if the Ruby installation is quite old. In those cases, we could execute the GemRunner class directly:

$ java -jar ~/.m2/repository/org/jruby/jruby-complete/1.6.4/jruby-complete-1.6.4.jar \
       -rrubygems \
       -e "require 'rubygems/gem_runner'; Gem::GemRunner.new.run 'env'.split"

(replace the env string with whatever arguments you want to pass to gem).

For our webapp, we also need to install Sinatra itself, for which we’ll use Bundler. First, we need a Gemfile in the project root:

source 'http://rubygems.org'
gem 'sinatra'

Since we are lazy, we’ll add a script to invoke Bundler in the same way that we invoked gem above:

#!/bin/bash
GEM_PATH=`pwd`/lib/jruby/1.8 java \
-jar ~/.m2/repository/org/jruby/jruby-complete/1.6.4/jruby-complete-1.6.4.jar \
-S lib/jruby/1.8/bin/bundle install --path lib/

Note the GEM_PATH part. This sets the gem path for JRuby for that invocation, so that it will find the Bundler gem.

With this script, we can now install Sinatra:

$ chmod +x bundler.sh
$ ./bundler.sh

Fetching source index for http://rubygems.org/
Installing rack (1.3.2)
Installing tilt (1.3.3)
Installing sinatra (1.2.6)
Using bundler (1.0.18)

Since we opted to install Sinatra via Bundler, we should also require bundler/setup in our ruby script:

require 'rubygems'
require 'bundler/setup'
require 'sinatra'

get "/" do
"Hello World"
end

Save this script in src/main/ruby (to keep with Maven’s suggested directory layout). The project should look like this now:

Gemfile
Gemfile.lock
bundler.sh
pom.xml
src
+- main
   +- ruby
      +- server.rb
lib
+- jruby
   +- 1.8
      +- bin
      +- cache
      +- doc
      +- gems
         +- bouncy-castle-java-1.5.0146.1
         +- bundler-1.0.18
         +- jruby-openssl-0.7.4
         +- rack-1.3.2
         +- sinatra-1.2.6
         +- tilt-1.3.3
      +- specifications

Let’s test that this works:

$ GEM_PATH=`pwd`/lib/jruby/1.8 java \
    -jar ~/.m2/repository/org/jruby/jruby-complete/1.6.4/jruby-complete-1.6.4.jar \
    src/main/ruby/server.rb 

== Sinatra/1.2.6 has taken the stage on 4567 for development with backup from WEBrick
[2011-09-11 13:10:35] INFO  WEBrick 1.3.1
[2011-09-11 13:10:35] INFO  ruby 1.8.7 (2011-08-23) [java]
[2011-09-11 13:10:38] INFO  WEBrick::HTTPServer#start: pid=98483 port=4567

So far so good. Now for the jar part. JRuby has had the ability to load gems and scripts from a jar since at least 1.1.6. Nick Sieger blogged about the steps a while back. The jar basically needs to contain this directory structure:

bin
cache
doc
gems
specifications
META-INF
server.rb
**/*.class
**/*.rb

The bin, cache, doc, gems, and specifications folders come as-is from the lib/jruby/1.8 folder. The **/*.class stands for directories containing class files (e.g. compiled Java or JRuby).

In order to achieve this, we’re going to use the assembly plugin. There are other ways to generate a jar file (e.g. the jar or shade plugins), but the assembly plugin gives us the most control over the contents of the generated file.

First, we need to add a plugin section for the assembly plugin at the end of the pom.xml:

...
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<version>2.2-beta-3</version>
<executions>
<execution>
<id>assemble</id>
<goals>
<goal>single</goal>
</goals>
<phase>package</phase>
<configuration>
<appendAssemblyId>false</appendAssemblyId>
<descriptors>
<descriptor>assembly.xml</descriptor>
</descriptors>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

We are using version 2.2-beta-3 here because newer versions seem to have some problems running with pre-3.0 Maven versions.

Next, the assembly.xml file describes how the jar file should be assembled:

<assembly>
<id>artifact</id>
<formats>
<format>jar</format>
</formats>
<includeBaseDirectory>false</includeBaseDirectory>
<fileSets>
<fileSet>
<directory>${basedir}/src/main/ruby</directory>
<outputDirectory>/</outputDirectory>
</fileSet>
<fileSet>
<directory>${basedir}/lib/jruby/1.8</directory>
<outputDirectory>/</outputDirectory>
</fileSet>
</fileSets>
<dependencySets>
<dependencySet>
<outputDirectory>/</outputDirectory>
<outputFileNameMapping></outputFileNameMapping>
<unpack>true</unpack>
<unpackOptions>
<excludes>
<exclude>META-INF/MANIFEST.MF</exclude>
</excludes>
</unpackOptions>
</dependencySet>
</dependencySets>
</assembly>

This tells the assembly plugin to put files/directories from src/main/ruby and lib/jruby/1.8 into the root of the jar, and also unpack and then include all dependencies (i.e. JRuby) at the root.

The META-INF/MANIFEST.MF file that we tell the assembly plugin to exclude, tells Java various things about the jar, one of which is the main class to invoke if we run the jar via java -jar. In the case of JRuby, that will invoke the JRuby main in the same way as jruby would for a normal JRuby installation. We are excluding it here since we will user a different main class below.

Building this via

$ mvn clean install

will give us a jar that looks exactly like what we need. However since we excluded the manifests, we can’t run this jar just yet:

$ java -jar target/sinatra-test-1.0.0-SNAPSHOT.jar 

Failed to load Main-Class manifest attribute from
target/sinatra-test-1.0.0-SNAPSHOT.jar

Maven will have added a META-INF/MANIFEST file into the jar by itself, but it is basically useless for our purpose:

Manifest-Version: 1.0
Archiver-Version: Plexus Archiver
Created-By: 20.1-b02-383 (Apple Inc.)

Instead, we are going to use JRuby’s jar bootstrap mechanism described here. For this, we need a file called jar-bootstrap.rb at the root of the jar. We could either add a new file which then loads/requires our server.rb, or since our project is rather simple, we can simply rename the server.rb file:

$ mv src/main/ruby/server.rb src/main/ruby/jar-bootstrap.rb

We also need to tell the assembly plugin about our main class:

...
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<version>2.2-beta-3</version>
<executions>
<execution>
<id>assemble</id>
<goals>
<goal>single</goal>
</goals>
<phase>package</phase>
<configuration>
<archive>
<manifest>
<mainClass>org.jruby.JarBootstrapMain</mainClass>
</manifest>
</archive>
<appendAssemblyId>false</appendAssemblyId>
<descriptors>
<descriptor>assembly.xml</descriptor>
</descriptors>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

After we generated the new jar:

$ mvn clean install

We can now run it:

$ java -jar target/sinatra-test-1.0.0-SNAPSHOT.jar 

== Sinatra/1.2.6 has taken the stage on 4567 for development with backup from WEBrick
[2011-09-11 13:27:00] INFO  WEBrick 1.3.1
[2011-09-11 13:27:00] INFO  ruby 1.8.7 (2011-08-23) [java]
[2011-09-11 13:27:03] INFO  WEBrick::HTTPServer#start: pid=98783 port=4567

On a *nix system, we can even go one step further and create a self-executable jar using Brian’s trick.

Create a file src/main/sh/run.sh with the invocation commandline:

#!/bin/bash
java -jar "$0" "$@"

Make sure to have a couple of newlines at the end of the file. Then add this plugin section at the end after the assembly plugin declaration:

...
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>shell-maven-plugin</artifactId>
<version>1.0-beta-1</version>
<executions>
<execution>
<id>make-executable</id>
<phase>package</phase>
<goals><goal>shell</goal></goals>
<configuration>
<workDir>${baseDir}</workDir>
<chmod>true</chmod>
<keepScriptFile>false</keepScriptFile>
<script><![CDATA[
cat "${basedir}/src/main/sh/run.sh" > "${project.build.directory}/server"
cat "${project.build.directory}/${project.build.finalName}.jar" >> "${project.build.directory}/server"
chmod +x "${project.build.directory}/server"
]]></script>
</configuration>
</execution>
</executions>
</plugin>

That gives us an executable called server that we can run like this:

$ ./target/server 

== Sinatra/1.2.6 has taken the stage on 4567 for development with backup from WEBrick
[2011-09-11 13:34:07] INFO  WEBrick 1.3.1
[2011-09-11 13:34:07] INFO  ruby 1.8.7 (2011-08-23) [java]
[2011-09-11 13:34:07] INFO  WEBrick::HTTPServer#start: pid=99118 port=4567

And that’s it, an executable jar that doesn’t need a JRuby environment. Almost of all the stuff that we’ve been talking about here, is boiler-plate code, independent of whether we use Sinatra or something else. That makes it straightforward to add this to any JRuby application that is invoked on the commandline.

    Attend Tech Talks by Ning's Engineering & Ops teams at Ning HQ in downtown Palo Alto, CA!

    Archives by Category

    Search this Blog


    RSS