Making today worse so tomorrow seems better.
JNDI with Rails
29-JUN-09 – updated formating to work with the latest version of blarg
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 dependencies:
- 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 DBCP?
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 @RAILSROOT@ 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 the @ RAILS_ROOT @ :
jruby script/console
You will be greeted with
Loading development environment
Now 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:tmp
Just 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.
- About
- Technology, programming, the interwebs and other topics by members of the slackworks community.
- Contributors
- All Posts
-
- Aug 05 2009 Using New Relic add_method_tracer with Class methods
- Jun 30 2009 Estimating Key Collisions (Birthday Problem)
- Jun 29 2009 The Blarg is Back!
- Jul 23 2008 JNDI with Rails
- Jul 06 2008 JRuby Service in Rails
- May 16 2008 Wildcard DNS and Rails
- Jan 30 2008 Sprint Error "911" and the "Download Domain"
- Jan 16 2008 Getting a Good View of Your Couch
- Jan 12 2008 Bring Selenium to the integration party
- Jan 12 2008 Bj Makes Attachment_fu Happy
- Jan 03 2008 Restore Finder magic to a sparsebundle directory
- Dec 18 2007 First Post
0 comments