5 things you didn't know about ... the Java Scripting API

An easier way to do scripting on the Java platform

The Java™ language is more than you need for some projects, but scripting languages are famously lacking on the performance front. Find out how the Java Scripting API (javax.script) delivers the best of both worlds, by allowing you to invoke scripts from your Java programs, and vice versa.

Ted Neward, Principal, Neward & Associates

Ted Neward photoTed Neward is the principal of Neward & Associates, where he consults, mentors, teaches, and presents on Java, .NET, XML Services, and other platforms. He resides near Seattle, Washington.



27 July 2010

Also available in Chinese Russian Japanese Portuguese Spanish

About this series

So you think you know about Java programming? The fact is, most developers scratch the surface of the Java platform, learning just enough to get the job done. In this series, Ted Neward digs beneath the core functionality of the Java platform to uncover little-known facts that could help you solve even the stickiest programming challenges.

Many Java developers today are interested in using scripting languages on the Java platform, but using a dynamic language that has been compiled into Java bytecode isn't always possible. In some cases, it's quicker and more efficient to simply script parts of a Java application or to call the particular Java objects you need from within a script.

That's where javax.script comes in. The Java Scripting API, introduced in Java 6, bridges the gap between handy little scripting languages and the robust Java ecosystem. With the Java Scripting API, you can quickly integrate virtually any scripting language with your Java code, which opens up your options considerably when solving small problems on someone else's dime.

1. Executing JavaScript with jrunscript

Develop skills on this topic

This content is part of a progressive knowledge path for advancing your skills. See Become a Java developer

Each new Java platform release brings with it a new set of command-line tools buried away inside of the JDK's bin directory. Java 6 was no exception, and jrunscript is no small addition to the Java platform utilities.

Consider the basic problem of writing a command-line script for performance monitoring. The tool will borrow jmap (introduced in the previous article in the series) and run it every 5 seconds against a Java process, in order to get a feel for how the process is running. Normally, command-line shell scripts would do the trick, but in this case the server application is deployed on a variety of different platforms, including Windows® and Linux®. Sysadmins will testify that trying to write shell scripts that run on both platforms is a pain. The usual solution is to write a Windows batch file and a UNIX® shell script, and just keep the two in sync over time.

But, as any reader of The Pragmatic Programmer knows, this is a horrendous violation of the DRY (don't repeat yourself) principle and is a breeding ground for bugs and defects. What we'd really like to do is write some kind of OS-neutral script that can run across all the platforms.

The Java language is platform neutral, of course, but this really isn't a case for a "system" language. What we need is a scripting language — like JavaScript, for instance.

Listing 1 starts with a basic shell of what we want:

Listing 1. periodic.js
while (true)
{
    echo("Hello, world!");
}

Many, if not most, Java developers already know JavaScript (or ECMAScript; JavaScript is an ECMAScript dialect owned by Netscape) thanks to our forced interactions with web browsers. The question is, how would a system administrator run this script?

The solution, of course, is the jrunscript utility that ships with the JDK, as shown in Listing 2:

Listing 2. jrunscript
C:\developerWorks\5things-scripting\code\jssrc>jrunscript periodic.js
Hello, world!
Hello, world!
Hello, world!
Hello, world!
Hello, world!
Hello, world!
Hello, world!
...

Note that you could also use a for loop to execute the script a given number of times before quitting. Basically, jrunscript lets you do almost everything you normally would do with JavaScript. The only exception is that the environment is not a browser, so there's no DOM. The top-level functions and objects available are therefore slightly different.

Because Java 6 ships with the Rhino ECMAScript engine as a part of the JDK, jrunscript can execute any ECMAScript code that is fed to it, either from a file (as shown here) or in a more interactive shell environment called a REPL ("Read-Evaluate-Print-Loop"). Just run jrunscript by itself to access the REPL shell.


2. Accessing Java objects from a script

Being able to write JavaScript/ECMAScript code is nice, but we don't want to have to rebuild everything we use in the Java language from scratch — that would defeat the purpose. Fortunately, anything using the Java Scripting API engines has full access to the entire Java ecosystem because, at heart, everything is still Java bytecode. So, going back to the earlier problem, we could launch processes from the Java platform using the traditional Runtime.exec() call, as shown in Listing 3:

Listing 3. Runtime.exec() launches jmap
var p = java.lang.Runtime.getRuntime().exec("jmap", [ "-histo", arguments[0] ])
p.waitFor()

The arguments array is the ECMAScript standard built-in reference to the arguments passed to this function. In the case of the top-level script environment, this is the array of arguments passed to the script itself (the command-line parameters). So, in Listing 3, the script is expecting an argument containing the VMID of the Java process to map.

Alternately, we could take advantage of the fact that jmap is a Java class and just call its main() method, like in Listing 4. This approach eliminates the need to "pipe" the Process object's in/out/err streams.

Listing 4. JMap.main()
var args = [ "-histo", arguments[0] ]
Packages.sun.tools.jmap.JMap.main(args)

The Packages syntax is a Rhino ECMAScript notation used to refer to a Java package outside of the core java.* packages already set up within Rhino.


3. Calling into scripts from Java code

Calling Java objects from a script is only half of the story: The Java scripting environment also provides the ability to invoke scripts from within Java code. Doing so just requires instantiating a ScriptEngine, then loading the script in and evaluating it, as shown in Listing 5:

Listing 5. Scripting on the Java platform
import java.io.*;
import javax.script.*;

public class App
{
    public static void main(String[] args)
    {
        try
        {
            ScriptEngine engine = 
                new ScriptEngineManager().getEngineByName("javascript");
            for (String arg : args)
            {
                FileReader fr = new FileReader(arg);
                engine.eval(fr);
            }
        }
        catch(IOException ioEx)
        {
            ioEx.printStackTrace();
        }
        catch(ScriptException scrEx)
        {
            scrEx.printStackTrace();
        }
    }
}

The eval() method can also operate against a straight String, so the script needn't come from a file on the filesystem — it can come from a database, the user, or even be manufactured within the application based on circumstance and user action.


4. Binding Java objects into script space

Just invoking a script isn't enough: Scripts often want to interact with objects created from within the Java environment. In these cases, the Java host environment must create objects and bind them so that they're easy for the script to find and use. This is a task for the ScriptContext object shown in Listing 6:

Listing 6. Binding objects for scripts
import java.io.*;
import javax.script.*;

public class App
{
    public static void main(String[] args)
    {
        try
        {
            ScriptEngine engine = 
                new ScriptEngineManager().getEngineByName("javascript");
                
            for (String arg : args)
            {
                Bindings bindings = new SimpleBindings();
                bindings.put("author", new Person("Ted", "Neward", 39));
                bindings.put("title", "5 Things You Didn't Know");
                
                FileReader fr = new FileReader(arg);
                engine.eval(fr, bindings);
            }
        }
        catch(IOException ioEx)
        {
            ioEx.printStackTrace();
        }
        catch(ScriptException scrEx)
        {
            scrEx.printStackTrace();
        }
    }
}

Accessing the bound object is straightforward — the name of the bound object is introduced at the script level as a member of the global namespace, so using the Person object from Rhino is as easy as Listing 7:

Listing 7. Who wrote this article?
println("Hello from inside scripting!")

println("author.firstName = " + author.firstName)

As you can see, JavaBeans-style properties are reduced down to straight name accessors, as if they were fields.


5. Compiling oft-used scripts

The drawback of scripting languages has always been performance. The reason is that in most cases, the scripting language is interpreted "on the fly" and loses time and CPU cycles having to parse and validate text as it's executed. Many scripting languages running on the JVM ultimately transform the incoming code into Java bytecode, at least the first time the script is parsed and validated; this on-the-fly compilation gets tossed when the Java program shuts down. Keeping frequently used scripts in bytecode form would give us a sizable performance boost.

We can do this in a natural and meaningful way with the Java Scripting API. If the ScriptEngine returned implements the Compilable interface, then the compile method on that interface can be used to compile the script (passed in as either a String or a Reader) into a CompiledScript instance, which can then be used to eval() the compiled code over and over again, with different bindings. This is shown in Listing 8:

Listing 8. Compiling interpreted code
import java.io.*;
import javax.script.*;

public class App
{
    public static void main(String[] args)
    {
        try
        {
            ScriptEngine engine = 
                new ScriptEngineManager().getEngineByName("javascript");
                
            for (String arg : args)
            {
                Bindings bindings = new SimpleBindings();
                bindings.put("author", new Person("Ted", "Neward", 39));
                bindings.put("title", "5 Things You Didn't Know");
                
                FileReader fr = new FileReader(arg);
                if (engine instanceof Compilable)
                {
                    System.out.println("Compiling....");
                    Compilable compEngine = (Compilable)engine;
                    CompiledScript cs = compEngine.compile(fr);
                    cs.eval(bindings);
                }
                else
                    engine.eval(fr, bindings);
            }
        }
        catch(IOException ioEx)
        {
            ioEx.printStackTrace();
        }
        catch(ScriptException scrEx)
        {
            scrEx.printStackTrace();
        }
    }
}

In most cases, the CompiledScript instance should be held somewhere in long-term storage (servlet-context, for example) in order to avoid recompiling the same script over and over again. Should the script contents change, however, you must create a new CompiledScript to reflect the change; once compiled, the CompiledScript no longer executes the original script file's contents.


In conclusion

The Java Scripting API is a huge step forward in extending the reach and functionality of Java programs, and it brings the productivity gains associated with scripting languages into the Java environment. Coupled with jrunscript— which is obviously not all that complex of a program to write —javax.script gives Java developers the benefits of scripting languages like Ruby (JRuby) and ECMAScript (Rhino) without having to surrender the ecosystem and scalability of the Java environment.

Coming up next in the 5 things series: JDBC.

Resources

Learn

Discuss

  • Get involved in the My developerWorks community. Connect with other developerWorks users while exploring the developer-driven blogs, forums, groups, and wikis.

Comments

developerWorks: Sign in

Required fields are indicated with an asterisk (*).


Need an IBM ID?
Forgot your IBM ID?


Forgot your password?
Change your password

By clicking Submit, you agree to the developerWorks terms of use.

 


The first time you sign into developerWorks, a profile is created for you. Information in your profile (your name, country/region, and company name) is displayed to the public and will accompany any content you post, unless you opt to hide your company name. You may update your IBM account at any time.

All information submitted is secure.

Choose your display name



The first time you sign in to developerWorks, a profile is created for you, so you need to choose a display name. Your display name accompanies the content you post on developerWorks.

Please choose a display name between 3-31 characters. Your display name must be unique in the developerWorks community and should not be your email address for privacy reasons.

Required fields are indicated with an asterisk (*).

(Must be between 3 – 31 characters.)

By clicking Submit, you agree to the developerWorks terms of use.

 


All information submitted is secure.

Dig deeper into Java technology on developerWorks


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=1
Zone=Java technology
ArticleID=502431
ArticleTitle=5 things you didn't know about ... the Java Scripting API
publish-date=07272010