Discover new dimensions of scripted Java

BeanShell scripts become real Java classes

Scripting, albeit slower than execution of precompiled byte code, has many advantages. First, it allows high-level code execution at runtime, without compiler access. This proves particularly useful in situations where the compiler is no longer available (deployment environment) or for languages where compilers simply do not exist. Second, scripted code provides extension points to modular programs. For example, jEdit, an excellent text editor, uses BeanShell for storing user-defined actions (macro definitions). The popular Web browser Mozilla utilizes JavaScript extensively to provide highly configurable and extensible user interface components. I could cite numerous more examples—the need for scripted code execution at runtime is evident by the sheer number of different language interpreters available (see Resources for a list).

In spite of being so popular, incorporating support for a scripting framework usually requires explicit code fragments that evaluate the interpreted code, handle exceptions, and pass parameters between the script and the rest of the application (Ramnivas Laddad explains this task in "Scripting Power Saves the Day for Your Java Apps," (October 1999)). While all this complexity may seem reasonable for languages that do not share Java's object-oriented paradigm, in this article, I show that this often painful overhead can be quite easily trimmed for object-oriented language interpreters.

This article shows how to freely and transparently mix interpreted (scripted) code with precompiled Java code. Moreover, I attempt to mix the code in such a way that neither the JVM nor other running classes can distinguish between precompiled classes and scripted classes. I focus on BeanShell because it is widely known and extends Java syntax to make the examples easier to understand. You could adopt this article's approach to any object-oriented scripting language interpreter.

I assume you are familiar with certain Java topics, such as reflection and class loading. The article provides learning opportunities in dynamic code compilation and loading, and utilization of scripted code in a runtime environment. Furthermore, it also demonstrates the powerful new synergies that can be achieved by combining various open source projects like BCEL (Byte Code Engineering Library), BeanShell, and Ant.

Think big: Set up the goals

BeanShell is an excellent Java interpreter with several extensions to the Java syntax that make it particularly interesting. The script can access real Java classes, execute their methods, load new classes if necessary, or even implement an interface and return to the JVM as an instance of that interface. For example, the Java code below creates a fully functional scripted instance of the java.lang.Runnable interface:

Interpreter interpreter = new Interpreter();
// Define a method called run() in the interface
interpreter.eval("run() { ... }");
// Fetch a reference to the interpreter as a Runnable
Runnable runnable = (Runnable)interpreter.getInterface( Runnable.class );

The capability of acting as any Java interface is one of BeanShell's great advantages. Also note that the script does not even need to declare all of the interface's methods! If Java invokes an undefined method, the interpreter first attempts to evaluate the default handler for undefined methods—a method named simply invoke(). If the script also fails to specify that default handler, an exception will result.

Although many scripting languages are object-oriented, the dynamic nature of scripting engines normally prohibits them from exposing these objects directly to compiled code. As with most scripting languages, BeanShell scripts were, until recently, unable to serve as general Java classes. (The interface in the above example was implemented using the java.lang.reflect.Proxy class introduced in Java 1.3.) Scripts could not extend real Java classes or implement more than one interface. Furthermore, Java reflection did not work with scripted objects. If interface wrappers like the one shown above were not used, scripted code evaluation would require explicit calls to BeanShell's Interpreter class, which was inconvenient and less understandable. In this article we will discuss one implementation of true object support for the Beanshell language that is generally applicable to other scripting languages in Java. Later, we will compare briefly with how these features were implemented in BeanShell 2.0 (see sidebar, "On to BeanShell 2.0.")

Knowing what used to be BeanShell's weaknesses prior to version 2.0, we can start thinking of improvements. We will specifically try to achieve the following:

  • Real Java classes that somehow encapsulate the scripted BeanShell code. To be fully compatible with Java reflection mechanisms, these classes and their instances must expose method signatures.
  • The ability to extend a real Java class with a script and even extend a scripted class with another script (because scripted classes should behave just as any ordinary Java classes).
  • Neither the JVM nor the rest of the application should be aware of the scripted nature of the wrapped BeanShell scripts. We thus avoid explicit evaluation code, and referencing BeanShell classes proves unnecessary.
  • To design our improvements as an extension to BeanShell without modifying the project's core. We should therefore avoid changing any of BeanShell's classes.

A quick glance at BeanShell's syntax

When BeanShell evaluates a script, it parses the characters in the input stream into lexical entities, called ASTs; Beanshell then executes these lexical entities once they form self-contained structures, such as method declarations, variable assignments, and expressions. Almost every element of a script has an associated Namespace object, which holds variables and methods accessible from the scope of that element. Consider the following BeanShell code snippet:

int counter;
counter = 1;
int addTwo(first,second) {
   return first + second;
}
Integer counterNext() {
   global.counter++;
   return global.counter;
}

The code above declares one field, counter, and two methods (or rather functions), addTwo() and counterNext(). The global namespace is defined to be at the script's top level -- counter, method declarations, and the code of counter's value assignment belong to it. The explicit use of the global namespace inside counterNext() was necessary in earlier versions of BeanShell, where local variables were always the default. If the explicit namespace was omitted, a new variable counter would be created inside that method's scope (in its local namespace). The latest versions of BeanShell have adopted standard Java style scoping rules so global is now unecessary. Note how variables automatically convert between primitive and wrapper types (int and java.lang.Integer in counterNext()). Method addTwo()'s parameters are not even declared at all! We must consider these and other subtle syntactical differences between Java and BeanShell when designing our script binding solution.

Map BeanShell's extended syntax to Java class requirements

Unfortunately, we must sacrifice some of BeanShell's extended constructs to fit the Java language syntax. We can still use all of the benefits BeanShell provides inside the scripted code, but to the outside world of the JVM, scripts must appear just as regular Java classes. The assumed constraints and mapping of scripts' syntax to Java are described below:

  • One BeanShell script constitutes one Java class. For scripts kept in files, the name of the script file with the bsh file extension forms that class's name.
  • Functions declared in the global namespace form scripted class methods. All function parameters must be strongly typed (input parameters and return parameter, if present). We could theoretically allow loose typing by casting or wrapping parameters into an Object reference, but I leave this task as an exercise for you.
  • Any code blocks declared in the global namespace execute when an instance of the scripted object is first created. Hence, global code blocks are effectively equivalent to constructors.
  • Scripted classes always have a parameter-less constructor. This implies that any superclass the script extends must also have a public or protected parameter-less constructor.
  • extends and implements keywords for the scripted classes are declared in special comments so BeanShell's interpreter can ignore them (remember, we're striving to preserve the existing BeanShell syntax). These comments assume the form: /*% @extends package.ClassName %*/ and /*% @implements package.InterfaceName %*/.

These assumptions will make it easier for us to map between functions in a BeanShell script and Java class method signatures, which I show how to create in the next section.

Create real Java classes for scripts

All Java classes must be loaded to the JVM in the form of byte code. Moreover, before the classes load, they must pass the byte code verifier, which prevents potentially malicious (or simply corrupted) code from executing. Consequently, there seems to be only two ways of converting a BeanShell script into a Java class: generate all the byte code for the script, effectively compiling it, or use a lightweight Java wrapper class solely for providing appropriate runtime information to the JVM and delegate all method calls to the actual script. The former approach is much more complex and, in the end, accomplished best by the Java compiler itself, so we focus on the latter.

The wrapper class's real purpose is to merely provide the JVM with information about the script's extended superclass, implemented interfaces, and method signatures. All remaining complexity, like loading the script's code and initializing BeanShell's Interpreter object, is identical for all wrappers. Moving all that common code to a separate class named BshScriptWrapper decreases the amount of dynamically generated code. The BCEL library we will use for byte-code generation is quite complex, so moving code generation to one class also slightly increases code clarity. The lightweight wrapper class therefore only initializes a private instance of the BshScriptWrapper class and delegates all method calls to that instance. A class diagram should clarify the dependencies between these classes; see Figure 1.

Figure 1. The lightweight wrapper class and its associated objects

Let's analyze how the approach presented in Figure 1 works on a simple BeanShell script:

int counter;
counter = 1;
int addTwo(int first, int second) {
   return first + second;
}
Integer counterNext() {
   global.counter++;
   return global.counter;
}

For the script above, the corresponding lightweight wrapper class resembles the code below (I will explain this code's details later):

import com.dawidweiss.bsh.dynbinding.BshScriptWrapper;
public class SimpleExampleTyped {
   private final BshScriptWrapper scriptWrapper =
      new BshScriptWrapper(this, "SimpleExampleTyped",
         "SimpleExampleTyped.bsh");
   
   public int addTwo(int arg0, int arg1) {
      Object aobj[] = { new Integer(arg0), new Integer(arg1) };
      return ((Integer)scriptWrapper.invoke("addTwo@@{int}{int}@@int", aobj))
         .intValue();
   }
   
   public Integer counterNext() {
      Object aobj[] = new Object[0];
      return (Integer) scriptWrapper
         .invoke("counterNext@@@@java.lang.Integer", aobj);
   }
}

The private and final scriptWrapper field is initialized when the wrapper class loads into memory. At this time, the wrapper class attempts to load the actual BeanShell script and if this step fails, an InstantiationError is thrown. Thus, it is impossible to create the wrapper class without its associated script. Once the script loads, it is parsed, and method signatures are extracted from it. These signatures should naturally be identical to that of the wrapper class, unless the script has changed since the wrapper class was generated. This ends the wrapper class initialization process.

For all functions extracted from the script body, an appropriate delegate method exists in the wrapper class. All methods simply delegate the call to scriptWrapper, which handles further complexity of invoking BeanShell's interpreter:

public Object invoke(String methodName, Object [] args)
   throws InvocationTargetException {
   Object method = methods.get(methodName);
   if (method == null)
      throw new java.lang.NoSuchMethodError();
   try {
      Object retValue =
          ((BshMethod) method).invoke(args, interpreter, new CallStack());
      if (retValue instanceof bsh.Primitive)
          return Primitive.unwrap(retValue);
      return retValue;
   } catch (EvalError e) {
      throw new java.lang.reflect.InvocationTargetException(e,
         "Method evaluation error: " + methodName);
   }
}

The BshScriptWrapper class's invoke() method is quite straightforward: it first searches for the presence of a given method signature in the script. If the method exists, it invokes the BeanShell interpreter's local instance, otherwise it throws an exception of type NoSuchMethodError. Note that it is possible to (and quite easy to implement) hot-swap method implementation at runtime. Simple script reloading that changes the mapping between method signatures and their implementations would allow you to change the implementation of a class at runtime, without falling back to advanced techniques like classloader invalidation.

Limited superclass visibility from the script

The wrapper pattern used above efficiently and transparently hides the script from the JVM. From the opposite direction, this transparency is only partial. The script does not know its superclass or even methods (and fields) declared in its superclass because its local namespace lacks references to it. BeanShell's default scoping variables like this and global prove unhelpful because they point back to the local namespace. To allow the script to access its superclass's methods and fields, we bind a new special variable called self in the script's global namespace. The self object is a reference to the script's lightweight wrapper class (semantically it equals the this keyword in Java). Accessing superclass methods can thus be realized as follows:

void accessSuperClassMethod() {
   self.superClassMethod();
}

This approach is limited; please refer to this article's "Limitations" section for discussion.

Generate byte code for the wrapper class

Since we know what to expect from the lightweight wrapper class, we can now think about the technical problem of actually generating its byte code. We must first extract method signatures from the script using a hook class placed in the core BeanShell's bsh package. This hack is necessary because BeanShell's parse tree classes are declared in package scope and therefore not available from other packages. I must stress that "breaking in" to package scope of a sealed jar file may potentially end up in an IllegalAccessError being thrown at runtime. However, it is the only solution if we wish to refrain from making major modifications to the BeanShell code. Using the hook class, we extract for each script all its methods, their parameters, and return type.

For byte code generation, we use the widely known BCEL package from the Apache Jakarta Project. All code related to byte code generation is placed in one class called aptly a BCELDynamicClassProducer.

Now comes the heavy part. BCEL is a nicely designed package, but far from being intuitive, especially for those unfamiliar with raw Java byte code. Below, I present snippets of the code used to generate wrapper classes on the fly to intentionally avoid further explanations of BCEL (the curious reader will find references to BCEL in Resources).

First, we need to create the body of a wrapper class—extend the desired superclass, implement interfaces, and invoke the superclass constructor. We also need to add a private field pointing to the essential BshScriptWrapper class instance:

class BCELDynamicClassProducer
{
   InstructionFactory _factory;
   ConstantPoolGen _cp;
   ClassGen _cg;
   public BCELDynamicClassProducer(String className, String superclass,
      String [] interfaces, String scriptBody) {
   ...
   // Generate class with the given superclass and implemented interfaces
   _cg = new ClassGen(className, superclass, srcFile + ".bsh",
          Constants.ACC_PUBLIC | Constants.ACC_SUPER, interfaces);
   _cp = _cg.getConstantPool();
   _factory = new InstructionFactory(_cg, _cp);
   // Add private field to the class
   FieldGen field;
   InstructionList il = new InstructionList();
   field = new FieldGen(Constants.ACC_PRIVATE | Constants.ACC_FINAL,
               new ObjectType(BshScriptWrapper.class.getName()),
               "scriptWrapper", _cp);
   _cg.addField(field.getField());
   // Generate the constructor
   MethodGen method =
      new MethodGen(Constants.ACC_PUBLIC, Type.VOID, Type.NO_ARGS,
          new String [] {  }, "<init>", className, il, _cp);
   
   il.append(InstructionFactory.createLoad(Type.OBJECT, 0));
   il.append(_factory.createInvoke(superclass, "<init>", Type.VOID,
      Type.NO_ARGS, Constants.INVOKESPECIAL));
   ...

The wrapper class may either contain the entire script (in the scriptBody variable) or its resource name so that it can load at runtime. We generate different constructors of BshScriptWrapper, initializing the scriptWrapper field depending on the condition above:

...
   // Initialize the scriptWrapper field
   il.append(InstructionFactory.createLoad(Type.OBJECT, 0));
   il.append(_factory.createNew(BshScriptWrapper.class.getName()));
   il.append(InstructionConstants.DUP);
   il.append(InstructionFactory.createLoad(Type.OBJECT, 0));
   if (scriptBody == null) {
      il.append(new PUSH(_cp, className));
      il.append(new PUSH(_cp, srcFile + ".bsh"));
      il.append(
         _factory.createInvoke( BshScriptWrapper.class.getName(),
            "<init>", Type.VOID, 
            new Type [] { Type.OBJECT, Type.STRING, Type.STRING },
            Constants.INVOKESPECIAL));
   } else {
      il.append(new PUSH(_cp, scriptBody));
      il.append(
         _factory.createInvoke(
            BshScriptWrapper.class.getName(), "<init>",
               Type.VOID, new Type [] { Type.OBJECT, Type.STRING},
               Constants.INVOKESPECIAL));
   }
   il.append(_factory.createFieldAccess(className, "scriptWrapper",
      new ObjectType(BshScriptWrapper.class.getName()), Constants.PUTFIELD));
   il.append(InstructionFactory.createReturn(Type.VOID));
   ...

For each of the extracted method signatures, we then create a corresponding method in the wrapper and add the code to delegate the call to the BshScriptWrapper instance that handles all the complexity of invoking BeanShell. Because methods may have signatures with both primitive and object types, we box and unbox all primitives as needed. For example the following scripted method:

int addTwo(int first,int second) {
   return first + second;
}

requires the following code to be generated:

public int addTwo(int arg0, int arg1)
{
   Object aobj[] = { new Integer(arg0), new Integer(arg1) };
   return ((Integer) scriptWrapper.invoke("addTwo@@{int}{int}@@int", aobj))
      .intValue();
}

I skip the byte code generation for methods here because it is quite complex. Refer to the source code provided in Resources.

As mentioned before, BCEL code may seem confusing, but it does generate exactly what we need from the wrapper class. The resources provided with this article contain an offline generator of lightweight wrappers for *.bsh files. The generator can be invoked from the command line using java -jar bsh-binding.jar script.bsh and generates a .class file with the lightweight wrapper class. To see what it looks like, you can use one of many byte code decompilers (see Resources for a link to Jad decompiler). The previously shown example wrapper class was decompiled back to source code using Jad.

Achieve transparency of use: Custom classloader

So far, we have silently assumed that the wrapper class is available whenever it is needed. This is, of course, not the case. The script wrapper class must be generated and delivered to the JVM at the first attempt to reference the class of that name. The JVM detects that a class is not yet loaded and asks classloaders for its byte code. This fact is important—if we write our own classloader that intercepts these requests and returns some byte code to the JVM, these classes become available in the JVM just as if they were loaded from a jar file or any other .class file.

In our case, we generate lightweight script wrapper byte code on the fly instead of loading some precompiled class definition. Because class resolution occurs automatically and is handled by the JVM, Java code remains unaware of the class's source—it transparently switches between real Java classes and scripted classes as needed. Figure 2 depicts this process.

Figure 2. Interactions between JVM, classloaders, and our custom byte code generator when a new wrapper class is requested

Classloaders return instances of Class objects. To create a new object (an instance of a Class and therefore our wrapped BeanShell script), we must invoke the classloader's newInstance() method. In our case, this causes the wrapper class to load the script, and initialize the BeanShell interpreter and a namespace belonging to the object being created. Figure 3 illustrates that process.

Figure 3. The BshScriptWrapper instance attempts to load the script, and initialize its local namespace and BeanShell interpreter on behalf of the script wrapper class instance

From now on, any method invocation on the lightweight wrapper object will be first delegated to the local instance of BshScriptWrapper, which will in turn attempt to evaluate the method using the local instance of BeanShell's Interpreter, as shown in Figure 4.

Figure 4. Lightweight script wrapper instance (generated using BCEL) delegates method call to the local BshScriptWrapper and on to BeanShell's Interpreter object

We have finally reached the point where once the custom classloader has been created and initialized, the Java application cannot distinguish the wrapped scripts from the real Java classes. Script wrapper classes are created automatically, and instances of these classes load and evaluate BeanShell methods when they are invoked on the Java wrapper object. We can finally try our solution on real Java code.

Scripted classes in Java code

As a simple demonstration, I show a snippet of Java code that dynamically loads and executes a BeanShell script without even knowing that it is a BeanShell script:

package flowers;
import com.dawidweiss.bsh.dynbinding.*;
public class Flowers {
   public static void main(String [] args) throws Exception {
      // Initialize context classloader to BshScriptClassLoader
      ClassLoader loader = new BshScriptClassLoader(
            Thread.currentThread().getContextClassLoader());
      Thread.currentThread().setContextClassLoader(loader);
      
      Flower blue = (Flower) loader.loadClass( "flowers.BlueFlower" ).newInstance();
      Flower green = (Flower) loader.loadClass( "flowers.GreenFlower" ).newInstance();
      Flower lightBlue = (Flower) loader.loadClass( "flowers.LightBlueFlower" ).newInstance();
      
      System.out.println(blue + ", " + green + ", " + lightBlue);
   }
}

All flowers in the above code could be Java classes (blue, green, and light blue). Well, the Flower class is an abstract Java class, BlueFlower is a BeanShell script, LightBlueFlower is a subclass of BlueFlower, and GreenFlower is a real Java class. Therefore, we have both classes loaded from precompiled code (GreenFlower) and classes for which the code is dynamically generated (BlueFlower and LightBlueFlower). The LightBlueFlower is even more interesting as it is itself a script and inherits from a class that is also a script. All of these short files are presented below. Note that BeanShell "classes" have no package declaration. The package name is inferred from their full class name at runtime.

Flower.java

package flowers;
public abstract class Flower {
   protected abstract String getColor();
   public String toString() {
      return "I am a " + getColor() + " flower!";
   }
}

BlueFlower.bsh

/*% @extends flowers.Flower %*/
String getColor() {
   return "Blue";
}

GreenFlower.java

package flowers;
public class GreenFlower extends Flower {
   protected String getColor() {
      return "Green";
   }
}

LightBlueFlower.bsh

/*% @extends flowers.BlueFlower %*/
String getColor() {
   return "Light Blue";
}

In the examples above, all classes extend a common superclass, so it is possible to simply cast the object reference instantiated by the classloader and invoke the superclass's overriden methods. If the script were to form a standalone class, its methods would be unknown at compile time. So to invoke methods of such a class, you would either use Java reflection or implement some interfaces known at compile time. Otherwise, the compiler will complain about unresolved references (unless we can force the compiler to use our classloader, but with command-line tools like javac, this would prove difficult). Use of Java reflection is not a major drawback, although its heavy use slightly obscures code clarity.

Similar problems occur in the hypothetical situation where a real Java class extends a "scripted" class. Although that situation is possible (and I present an example of such inheritance), since no physical byte code is available for the superclass at compile-time, the Java compiler, as in the previous example, will complain about unresolved references.

The only sensible workaround I could think of was to actually save the lightweight wrapper class code to a .class file and make it accessible to the Java compiler. The wrapper code is, of course, required only for the compilation of the real Java class that extends it. It can be removed right away after compilation because it is regenerated on the fly at runtime anyway. Alternatively, you may leave the wrapper class accessible to the default classloader; thus, you don't need to install a custom classloader because the wrapper class will load just as any other Java class (from the physical byte code). Obviously, the wrapper class will work just as before and delegate all method calls to the BeanShell script it was created for.

In this article's example code, you will find an example of an Ant file that compiles a script to the wrapper code, then compiles the Java class that extends the script, and invokes its methods using a superclass reference. Because we left the wrapper class available both at compile-time and at runtime, using reflection or even setting up the custom classloader proves unnecessary. Below is a listing of classes using precompiled script wrappers for comparison with the method used previously:

Flowers.java

package flowers;
import com.dawidweiss.bsh.dynbinding.*;
public class Flowers {
   public static void main(String [] args) throws Exception {
      Flower blue = new BlueFlower(); // Scripted class
      Flower green = new GreenFlower(); // Real class
      Flower lightBlue = new LightBlueFlower(); // Real class extending scripted class
      System.out.println(blue + ", " + green + ", " + lightBlue);
   }
}

LightBlueFlower.java

package flowers;
public class LightBlueFlower extends BlueFlower {
   public String getColor() {
      return "Light Blue Flower";
   }
}

A practical example: Dynamic Ant tasks

The examples I have shown so far only demonstrate the potential that BeanShell's dynamic classes offer. I now show how scripted Java classes can benefit real-life applications.

Voted 2003's Most Useful Java Community-Developed Technology in the JavaWorld Editors' Choice Awards, Ant is a tool familiar to most Java developers. Tasks and datatypes in Ant are specified as subclasses of the org.apache.tools.ant.Task class or generally any JavaBeans. When the build file executes and all of the task's nested elements have initialized, the execute() method is invoked and the task may perform its duty. This is, of course, an oversimplification, but serves this demonstration's purpose.

Until recently, the only way to add a new task to Ant was to have precompiled Java code of a class implementing the task and use Ant's taskdef to create a mapping between the task and its implementation. Since we have the ability to create new classes at runtime, it would be natural to define new tasks inside Ant's build file, without precompilation. Such inline tasks would prove useful for ad-hoc programming or even for prototyping new task definitions.

Adding such functionality to Ant is trivial when we use dynamic BeanShell classes. First, we must write a regular Java-based task that defines new BeanShell inline tasks:

public class InlineBshTaskdef extends Task {
   private StringBuffer scriptBody = new StringBuffer();
   private File         scriptFile;
   private String       name;
   private Class        clazz;
   
   private static BshScriptClassLoader bshLoader;
   public InlineBshTaskdef() {
      synchronized (this.getClass()) {
         if (bshLoader == null) {
            bshLoader = new BshScriptClassLoader(
            this.getClass().getClassLoader());
         }
      }
   }
   public void addText(String msg) {
      scriptBody.append( msg );
   }
   public void setName(String taskName) {
      name = taskName;
   }
   public void execute()
      throws BuildException {
      initialize();
      this.getProject().addTaskDefinition(name, clazz);
   }
   protected void initialize() throws BuildException {
      String script = scriptBody.toString().trim();
      if (name == null)
         throw new BuildException("name attribute is required.");
      if (script.length() == 0)
         throw new BuildException("BeanShell script must be "
            + "present inside the body of task definer");
      String className = "tmp.DynGenBshClass$" + name; 
      byte[] classImpl;
      try {
         // Generates the byte code of the class 
         classImpl =
            BshWrapperClassGenerator.generateClassStub(script, className, false);
      } catch (EvalError e) {
         throw new BuildException("BeanShell script evaluation error.", e);
      }
      // And define the class in our custom class loader
      clazz = bshLoader.defineClass(className, classImpl);
   }
}

For any text appearing inside the XML element defined for this task, Ant invokes the addText() method. Finally, the execute() method generates the byte code for the script wrapper class and defines it in the JVM using the custom classloader (because the ClassLoader object's resolveClass() method is protected by default and we cannot use any classloader for this purpose). Then, by invoking addTaskDefinition() on the project object, we add this wrapper class to the active set of Ant tasks.

To use the inline BeanShell tasks in the Ant build file, we must first create the definer task. Then we can freely create new tasks inside the build file and immediately put them to use:

<project name="inlineTask" default="example" basedir="." >
   <path id="bsh-binding">
      <fileset dir=".." includes="lib/*.jar" />
      <path path="src" />
   </path>
   <target name="prepare">
      <taskdef name="bshtaskdef" classname="InlineBshTaskdef"
         classpathref="bsh-binding"/>
   </target>
   <target name="example" depends="prepare">
      <bshtaskdef name="counterTask"><![CDATA[
      String message;
      int    min;
      int    max;
      
      void setMin(int min) {
          global.min = min;
      }
      
      void setMax(int max) {
          global.max = max;
      }
      
      void setMessage(String message) {
          global.message = message;
      }
      
      void execute() {
         if (message != null) System.out.print(message);
         for (int i=min;i<max;i++) {
            System.out.print(i);
            if (i+1 != max) System.out.print(",");
         }
      }
      </bshtaskdef>
      
      <!-- use the defined task right away! -->
      <counterTask min="0" max="10" message="The numbers from 0 to 10 are: " />
      <counterTask min="5" max="20" message="And the numbers from 5 to 20 are: " />
   </target>
</project>

Embedding BeanShell scripts inside an Ant build file to create new tasks is not the only possibility. The source attached to this article contains more examples.

Limitations

The presented solution for wrapping BeanShell scripts into real Java classes is a proof-of-concept and is certainly not free of limitations. I can name a few of them:

  • The script has access only to public members of its superclass—the BeanShell interpreter is physically not a subclass of the extended class and therefore would have to illegally access a protected member of that class. For a similar reason, methods cannot call their superclasses' overriden counterparts (the self object points back to the same method, such a call causes deadlock and stack overflow).
  • You cannot declare protected, private, or static members inside the script. All members are public. This is partially because of BeanShell syntax, partially because implementing scope keywords results in a lot of code, and usability does not grow significantly.
  • BeanShell's classes are mostly in package scope, and to extract method signatures from scripts, we must place a custom class inside that package. Having classes in the same logical package and different physical locations may, in some circumstances, cause package access violation and result in an IllegalAccessException.

We could introduce many other improvements to the solution presented in this article, but the point is to show that the transparent use of scripted code is possible and can prove beneficial. Moreover, such integration can occur without explicit changes to the existing Java code. When performance comes into view, you can simply rewrite the scripted classes into real Java code and thus achieve significant speedup.

Some of BeanShell's limitations will certainly disappear with new releases. For example, as already mentioned, the scoping rules will become more Java-like, thus removing the need for explicit prefixing with the global namespace. In further perspective, BeanShell is also supposed to provide syntax for protected and private script members, and even dynamically compiled classes! Such a feature would improve the scripting world and render tricky solutions, like the one presented in this article, unnecessary.

Conclusions

I have presented a simple, yet effective solution for making BeanShell scripts appear as real Java classes to Java applications. I showed that by combining the power of dynamically generated code with custom classloaders, you can transparently mix scripted and precompiled code. I also illustrated that the complexity of using any object-oriented scripting framework can be entirely hidden from a Java application.

Especially for prototyping and verifying spike solutions, scripting and dynamically generated wrapper classes are a very attractive (and addictive) alternative.

I wish to thank all contributors of the aforementioned open source projects. Without their time and effort, this work would not be possible. Special thanks to Pat Niemeyer and Mike Smith for help putting this article together.

Dawid Weiss holds a master's degree in software engineering and works as a senior software developer and scientist at the Laboratory of Intelligent Decision Support Systems, Poznan University of Technology, Poland. He has long-term experience working with low-level languages (assembly, C), but now specializes in Java technologies, multithreading, and performance issues. He is currently writing a PhD thesis about Web mining and working on two large Java software projects.

Learn more about this topic

Join the discussion
Be the first to comment on this article. Our Commenting Policies
See more