Java programming dynamics, Part 5: Transforming classes on-the-fly

Learn how to modify classes as they're being loaded with Javassist

After a short hiatus, Dennis Sosnoski is back with Part 5 of his Java programming dynamics series. You've seen how to write a program that transforms Java class files to change code behavior. In this installment, Dennis shows you how to combine transformation with the actual loading of classes using the Javassist framework, for flexible "just-in-time" aspect-oriented feature handling. This approach lets you decide what you want to change at runtime, and potentially make different modifications each time you run a program. Along the way you'll also get a deeper look at the general issues of classloading into the JVM.

Share:

Dennis Sosnoski, President, Sosnoski Software Solutions, Inc.

Photo of Dennis SosnoskiDennis Sosnoski is the founder and lead consultant of Seattle-area Java consulting company Sosnoski Software Solutions, Inc., specialists in J2EE, XML, and Web services support. His professional software development experience spans over 30 years, with the last several years focused on server-side Java technologies. Dennis is a frequent speaker on XML and Java technologies at conferences nationwide, and chairs the Seattle Java-XML SIG. Contact Dennis at dms@sosnoski.com.



03 February 2004

Also available in Japanese Vietnamese

In Part 4, "Class transformations with Javassist," you learned how to use the Javassist framework to transform Java class files generated by the compiler, writing the modified class files back out. This type of class file transform step is great for making persistent changes, but not necessarily convenient when you want to make different changes each time you execute your application. For such transient changes, an approach that works when you actually start up your application is much better.

The JVM architecture gives us a convenient way of doing this -- by working with the classloader implementation. Using classloader hooks, you can intercept the process of loading classes into the JVM and transform the class representations before they're actually loaded. To illustrate how this works, I'm first going to demonstrate intercepting the classloading directly, then show how Javassist provides a convenient shortcut that you can use in your applications. Along the way I'll make use of pieces from the prior articles in this series.

Don't miss the rest of this series

Part 1, "Classes and class loading" (April 2003)

Part 2, "Introducing reflection" (June 2003)

Part 3, "Applied reflection" (July 2003)

Part 4, "Class transformation with Javassist " (September 2003)

Part 6, "Aspect-oriented changes with Javassist " (March 2004)

Part 7, "Bytecode engineering with BCEL" (April 2004)

Part 8, "Replacing reflection with code generation" (June 2004)

Loading zone

Normally you run a Java application by specifying the main class as a parameter to the JVM. This works fine for standard operations, but doesn't give you any way of hooking into the classloading process in time to be useful for most applications. As I discussed in Part 1 "Classes and classloading," many classes are loaded before your main class even begins to execute. Intercepting the loading of these classes requires a level of indirection in the execution of the program.

Fortunately, it's pretty easy to emulate the work done by the JVM in running the main class of your application. All you need to do is use reflection (as covered in Part 2) to first find the static main() method in the specified class, then call it with the desired command line arguments. Listing 1 gives sample code to do this (I've left out the imports and exceptions to keep it short):

Listing 1. Java application runner
 public class Run { public static void main(String[]
                args) { if (args.length >= 1) { try { // load the target class to be run Class
                clas = Run.class.getClassLoader(). loadClass(args[0]); // invoke "main" method of
                target class Class[] ptypes = new Class[] { args.getClass() }; Method main =
                clas.getDeclaredMethod("main", ptypes); String[] pargs = new String[args.length-1];
                System.arraycopy(args, 1, pargs, 0, pargs.length); main.invoke(null, new Object[] {
                pargs }); } catch ... } } else { System.out.println ("Usage: Run main-class
                args..."); } } }

To run your Java application using this class, you just need to name it as the target for the java command, following it with the main class for your application and any arguments you want passed to your application. In other words, if the command you use for launching your Java application normally is:

 java test.Test arg1 arg2 arg3

You'd instead launch it using the Run class with the command:

 java Run test.Test arg1 arg2 arg3

Intercepting classloading

Just on its own, the little Run class from Listing 1 isn't very useful. To accomplish my goal of intercepting the classloading process we need to go a step further, by defining and using our own classloader for the application classes.

Ask the expert: Dennis Sosnoski on JVM and bytecode issues

For comments or questions about the material covered in this article series, as well as anything else that pertains to Java bytecode, the Java binary class format, or general JVM issues, visit the JVM and Bytecode discussion forum, moderated by Dennis Sosnoski.

As we discussed in Part 1, classloaders use a tree-structured hierarchy. Each classloader (except the root classloader used by the JVM for core Java classes) has a parent classloader. Classloaders are supposed to check with their parent classloader before loading a class on their own, in order to prevent conflicts that can arise when the same class is loaded by more than one classloader in a hierarchy. This process of checking with the parent first is called delegation -- the classloaders delegate responsibility for loading a class to the classloader closest to the root that has access to that class information.

When the Run program from Listing 1 begins execution, it's already been loaded by the default System classloader for the JVM (the one that works off the classpath you define). To comply with the delegation rule for classloading, we'll need to make our classloader a true replacement for the System classloader, using all the same classpath information and delegating to the same parent. Fortunately, the java.net.URLClassLoader class used by current JVMs for the System classloader implementation provides an easy way to retrieve the classpath information, using the getURLs() method. To write our classloader, we can just subclass java.net.URLClassLoader, and initialize the base class to use the same classpath and parent classloader as the System classloader that loads the main class. Listing 2 gives the actual implementation of this approach:

Listing 2. A verbose classloader
 public class VerboseLoader extends URLClassLoader {
                protected VerboseLoader(URL[] urls, ClassLoader parent) { super(urls, parent); }
                public Class loadClass(String name) throws ClassNotFoundException {
                System.out.println("loadClass: " + name); return super.loadClass(name); } protected
                Class findClass(String name) throws ClassNotFoundException { Class clas =
                super.findClass(name); System.out.println("findclass: loaded " + name + " from this
                loader"); return clas; } public static void main(String[] args) { if (args.length
                >= 1) { try { // get paths to be used for loading ClassLoader base =
                ClassLoader.getSystemClassLoader(); URL[] urls; if (base instanceof URLClassLoader)
                { urls = ((URLClassLoader)base).getURLs(); } else { urls = new URL[] { new
                File(".").toURI().toURL() }; } // list the paths actually being used
                System.out.println("Loading from paths:"); for (int i = 0; i < urls.length; i++)
                { System.out.println(" " + urls[i]); } // load target class using custom class
                loader VerboseLoader loader = new VerboseLoader(urls, base.getParent()); Class clas
                = loader.loadClass(args[0]); // invoke "main" method of target class Class[] ptypes
                = new Class[] { args.getClass() }; Method main = clas.getDeclaredMethod("main",
                ptypes); String[] pargs = new String[args.length-1]; System.arraycopy(args, 1,
                pargs, 0, pargs.length); Thread.currentThread(). setContextClassLoader(loader);
                main.invoke(null, new Object[] { pargs }); } catch ... } } else { System.out.println
                ("Usage: VerboseLoader main-class args..."); } } }

We've subclassed java.net.URLClassLoader with our own VerboseLoader class that lists out all the classes being loaded, noting which ones are loaded by this loader instance (rather than a delegation parent classloader). Here again I've left out the imports and exceptions to keep the code concise.

The first two methods in the VerboseLoader class, loadClass() and findClass(), are overrides of standard classloader methods. The loadClass() method is called for each class requested from the classloader. In this case, we have it just print a message to the console and then call the base class version for actual handling. The base class method implements the standard classloader delegation behavior, first checking if the parent classloader can load the requested class, and only trying to load the class directly using the protected findClass() method if the parent classloader fails. For the VerboseLoader implementation of findClass(), we first call the overridden base class implementation, then print out a message if the call succeeds (returns without throwing an exception).

The main() method of VerboseLoader either gets the list of classpath URLs from the loader used for the containing class or, if used with a loader that's not an instance of URLClassLoader, just uses the current directory as the only classpath entry. Either way, it lists out the paths actually being used, then creates an instance of the VerboseLoader class and uses it to load the target class named on the command line. The rest of the logic, to find and call the main() method of the target class, is the same as the Listing 1 Run code.

Listing 3 shows an example of the VerboseLoader command line and output, which is used to call the Run application from Listing 1:

Listing 3. Example output from Listing 2 program
 [dennis]$ java VerboseLoader Run Loading
                from paths: file:/home/dennis/writing/articles/devworks/dynamic/code5/ loadClass:
                Run loadClass: java.lang.Object findclass: loaded Run from this loader loadClass:
                java.lang.Throwable loadClass: java.lang.reflect.InvocationTargetException
                loadClass: java.lang.IllegalAccessException loadClass:
                java.lang.IllegalArgumentException loadClass: java.lang.NoSuchMethodException
                loadClass: java.lang.ClassNotFoundException loadClass:
                java.lang.NoClassDefFoundError loadClass: java.lang.Class loadClass:
                java.lang.String loadClass: java.lang.System loadClass: java.io.PrintStream Usage:
                Run main-class args...

In this case, the only class loaded directly by the VerboseLoader is the Run class. All the other classes used by the Run class are core Java classes, which are loaded by delegation through the parent classloader. Most -- if not all -- of these core Java classes will actually have been loaded during the start up of the VerboseLoader application itself, so the parent classloader will just return a reference to the previously created java.lang.Class instance.

Javassist intercepts

VerboseClassloader from Listing 2 shows the basics of intercepting classloading. To modify the classes as they're being loaded we could take this further, adding code to the findClass() method to access the binary class file as a resource and then working with the binary data. Javassist actually includes the code to do this type of interception directly, so rather than taking this example further, we'll see instead how to use the Javassist implementation.

Intercepting classloading with Javassist builds on the same javassist.ClassPool class we worked with in Part 4. In that article, we requested a class by name directly from the ClassPool, getting back the Javassist representation of the class in the form of a javassist.CtClass instance. That's not the only way to use a ClassPool, though -- Javassist also provides a classloader that uses the ClassPool as its source of class data, in the form of the javassist.Loader class.

To let you work with the classes as they're being loaded, the ClassPool uses an Observer pattern. You can pass an instance of the expected observer interface, javassist.Translator, to the constructor of the ClassPool. Each time a new class is requested from the ClassPool it calls the onWrite() method of the observer, which can modify the class representation before it's delivered by the ClassPool.

The javassist.Loader class includes a convenient run() method that loads a target class and calls the main() method of that class with a supplied array of arguments (as in the Listing 1 code). Listing 4 demonstrates using the Javassist classes and this method to load and run a target application class. The simple javassist.Translator observer implementation in this case just prints out a message about the class being requested.

Listing 4. Javassist application runner
 public class JavassistRun { public static void
                main(String[] args) { if (args.length >= 1) { try { // set up class loader with
                translator Translator xlat = new VerboseTranslator(); ClassPool pool =
                ClassPool.getDefault(xlat); Loader loader = new Loader(pool); // invoke "main"
                method of target class String[] pargs = new String[args.length-1];
                System.arraycopy(args, 1, pargs, 0, pargs.length); loader.run(args[0], pargs); }
                catch ... } } else { System.out.println ("Usage: JavassistRun main-class args...");
                } } public static class VerboseTranslator implements Translator { public void
                start(ClassPool pool) {} public void onWrite(ClassPool pool, String cname) {
                System.out.println("onWrite called for " + cname); } } }

Here's an example of the JavassistRun command line and output, using it to call the Run application from Listing 1:

 [dennis]$java -cp .:javassist.jar JavassistRun Run onWrite called
                for Run Usage: Run main-class args...

Runtime timing

The method timing modification we examined in Part 4 can be a useful tool for isolating performance issues, but it really needs a more flexible interface. In that article, we just passed the class and method name as command-line parameters to my program, which loaded the binary class file, added the timing code, then wrote the class back out. For this article, we'll convert the code to use a load-time modification approach, and to support pattern-matching for specifying the classes and methods to be timed.

Changing the code to handle modifications as the classes are loaded is easy. Building off the javassist.Translator code from Listing 4, we can just call the method that adds the timing information from onWrite() when the class name being written matches the target class name. Listing 5 shows this (without all the details of addTiming() -- see Part 4 for this).

Listing 5. Adding timing code at load-time
 public class TranslateTiming { private static
                void addTiming(CtClass clas, String mname) throws NotFoundException,
                CannotCompileException { ... } public static void main(String[] args) { if
                (args.length >= 3) { try { // set up class loader with translator Translator xlat
                = new SimpleTranslator(args[0], args[1]); ClassPool pool =
                ClassPool.getDefault(xlat); Loader loader = new Loader(pool); // invoke "main"
                method of target class String[] pargs = new String[args.length-3];
                System.arraycopy(args, 3, pargs, 0, pargs.length); loader.run(args[2], pargs); }
                catch (Throwable ex) { ex.printStackTrace(); } } else { System.out.println("Usage:
                TranslateTiming" + " class-name method-mname main-class args..."); } } public static
                class SimpleTranslator implements Translator { private String m_className; private
                String m_methodName; public SimpleTranslator(String cname, String mname) {
                m_className = cname; m_methodName = mname; } public void start(ClassPool pool) {}
                public void onWrite(ClassPool pool, String cname) throws NotFoundException,
                CannotCompileException { if (cname.equals(m_className)) { CtClass clas =
                pool.get(cname); addTiming(clas, m_methodName); } } } }

Pattern methods

Besides making the method timing code work at load-time, as shown in Listing 5, it would be nice to add flexibility in specifying the method(s) to be timed. I started out implementing this using the regular expression matching support in the Java 1.4 java.util.regex package, then realized it wasn't really giving me the kind of flexibility I wanted. The problem was that the kind of patterns that are meaningful to me for selecting classes and methods to be modified don't fit well into the regular expression model.

So what kind of patterns are meaningful for selecting classes and methods? What I wanted was the ability to use any of several characteristics of the class and method in the patterns, including the actual class and method name, the return type, and the call parameter type(s). On the other hand, I didn't need really flexible comparisons on the names and types -- a simple equals comparison handled most of the cases I was interested in, and adding basic wildcards to the comparisons took care of the rest. The easiest approach to handling this was just to make the patterns look like standard Java method declarations, with a few extensions.

For some examples of this approach, here are several patterns that will match the String buildString(int) method of the test.StringBuilder class:

 java.lang.String test.StringBuilder.buildString(int)
                test.StringBuilder.buildString(int) *buildString(int) *buildString

The general pattern of these patterns is first an optional return type (with exact text), then the combined class and method name pattern (with "*" wildcard characters), and finally the list of parameter type(s) (with exact text). If the return type is present, it must be separated from the method name match by a space, while the list of parameters follows the method name match. To make the parameter match flexible, I set it up to work in two ways. If the parameters are given as a list surrounded by parentheses, they must exactly match the method parameters. If they're instead surrounded by square braces ("[]"), the types listed must all be present as parameters of a matching method, but the method may use them in any order and may also use additional parameters. So *buildString(int, java.lang.String) matches any method with a name ending in "buildString" and taking exactly two parameters, an int and a String, in that order. *buildString[int,java.lang.String] matches methods with the same names, but taking two or more parameters, one of which is an int and another a java.lang.String.

Listing 6 gives an abbreviated version of the javassist.Translator subclass I wrote to handle these patterns. The actual matching code isn't really relevant to this article, but it's included in the download file (see Resources) if you'd like to look it over or use it yourself. The main program class that uses this TimingTranslator is BatchTiming, also included in the download file.

Listing 6. Pattern-matching translator
 public class TimingTranslator implements
                Translator { public TimingTranslator(String pattern) { // build matching structures
                for supplied pattern ... } private boolean matchType(CtMethod meth) { ... } private
                boolean matchParameters(CtMethod meth) { ... } private boolean matchName(CtMethod
                meth) { ... } private void addTiming(CtMethod meth) { ... } public void
                start(ClassPool pool) {} public void onWrite(ClassPool pool, String cname) throws
                NotFoundException, CannotCompileException { // loop through all methods declared in
                class CtClass clas = pool.get(cname); CtMethod[] meths = clas.getDeclaredMethods();
                for (int i = 0; i < meths.length; i++) { // check if method matches full pattern
                CtMethod meth = meths[i]; if (matchType(meth) && matchParameters(meth)
                && matchName(meth)) { // handle the actual timing modification
                addTiming(meth); } } } }

Up next

In the last two articles, you've now seen how to use Javassist for handling basic transformations. For the next article, we'll look into the advanced features of this framework that provide search-and-replace techniques for editing bytecode. These features make systematic changes to program behavior easy, including changes such as intercepting all calls to a method or all accesses of a field. They're the key to understanding why Javassist is a great framework for aspect-oriented support in Java programs. Check back next month to see how you can use Javassist to unlock aspects in your applications.


Download

DescriptionNameSize
Code samplej-dyn0203-source.zip318 KB

Resources

Comments

developerWorks: Sign in

Required fields are indicated with an asterisk (*).


Need an IBM ID?
Forgot your IBM ID?


Forgot your password?
Change your password

By clicking Submit, you agree to the developerWorks terms of use.

 


The first time you sign into developerWorks, a profile is created for you. Information in your profile (your name, country/region, and company name) is displayed to the public and will accompany any content you post, unless you opt to hide your company name. You may update your IBM account at any time.

All information submitted is secure.

Choose your display name



The first time you sign in to developerWorks, a profile is created for you, so you need to choose a display name. Your display name accompanies the content you post on developerWorks.

Please choose a display name between 3-31 characters. Your display name must be unique in the developerWorks community and should not be your email address for privacy reasons.

Required fields are indicated with an asterisk (*).

(Must be between 3 – 31 characters.)

By clicking Submit, you agree to the developerWorks terms of use.

 


All information submitted is secure.

Dig deeper into Java technology on developerWorks


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=1
Zone=Java technology
ArticleID=10908
ArticleTitle=Java programming dynamics, Part 5: Transforming classes on-the-fly
publish-date=02032004