Java programming dynamics, Part 4: Class transformation with JavassistUsing Javassist to transform methods in bytecode ![]() | ![]() |
![]() |
Level: Intermediate Dennis Sosnoski (dms@sosnoski.com), President, Sosnoski Software Solutions, Inc. 16 Sep 2003 Bored with Java classes that execute just the way the source code was written? Then cheer up, because you're about to find out about twisting classes into shapes never intended by the compiler! In this article, Java consultant Dennis Sosnoski kicks his Java programming dynamics series into high gear with a look at Javassist, the bytecode manipulation library that's the basis for the aspect-oriented programming features being added to the widely used JBoss application server. You'll find out the basics of transforming existing classes with Javassist and see both the power and the limitations of this framework's source code approach to classworking. After covering the basics of the Java class format and runtime access through reflection, it's time to move this series on to more advanced topics. This month I'll start in on the second part of the series, where the Java class information becomes just another form of data structure to be manipulated by applications. I'll call this whole topic area classworking. I'll start classworking coverage with the Javassist bytecode manipulation library. Javassist isn't the only library for working with bytecode, but it does have one feature in particular that makes it a great starting point for experimenting with classworking: you can use Javassist to alter the bytecode of a Java class without actually needing to learn anything about bytecode or the Java virtual machine (JVM) architecture. This is a mixed blessing in some respects -- I don't generally advocate messing with technology you don't understand -- but it certainly makes bytecode manipulation much more accessible than with frameworks where you work at the level of individual instructions. Javassist lets you inspect, edit, and create Java binary classes. The inspection aspect mainly duplicates what's available directly in Java through the Reflection API, but having an alternative way to access this information is useful when you're actually modifying classes rather than just executing them. This is because the JVM design doesn't provide you any access to the raw class data after it's been loaded into the JVM. If you're going to work with classes as data, you need to do so outside of the JVM.
Javassist uses the Classes loaded in a class pool are represented by Fields, methods, and constructors are represented by Javassist lets you completely replace the bytecode body of a method or constructor, or selectively add bytecode at the beginning or end of the existing body (along with a couple of other variations for constructors). Either way, the new bytecode is passed as a Java-like source code statement or block in a The source code accepted by Javassist doesn't exactly match the Java language, but the main difference is just the addition of some special identifiers used to represent the method or constructor parameters, method return value, and other items you may want to use in your inserted code. These special identifiers all start with the There are also some restrictions on what you can do in the source code you pass to Javassist. The first restriction is the actual format, which must be a single statement or block. This isn't much of a restriction for most purposes, because you can put any sequence of statements you want in a block. Here's an example using the special Javassist identifiers for the first two method parameter values to show how this works:
A more substantial limitation on the source code is that there's no way to refer to local variables declared outside the statement or block being added. This means that if you're adding code at both the start and end of a method, for instance, you generally won't be able to pass information from the code added at the start to the code added at the end. There are ways around this limitation, but the workarounds are messy -- you generally need to find a way to merge the separate code inserts into a single block.
For an example of applying Javassist, I'll use a task I've often handled directly in source code: measuring the time taken to execute a method. This is easy enough to do in the source; you just record the current time at the start of the method, then check the current time again at the end of the method and find the difference between the two values. If you don't have source code, it's normally much more difficult to get this type of timing information. That's where classworking comes in handy -- it lets you make changes like this for any method, without needing source code.
Listing 1 shows a (bad) example method that I'll use as a guinea pig for my timing experiments: the Listing 1. Method to be timed
Because I have the source code available for this method, I'll show you how I would add the timing information directly. This will also serve as the model for what I want to do using Javassist. Listing 2 shows just the Listing 2. Method with timing
Getting the same effect by using Javassist to manipulate the class bytecode seems like it should be easy. Javassist provides ways to add code at the beginning and end of methods, after all, which is exactly what I did in the source code to add timing information for the method. There's a hitch, though. When I described how Javassist lets you add code, I mentioned that the added code could not reference local variables defined elsewhere in the method. This limitation blocks me from implementing the timing code in Javassist the same way I did in the source code; in that case, I defined a new local variable in the code added at the start and referenced that variable in the code added at the end. So what other approach can I use to get the same effect? Well, I could add a new member field to the class and use that instead of a local variable. That's a smelly kind of solution, though, and suffers from some limitations for general use. Consider what would happen with a recursive method, for instance. Each time the method called itself, the saved start time value from the last call would be overwritten and lost. Fortunately there's a cleaner solution. I can keep the original method code unchanged and just change the method name, then add a new method using the original name. This interceptor method can use the same signature as the original method, including returning the same value. Listing 3 shows what a source code version of this approach would look like: Listing 3. Adding an interceptor method in the source
This approach of using an interceptor method works well with Javassist. Because the entire body of the method is a single block, I can define and use local variables within the body without any problems. Generating the source code for the interception method is also easy -- it only needs a few substitutions to work for any possible method. Implementing the code to add method timing uses some of the Javassist APIs described in Javassist basics. Listing 4 shows this code, in the form of an application that takes a pair of command-line arguments giving the class name and method name to be timed. The Listing 4. Adding the interceptor method with Javassist
|