Java excels at building large-scale, robust, and performant systems, but its static typing and verbosity can be a barrier for rapid prototyping, scripting, or expressing complex business rules in a dynamic way. Ruby, with its elegant syntax, dynamic nature, and "developer happiness" focus, offers a compelling contrast. JRuby, a 100% Java implementation of the Ruby interpreter, provides the perfect bridge, allowing you to embed a full Ruby runtime directly inside your Java applications.
This article explores the why and how of embedding Ruby in Java using JRuby, from simple scripting to advanced bidirectional integration.
Why Embed Ruby in Java? Key Use Cases
- Scripting and Configuration: Allow end-users or administrators to customize application behavior with Ruby scripts instead of complex XML/JSON configuration files.
- Domain-Specific Languages (DSLs): Let business experts write logic in a custom, Ruby-based DSL that you provide, which is then executed safely within the Java application's context.
- Rapid Feature Prototyping: Develop and test new features or complex algorithms quickly in Ruby before committing to a final, production-ready Java implementation.
- Legacy Integration: Gradually modernize a legacy Ruby (e.g., Rails) application by embedding it within a new Java service, allowing for a piecemeal migration.
- Testing: Use Ruby's excellent testing libraries (like RSpec) to write tests for your Java code.
Core Components: The JRuby Embedding API
The primary entry point for embedding is the org.jruby.embed.ScriptingContainer class. It is a facade that handles the Ruby runtime, variable sharing, and script execution.
Maven Dependency:
To get started, you need to add the JRuby dependency to your project.
<dependency> <groupId>org.jruby</groupId> <artifactId>jruby-complete</artifactId> <version>9.4.6.0</version> <!-- Check for the latest version --> </dependency>
Basic Embedding Patterns
1. Simple Script Execution
The most straightforward use case is running a string of Ruby code.
import org.jruby.embed.ScriptingContainer;
public class SimpleEmbedding {
public static void main(String[] args) {
// Create the container. This starts a JRuby runtime.
ScriptingContainer container = new ScriptingContainer();
// Run a simple Ruby script
container.runScriptlet("puts 'Hello from Ruby!'");
// Run a script that returns a value to Java
Object result = container.runScriptlet("3 + 4");
System.out.println("Result was: " + result); // Result was: 7
System.out.println("Java type: " + result.getClass()); // Java type: class java.lang.Integer
}
}
2. Sharing Variables: Java → Ruby
You can expose Java objects to the Ruby runtime, making them available as global variables or local variables.
import org.jruby.embed.ScriptingContainer;
public class ShareToRuby {
public static void main(String[] args) {
ScriptingContainer container = new ScriptingContainer();
// Share a Java String as a Ruby global variable
container.put("$java_message", "Hello from Java!");
// Share a Java object as a Ruby local variable
MyJavaBean bean = new MyJavaBean("John", 30);
container.put("user", bean);
// The Ruby code can now access these variables
container.runScriptlet("puts $java_message"); // Hello from Java!
container.runScriptlet("puts \"User: #{user.name}, Age: #{user.age}\"");
}
}
// A simple JavaBean
class MyJavaBean {
public String name;
public int age;
public MyJavaBean(String name, int age) {
this.name = name;
this.age = age;
}
// Getters are required for Ruby to access them as properties
public String getName() { return name; }
public int getAge() { return age; }
}
3. Sharing Variables: Ruby → Java
You can also retrieve values computed in the Ruby script back into Java.
import org.jruby.embed.ScriptingContainer;
public class ShareFromRuby {
public static void main(String[] args) {
ScriptingContainer container = new ScriptingContainer();
// Run a script that defines variables
container.runScriptlet("$ruby_global = 'Data from Ruby'");
container.runScriptlet("local_list = [1, 'two', 3.0]");
// Retrieve the global variable from Ruby
String globalData = (String) container.get("$ruby_global");
System.out.println("Global: " + globalData); // Global: Data from Ruby
// Retrieve the local variable (a Ruby array)
Object rubyList = container.get("local_list");
System.out.println("List: " + rubyList); // List: [1, two, 3.0]
System.out.println("Java type: " + rubyList.getClass()); // class org.jruby.RubyArray
}
}
Advanced Integration: Calling Ruby Methods from Java
A more powerful pattern is to define a Ruby class or method and then call it directly from Java, passing arguments and getting return values.
import org.jruby.embed.ScriptingContainer;
import org.jruby.embed.InvokeFailedException;
public class CallRubyMethod {
public static void main(String[] args) {
ScriptingContainer container = new ScriptingContainer();
// Define a Ruby class with a method
container.runScriptlet("""
class RubyCalculator
def add(a, b)
a + b
end
def greet(name)
"Hello, \#{name}!"
end
end
$calculator = RubyCalculator.new
""");
// Get the Ruby object instance
Object calculator = container.get("$calculator");
// Call the 'add' method on the Ruby object
// The callMethod is a varargs method, so you can pass any number of arguments.
Object result1 = container.callMethod(calculator, "add", 5, 3);
System.out.println("5 + 3 = " + result1); // 5 + 3 = 8
// Call the 'greet' method
Object result2 = container.callMethod(calculator, "greet", "Java Developer");
System.out.println(result2); // Hello, Java Developer!
// Handle potential errors
try {
container.callMethod(calculator, "nonexistent_method");
} catch (InvokeFailedException e) {
System.err.println("Ruby method call failed: " + e.getCause());
}
}
}
Patterns for Production Use
1. Running Ruby Script Files
Instead of embedding scripts as strings, you can load and execute entire .rb files, which is much more maintainable.
import org.jruby.embed.ScriptingContainer;
import java.nio.file.Paths;
public class RunRubyFile {
public static void main(String[] args) {
ScriptingContainer container = new ScriptingContainer();
// Run a Ruby script from the filesystem
try {
container.runScriptlet(Paths.get("scripts/my_business_logic.rb"));
} catch (Exception e) {
e.printStackTrace();
}
// Now methods and classes from that file are available
Object processor = container.runScriptlet("MyBusinessLogic.new");
container.callMethod(processor, "execute");
}
}
2. Implementing a Scripting Service
A clean architectural pattern is to create a dedicated service for Ruby execution.
import org.jruby.embed.ScriptingContainer;
import org.jruby.exceptions.RaiseException;
public class RubyScriptingService {
private final ScriptingContainer container;
public RubyScriptingService() {
this.container = new ScriptingContainer();
// Pre-load common libraries or helper scripts
container.runScriptlet("require 'json'");
}
public Object executeScript(String script, Map<String, Object> bindings) {
try {
// Inject all bindings into the Ruby runtime
if (bindings != null) {
bindings.forEach(container::put);
}
return container.runScriptlet(script);
} catch (RaiseException e) {
// Handle Ruby exceptions specifically
System.err.println("Ruby Error: " + e.getException().getMessage());
throw new RuntimeException("Script execution failed", e);
} finally {
// Clean up specific bindings if needed
}
}
public void close() {
container.terminate();
}
}
Performance and Threading Considerations
- Runtime Cost: Creating a
ScriptingContaineris expensive. Reuse it throughout your application's lifecycle. - Thread Safety: A single
ScriptingContaineris not thread-safe. For multi-threaded applications, you have two options:- Use a
ThreadLocal<ScriptingContainer>to have one container per thread. - Use a pool of containers.
- Use a
- Just-In-Time (JIT) Compilation: JRuby can JIT-compile frequently executed Ruby code to Java bytecode, offering performance comparable to Java for long-running scripts.
Conclusion
Embedding Ruby via JRuby opens up a world of possibilities for Java applications. It combines Java's enterprise strength with Ruby's dynamic flexibility, allowing you to:
- Externalize business logic into maintainable Ruby scripts.
- Create expressive DSLs for domain experts.
- Rapidly prototype complex features.
- Leverage the rich ecosystem of Ruby gems within a Java context.
The ScriptingContainer API makes the integration remarkably straightforward, handling the complex type conversions and runtime management behind a simple facade. Whether you're adding scripting capabilities to an existing application or building a new polyglot system from the ground up, JRuby provides a mature, robust, and powerful solution for blending these two iconic languages.
Further Reading: Explore the Bean Scripting Framework (BSF) or the newer JSR 223 (Scripting for the Java Platform) for a more standardized, container-agnostic approach to embedding various scripting languages, including JRuby.