I've been getting a small amount of flack about Tapestry and HiveMind's use of Javassist. Yes, its inside the evil
JBoss camp. Yes, it has a wierd MPL/LGPL dual license. Yes, the documentation is an abomination. Yes, the API is so ugly that I always craft an insulation layer on top of it. Yes, there are are other bytecode toolkits out there. So why am I so wedded to Javassist?
Because it's so damn powerful and expressive.
A lot of the magic in HiveMind and Tapestry 4 is due to Javassist, and Tapestry 5 is even more wedded to it.
Much of what HiveMind does could be done using JDK dynamic proxies. HiveMind uses proxies to defer creation of services until just needed ... you invoke a method on the proxy and it will go create the real object and re-invoke the method on that real service object. You code never has to worry about whether the service exists yet or not, it simply gets created as needed.
You can do things like that using JDK proxies, but proxies are not going to be as optimized by Hotspot as real Java classes. The core of dynamic proxies is to use reflection, each method invocation on the proxy turns into a reflective method invocation by the proxy's handler. There's further overhead creating an array of objects to store the parameters.
Simple proxies like that can certainly be written using other toolkits like ASM.
Because these proxies are so common in Tapestry 5, my insulation layer can build the whole proxy as a single call; the insulation layer translates this to Javassist API:
public void proxyMethodsToDelegate(Class serviceInterface, String delegateExpression,
String toString)
{
addInterface(serviceInterface);
MethodIterator mi = new MethodIterator(serviceInterface);
while (mi.hasNext())
{
MethodSignature sig = mi.next();
String body = format("return ($r) %s.%s($$);", delegateExpression, sig.getName());
addMethod(Modifier.PUBLIC, sig, body);
}
if (!mi.getToString())
addToString(toString);
}
Here, delegate expression is the name of the variable to read, or the name of the method to execute, that provides a proxy. The only real part of this code that is Javassist is that code snippet: return ($r) %s.%s($$);
. The first %s
is the delegate expression; the second is the name of the method. Thus this may be something like: return ($r) _delegate.performOperation($$);
Javassist has a special cast, ($r)
that says “cast to the method's return type, possibly void”. It will unwrap boxed values to primitives, as necessary. The $$
means “pass the list of parameters to the method”.
Thus we can see how quickly we can build up new methods that invoke corresponding methods on some other object.
In Tapestry 5, the real workhorse is the ClassTransformation system which is used, with Javassist, to transform classes as they are loaded into memory. This is how Tapestry 5 hooks into the fields of your class to perform injections and state management. Tapestry 4 did the same thing using abstract properties and a runtime concrete subclass … this is much more pleasant.
Some of the trickiest code relates to component parameters; there are runtime decisions to be made based on whether the parameter is or is not bound, and whether the component is or is not currently rendering, and whether caching is or is not enabled for the parameter. Here’s just part of that logic, as related to reading a parameter.
private void addReaderMethod(String fieldName, String cachedFieldName,
String invariantFieldName, boolean cache, String parameterName, String fieldType,
String resourcesFieldName, ClassTransformation transformation)
{
BodyBuilder builder = new BodyBuilder();
builder.begin();
builder.addln(
"if (%s || ! %s.isLoaded() || ! %<s.isBound(\"%s\")) return %s;",
cachedFieldName,
resourcesFieldName,
parameterName,
fieldName);
String cast = TransformUtils.getWrapperTypeName(fieldType);
builder.addln(
"%s result = ($r) ((%s) %s.readParameter(\"%s\", $type));",
fieldType,
cast,
resourcesFieldName,
parameterName);
builder.add("if (%s", invariantFieldName);
if (cache)
builder.add(" || %s.isRendering()", resourcesFieldName);
builder.addln(")");
builder.begin();
builder.addln("%s = result;", fieldName);
builder.addln("%s = true;", cachedFieldName);
builder.end();
builder.addln("return result;");
builder.end();
String methodName = transformation.newMemberName("_read_parameter_" + parameterName);
MethodSignature signature = new MethodSignature(Modifier.PRIVATE, fieldType, methodName,
null, null);
transformation.addMethod(signature, builder.toString());
transformation.replaceReadAccess(fieldName, methodName);
}
That last line, "replaceReadAccess", is also key: it finds every place in the class where existing code read the field, and replaces it with an invocation of the method that contains all the parameter reading logic … a method that was just dynamically added to the class. A typical implementation of a parameter writer method might look like:
private int _$read_parameter_value()
{
if (_$value_cached || ! _$resources.isLoaded() || ! _$resources.isBound("value")) return _value;
int result = ($r) ((java.lang.Integer) _$resources.readParameter("value", $type));
if (_$value_invariant || _$resources.isRendering())
{
_value = result;
_$value_cached = true;
}
return result;
}
The point of these examples is this: we’re doing some complex code creation and transformation and Javassist makes it easy to build up that logic by assembling Java-like scripting code. I’m not sure what the equivalents code transformations would look like in, say, ASM but I can’t see it being as straightforward and easy to debug. Javassist lets me focus on Tapestry and not on bytecode and that makes it invaluable.