Scripting Support in Mustang

Scripting Support in Mustang

By Paul Jensen, OCI Partner

February 2006


Introduction to JSR 223

The upcoming JDK 6.0 (Mustang) release does not approach the magnitude of enhancements in JDK 5.0, but does offer several interesting updates. Among these updates is the incorporation of JSR 223 Scripting for the JavaTM Platform. Essentially, this introduces a standard framework to allow scripting language programs to be executed from and have access to the Java platform. It provides many of the capabilities of BSF (Bean Scripting Framework). As with BSF, JSR 223 provides a common interface for integration of a variety of scripting languages. The specification includes examples incorporating PHP and Javascript. Eventual support for a wide range of scripting languages (BeanShell, Jython, Ruby, etc) is expected. At the time of this writing, the author could find support for only JavaScript and Pnuts languages.

The examples in this article are based on J2SE 1.6.0-rc-b69. J2SE 1.6 includes a modified version of the Rhino JavaScript engine integrated via JSR 223 and a command-line scripting shell, jrunscript, which leverages this support. The shell is of limited use in most applications, but can be a valuable tool in learning and prototyping in Java and arbitrary scripting languages. The jrunscript syntax is:

Usage: jrunscript [options] [arguments...] 
where [options] include:

-classpath, -cp  Specify where to find user class files
-D= Set a system property
-J Passdirectly to the runtime system
-l Use specified scripting language
-e

Accessing the Scripting Engine

The fundamental interface for all scripting engines is javax.script.ScriptEngine. This interface defines a set of methods expected to be supported by all implementations. An implementation is obtained from the ScriptEngineManager. ScriptEngine implementations are deployed as Jar files (typically in lib/ext). Each such jar contains a text file resource META-INF/services/javax.script.ScriptEngineFactory defining one or more classes implementing ScriptEngineFactory. The ScriptEngineManager utilizes these factories to create specific implementations. The ScriptEngineManager also provides metadata defining the supported script type and version information which is used to resolve requests for a scripting implementation. Obtaining an engine is straightforward:

  ScriptEngineManager manager = new ScriptEngineManager();
  ScriptEngine jsEngine = manager.getEngineByName("js");

The primary method of the ScriptEngine, eval(), executes a script directly passed as a String or accessed through a Reader. These two variations may be additionally parameterized with either a Bindings or ScriptContext object.

  1. Object eval(String script)
  2. Object eval(Reader reader)
  3. Object eval(String script, Bindings n)
  4. Object eval(Reader reader, Bindings n)
  5. Object eval(String script, ScriptContext context)
  6. Object eval(Reader reader, ScriptContext context)

ScriptContext associates a ScriptEngine with objects in the Java application. Every engine has a default ScriptContext (which can be altered through the API). It groups objects by scope. Each scope has a related Bindings instance containing these objects. Bindings is simply a Map<String, Object> which associates scripting variable names with Java instances.

Two scopes are predefined, ScriptContext.GLOBAL_SCOPE and ScriptContext.ENGINE_SCOPE. The first contains attributes shared by all Engines created via a given factory, while the second is specific to the engine implementation. The default context and scope is used by the single argument eval() methods. The Scripting API includes many instances of such convenience methods. A binding or context passed to an eval effectively replaces the default bindings or context for the scope of the method call.

Additional scopes may be defined by contexts specific to an Engine. For example, the HttpScriptContext used in web-based applications defines request, session, and applications scopes corresponding to Servlet scopes.

The below example summarizes basic usage of the Scripting API:

  1. // simple script execution
  2. jsEngine.eval("print('Hello')");
  3.  
  4. // bind scripting variables and use in a script
  5. jsEngine.put("a", 3);
  6. Integer b = 2;
  7. jsEngine.put("b", b);
  8. Object retval = jsEngine.eval("c = a + b; print('a + b = ' + c);");
  9.  
  10. // retrieve the bindings we previously added
  11. Bindings bindings = jsEngine.getBindings(ScriptContext.ENGINE_SCOPE);
  12. boolean containsKey = bindings.containsKey("a");
  13. System.out.println(containsKey);

Results in:

Hello
  a + b = 5
  true

The checked ScriptException is thrown by methods of the Scripting API. Depending on the Engine, the errant line and column number of the script will be included in the exception.

Compilable and Invocable

Beyond the based ScriptEngine capability, certain implementations may advertise additional capability by implementing the Compilable or Invocable interfaces.

An engine implementing Compilable supports the re-execution of intermediate (e.g. compiled) code from previous compilations. The compile() method returns a CompiledScript based on either a String or Reader. The CompiledScript may be executed repeatedly via its eval() method.

  1. Compilable eng = (Compilable)jsEngine;
  2. CompiledScript script = eng.compile("function test() { return 10; }; test();");
  3. Object compiledOutput = script.eval();
  4. System.out.println(compiledOutput); // prints 10.0

An engine implementing Invocable supports the calling of individual functions scripted in the engine via the invoke() method. Arguments and return values are converted between script and Java types based on the engine implementation. Below is a simple example:

  1. jsEngine.eval("function test(val) { return val; }");
  2. Invocable jsInvoke = (Invocable)jsEngine;
  3. Object invoke = jsInvoke.invoke("test", new Object[] {111.0});
  4. System.out.println(invoke); // prints 111.0

Scripts may be used to implement arbitrary interfaces without direct dependence on the given interface. The following example illustrates definition of a run() method which is bound as the implementation of a Runnable interface at runtime based only on method signature. Below, the invoke() and run() calls invoke the same underlying scripted method.

  1. jsEngine.eval("function run() { print ('running..'); }");
  2. Object invoke2 = jsInvoke.invoke("run",null); // prints running...
  3.  
  4. Runnable r = jsInvoke.getInterface(Runnable.class);
  5. r.run(); // prints running...
  6.  

Summary

While not part of J2SE, the JSR 223 specification describes an optional Web Scripting Framework which includes an HttpScriptContext integrated with an HttpScriptServlet located a proposed javax.script.http package. In fact, PHP and JavaScript integration for web page generation was an initial impetus for the JSR. This scope was expanded over time to be the much more general solution described above. We can anticipate that future versions of J2EE will introduce the originally envisioned Web Scripting Framework.

So what is gained through JSR 223 and how can it be put to use? Certainly scripting is a convenient and efficient means to learn and prototype. In the context of production Java applications, it can also be useful to provide scriptable extension points for sophisticated users. It can also be useful for altering the state of running applications for debugging and monitoring where alternatives such as JMX are not available or sufficient. Naturally, security concerns will dictate reasonable usage for these scenarios.

However, the above only scratches the surface of the capabilities of scripting languages. In general, they offer greater flexibility and power than statically-typed languages like Java. Scripting languages continue to gain in popularity and capability, providing significant productivity gains over Java only solutions (witness Ruby on Rails). JSR 223 defines an integration standard, establishing a foundation for leveraging the strengths of both approaches.

References

secret