I recently worked with my client to add an extension mechanism (in this case, a very simple plug-in system) to their intranet Java web application. It's initially meant for use by professional services staff and possibly other advanced users in the future. Our first thought was to go with JavaScript via the Java Scripting API, since that pair of technologies was already in use elsewhere in the system for simple filtering expressions (e.g., "value > 10 && value < 100"). I wrote a few examples and discovered that the code quickly became a weird hybrid of Java and JavaScript that was very hard to read. ("Is that a Java String or a JavaScript String?…") After a brief conversation with my client, we decided to instead go with Groovy for several reasons. We can keep the code around as Strings for now, but easily compile it to class files later if our needs change. Groovy's integration with Java is excellent and easy to understand. If the user knows Java, but isn't comfortable with Groovy's many cool features, he or she can write normal Java (except for inner classes) and have it interpreted/compiled as Groovy. What follows is a description of how I set it up. Please let me know what you think of this approach, especially if you can suggest a way to improve it.
Each extension is a Groovy class extending a Java abstract adapter class that provides default implementations of all but one of the methods in a Java interface (ScriptingInterface). The Java code used to load the extension gets a GroovyClassLoader using the following code:
GroovyClassLoader loader = new GroovyClassLoader(getClass().getClassLoader());
I use the following code to get the relevant class and instantiate it via its no-arg constructor, catching the (entertainingly-named) MultipleCompilationErrorsException along with several other exceptions:
String code = "<Groovy>"; Class<? extends ScriptingInterface> clazz = loader.parseClass(code); ScriptingInterface script = clazz.newInstance();
Does that sound like a reasonable way to do it? It definitely works, but I'm not sure it's the best way.
Update: Ack! For some reason, my Blogger settings changed from "New Posts Have Comments" to "New Posts Do Not Have Comments" through no action of my own! While I try to figure out how to fix it (now that this post is no longer new), you can add any comments to this post's listing on DZone.
Update2: Problem fixed. It turned out to be possible to turn on comments for a single post via the Blogger post editor.
Now that comments are working, here's a comment I wrote on DZone:
ReplyDeleteWhen I was first experimenting with JavaScript, I used a similar approach to that. I implemented the functions in JavaScript necessary to satisfy the ScriptingInterface and then used the Invocable.getInterface() method (see http://java.sun.com/javase/6/docs/api/javax/script/Invocable.html ) to get an Object (probably a dynamic proxy) that supposedly implemented the interface. There were a few problems with this approach. If the script failed to include a function or otherwise contained mistakes, I didn't find that out until trying to call the associated method, making it impossible to provide useful validation. Also, the interface grew to have 10 methods, most of which had obvious and simple default implementions. However, there was no way for the scripted code to extend an abstract class, except perhaps using some mixed Java/JavaScript hackery.
When I switched to Groovy and started using the GroovyClassLoader directly, I immediately got useful compilation error messages with descriptions and line numbers. I was also able to write Groovy classes that extended an abstract adapter class providing default implementations for 9 out of the 10 methods - a major time saver for most scripts. It's possible that the ScriptEngine implementation for Groovy would have more responsive error reporting than the one for JavaScript, but I don't believe I would have been able to extend an abstract class.
Be careful with what you deploy. Keeping scripts as Strings is great, but be warned that Groovy version upgrades may break the syntax that is in your String. So if you deploy a String to the field and then try to upgrade your software to a new Groovy version, and your client feeds your software the old String... then you'll get compile time errors. In hindsight, we should have shipped .class files and not Strings.
ReplyDeleteThanks, that's a good point! I don't think the problem is likely to bite us (at least not in a way that matters) for a few reasons: the Groovy we're writing is really simple, the amount of code being written is small, and the product is being sold in low volumes.
ReplyDeleteWonderful, Actually I'm Using a mechanism like that in a software that I'm creating these days.
ReplyDeleteI needed to be able to add/modify processing functions on the fly to process some text and plug it into the application without the need to restart it.
So, I used groovy and created an autoloader that checks periodicallly the SHA1 of the files for any updates in the groovy files and reloads them and add the newly added groovy files to the scripts folder to add more functionality.
Groovy is so handy in these situations.
Hi Dino. I'm glad you enjoyed it!
ReplyDeleteI'm curious about something. For the code you mention, do you first check the last modified date of the files before computing the hash? If the date hasn't changed, you should be able to avoid computing the hash, which I believe is a fairly expensive operation (proportional to the size of the files, I think).