How to Run Javascript on the JVM in Just Fifteen Minutes
what and whyThis post is about executing Javascript inside the JVM without using a browser. Besides the fact that people are talking about
running Javascript on the server (
again, and
again), here’s why my colleagues and I used it on a recent project:
We have some logic that needs to run on the server and on the client, depending on when the application applies it. There is like an incredibly complex form validation involed. Think of a loan application, for example. Zillions of rules like “at least five years at current location
or at most three locations in ten years
or owns current location for at least one year.” The whole thing forms a big logical expression that needs to be evaluated in such a way that we can report which pieces are missing or do not meet requirements (
Declined because income is insufficient and does not state purpose of loan).
There are a couple of ways to handle this. One is to submit the form back to the server for validation. Another is to write everything in Java, but use
a sophisticated tool to render the Java into Javascript. Naturally, our team chose a third option, The Rails Way (available for
pre-order).
We have a Domain-Specific Language for describing the rules. Business users use the DSL, and another tool writes code from that. We could, in theory, write Java methods for the server
and write Javascript for the client. We chose to start with Javascript, and we’ll write Java for the server if running Javascript on the server turns out to be
unperformant slow.
In the mean time, we decided that having some Java make a simple function call to a Javascript function and process a simple result was a reasonable first step. As a side benefit, we run all our server-side Javascript unit tests in Java test suites alongside our Java unit tests.
And after some fiddling around, we got Javascript working on the JVM. My bet is that you can get it working too, and it won’t take more than fifteen minutes.
Care to try it?
step zero: the Java Virtual Machine (JVM)You’ll need JDK 1.5 or 1.6 from Sun. If you already have this, move on to step one. Still reading? You’ll need to do a big install before we go further.
Go to the
downloads page and download the latest thing they have on offer with the words “JDK” in it. You won’t need JEE (the framework formerly known as J2EE) for this exercise, but if you know what it is you know enough to decide whether to download it.
Right now, you want
JDK 6u2. Go get it and suffer through the installation process.
Step one: Bean Scripting FrameworkJava6 has a new framework for running “scripting” languages, and it’s built into Java6. We’re not going to use it today, just because some of you may still need to make stuff work with JDK 1.5 in production. Instead, we’re going to go get the Jakarta Bean Scripting Framework (BSF).
You can download it here. We’ll need
bsf.jar
.
step two: fix gotchasYMMV, but I found that I couldn’t get BSF working without including the
Jakarta Commons-Logging jar. So if you don’t have this floating around, go
here and download it. I experimented, and I could ignore everything except
commons-logging-1.1.jar
. If that was missing, BSF kakked.
step three: RhinoSince we’re going to run Javascript, we need an interpreter.
Rhino to the rescue.
Download it. We’ll need
js.jar
.
step four: keeping things organizedReady to code? Let’s start with a directory for all of our stuff. Call it
hello_javascript
. For the sake of keeping thing simple, set up the sub-structure as follows:
hello_javascript
hello_javascript\lib
You may be using a fancy IDE, you may be using a text editor and have to graft your classpaths together with chicken wire. The important thing is that your classpath, besides including all of Java’s required stuff, and your own Java classes, also includes
bsf.jar
,
commons-logging-1.1.jar
and
js.jar
.
We’ll put all three in the
lib
subdirectory:
hello_javascript\lib\bsf.jar
hello_javascript\lib\commons-logging-1.1.jar
hello_javascript\lib\js.jar
step five: “Hello, Javascript”Let’s write some Java: create the following subdirectories and put a file called
HelloJavascript.java
in it:
hello_javascript\com\raganwald\public\HelloJavascript.java
Let’s give it some code:
package com.raganwald.public;
import org.apache.bsf.BSFManager;
public class HelloJavascript {
public static void main (final String[] argv) {
final BSFManager manager = new BSFManager();
final Object jso = manager.eval("javascript", "(java)", 1, 1, "'hello, Javascript'");
System.out.println(jso.toString());
}
}
Run your new Java application. Did you see that? It interpreted some Javascript
in the JVM without a browser. Check your watch. Did you need more than a quarter of an hour? I didn’t think so.
You can try more ambitious code:
manager.eval(
"javascript", "(java)", 1, 1,
"var f = function (what) { return 'hello, ' + what; }; f('Javascript);");
including other files is an exercise left for the readerI didn’t find an easy way to get Javascript files to include other Javascript files. This isn’t the worst thing in the world, but you certainly don’t want to write anything substantial inside of Java strings. So try experimenting with reading javascript files right off the classpath.
I created a subdirectory called
javascript
:
hello_javascript\javascript
And you can read Javascript into your strings or Stringbuffers with some fairly simple code, thanks to a utility built into BSF:
import org.apache.bsf.util.IOUtils;
// ...
static String readScript(final String fileName) throws Exception {
final FileReader in = new FileReader(fileName);
return IOUtils.getStringFromReader(in);
}
That reads some script into a string. You can then prepend it to whatever you want to evaluate. Note that if you want to set up some sort of simple checking to make sure that you don’t “include” the same file twice, you will need to write yourself a little framework for that, perhaps using a
Set
to keep track of what you’ve already loaded.
garbage in, garbage out
Prototype and Scriptaculous are the Javascript libraries that make slick transitions and UI effects easy one-liners. Prototype does more than just make an application look good: it adds Ruby and Smalltalk-like methods for handling Hashes, Arrays, and the DOM.
This book is one of the fastest ways to get up to speed on taking Javascript to the next level.
This is nice, and with a little work you could make a program that reads paths to Javascript files off the commend line and executes them. But to make things really interesting, you want to find a way to get Java data into your JavaScript and do something useful with the results, not just print it as a String.
BSF provides a way to inject objects into the scripting language’s environment, so you could use that facility. When writing automated unit tests for that particular project, I chose a simpler route: I serialized the data into JSON and used that to call a Javascript function directly via BSF:
manager.eval("javascript", "(java)", 1, 1,
"myJavascriptFunction(" + myJSONString + ");");
This is a
really bad idea if your JSON is handed you from an insecure source, such as a public web page calling you back via
XMLHttpRequest
, but if you trust your source, this works wonderfully.
Now what do you do with the result? If you are generating something esoteric like a Javascript function, I have no idea. In my own case, I return all values as simple trees of Hashes (Javascript objects without any special methods) and Arrays. I convert those into Java trees of Maps and Arrays:
import org.mozilla.javascript.NativeArray;
import org.mozilla.javascript.ScriptableObject;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
// ...
static List unwrapNativeArray (final NativeArray na) {
return new ArrayList<Object> () {{
for (int i = 0; i < na.getLength(); ++i) {
add(unwrapNative(na.get(i, null)));
}
}};
}
static List unwrapPrototypeArray (final ScriptableObject sObj) {
return new ArrayList<Object> () {{
final List<Object> sObjIds = Arrays.asList(sObj.getAllIds());
for (int i = 0; sObjIds.contains(i); ++i) {
add(unwrapNative(sObj.get(i, null)));
}
}};
}
static Map unwrapObject (final ScriptableObject sObj) {
return new HashMap<String, Object> () {{
for (Object id: sObj.getAllIds()) {
put(id.toString(), unwrapNative(sObj.get(id.toString(), null)));
}
}};
}
static Object unwrapNative (final Object obj) {
if (obj instanceof NativeArray) {
return unwrapNativeArray((NativeArray) obj);
}
else if (obj instanceof ScriptableObject) {
final ScriptableObject sObj = (ScriptableObject) obj;
final List<Object> sObjIds = Arrays.asList(sObj.getAllIds());
if (sObjIds.contains("keys")) { // a prototype enumerable/hash
return unwrapObject(sObj);
}
else if (sObjIds.contains("flatten")) { // a prototype enumerable/array
return unwrapPrototypeArray(sObj);
}
else return unwrapObject(sObj);
}
else return obj;
}
Check your watch. Are you still under fifteen minutes? Great!
Labels: lispy, popular