I like to explain the relationship between Rails and Java is that Rails makes the things I hate doing easy and Java makes the things I like doing possible.
In this mind set, when using Rails on JRuby I want to do as little configuration in Java (XML sit-ups are bad) and leverage Rails as much as possible (a few yml stretches are good). The most common configuration cross over is the database connection. This is where JNDI helps out, now both sides can use the same connection pool.
Putting your ducks in a row
For the following examples to work, you will need the follow depedencies:
- Sun’s JNDI File System Service Provider. Follow “Download JNDI 1.2.1 & More” to download File System Service Provider. You will need the fscontext and the providerutil jars both in you classpath. I would like to take this moment and say that Sun needs to get their head out of the mud and join the Maven party, instead of forcing people to navigate their cumbersome site.
- Maven will make you hate less. Manually managing the cadre of jars that Java demands will just chalk you full of spite. While Maven does not alleviate the problem, it does put a purdy bow on it
- A given, you must already have gone through the steps of setting up a JRuby runtime. Steps might be to strong of a word, basically it is unpackage and go.
Configuring Rails to create its own JNDI and Connection Pool
Now it is time to get the show on the road. The following setups will show how to startup a JNDI instance, register a Connection Pool, and then use the same Connection Pool in Rails and in a Spring
Setting up the database.yml
The latest version of activerecord-jdbc already supports using JNDI for connections. By setting additional information in database.yml
, jndi_factory_initial
and jndi_provider_url
, a connection pool can be built and registered into JNDI. The jndi_factory_initial
relates to the JNDI property java.naming.factory.initial
, which boils down to the JNDI service that is going to be created. The jndi_provider_url
relates to the JNDI property java.naming.provider.url
, which is the URL to connect to the JNDI service. The standard setting, jndi
, is going to be used as the location to store and retrieve the Connection Pool.
Extended database.yml
development: adapter: jdbc jndi: jdbc/shared jndi_factory_initial: com.sun.jndi.fscontext.RefFSContextFactory jndi_provider_url: file:tmp driver: mysql url: jdbc:mysql://localhost/deployer_devel username: the_username password: a_password
Firing up the JNDI instance
The following copied into the environment.rb
will create a JNDI instance and register a MySQL Datasource to create a Connection Pool. I would like to point out that the mentioned fscontext and the providerutil jars need to be in your classpath for this to work.
# include Sun JNDI classes include_class "java.lang.System" include_class "javax.naming.Context" include_class "javax.naming.InitialContext" include_class "javax.naming.Reference" include_class "javax.naming.StringRefAddr" include_class "javax.sql.DataSource" # include Mysql datasource class include_class "com.mysql.jdbc.jdbc2.optional.MysqlConnectionPoolDataSource" # Create JNDI dir where it stores binding information, if it does not exist # WARNING: This is hardcoded to match JNDI definition in database.yml! if !File.exist? "tmp/jdbc" Dir.mkdir( 'tmp/jdbc' ) end # ActiveRecord config active_record_config = ActiveRecord::Base.establish_connection.config # Register JNDI properties from ActiveRecord config System.setProperty(Context::INITIAL_CONTEXT_FACTORY, active_record_config[:jndi_context_factory]); System.setProperty(Context::PROVIDER_URL, active_record_config[:jndi_provider_url]); # JNDI context intial_context = InitialContext.new(); # Create MySQL datasource data_source = MysqlConnectionPoolDataSource.new(); data_source.setUser( active_record_config[:username] ); data_source.setPassword( active_record_config[:password] ); data_source.setUrl( active_record_config[:url] ) intial_context.rebind("jdbc/datasource", data_source); # Construct DBCP SharedPoolDataSource reference ref = Reference.new( "org.apache.commons.dbcp.datasources.SharedPoolDataSource", "org.apache.commons.dbcp.datasources.SharedPoolDataSourceFactory", nil); # Some exciting Connection Pool options ref.add(StringRefAddr.new("dataSourceName", "jdbc/datasource")); ref.add(StringRefAddr.new("initialSize", "2" )); ref.add(StringRefAddr.new("maxActive" , "30" )); ref.add(StringRefAddr.new("maxIdle" , "2" )); ref.add(StringRefAddr.new("testOnBorrow", "true" )); ref.add(StringRefAddr.new("validationQuery", "SELECT 1" )); ref.add(StringRefAddr.new("connectionProperties", "autoReconnect=true;" )); intial_context.rebind( active_record_config[:jndi], ref);
Why a Mysql datasource and a DBCP Connection Pool you ask? The Mysql datasource is bare bones, only interested in data connection properties such as login, url, etc. The DBCP datasouce is tricked out as a connection pool, monitoring the number of active and idle connections to expand the connection pool as needed (among other neat features).
Hooking up Java to the Rails JNDI
Here is where the magic happens, the Java equivalent of ActiveRecord::Base.establish_connection.config
.
IRubyObject arBase = runtime.getModule( "ActiveRecord" ).getConstant( "Base" ); IRubyObject establishConnection = arBase.callMethod( runtime.getCurrentContext(), "establish_connection" ); RubyHash arConfig = establishConnection.callMethod( runtime.getCurrentContext(), "config" ).convertToHash();
Oh my, that is a mouth full. JRuby provides the loosey goosey ways of Ruby by wrapping everything in IRubyObject interfaces and indirectly executing operations. This Java block allows access to the configuration of ActiveRecord, so all the jndi properties set in the database.yml
can be plucked out. Here is a slimmed down version of RailsJndiService for JNDI.
package slackworks; // Sun JSE import java.io.IOException; import java.sql.Connection; import java.sql.SQLException; import java.util.Properties; import javax.naming.InitialContext; import javax.naming.NamingException; import javax.sql.DataSource; // JRuby import org.jruby.Ruby; import org.jruby.RubyHash; import org.jruby.RubySymbol; import org.jruby.runtime.builtin.IRubyObject; import org.jruby.runtime.load.BasicLibraryService; public class RailsSpringService implements BasicLibraryService { protected Ruby runtime; private static Properties jndiProperties; private static String jndiDataSourceName; public boolean basicLoad(final Ruby runtime) throws IOException { // JNDI properties Properties properties = new Properties(); // ActiveRecord::Base.establish_connection.config[:jndi] IRubyObject arBase = runtime.getModule( "ActiveRecord" ).getConstant( "Base" ); IRubyObject establishConnection = arBase.callMethod( runtime.getCurrentContext(), "establish_connection" ); RubyHash arConfig = establishConnection.callMethod( runtime.getCurrentContext(), "config" ).convertToHash(); String jndi = arConfig.get( RubySymbol.newSymbol( runtime, "jndi" ) ).toString(); // Get initial context factory from ActiveRecord String context_factory = arConfig.get( RubySymbol.newSymbol( runtime, "jndi_factory_initial" ) ).toString(); properties.setProperty( "java.naming.factory.initial", context_factory ); // Get provider url from ActiveRecord String provider_url = arConfig.get( RubySymbol.newSymbol( runtime, "jndi_provider_url" ) ).toString(); properties.setProperty( "java.naming.provider.url", provider_url ); return true; } public static Properties getJndiProperties() { return jndiProperties; } public static void setJndiProperties(Properties jndiProperties) { DeployerService.jndiProperties = jndiProperties; } public static String getJndiDataSourceName() { return jndiDataSourceName; } public static void setJndiDataSourceName(String jndiDataSourceName) { DeployerService.jndiDataSourceName = jndiDataSourceName; } public static boolean testOpenConnection() throws SQLException, NamingException { InitialContext initialContext = new InitialContext( jndiProperties ); DataSource ds = (DataSource) initialContext.lookup( getJndiDataSourceName() ); Connection conn = ds.getConnection(); return !conn.isClosed(); } }
Using the powers of Maven, the pom.xml
has been tweaked so that the task ‘mvn package
’ will create lib/slackworks
with the rails_jndi.jar
and all its dependencies. For the lazy, I suggest simply using rails_jndi in git://sprocket.slackworks.com/srv/git/samples.git. The lib/slackworks
directory need to be copied to the RAILS_ROOT
and should contain:
- avalon-framework-4.1.3.jar
- log4j-1.2.12.jar
- servlet-api-2.3.jar
- commons-dbcp-1.2.2.jar
- commons-logging-1.1.jar
- commons-pool-1.3.jar
- logkit-1.0.1.jar
- mysql-connector-java-5.1.6.jar
- jruby-1.1RC3.jar
- rails_jndi.jar
Why so many jars? I have no idea. I think a common-dbcp is married to commons-pool and she does not let him leave the house alone. I bet logkit is log4j runty little brother and someone must be friends with servlet-api and snuck him into the party.
The classpath needs to be manually set to include everything in lib/slackworks
, i.e. for Java 6, export CLASSPATH=/path/to/rails/lib/slackworks/*
.
Testing with console
Now that everything is in place- the environment.rb setup, the RailsJndiService has been built, everything cozied up in lib/slackworks, and the classpath has been set. Time to fire up the console for a test run. Simply call from theRAILS_ROOT
:
jruby script/consoleYou will be greeted with
Loading development environmentNow load the RailsJndiService, via
require 'slackworks/rails_jndi'and you will be see the RailsJndiService retrieve the Connection Pool from JNDI
Loading Rails JNDI Service Using ActiveRecord datasource at JNDI url: jdbc/shared Using ActiveRecord JNDI Intial Context Factory: com.sun.jndi.fscontext.RefFSContextFactory Using ActiveRecord property JNDI Provider URL: file:tmpJust to be sure, run the test method on RailsJndiService, via:
include_class 'slackworks.RailsJndiService' RailsJndiService.testOpenConnection()and you will be treated to a
true
return, meaning a valid connection is open from the datasource.
Now to be super sure, run an a simple ActiveRecord test:
ActiveRecord::Base.connected?
and you will be treated to another true
return. Hooray, both Java and Rails are both using the same Connection Pool!
Where is the Beef?
So the examples are not that exciting on their own, unless you are in the face paced business of testing database connections. What it does show is how to unifiy database access in JRuby in Rails. Using JNDI also opens up a whole spectrum of integration with JEE applications.