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 object is a handle for dealing
with a class file. The following program is a very simple example:
CtClass cc = CtClass.forName("test.Rectangle");
cc.setSuperclass(CtClass.forName("test.Point"));
cc.compile();
This program first obtains a CtClass object representing
a class test.Rectangle. The class file defining
test.Rectangle is read through the current class loader
and the bytecode is stored in the CtClass object. Then
this program modifies the bytecode so that the superclass of
test.Rectangle changes into a class test.Point.
Finally, this program writes the modified bytecode back into the class
file.
Javassist also provides a method for directly obtaining the
modified bytecode. To do this, call toBytecode():
byte[] b = cc.toBytecode();
To expand the class search path used by the constructor of
CtClass, the static method CtClass.getClassPath()
must be called. The returned object represents a class search path.
An additional search path can be added to this object:
ClassPath cp = CtClass.getClassPath();
cp.addPath("/usr/local/javalib");
This program adds /usr/local/javalib to the search path.
The following creation of CtClass objects uses this added
path.
The search path that can be added is not only a directory but also a URL:
ClassPath cp = CtClass.getClassPath();
cp.addPath("www.foo.com", 80, "/java/", "com.foo.");
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.
To define a new class from scratch, the class CtNewClass
must be instantiated.
CtClass cc = new CtNewClass();
cc.setName("Empty");
cc.compile();
The class CtNewClass is a subclass of
CtClass. This program defines a class Empty
including no members. The bytecode of the Empty class is
stored in a class file Empty.class.
A new class can be also defined as a copy of an existing class.
To do this, the class CtClass must be directly instantiated
and given a new name:
CtClass cc = new CtClass("Person");
cc.setName("Human");
cc.compile();
This program first reads the class file defining a class
Person. Then it modifies the bytecode so that
the class name is changed into Human.
The object created by
new CtClass("Person")is not identical to the object returned by
CtClass.forName("Person")The new operator creates a new copy of the class
definition, which is intended to be renamed for defining a new class.
Therefore, changes on that created object does not affect the rest of the
program. Suppose that a class Cat inherits from a class
Animal and Animal inherits from a class
Creature. The following program:
CtClass a = new CtClass("Animal");
a.setSuperclass(CtClass.forName("Organism"));
CtClass a2 = CtClass.forName("Cat").getSuperclass();
System.out.println(a2.getSuperclass().getName());
does not print Organism but Creature.
setName() can be called only on the CtClass
objects directly instantiated by the new operator.
It cannot be called on the CtClass objects returned by
forName().
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.
A simple class loader using Javassist is as follows:
import javassist.*;
public class SimpleLoader extends ClassLoader {
/* Call MyApp.main().
*/
public static void main(String[] args) throws Throwable {
SimpleLoader s = new SimpleLoader();
Class c = s.loadClass("MyApp");
c.getDeclaredMethod("main", new Class[] { String[].class })
.invoke(null, new Object[] { args });
}
/* Finds a specified class. The bytecode for that class can be modified.
*/
protected Class findClass(String name) throws ClassNotFoundException {
try {
CtClass cc = CtClass.forName(name);
// modify cc here
byte[] b = cc.toBytecode();
return defineClass(name, b, 0, b.length);
} catch (CannotCompileException e) {
throw new ClassNotFoundException();
}
}
}
The class MyApp is an application program.
To execute this program, do as follows:
% java SimpleLoaderThe class loader loads the class MyApp and calls
MyApp.main() with the command line parameters.
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. Hence, the
MyApp class cannot directly access the class
SimpleLoader.
javassist.LoaderJavassist provides a class loader
javassist.Loader. This class loader uses a
javassist.ClassPath object for reading bytecode. To intercept
class loading and modify bytecode, a class implementing the interface
javassist.UserClassPath must be defined and its instance must be
registered to the ClassPath object.
Suppose that an instance of MyLoader implementing
UserClassPath performs modification of class files.
To run an application class MyApp with the
MyLoader object, do as follows:
% java javassist.Run MyLoader MyApp arg1 arg2...
javassist.Run first instantiates MyLoader.
The class MyLoader must provide a constructor receiving
no parameters. Then it creates a Loader object and registers
the MyLoader object to it. Finally, it makes the
Loader object load and run
the application class MyApp.
The bytecode of the application program is modified by the
MyLoader object.
Instead of using javassist.Run, you can define a
start-up program like this:
public class MyMain {
public static void main(String[] args) throws Throwable {
Loader cl = (Loader)Main.class.getClassLoader();
UserClassPath myLoader = new MyLoader();
cl.getClassPath().addPath(myLoader);
cl.run("MyApp", args);
}
}
This program is executed as follows:
% java javassist.Loader MyMain arg1 arg2...
This program execution is equivalent to the execution using
javassist.Run.
Although MyMain registers only one
UserClassPath object to Loader, you can also
extend MyMain so that it registers multiple
UserClassPath objects. For example,
public class MyMain {
public static void main(String[] args) throws Throwable {
Loader cl = (Loader)Main.class.getClassLoader();
ClassPath cp = cl.getClassPath();
UserClassPath myLoader = new MyLoader();
cp.addPath(myLoader);
UserClassPath myLoader2 = new MyLoader2();
cp.addPath(myLoader2);
cl.run("MyApp", args);
}
}
Registering multiple
UserClassPath objects is not supported by
javassist.Run.
UserClassPathUserClassPath is an interface providing two
methods:
InputStream openClassfile(String classname) InputStream filterClassfile(String classname, InputStream in)
openClassfile() is a method for reading a class
file from a non-standard resource. If you do not need to use
a non-standard resource, this method should be:
InputStream openClassfile(String classname) throws NotFoundException {
return null;
}
filterClassfile() modifies bytecode when it is loaded
into the JVM. Its definition is typically something like this:
InputStream filterClassfile(String classname, InputStream in)
throws IOException, CannotCompileException
{
/* Either of the following two lines is wrong:
* CtClass cc = CtClass.forName(classname);
* CtClass cc = new CtClass(classname);
*/
CtClass cc = new CtClass(in);
/* modify cc here */
return cc.getInputStream();
}
This method receives an input stream for reading a class file
defining a class specified by classname. If no modification
is needed, this method returns the received input stream without any
changes.
Note that a CtClass object is directly instantiated
with the input stream in; CtClass.forName()
is not called. This is because CtClass.forName() uses
ClassPath and thus it recursively calls
filterClassfile(). If an input stream is passed to
a constructor of CtClass, the constructor does not
use ClassPath for obtaining a class file.
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
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:
CtClass cc = CtClass.forName("java.lang.String");
cc.addField(new CtNewField(CtClass.intType, "hiddenValue"));
cc.compile();
This program produces a file "./java/lang/String.class".
To run your program MyApp
with this modified String class, do as follows:
% java -Xbootclasspath/p:. MyApp arg1 arg2...
Suppose that the definition of MyApp is as follows:
public class MyApp {
public static void main(String[] args) throws Exception {
System.out.println(String.class.getField("hiddenValue").getName());
}
}
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.
CtClass provides methods for introspection. The
introspective ability of Javassist is compatible with that of
the Java reflection API. CtClass provides
getName(), getSuperclass(),
getMethods(), and so on.
CtClass also provides methods for modifying a class
definition. It allows to add a new field, constructor, and method.
Instrumenting a method body is also possible. For more details,
see javassist.CodeConverter.