Monday, December 18, 2006

JRake, Part 3: Running

So far, we've managed to compile and test our Java code using 100% JRuby. Now we just need a way to run it.

Assuming our application is web-based, the obvious solution is to wrap it up in a war file and deploy it into a container. But as anyone who has worked on a large project knows, creating and deploying war files can be a painfully slow process. Our original mission was to cut as much time as possible off our edit-compile-run cycle, so this clearly isn't the right solution. (We will need to generate war files at some point, of course, for deploying into production environments, but we'll save that work for another day).

Some app servers have an "autodeploy" feature that lets you deploy your app into an already running server, which is a huge step in the right direction - though things can still take a fair bit of time to deploy. Even when autodeploy works, it requires an extra alt-tab (command-tab for you mac users) over to the shell to kick off the build, followed by an alt-tab over to the browser to see the results, which wastes another second or two. There's also timing issue: if you hit "refresh" in the browser before the build and deploy have finished, you won't see your changes (or worse, you'll see some of your changes). So while autodeploy is definitely a big improvement, it's still not an ideal solution.

What we really want is for Java code to work the same way that dynamically typed languages do: you change your code, hit "save", and view the results in the browser.

JRake Server

I think I've managed to achieve something close to this, by setting up a Jetty server to act as a kind of proxy for my main application. Whenever a request comes in, it performs the following steps:

  1. Calls out to the JRake script to compile any out-of-date code.
  2. Creates a new classloader, and uses it to reload the application's main servlet class.
  3. Creates an instance of the servlet and forwards the original HTTP request on to it.

The upshot is that I can now make changes in my IDE, and see those changes (almost) immediately in my browser. No new virtual machines are started up along the way, no extra alt-tabs are needed, and I don't have to worry about hitting the web page before the deploy is complete. These may seem like minor points, but when you're doing this stuff eight hours a day, those little annoyances can add up.

Here's the code for the main program, which starts up the Jetty server:

require 'java'
require 'reload_handler'

server = org.mortbay.jetty.Server.new(3030)
handler = ReloadHandler.new('lib/rakefile.rb', :main_compile, ['tmp/main/'], 'com.example.HelloWorldServlet')
server.set_handler(handler)

server.start
puts "\nHit stop server\n\n"
$stdin.readline
server.stop

And the code for the reload handler:

require 'java'
require 'rake'
require 'jrake'

class ReloadHandler < org.mortbay.jetty.Handler

# todo: Should extend AbstractHandler
# (need JRuby to support extending abstract classes)

def initialize(rakefile, target, classpath, classname)
super()

@rakefile = rakefile
@target = target
@classpath = classpath
@classname = classname
end

def handle(target,request,response,dispatch)
begin
# Compile any out of date files
Rake.application.clear
load @rakefile
target = Rake.application.lookup(@target)
raise "Target not found: #{@target}" if target.nil?
target.invoke

# Load the servlet class, create an instance, and delegate to it
servlet_class = load_class(@classpath, @classname)
servlet = servlet_class.new_instance
param_types = to_java_array(java.lang.Class, [
javax.servlet.ServletRequest,
javax.servlet.ServletResponse
])
method = servlet_class.get_method("service", param_types)
args = to_java_array(java.lang.Object, [request,response])
method.invoke(servlet, args)

rescue Exception => e
trace = "#{e.class}: #{e.message}<br/>#{e.backtrace.join('<br/>')}"
response.writer.write("
<html>
<body>
<pre>{trace}</pre>
</body>
</html>
")
end

request.handled = true
end

def start
end

def stop
end

def setServer(server)
@server = server
end

def getServer
@server
end
end

Again, I've set up a sample project that includes all the pieces - JRuby, Rake, Jetty, and associated scripts. You can get it from subversion here:

svn://svn.foemmel.com/blog/jrake/running

If you're interested in the scripts but don't want to download all the third-party stuff, just check out the lib directory:

svn://svn.foemmel.com/blog/jrake/running/lib

No comments: