|
|
|
Contents: |
|
|
|
Related content: |
|
|
|
Subscriptions: |
|
|
| Learn how to modify classes as they're being loaded with
Javassist
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.
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 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. |
|
|