Evolving the Meta Programming in Tapestry 5
I've set a goal of removing Javassist from Tapestry 5 and I've made some nice advances on that front. Tapestry uses Javassist inside the web framework layer to load and transform component classes.
All that code is now rewritten to updated APIs that no longer directly expose Javassist technology. In other words, where in the past, the transformer code would write psuedo-Java and add it to a method using Javassist (for example adding value = null; to the containingPageDidDetach() method), Tapestry 5.2 will instead add advice to the (otherwise empty) containingPageDidDetach() method, and the advice will use a FieldAccess object to set the value field to null.
Basically, I've vastly reduced the number of operations possible using the ClassTransformation API. Before, it was pretty much unbounded due to the expressive power of Javassist. Now a small set of operations exist that can be combined into any number of desired behaviors:
- Add new implemented interfaces to a component Class
- Add new fields to a Class
- Initialize the value of a field to a fixed value, or via a per-instance callback
- Delegate read and write access to a field to a provided FieldValueConduit delegate
- Add new methods to a component Class with empty implementations
- Add advice to any method of a class
- Create a MethodAccess object from a method, to allow a method to be invoked (regardless of visibility)
- Create a FieldAccess object from a field, to allow the field to be read or updated (regardless of visibility)
What's amazing is that these few operations, combined in different ways, supports all the different meta-programming possible in Tapestry 5.1. There's costs and benefits to this new approach.
There will be many more objects associated with each component class: new objects to represent advice on methods, and new objects to provide access to private component fields and methods.
Javassist could be brutally efficient, the new approach adds several layers of method invocation that was not present in 5.1.
Incorrect use of method advice can corrupt or disable logic provided by the framework and is hard to debug.
We can eventually switch out Javassist for a more stable, more mainstream, better supported framework such as ASM. ASM should have superior performance to Javassist (no tedious Java-ish parse and compile, just raw bytecode manipulation).
The amount of generated bytecode is lower is many cases. Fewer methods and fields to accomplish the same behavior.
The generated bytecode is more regular across different utilizations: fewer edge cases, less untested, generated bytecode
Key logic returns to "normal" code space, rather than being indirectly generated into "Javassist" code space ... this is easier to debug as there's some place to put your breakpoints!
Overall, I'm pretty happy with what's been put together so far. In the long run, we'll trade instantiation of long lived objects for dynamic bytecode generation. There's much more room to create ways to optimize memory utilization and overall resource utilization and the coding model is similar (closures and callbacks vs. indirect programming via Javassist script). I'm liking it!