Tuesday, 26 May 2009

Java: dynamically loading scripting engines (Groovy, JRuby and Jython)

Java 6 (via JSR 223) added scripting language support to the standard library. The JVM ships with the Mozilla Rhino JavaScript engine included. Where an engine is available, it is easy to add support for other JVM scripting languages. It is useful to be able to load these engines dynamically.

This Java 6 code uses the URLClassLoader class to load and invoke a selection of engines at runtime:

public class HelloScripts {

  /**
   @param name
   *          the engine name
   @param script
   *          the script to be invoked
   @param directories
   *          the directories JAR files should be loaded from
   @throws ScriptException
   */
  private static void invokeScript(String name,
      String script, String... directories)
      throws ScriptException {
    ClassLoader loader = new URLClassLoader(
        buildClassPath(directories));
    ClassLoader oldLoader = Thread.currentThread()
        .getContextClassLoader();
    try {
      Thread.currentThread().setContextClassLoader(loader);
      ScriptEngineManager seManager = new ScriptEngineManager(
          loader);
      ScriptEngine engine = seManager.getEngineByName(name);
      if (engine == null) {
        throw new IllegalStateException("No engine for "
            + name);
      }
      engine.eval(script);
    finally {
      Thread.currentThread().setContextClassLoader(
          oldLoader);
    }
  }

  private static URL[] buildClassPath(String... directories) {
    try {
      final List<URL> classPath = new ArrayList<URL>();
      for (String directory : directories) {
        for (File pathname : new File(directory)
            .listFiles()) {
          if (pathname.isFile()
              && pathname.toString().toLowerCase()
                  .endsWith(".jar")) {
            URL url = pathname.toURI().toURL();
            classPath.add(url);
          }
        }
      }
      return classPath.toArray(new URL[classPath.size()]);
    catch (MalformedURLException e) {
      throw new IllegalStateException(e);
    }
  }

  public static void main(String[] args)
      throws ScriptException {
    invokeScript("ECMAScript",
        "println('Hello, ECMAScript!');");

    invokeScript("groovy""println 'Hello, Groovy!';",
        System.getenv("GROOVY_HOME""/lib");

    invokeScript("jruby""puts 'Hello, JRuby!'", System
        .getenv("SCRIPT_ENGINES")
        "/jruby/build", System.getenv("JRUBY_HOME")
        "/lib");

    invokeScript("jython""print 'Hello, Jython!'", System
        .getenv("SCRIPT_ENGINES")
        "/jython/build", System.getenv("JYTHON_HOME"));
  }

}

Note that the ClassLoader used to load the engine is set as the context ClassLoader around any calls to the engine. Failure to do this may result in errors.

Expected output:

Hello, ECMAScript!
Hello, Groovy!
Hello, JRuby!
Hello, Jython!

Configuration

As well as the libraries for the scripting languages, some of the implementations require a third-party scripting engine from scripting.dev.java.net. Later versions may include support by default, in which case the external library can be dropped from the classpath.

Language Version Engine Name Requires External Engine
JavaScript (Mozilla Rhino) 1.6 (built in) ECMAScript no
Groovy Groovy Version: 1.6.0 groovy no
JRuby jruby 1.2.0 (ruby 1.8.6 patchlevel 287) (2009-03-16 rev 9419) jruby yes
Jython Jython 2.2.1 jython yes

Run on a Windows PC, the code uses the following environment variables set to the library installation directories:

GROOVY_HOME=C:\Java\groovy-1.6.0
JRUBY_HOME=C:\Java\jruby-1.2.0
JYTHON_HOME=C:\Java\jython2.2.1
SCRIPT_ENGINES=C:\Java\jsr223-engines

2 comments:

  1. I am trying to use this approach to load the latest version of Rhino. My environment has an older version already implemented, and I cannot seem to load the new one, even when I use the URL class loader AND create a script engine manager with that class loader as well.

    Is it possible to override something that's already built in?

    ReplyDelete
  2. ClassLoaders will try to load any given class from the parent ClassLoader first, so anything on the JRE classpath will be loaded before anything specific to a given URLClassLoader.

    That said, anything that goes into the JRE usually gets its own com.sun.foo namespace to avoid collisions, so there shouldn't be a problem here.

    I was able to invoke Rhino (rhino1_7R2) by adding the following command to the above program:

    invokeScript(
    "ECMAScript",
    "print('Hello, Rhino!');",
    System.getenv("SCRIPT_ENGINES")
    + "/javascript/build",
    System.getenv("ECMA_HOME"));

    ECMA_HOME just contains js.jar. Note that I used an engine from the set described in the post.

    ReplyDelete

All comments are moderated