Monday, March 5, 2007

Java Through Ruby Through Java

Java => Ruby

Now that JRuby has matured I thought I would see what it would take to inject Java objects into Ruby objects via a Spring context. Here is how this works:

First, create a Ruby class that will receive your Java objects:

ruby_messenger.rb:

require 'java'

include_class 'com.meagle.common.spring.jruby.JRubySpring'
include_class 'com.meagle.common.service.UserService'
include_class 'com.meagle.common.service.impl.BaseUserServiceImpl'

class RubyMessenger < JRubySpring

def printMe
p @@message
p @@userService.to_s
user = @@userService.findUser('admin')
p "User first name: #{user.firstName}"
#p user.to_xml
end

def setMessage(message)
@@message = message
end

def getMessage
@@message
end

def setUserService(userService)
@@userService = userService
end

end

RubyMessenger.new # this last line is not essential (but see below)
This class will receive a user service object that allows us to make calls to a transactional Spring bean to a DAO defined in the main Spring configuration.

Furthermore, I am injecting a simple String into the Ruby class from the Spring context called 'message'. Notice that the class extends an interface in Java called JRubySpring. This interface is used to expose methods on the Ruby object to our Java application. Here is what this Java interface looks like:

JRubySpring.java:

public interface JRubySpring {
void printMe();

void setUserService(UserService service);

String getMessage();

void setMessage(String message);
}
Now we have to tell Spring about our Ruby class and the properties to inject into the class. This can be wired up in a Spring 2.0 context configuration file like this:

applicationContext-jruby.xml

<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:lang="http://www.springframework.org/schema/lang"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-2.0.xsd
http://www.springframework.org/schema/lang
http://www.springframework.org/schema/lang/spring-lang-2.0.xsd">

<?xml version="1.0" encoding="UTF-8"?>

<lang:jruby id="jrubyService"
script-interfaces="com.meagle.common.spring.jruby.JRubySpring"
script-source="classpath:ruby_messenger.rb">

<lang:property name="message" value="Hello from Ruby!"/>

<lang:property name="userService">
<ref bean="userService"/>
</lang:property>

</lang:jruby>

</beans>

This instructs Spring to use the ruby_messenger.rb class from the classpath and allow Java classes to use the JRubySpring interface to invoke methods on the Ruby class. Furthermore, you can see the properties that are being injected into the Ruby class here.

Finally, we need a way to test this out. Again, Spring can help us test this using the AbstractSpringIntegrationTestBase class to help with initializing a Spring context and running test scenarios. Here is the test class:

JRubySpringTest.java

public class JRubySpringTest extends AbstractSpringIntegrationTestBase {
private UserService userService;

public JRubySpringTest() {
setDefaultRollback(true);
}

protected String[] getConfigLocations() {
return new String[]{
"applicationContext-core.xml",
"applicationContext-jruby.xml"
};
}

JRubySpring jrubyService;

public void testGetUser() {
System.out.println("Printing messages from plain old Java Spring context...");
System.out.println(jrubyService.getMessage());
User user = userService.findUser("admin");
System.out.println("user = " + user.getFirstName());
System.out.println("Printing messages from a Spring context injected
into a Ruby object...");
jrubyService.printMe();
}

public JRubySpring getJrubyService() {
return jrubyService;
}

public void setJrubyService(JRubySpring jrubyService) {
this.jrubyService = jrubyService;
}

public void setUserService(UserService userService) {
this.userService = userService;
}
}
This test class will initialize Spring and autowire the Ruby class into the test class with the Java/JRuby properties injected into the Ruby class as singletons.




Here is some sample output when running this test class:

03.05.2007 13:30:40,062 INFO dao.hibernate.JRubySpringTest.startNewTransaction:318 ->
Began transaction (1): transaction manager [org.springframework.orm.hibernate3.HibernateTransactionManager@1284f8e]; default rollback = true

Printing messages from plain old Java Spring context...

Hello from Ruby!

user = System

Printing messages from a Spring context injected into a Ruby object...

"Hello from Ruby!"


"com.meagle.common.service.impl.BaseUserServiceImpl@595bcd"

"User first name: System"

03.05.2007 13:30:40,312 INFO dao.hibernate.JRubySpringTest.endTransaction:284 ->
Rolled back transaction after test execution
03.05.2007 13:30:40,328 INFO context.support.ClassPathXmlApplicationContext.doClose:599 ->
Closing application context [org.springframework.context.support.ClassPathXmlApplicationContext;hashCode=6504030]

Ruby => Java

So what if you want to have an irb session and communicate with your Java objects. For this you just need your existing Spring context and a bootstrapping file for Ruby to initialize a JRuby console. It goes like this:

jruby_console.rb

# To use JRuby and Java Spring contexts together do the following:
#
# 1. Download the JRuby irb jar file from:
# http://jruby.codehaus.org/Running+the+JRuby+Console+(graphical+IRB)
#
# 2. Add the jruby-console-0.9.2.jar file to your Java project classpath
#
# 3. Create a new configuration to run a Java application and set the main class to
# org.jruby.demo.IRBConsole
#
# 4. Of course make sure your classpath is set correctly for your Spring config and
# other classes/jars you need to test.
#
# 5. Run the application which will load the JRuby irb console
#
# 6. In the irb window type > load 'jruby_console.rb' to initialize this script.
#
# 7. This will load your Spring configuration. Access your Spring beans as
# instance variables like this @beanName.
#
# 8. That was easy

require 'java'

# require this file if you want to include your domain objects instead of
# including them one at a time.
require 'domain_core.rb'

# include this line to include all of your specific Java domain objects
include Domain::Core

include_class('org.springframework.context.support.ClassPathXmlApplicationContext')

springConfigs = ["applicationContext-core.xml"]
@context = ClassPathXmlApplicationContext.new(springConfigs.join(","))

if @context.nil?
p "*********Could Not initialize Spring context*********"

return
else
p "*********Spring context initialized as @context*********"
end

p "(#{@context.beanDefinitionNames.length}) beans initialized in this context:"
beans = ""

# Assign non-abstract beans are initialized to instance variables.
# Example: The 'userService' bean name will be accessible via the
# instance variable '@userService'
@context.beanDefinitionNames.sort.each do |bean|
unless bean =~ /^abstract/
instance_variable_set("@#{bean}", @context.getBean("#{bean}"))
beans << bean + ", "
end
end

p beans
p "=========================================================================="
p "Hey you - access your beans as instance variable like this @springBeanName"
p "=========================================================================="

Now you can do sadistic stuff like this:

 Welcome to the JRuby IRB Console

irb(main):001:0> load 'jruby_console.rb'
"*********Spring context initialized as @context*********"
"(4) beans initialized in this context:"
dataSource, sessionFactory, userDAO, userService"
"=========================================================================="
"Hey you - access your beans as instance variable like this @springBeanName"
"=========================================================================="
=> true

irb(main):002:0> user = @userService.find(33, User.new.class)
=> #

irb(main):003:0> p user.firstName
"Mark"
=> nil
irb(main):004:0>