I have two worlds that need to collide. Rails solves the decade long problem of abusive Java web stacks and Java provides mature codebase to the Rails paradigm. JRuby has brought me the spark, now if I can only get the fire burning.
The problem: I need a Spring context to startup with Rails and use the config/database.yml.
The solution: A JRuby Service.
I want to expose tons of existing Java code and libs into Rails, but do not want to deal with a segmented world. Everything should be configured and started from one place, this is were a JRuby Service helps. The JRuby runtime will automatically load a class implementing a JRuby service interface (such as org.jruby.runtime.load.BasicLibraryService
) when it is packaged in a specifically named jar at a specifically named placed.
Originally I was jumping though hoops trying to call static singletons, but ended up scratching my head how to get access to the Rails runtime. After finding hints from Ola Bini, with some searching and source diving, I found the javadoc for org.jruby.runtime.load.LoadService
has the key. (An aside, it would be nice if the JRuby folks published the JavaDocs, even if it changed with every release).
Except from LoadService.java
JavaDoc:
How to make a class that can get required by JRuby
First, decide on what name should be used to require the extension. In this purely hypothetical example, this name will be ‘active_record/connection_adapters/jdbc_adapter’. Then create the class name for this require-name, by looking at the guidelines above. Our class should be named active_record.connection_adapters.JdbcAdapterService, and implement one of the library-interfaces. The easiest one is BasicLibraryService, where you define the basicLoad-method, which will get called when your library should be loaded.
The next step is to either put your compiled class on JRuby’s classpath, or package the class/es inside a jar-file. To package into a jar-file, we first create the file, then rename it to jdbc_adapter.jar. Then we put this jar-file in the directory active_record/connection_adapters somewhere in JRuby’s load path.
The short and skinny of this? The package defines the directory and the class name determines the jar name. The package period separators are converted to slashes, so the package com.slackworks
would be the directory com/slackworks
. For brevity, this example uses the simple package slackworks
, for a directory of slackworks
. The class name, sans Service
and converted from CamelCase to under scores determines the jar name. So the RailsSpringService
class translates to rails_spring.jar
Building a JRuby Service
Step 0: Have a working JRuby setup.
Already done the JRuby installation dance with a rails instance with JDBC for MySQL to monkey with. All of the following source can be pulled from git://sprocket.slackworks.com/srv/git/samples.git
Step 1: Implementing BasicLibraryService
.
For org.jruby.runtime.load.BasicLibraryService
, this is fairly straight forward. All you need is to implement the basicLoad
method. In addition, the org.jruby.Ruby
has a static ThreadLocal
reference for the Ruby runtime using methods getCurrentInstance
and setCurrentInstance
. This will be used as a place for the beans being created in the Spring Context to access the Ruby runtime. Just be careful of the ThreadLocal restrictions in multi-threaded environments. In this scenario of using the Ruby runtime to help build the Spring context, it is fine.
/** * JRuby Service to load a Spring Context */ public class RailsSpringService implements BasicLibraryService { private static ApplicationContext applicationContext; /** * Executed by JRuby */ public boolean basicLoad(final Ruby runtime) throws IOException { // Set the Ruby runtime into the TheadLocal static reference Ruby.setCurrentInstance( runtime ); // Load the Spring context applicationContext = new ClassPathXmlApplicationContext( new String[] { "applicationContext.xml" } ); return true; } /** * Get access to the Spring Context */ public static ApplicationContext getApplicationContext() { return applicationContext; } }
Complete source at RailsSpringService.java which includes the package declaration that deteremines the directory
Step 2: Setting up the Spring context.
I am not going to get into the knitty gritty about Spring, only showing how Spring can load the rails’config/database.yml
to setup a Datasource. The juicy part is the following:
// Get the RAILS_ENV from the JRuby runtime Ruby jruby = Ruby.getCurrentInstance(); RubyModule kernel = jruby.getKernel(); String rails_env = kernel.getConstant("RAILS_ENV").asJavaString();This is called by the YamlConfig.java constructor to correctly parse the database.yml and use the configuration for the running
RAILS_ENV
.
The DatabaseConfig.java is an extension of YamlConfig specific for handling the database.yml. Lastly, DataSource.java is an extension of Apache Commons DBCP BasicDataSource.java that is constructed using DatabaseConfig.
This allows for a applicationContext.xml of:<?xml version="1.0" encoding="UTF-8"?> <beans> <!-- Rails Database Config --> <bean id="databaseConfig" class="slackworks.rails.DatabaseConfig"> <constructor-arg value="config/database.yml" /> </bean> <!-- Datasource --> <bean id="dataSource" class="slackworks.rails.DataSource" destroy-method="close"> <constructor-arg ref="databaseConfig" /> <property name="initialSize" value="2" /> <property name="maxActive" value="15" /> <property name="maxIdle" value="2" /> <property name="testOnBorrow" value="true" /> </bean> </beans>
Where the databaseConfig
bean loads from the config/database.yml
, using the Rails’ RAILS_ENV
, which gets passed to the dataSource
bean. Now we have a DataSource that is in synch with Rails.
Step 3: Packaging Rails Spring Service.
Using the powers of Maven, the pom.xml has been tweaked so that ‘mvn package’ will create lib/slackworks with the rails_spring.jar and all its dependencies. The lib/slackworks directory need to be copied to the RAILS_ROOT
Step 4: Set the classpath.
JRuby is on the road to having jars automatically be loaded into the JVM, but I do not know of a smart way to automatically load the rails_spring.jar dependencies. To alleviate at, the classpath needs to be manually set to include everything in lib/slackworks
, i.e. for Java 6, export CLASSPATH=/rails-app/lib/slackworks/*
.
Step 5: Testing with console.
Now that everything is in place, the classpath has been set, time to fire it up for a test run. Simply call from theRAILS_ROOT
,
jruby script/consoleYou will be greet with the normal
Loading development environmentNow load the RailsSpringService, via
require 'slackworks/rails_spring'
and you will be spammed with logging output as Spring starts up and reads the database.yml
Step 6: Load RailsSpringService in Rails.
In the config/environment.rb
, at the bottom at
require 'slackworks/rails_spring'
Now when Rails starts up, you will be greeted by the same output as Spring starts up, the worlds have collided. Accessing the Spring context from Rails:
include_class 'slackworks.RailsSpringService' dataSource = RailsSpringService.getApplicationContext().getBean( 'dataSource' )
Next Steps
Instead of ActiveRecord directly creating JDBC connections, create a ActiveRecord adapter that builds a DataSource to dole out connections. The DataSource can be crammed into JNDI, providing a easy access for the Spring Context. This removes the wasteful need of having to start a separate set of database connections for the Spring Context.
Caveat.
The world of JRuby is still a moving target, which should be no suprise to anyone, considering the JRuby team has sprinkled the web and the source with tidbits stating such. Thusly meaning, your mileage may vary.