IBM Skip to main content
Search for:   within 
      Search help  
     IBM home  |  Products & services  |  Support & downloads   |  My account

developerWorks > Java technology
developerWorks
Java programming dynamics, Part 5: Transforming classes on-the-fly
Codee-mail it!
Contents:
Loading zone
Runtime timing
Up next
Resources
About the author
Rate this article
Related content:
Understanding bytecode makes you a better programmer
Improve modularity with aspect-oriented programming
IBM Developer Kits for the Java platform (downloads)
Subscriptions:
dW newsletters
dW Subscription
(CDs and downloads)
Learn how to modify classes as they're being loaded with Javassist

Level: Intermediate

Dennis M. Sosnoski (mailto:dms@sosnoski.com?cc=&subject=Transforming classes on-the-fly)
President, Sosnoski Software Solutions, Inc.
3 Feb 2004

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.

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)

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.

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.

Resources

About the author
Photo of Dennis Sosnoski Dennis 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.


Codee-mail it!

What do you think of this document?
Killer! (5) Good stuff (4) So-so; not bad (3) Needs work (2) Lame! (1)

Comments?



developerWorks > Java technology
developerWorks
  About IBM  |  Privacy  |  Terms of use  |  Contact