Dawid Weiss
Dynamic BeanShell binding and ANT tasks
http://www.cs.put.poznan.pl/dweiss/xml/projects/bsh-binding/index.xml

A full version of this article has been published by JavaWorld. [outlink]

Please note that BeanShell 2.0 introduces most of the features described here as part of the language (and with much lower overhead because code generation is performed using AsmLib, not BCEL).

The idea: mixing the scripted with the compiled

BeanShell [outlink] is a very neat Java interpreter (written itself in Java). It is used in numerous products, for example in JEdit [outlink], because it allows execution of Java code at runtime, without any access to compiler (the code is interpreted, not compiled to bytecode). Beanshell also provides a way of implementing arbitrary Java interfaces and other functionality making it particularly suited for many applications.

In spite of all BeanShell's advantages, it also has a number of limitations:

  • BSH scripts are not Java classes and cannot be referenced or extended as such (they can only implement interfaces)
  • BSH scripts must be evaluated explicitly, meaning it is not possible to transparently reference a "class" which is implemented as a BeanShell script. The ideal would be that BeanShell scripts are absolutely transparent to the rest of the Virtual Machine (i.e. they behave just as any other Java class, but instead of precompiled code, contain scripted methods).
  • Programs willing to use BeanShell must be aware they invoke scripts or scripted methods (it is not transparent).

I have fiddled for quite a while with the thought of how these limitations could be avoided and finally created this (quite tricky) project. The solution uses BCEL [outlink] open source project and provides a way to do the following:

  • BSH scripts can extend real Java classes (not only implement interfaces)
  • BSH scripts can be associated with packages (just as any Java classes)
  • Real Java classes can extend and manipulate scripts just as if they were real classes (at runtime the subclass of a "scripted" class still invokes the scripted methods that superclass)
  • BSH scripts can implement more than one Java interface
  • BSH scripts can be loaded and referenced just as if they were real Java classes - they are fully transparent to JVM (this assumes that a special classloader is used - it is provided with the project).

One of the gains: dynamic ANT tasks!

One could ask whether any of the above would be of any use in real life. In my opinion there are several options for using "dynamic" BeanShell classes, starting at rapid application development (because such classes require no compiler) and ending at modularization (because such classes may be dynamically loaded and edited). I came up with one particularly interesting application: dynamic tasks for ANT [outlink]. With dynamic BSH binding you can create tasks inside the ANT script being executed and immediately use them! Consider this example:

<!-- Define an embedded task and use it immediately -->
<target name="example" depends="prepare">

    <!-- define the task -->
    <embeddedtaskdef name="counterTask"><![CDATA[
        String message;
        int    min;
        int    max;
        
        void setMin(int min) {
            // note how 'global' is used instead of 'this'
            // global scope refers to the task instance anyway.
            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(",");
            }
        }
    ]]></embeddedtaskdef>
    
    <!-- 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>
        
A task embedded inside the ANT build file

The example above defines a new task counterTask, which is used immediately after it is defined (all conventions of JavaBeans apply - you must define setters/ getters accordingly). The task definer embeddedtaskdef is part of this project and must itself be defined earlier in the build file, using standard task definition tasks:

<available classname="com.dawidweiss.bsh.dynbinding.ant.DynamicBSHTask"
           classpathref="bsh-binding"
           property="bsh-binding.available" value="true" />
<fail message="Cannot find bsh-binding JAR, build the project with 'jar' target first."
      unless="bsh-binding.available" />

<!-- Define a new taskdef task, which is capable of creating inline scripted tasks -->
<taskdef name="embeddedtaskdef"
         classname="com.dawidweiss.bsh.dynbinding.ant.DynamicBSHTask">
    <classpath refid="bsh-binding" />
</taskdef>
        
Dynamic task loader task definition

The above example is the simplest one can think of. ANT tasks can be read (in a scripted form) from local files or even accessed just as if they were real Java classes (the scripts must be organized in a directory structure reflecting their packages and the top directory must be added to classpath just as with regular classes). One may find more examples in examples folder of the project as well as in test cases.

How does it work?

The BSH script is first read and parsed. All method declarations are then extracted (please note that methods must be strongly typed). A real Java class is then created (using BCEL), and it contains all signatures of the methods acquired in the previous step. This class delegates all method calls to their scripted counterparts. The example below shows a BSH script and its associated wrapper class:

/*% @extends java.lang.Object %*/
/*% @implements com.dawidweiss.bsh.dynbinding.ExampleInterface %*/

// --- This is equivalent to instance-scope variables

int counter = 0;

// --- This is equivalent to a instance init block

counter = 10;

// --- This is equivalent to instance methods

int getCounter()
{
	return global.counter;
}

void setCounter(int newValue)
{
	global.counter = newValue;
}
        
BSH script with some custom extensions (extends and implements keywords)
/*
 * Decompiled by Jad v1.5.8e2. Copyright 2001 Pavel Kouznetsov.
 */
package com.dawidweiss.bsh.dynbinding.wrappers;

import java.lang.reflect.InvocationTargetException;

import com.dawidweiss.bsh.dynbinding.BSHScriptWrapper;
import com.dawidweiss.bsh.dynbinding.ExampleInterface;

public class ScriptedSuperclass
    implements ExampleInterface
{
    public void setCounter(int arg0)
    {
        Object aobj[] = { new Integer(arg0) };
        try {
            scriptWrapper.invoke("setCounter", aobj);
        } catch (InvocationTargetException e) {
            throw new RuntimeException( e.getCause().toString() );
        }
    }

    public int getCounter()
    {
        Object aobj[] = new Object[0];
        try {
            return ((Integer)scriptWrapper.invoke("getCounter", aobj)).intValue();
        } catch (InvocationTargetException e) {
            throw new RuntimeException( e.getCause().toString() );
        }
    }

    private final BSHScriptWrapper scriptWrapper =
        new BSHScriptWrapper(this, "com.dawidweiss.bsh.dynbinding.wrappers.ScriptedSuperclass",
                                   "ScriptedSuperclass.bsh");
}
        
The dynamically created delegation class (decompiled to source code using JAD)

Please note that the wrapper class has no embedded script code - it is loaded dynamically when the wrapper class needs it.

Download and play with it!

Please feel free to download source code and binaries of the project. The package contains examples and JUnit test cases, which should make it easier to understand how to use the dynamic BSH binding. In case of any questions, feel free to e-mail me.


(c) Dawid Weiss. All rights reserved unless stated otherwise.