Java scripting languages: Which is right for you?

When hooking scripting language support into a Java app, choose wisely

Some Java applications' requirements make integration with a scripting language necessary. For example, your users may need to write scripts that drive the application, extend it, or contain loops and other flow-control constructs. In such cases, it's sensible to support a scripting language interpreter that can read user scripts, then run them against your Java application's classes. To accomplish that task, run a Java-based scripting language interpreter in the same JVM as your application.

Support libraries, such as IBM's Bean Scripting Framework or the library Ramnivas Laddad developed in "Scripting Power Saves the Day for Your Java Apps" (JavaWorld, October 1999), successfully help you plug different scripting languages into your Java program. Such frameworks don't require major changes to your Java application, and they let your Java program run scripts written in Tcl, Python, and other languages.

A user's scripts can also directly reference your Java application's classes, just as if those scripts were another part of your program. That's good and bad. It's good if you want scripting to drive regression tests against your program and need to make low-level calls from the script into your application. It's bad if a user's script operates against your program's internals instead of against an agreed-upon API, thus compromising your program's integrity. So plan on publishing the API you want your users to write scripts against and clarify that the rest of the program remains off limits. You could also obfuscate the class names and methods you don't want customers to write scripts against but leave the API classes and method names alone. By doing so, you'll make it less likely an adventurous user would code against a class you didn't want them to.

Hooking in several scripting languages into your Java program is remarkable, but think twice if you're writing a commercial application -- you're opening a can of worms by trying to be all things to all users. You must consider the configuration management issue, since at least some of the different scripting interpreters get updated and released periodically. So you'll need to ensure which version of each scripting interpreter makes sense with which release of your application. If a user puts a newer version of one of these interpreters in your application's install tree (hoping to fix a bug in the older version), they will now run an untested configuration of your Java application. Days or weeks later, when the user finds and reports a bug in your application unearthed by the newer scripting interpreter version, they probably won't mention the scripting interpreter change to your customer support staff -- making it difficult for your engineers to reproduce the problem.

Moreover, customers likely will insist you offer a bug fix for the scripting interpreter your application supports. Some interpreters are actively maintained and updated through an open source model; in those cases experts may help you work around the problem, patch the interpreter, or get a bug fix included in a future release. That's important because without support, you could be stuck with the unpleasant task of fixing the problem yourself, and scripting interpreters run from between 25,000 to 55,000 lines of code.

To avoid the fix-it-yourself scenario, you can thoroughly test any scripting interpreter you plan to support with your application. For each interpreter, ensure that the interpreter gracefully handles the most common usage scenarios, that big memory chunks don't leak when you hammer on the interpreter with long and demanding scripts, and that nothing unexpected happens when you put your program and scripting interpreters in the hands of demanding beta-testers. Yes, such up-front testing costs time and resources; nevertheless, testing is time well spent.

The solution: Keep it simple

If you must support scripting in your Java application, pick a scripting interpreter that best suits your application needs and customer base. Thusly, you simplify the interpreter-integration code, reduce customer support costs, and improve your application's consistency. The hard question is: if you must standardize on just one scripting language, which one do you choose?

I compared several scripting interpreters, starting with a list of languages including Tcl, Python, Perl, JavaScript, and BeanShell. Then, without doing a detailed analysis, I bumped Perl from consideration. Why? Because there isn't a Perl interpreter written in Java. If the scripting interpreter you choose is implemented in native code, like Perl, then the interaction between your application and the script code is less direct, and you must ship at least one native binary with your Java program for each operating system you care about. Since many developers choose Java because of the language's portability, I stay true to that advantage by sticking with a scripting interpreter that does not create a dependence on native binaries. Java is cross-platform, and I want my scripting interpreter to be also. In contrast, Java-based interpreters do exist for Tcl, Python, JavaScript, and BeanShell, so they can run in the same process and JVM as the rest of your Java application.

Based on those criteria, the scripting interpreter comparison list comprises:

  • Jacl: The Tcl Java implementation
  • Jython: The Python Java implementation
  • Rhino: The JavaScript Java implementation
  • BeanShell: A Java source interpreter written in Java

Now that we've filtered the scripting interpreter language list down to Tcl, Python, JavaScript, and BeanShell, that brings us to the first comparison criteria.

The first benchmark: Feasibility

For the first benchmark, feasibility, I examined the four interpreters to see if anything made them impossible to use. I wrote simple test programs in each language, ran my test cases against them, and found that each performed well. All worked reliably or proved easy to integrate with. While each interpreter seems a worthy candidate, what would make a developer choose one over another?

  • Jacl: If you desire Tk constructs in your scripts to create user interface objects, look at the Swank project for Java classes that wrap Java's Swing widgets into Tk. The distribution does not include a debugger for Jacl scripts.
  • Jython: Supports scripts written in the Python syntax. Instead of using curly braces or begin-end markers to indicate flow of control, as many languages do, Python uses indentation levels to show which blocks of code belong together. Is that a problem? It depends on you and your customers and whether you mind. The distribution does not include a debugger for Jython scripts.
  • Rhino: Many programmers associate JavaScript with Webpage programming, but this JavaScript version doesn't need to run inside a Web browser. I found no problems while working with it. The distribution comes with a simple but useful script debugger.
  • BeanShell: Java programmers will immediately feel at home with this source interpreter's behavior. BeanShell's documentation is nicely done, but don't look for a book on BeanShell programming at your bookstore -- there aren't any. And BeanShell's development team is very small, too. However, that's only a problem if the principals move on to other interests and others don't step in to fill their shoes. The distribution does not include a debugger for BeanShell scripts.

The second benchmark: Performance

For the second benchmark, performance, I examined how quickly the scripting interpreters executed simple programs. I didn't ask the interpreters to sort huge arrays or perform complex math. Instead, I stuck to basic, general tasks such as looping, comparing integers against other integers, and allocating and initializing large one- and two-dimensional arrays. It doesn't get much simpler than that, and these tasks are common enough that most commercial applications will perform them at one time or another. I also checked to see how much memory each interpreter required for instantiation and to execute a tiny script.

For consistency, I coded each test as similarly as possible in each scripting language. I ran the tests on a Toshiba Tecra 8100 laptop with a 700-MHz Pentium III processor and 256 MB of RAM. When invoking the JVM, I used the default heap size.

In the interest of offering perspective for how fast or slow these numbers are, I also coded the test cases in Java and ran them using Java 1.3.1. I also reran the Tcl scripts I wrote for the Jacl scripting interpreter inside a native Tcl interpreter. Consequently, in the tables below, you can see how the interpreters stack up against native interpreters.

Table 1. For loop counting from 1 to 1,000,000
Scripting interpreter
Time
Java10 milliseconds
Tcl1.4 seconds
Jacl140 seconds
Jython1.2 seconds
Rhino5 seconds
BeanShell80 seconds
Table 2. Compare 1,000,000 integers for equality
Scripting interpreter
Time
Java10 milliseconds
Tcl2 seconds
Jacl300 seconds
Jython4 seconds
Rhino8 seconds
BeanShell80 seconds
Table 3. Allocate and initialize a 100,000 element array
Scripting interpreter
Time
Java10 milliseconds
Tcl.5 seconds
Jacl25 seconds
Jython1 second
Rhino1.3 seconds
BeanShell22 seconds
Table 4. Allocate and initialize a 500 x 500 element array
Scripting interpreter
Time
Java20 milliseconds
Tcl2 seconds
Jacl45 seconds
Jython1 second
Rhino7 seconds
BeanShell18 seconds
Table 5. Memory required to initialize the interpreter in the JVM
Scripting interpreter
Memory size
JaclAbout 1 MB
JythonAbout 2 MB
RhinoAbout 1 MB
BeanShellAbout 1 MB

What the numbers mean

Jython proves the fastest on the benchmarks by a considerable margin, with Rhino a reasonably close second. BeanShell is slower, with Jacl bringing up the rear.

Whether these performance numbers matter to you depends on the tasks you want to do with your scripting language. If you have many hundreds of thousands of iterations to perform in your scripting functions, then Jacl or BeanShell might prove intolerable. If your scripts run few repetitive functions, then the relative differences in speeds between these interpreters seem less important.

It's worth mentioning that Jython doesn't seem to have built-in direct support for declaring two-dimensional arrays, but this can be worked around by using an array-of-arrays structure.

Although it was not a performance benchmark, it did take me more time to write the scripts in Jython than for the others. No doubt my unfamiliarity with Python caused some of the trouble. If you are a proficient Java programmer but are unfamiliar with Python or Tcl, you may find it easier to get going writing scripts with JavaScript or BeanShell than you will with Jython or Jacl, since there is less new ground to cover.

The third benchmark: Integration difficulty

The integration benchmark covers two tasks. The first shows how much code instantiates the scripting language interpreter. The second task writes a script that instantiates a Java JFrame, populates it with a JTree, and sizes and displays the JFrame. Although simple, these tasks prove valuable because they measure the effort to start using the interpreter, and also how a script written for the interpreter looks when it calls Java class code.

Jacl

To integrate Jacl into your Java application, you add the Jacl jar file to your classpath at invocation, then instantiate the Jacl interpreter prior to executing a script. Here's the code to create a Jacl interpreter:

import tcl.lang.*;
public class SimpleEmbedded {
   public static void main(String args[]) {
      try {
        Interp interp = new Interp();
      } catch (Exception e) {
      }
} 

The Jacl script to create a JTree, put it in a JFrame, and size and show the JFrame, looks like this:

package require java
set env(TCL_CLASSPATH) 
set mid [java::new javax.swing.JTree]
set f [java::new javax.swing.JFrame]
$f setSize 200 200
set layout [java::new java.awt.BorderLayout]
$f setLayout $layout
$f add $mid 
$f show

Jython

To integrate Jython with your Java application, add the Jython jar file to your classpath at invocation, then instantiate the interpreter prior to executing a script. The code that gets you this far is straightforward:

import org.python.util.PythonInterpreter;
import org.python.core.*;
public class SimpleEmbedded {
    public static void main(String []args) throws PyException {
        PythonInterpreter interp = new PythonInterpreter();
   }
}

The Jython script to create a JTree, put it in a JFrame, and show the JFrame is shown below. I avoided sizing the frame this time:

from pawt import swing
import java, sys
frame = swing.JFrame('Jython example', visible=1)
tree = swing.JTree()
frame.contentPane.add(tree)
frame.pack()

Rhino

As with the other interpreters, you add the Rhino jar file to your classpath at invocation, then instantiate the interpreter prior to executing a script:

import org.mozilla.javascript.*;
import org.mozilla.javascript.tools.ToolErrorReporter;
public class SimpleEmbedded {
    public static void main(String args[]) {
        Context cx = Context.enter();
   }
}

The Rhino script to create a JTree, put it in a JFrame, and size and show the JFrame proves simple:

importPackage(java.awt);
importPackage(Packages.javax.swing);
frame = new Frame("JavaScript");
frame.setSize(new Dimension(200,200)); 
frame.setLayout(new BorderLayout());
t = new JTree();
frame.add(t, BorderLayout.CENTER);
frame.pack();
frame.show();
1 2 Page
Join the discussion
Be the first to comment on this article. Our Commenting Policies
See more