Metaprogramming lets you define new constructs in your programming language. In safety languages like Java, metaprogramming is not a standard feature, although it can be done. Not surprisingly, the reaction most Java developers have to metaprogramming typically ranges from "useless" to "catastrophic". I was once in that category. But in the words of Paul Graham, sometimes doing the right thing involves "changing the language to suit the problem". This is a powerful concept that, when used with care, can greatly reduce development time. The Ruby language is especially well-equipped for metaprogramming. This article will show a simple metaprogramming technique that extends Ruby so that Java packages are mapped onto to Ruby modules.
Defining the Problem
The large amount of existing Java code makes RJB one of Ruby's most useful integration tools. RJB provides a lightweight mechanism for working with Java classes from Ruby, while simultaneously allowing for the use of C-extensions and everything else the C implementation of Ruby offers.
Let's say you'd like to do image processing with Java's
ImageIO class. This could be accomplished with RJB via:
require 'rubygems' require_gem 'rjb' require 'rjb' @ImageIO = Rjb::import 'javax.imageio.ImageIO' @ImageIO.getReaderFormatNames # => an array of format names
Here, the instance variable
ImageIO holds a reference to the Java class of the same name. This isn't too bad, but can we do better?
Imagine a situation in which many Java classes need to be imported. This would lead to many variable assignments like the one above. The resulting duplications of Java class names and variable names doesn't smell especially good, nor does it scale well. In addition, the large number of these variable assignments would add a lot of mental overhead when reading and writing the code.
What we'd really like is if Ruby had a way for us to map a Java package/class hierarchy onto Ruby module/class hierarchy. We could then forget about the differences between Ruby and Java, and just get to work. For example:
#... jrequire 'javax.imageio.ImageIO' #... Javax::Imageio::ImageIO.getReaderFormatNames
Our solution will involve translating nested Java package/class constructs into nested Ruby module/class constructs at runtime. We'll need the ability to create new module hierarchies in running code, one of the problems metaprogramming solves. We'll also have to deal with capitalization: Java package names are all lowercase, but Ruby module names start with a capital letter. So
java.lang.System will become
Java::Lang::System. By mapping the Java package namespace onto the Ruby module namespace, we'll reduce the odds of creating a Ruby/Java class name collision, such as with
Let's create a small library to illustrate these points. The code will take advantage of Ruby's
const_set method, which allows new constants (and therefore new modules and classes) to be defined at runtime. Save the following code into a file called java.rb:
require 'rubygems' require_gem 'rjb' require 'rjb' module Kernel def jrequire(qualified_class_name) java_class = Rjb::import(qualified_class_name) package_names = qualified_class_name.to_s.split('.') java_class_name = package_names.delete(package_names.last) new_module = self.class package_names.each do |package_name| module_name = package_name.capitalize if !new_module.const_defined?(module_name) new_module = new_module.const_set(module_name, Module.new) else new_module = new_module.const_get(module_name) end end return false if new_module.const_defined?(java_class_name) new_module.const_set(java_class_name, java_class) return true end end
Using this library consists of
requiring it, applying the new
jrequire command, and manipulating the resulting class:
require 'java' jrequire 'javax.imageio.ImageIO' Javax::Imageio::ImageIO.getReaderFormatNames # => ["BMP", "jpeg", "bmp", "wbmp", "gif", "JPG", "png", "jpg", "WBMP", "JPEG"]
You can eliminate the need to use the fully-qualified module name by adding an
include statement, just as you would with any other Ruby module:
require 'java' jrequire 'javax.imageio.ImageIO' include Javax::Imageio ImageIO.getReaderFormatNames # => ["BMP", "jpeg", "bmp", "wbmp", "gif", "JPG", "png", "jpg", "WBMP", "JPEG"]
One Possible Variation
An interesting variation on the approach given here would be to override Ruby's
require method itself to accept fully-qualified Java class names. Then something even more Rubyesque could be used:
require 'java' require 'javax/imageio/ImageIO' Javax::Imageio::ImageIO.getReaderFormatNames
Other Examples of Ruby-Java Metaprogramming
This tutorial has shown a simple and practical application of Ruby's built-in metaprogramming capabilities. The careful use of metaprogamming is a powerful way to reduce code complexity and build a more consistent programming environment. Look for more metaprogramming techniques to appear in future releases of the Ruby CDK library.