Getting Started with Javassist

Shigeru Chiba

Next page


1. Reading bytecode

Javassist is a class library for dealing with Java bytecode. Java bytecode is stored in a binary file called a class file. Each class file contains one Java class or interface.

The class Javassist.CtClass is an abstract representation of a class file. A CtClass (compile-time class) object is a handle for dealing with a class file. The following program is a very simple example:

This program first obtains a ClassPool object, which controls bytecode modification with Javassist. The ClassPool object is a container of CtClass object representing a class file. It reads a class file on demand for constructing a CtClass object and contains the constructed object until it is written out to a file or an output stream.

The ClassPool object is used to maintain one-to-one mapping between classes and CtClass objects. Javassist never allows two distinct CtClass objects to represent the same class. This is a crucial feature to consistent program transformaiton. If you need, however, you can deal with multiple instances of ClassPool at the same time. To create a new instance of ClassPool, write the following code:

ClassPool.getDefault() is just a singleton factory method provided for convenience.

To modify the definition of a class, the users must first obtain a reference to the CtClass object representing that class. ClassPool.get() is used for this purpose. In the case of the program above, the CtClass object representing a class test.Rectangle is obtained from the ClassPool object and it is assigned to a variable cc. Then it is modified so that the superclass of test.Rectangle is changed into a class test.Point. This change is reflected on the original class file when ClassPool.writeFile() is finally called.

Note that writeFile() is a method declared in not CtClass but ClassPool. If this method is called, the ClassPool finds a CtClass object specified with a class name among the objects that the ClassPool contains. Then it translates that CtClass object into a class file and writes it on a local disk.

There is also writeFile() defined in CtClass. Thus, the last line in the program above can be rewritten into:

This method is a convenient method for invoking writeFile() in ClassPool with the name of the class represented by cc.

Javassist also provides a method for directly obtaining the modified bytecode. To do this, call write():

The contents of the class file for test.Rectangle are assigned to a variable b in the form of byte array. writeFile() also internally calls write() to obtain the byte array written in a class file.

The default ClassPool returned by a static method ClassPool.getDefault() searches the same path as the underlying JVM. The users can expand this class search path if needed. For example, the following code adds a directory /usr/local/javalib to the search path:

The search path that the users can add is not only a directory but also a URL:

This program adds "http://www.foo.com:80/java/" to the class search path. This URL is used only for searching classes belonging to a package com.foo.

You can directly give a byte array to a ClassPool object and construct a CtClass object from that array. To do this, use ByteArrayClassPath. For example,

The obtained CtClass object represents a class defined by the class file specified by b.

Since ClassPath is an interface, the users can define a new class implementing this interface and they can add an instance of that class so that a class file is obtained from a non-standard resource.

If you want to directly construct a CtClass object from a class file but you do not know the fully-qualified name of the class, then you can use makeClass() in CtClass:

makeClass() returns the CtClass object constructed from the given input stream. You can use makeClass() for eagerly feeding class files to the ClassPool object. This might improve performance if the search path includes a large jar file. Since the ClassPool object reads a class file on demand, it might repeatedly search the whole jar file for every class file. makeClass() can be used for optimizing this search. The CtClass constructed by makeClass() is kept in the ClassPool object and the class file is never read again.


2. Defining a new class

To define a new class from scratch, makeClass() must be called on a ClassPool.

This program defines a class Point including no members.

A new class can be also defined as a copy of an existing class. The program below does that:

This program first obtains the CtClass object for class Point. Then it gives a new name Pair to that CtClass object. If get("Point") is called on the ClassPool object, then a class file Point.class is read again and a new CtClass object for class Point is constructed again.


3. Modifying a class at load time

If what classes are modified is known in advance, the easiest way for modifying the classes is as follows:

If whether a class is modified or not is determined at load time, the users can write an event listener so that it is notified when a class is loaded into the JVM. A class loader (java.lang.ClassLoader) working with Javassist must call ClassPool.write() for obtaining a class file. The users can write an event listener so that it is notified when the class loader calls ClassPool.write(). The event-listener class must implement the following interface:

The method start() is called when this event listener is registered to a ClassPool object. The method onWrite() is called when write() (or similar methods) is called on the ClassPool object. The second parameter of onWrite() is the name of the class to be written out.

Note that start() or onWrite() do not have to call write() or writeFile(). For example,

All the classes written out by write() are made public just before their definitions are translated into an byte array.

overview

The two methods start() and onWrite() can modify not only a CtClass object specified by the given classname but also any CtClass objects contained in the given ClassPool. They can call ClassPool.get() for obtaining any CtClass object. If a modified CtClass object is not written out immediately, the modification is recorded until that object is written out.

sequence diagram

To register an event listener to a ClassPool, it must be passed to a constructor of ClassPool. Only a single event listener can be registered. If more than one event listeners are needed, multiple ClassPools should be connected to be a single stream. For example,

This program connects two ClassPools. If a class loader calls write() on c2, the specified class file is first modified by t1 and then by t2. write() returns the resulting class file. First, onWrite() on t1 is called since c2 obtains a class file by calling write() on c1. Then onWrite() on t2 is called. If onWrite() called on t2 obtains a CtClass object from c2, that CtClass object represents the class file that t1 has modified.

two translators


4. Class loader

Javassist can be used with a class loader so that bytecode can be modified at load time. The users of Javassist can define their own version of class loader but they can also use a class loader provided by Javassist.

Using a class loader is not easy. Especially if you are a beginner, you should separate your program into an application program and an instrumentation program and each of the two programs should be loaded by a single class loader. You should avoid loading part of the application program with the default class loader and the rest of the program with a user-defined class loader.


4.1 Using javassist.Loader

Javassist provides a class loader javassist.Loader. This class loader uses a javassist.ClassPool object for reading a class file.

For example, javassist.Loader can be used for loading a particular class modified with Javassist.

This program modifies a class test.Rectangle. The superclass of test.Rectangle is set to a test.Point class. Then this program loads the modified class into the JVM, and creates a new instance of the test.Rectangle class.

The users can use a javassist.Translator object for modifying class files. Suppose that an instance of a class MyTranslator, which implements javassist.Translator, performs modification of class files. To run an application class MyApp with the MyTranslator object, write a main class:

To run this program, do:

The class MyApp and the other application classes are translated by MyTranslator.

Note that application classes like MyApp cannot access the loader classes such as Main, MyTranslator and ClassPool because they are loaded by different loaders. The application classes are loaded by javassist.Loader whereas the loader classes such as Main are by the default Java class loader.

In Java, for security reasons, a single class file may be loaded into the JVM by two distinct class loaders so that two different classes would be created. For example,

Suppose that a class Box is loaded by a class loader L1 while a class Window is loaded by a class loader L2. Then, the obejcts returned by getBase() and getSize() are not instances of the same class Point. getBase() returns an instance of the class Point loaded by L1 whereas getSize() returns an instance of Point loaded by L2. The two versions of the class Point are distinct. They belong to different name spaces. For more details, see the following paper:

To avoid this problem, the two class loaders L1 and L2 must delegate the loading operation of the class Point to another class loader, L3, which is a parent class loader of L1 and L2. delegateLoadingOf() in javassist.Loader is a method for specifying what classes should be loaded by the parent loader.

If L1 is the parent class loader of L2, that is, if L1 loads the class of L2, then L2 can delegate the loading operation of Point to L1 for avoiding the problem above. However, this technique does not work in the case below:

Since all the classes included in a class definition loaded by a class loader L1 are also loaded by L1, the class of the field win in Point is now the class Window loaded by L1. Thus size.win = this in getSize() raises a runtime exception because of type mismatch; the type of size.win is the class Point loaded by L1 whereas the type of this is the class Point loaded by L2.

javassist.Loader searches for classes in a different order from java.lang.ClassLoader. ClassLoader first delegates the loading operations to the parent class loader and then attempts to load the classes only if the parent class loader cannot find them. On the other hand, javassist.Loader attempts to load the classes before delegating to the parent class loader. It delegates only if:

This search order allows loading modified classes by Javassist into the JVM. However, it delegates to the parent class loader if it fails to find modified classes for some reason. Once a class is loaded by the parent class loader, the other classes used by that class will be also loaded without modification by the parent class loader. If your program fails to load a modified class, you should make sure whether all the classes using that class have been loaded by javassist.Loader.


4.2 Writing a class loader

A simple class loader using Javassist is as follows:

The class MyApp is an application program. To execute this program, first put the class file under the ./class directory, which must not be included in the class search path. The directory name is specified by insertClassPath() in the constructor. You can choose a different name instead of ./class if you want. Then do as follows:

The class loader loads the class MyApp (./class/MyApp.class) and calls MyApp.main() with the command line parameters. Note that MyApp.class must not be under the directory that the system class loader searches. Otherwise, the system class loader, which is the parent loader of SimpleLoader, loads the class MyApp.

This is the simplest way of using Javassist. However, if you write a more complex class loader, you may need detailed knowledge of Java's class loading mechanism. For example, the program above puts the MyApp class in a name space separated from the name space that the class SimpleLoader belongs to because the two classes are loaded by different class loaders. Hence, the MyApp class cannot directly access the class SimpleLoader.


4.3 Modifying a system class

The system classes like java.lang.String cannot be loaded by a class loader other than the system class loader. Therefore, SimpleLoader or javassist.Loader shown above cannot modify the system classes at loading time.

If your application needs to do that, the system classes must be statically modified. For example, the following program adds a new field hiddenValue to java.lang.String:

This program produces a file "./java/lang/String.class".

To run your program MyApp with this modified String class, do as follows:

Suppose that the definition of MyApp is as follows:

If the modified String class is correctly loaded, MyApp prints hiddenValue.

Note: Applications that use this technique for the purpose of overriding a system class in rt.jar should not be deployed as doing so would contravene the Java 2 Runtime Environment binary code license.


Next page


Java(TM) is a trademark of Sun Microsystems, Inc.
Copyright (C) 2000-2003 by Shigeru Chiba, All rights reserved.