Java programming dynamics, Part 6: Aspect-oriented changes with Javassist Using Javassist for bytecode search-and-replace transformations ![](https://i-blog.csdnimg.cn/blog_migrate/c6ccbc96c20bebb0d73e8e6368158ef5.gif) | ![developerWorks](https://i-blog.csdnimg.cn/blog_migrate/60cd38ab37758b403bb4b466407686b8.png) |
Level: Intermediate
Dennis Sosnoski (dms@sosnoski.com), President, Sosnoski Software Solutions, Inc.
02 Mar 2004
Java consultant Dennis Sosnoski saves the best for last in his three-part coverage of the Javassist framework. This time he shows how the Javassist search-and-replace support makes editing Java bytecode practically as easy as a text editor's Replace All command. Want to report all writes to a particular field or patch in a change to a parameter passed in a method call? Javassist makes it easy, and Dennis shows you how.
<script type="text/javascript" language="JavaScript">
</script>
Part 4 and Part 5 of this series covered how you can use Javassist for localized changes to binary classes. This time you'll learn about an even more powerful way of using the framework, taking advantage of Javassist's support for finding all uses of a particular method or field in the bytecode. This feature is at least as important to Javassist's power as its support for a source code-like way of specifying bytecode. Support for selective replacement of operations is also the feature that makes Javassist an excellent tool for adding aspect-oriented programming features to standard Java code.
In Part 5 you saw how Javassist lets you intercept the classloading process -- and even make changes to the binary class representations as they're being loaded. The systematic bytecode transformations I'm covering in this article can be used for either static class file transformations or runtime interception, but they're especially useful when used at runtime.
Handling bytecode modifications
Javassist provides two separate ways to handle systematic bytecode modifications. The first technique, using the javassist.CodeConverter
class, is a little simpler to use but a lot more limited in terms of what you can accomplish. The second technique uses custom subclasses of the javassist.ExprEditor
class. This takes a little more work, but the added flexibility more than makes up for the effort. I'll look at examples of both approaches in this article.
Code conversion
The first Javassist technique for systematic bytecode modification uses the javassist.CodeConverter
class. To take advantage of this technique, you just create an instance of the CodeConverter
class and configure it with one or more transformation actions. Each transformation is configured using a method call that identifies the type of transformation. These fall into three categories: method call transformations, field access transformations, and a new object transformation.
![](https://i-blog.csdnimg.cn/blog_migrate/c6ccbc96c20bebb0d73e8e6368158ef5.gif) |
|
Listing 1 gives an example of using a method call transformation. In this case, the transformation just adds a notification that a method is being called. In the code, I first get the javassist.ClassPool
instance I'm going to use throughout, configuring it to work with a translator (as previously seen in Part 5) Then, I access a pair of method definitions through the ClassPool
. The first method definition is for the "set"-style method to be monitored (with the class and method name from command line arguments), the second is for the reportSet()
method within the TranslateConvert
class that will report a call to the first method.
Once I have the method information, I can use the CodeConverter
insertBeforeMethod()
to configure a transformation that adds a call to the reporting method before each call to the set method. Then all that needs to be done is to apply this converter to one or more classes. In the Listing 1 code, I do this within the onWrite()
method of the ConverterTranslator
inner class with the call to the instrument()
method of the class object. This will automatically apply the transformation to every class loaded from the ClassPool
instance.
Listing 1. Using CodeConverter
public class TranslateConvert { public static void main(String[] args) { if (args.length >= 3) { try { // set up class loader with translator ConverterTranslator xlat = new ConverterTranslator(); ClassPool pool = ClassPool.getDefault(xlat); CodeConverter convert = new CodeConverter(); CtMethod smeth = pool.get(args[0]). getDeclaredMethod(args[1]); CtMethod pmeth = pool.get("TranslateConvert"). getDeclaredMethod("reportSet"); convert.insertBeforeMethod(smeth, pmeth); xlat.setConverter(convert); Loader loader = new Loader(pool); // invoke "main" method of application class String[] pargs = new String[args.length-3]; System.arraycopy(args, 3, pargs, 0, pargs.length); loader.run(args[2], pargs); } catch ... } } else { System.out.println("Usage: TranslateConvert " + "clas-name set-name main-class args..."); } } public static void reportSet(Bean target, String value) { System.out.println("Call to set value " + value); } public static class ConverterTranslator implements Translator { private CodeConverter m_converter; private void setConverter(CodeConverter convert) { m_converter = convert; } public void start(ClassPool pool) {} public void onWrite(ClassPool pool, String cname) throws NotFoundException, CannotCompileException { CtClass clas = pool.get(cname); clas.instrument(m_converter); } } }
|
That's a fairly complex operation to configure, but once it's set up it works painlessly. Listing 2 gives a code sample to use as a test case. Here the Bean
provides a test object with bean-like get and set methods, which are used by the BeanTest
program to access the values.
Listing 2. A bean tester
public class Bean { private String m_a; private String m_b; public Bean() {} public Bean(String a, String b) { m_a = a; m_b = b; } public String getA() { return m_a; }
public String getB() { return m_b; }
public void setA(String string) { m_a = string; }
public void setB(String string) { m_b = string; } }
public class BeanTest { private Bean m_bean; private BeanTest() { m_bean = new Bean("originalA", "originalB"); } private void print() { System.out.println("Bean values are " + m_bean.getA() + " and " + m_bean.getB()); } private void changeValues(String lead) { m_bean.setA(lead + "A"); m_bean.setB(lead + "B"); } public static void main(String[] args) { BeanTest inst = new BeanTest(); inst.print(); inst.changeValues("new"); inst.print(); } }
|
Here's the output if I just run the Listing 2 BeanTest
program directly:
[dennis]$ java -cp . BeanTest Bean values are originalA and originalB Bean values are newA and newB
|
If I run it using the Listing 1 TranslateConvert
program and specify one of the set methods to monitor, the output will look like this:
[dennis]$ java -cp .:javassist.jar TranslateConvert Bean setA BeanTest Bean values are originalA and originalB Call to set value newA Bean values are newA and newB
|
Everything works the same as before, but now there's a notification that the selected method is being called during the execution of the program.
In this case, the same effect could easily have been achieved in other ways, for instance by adding code to the actual set method body using the techniques from Part 4. The difference here is that by adding the code at the point of use, I gain flexibility. For instance, I could easily modify the TranslateConvert.ConverterTranslator
onWrite()
method to check the class name being loaded, and only transform classes that are included in a list I'm interested in observing. Adding code directly to the set method body wouldn't allow such selective monitoring.
The flexibility provided by systematic bytecode transformations is what makes them a powerful tool for implementing aspect-oriented extensions for standard Java code. You'll see more of this in the remainder of this article.
Conversion limits
The transformations handled by CodeConverter
are useful, but limited. For instance, if you want to call a monitoring method before or after a target method is called, that monitoring method must be defined as static void
and must take a single parameter of the class of the target method, followed by the same number and types of parameters as the target method. When the monitoring method is called, it's passed the actual target object as its first argument, followed by all the arguments for the target method.
This rigid structure means that the monitoring methods need to be precisely matched to the target class and method. By way of example, suppose I change the definition of the reportSet()
method in Listing 1 to take a generic java.lang.Object
parameter in hopes of making it usable with different target classes:
public static void reportSet(Object target, String value) { System.out.println("Call to set value " + value); }
|
This compiles fine, but when I try it out it breaks:
[dennis]$ java -cp .:javassist.jar TranslateConvert Bean setA BeanTest Bean values are A and B java.lang.NoSuchMethodError: TranslateConvert.reportSet(LBean;Ljava/lang/String;)V at BeanTest.changeValues(BeanTest.java:17) at BeanTest.main(BeanTest.java:23) at ...
|
There are ways to get around this limitation. One solution would be to actually generate a custom monitoring method at runtime that matches the target method. That's a lot of effort to go through, though, and I'm not even going to try it for this article. Fortunately, Javassist also provides another way of handling systematic bytecode transformations. This other way, using javassist.ExprEditor
, is both more flexible and more powerful than CodeConverter
.
Class mutilation made easy
Bytecode transformations with javassist.ExprEditor
build on the same principles as those done using CodeConverter
. The ExprEditor
approach is perhaps a little harder to understand, though, so I'll start off by demonstrating the basic principles and then add in the actual transformations.
Listing 3 shows how you can use ExprEditor
to report the basic items that are potential targets for aspect-oriented transformations. Here I subclass ExprEditor
in my own VerboseEditor
, overriding three of the base class methods -- all named edit()
, but with different parameter types. As in the Listing 1 code, I actually use this subclass from within the onWrite()
method of the DissectionTranslator
inner class, passing an instance in the call to the instrument()
method of the class object for every class loaded from our ClassPool
instance.
Listing 3. A class dissector
public class Dissect { public static void main(String[] args) { if (args.length >= 1) { try { // set up class loader with translator Translator xlat = new DissectionTranslator(); ClassPool pool = ClassPool.getDefault(xlat); Loader loader = new Loader(pool); // invoke the "main" method of the application class String[] pargs = new String[args.length-1]; System.arraycopy(args, 1, pargs, 0, pargs.length); loader.run(args[0], pargs); } catch (Throwable ex) { ex.printStackTrace(); } } else { System.out.println ("Usage: Dissect main-class args..."); } } public static class DissectionTranslator implements Translator { public void start(ClassPool pool) {} public void onWrite(ClassPool pool, String cname) throws NotFoundException, CannotCompileException { System.out.println("Dissecting class " + cname); CtClass clas = pool.get(cname); clas.instrument(new VerboseEditor()); } } public static class VerboseEditor extends ExprEditor { private String from(Expr expr) { CtBehavior source = expr.where(); return " in " + source.getName() + "(" + expr.getFileName() + ":" + expr.getLineNumber() + ")"; }
public void edit(FieldAccess arg) { String dir = arg.isReader() ? "read" : "write"; System.out.println(" " + dir + " of " + arg.getClassName() + "." + arg.getFieldName() + from(arg)); }
public void edit(MethodCall arg) { System.out.println(" call to " + arg.getClassName() + "." + arg.getMethodName() + from(arg)); }
public void edit(NewExpr arg) { System.out.println(" new " + arg.getClassName() + from(arg)); } } }
|
Listing 4 shows the output generated by running the Listing 4 Dissect
program on the Listing 2 BeanTest
program. This gives the detailed breakdown of what's being done within each method of each class loaded, listing all method calls, field accesses, and new object creations.
Listing 4. BeanTest dissected
[dennis]$ java -cp .:javassist.jar Dissect BeanTest Dissecting class BeanTest new Bean in BeanTest(BeanTest.java:7) write of BeanTest.m_bean in BeanTest(BeanTest.java:7) read of java.lang.System.out in print(BeanTest.java:11) new java.lang.StringBuffer in print(BeanTest.java:11) call to java.lang.StringBuffer.append in print(BeanTest.java:11) read of BeanTest.m_bean in print(BeanTest.java:11) call to Bean.getA in print(BeanTest.java:11) call to java.lang.StringBuffer.append in print(BeanTest.java:11) call to java.lang.StringBuffer.append in print(BeanTest.java:11) read of BeanTest.m_bean in print(BeanTest.java:11) call to Bean.getB in print(BeanTest.java:11) call to java.lang.StringBuffer.append in print(BeanTest.java:11) call to java.lang.StringBuffer.toString in print(BeanTest.java:11) call to java.io.PrintStream.println in print(BeanTest.java:11) read of BeanTest.m_bean in changeValues(BeanTest.java:16) new java.lang.StringBuffer in changeValues(BeanTest.java:16) call to java.lang.StringBuffer.append in changeValues(BeanTest.java:16) call to java.lang.StringBuffer.append in changeValues(BeanTest.java:16) call to java.lang.StringBuffer.toString in changeValues(BeanTest.java:16) call to Bean.setA in changeValues(BeanTest.java:16) read of BeanTest.m_bean in changeValues(BeanTest.java:17) new java.lang.StringBuffer in changeValues(BeanTest.java:17) call to java.lang.StringBuffer.append in changeValues(BeanTest.java:17) call to java.lang.StringBuffer.append in changeValues(BeanTest.java:17) call to java.lang.StringBuffer.toString in changeValues(BeanTest.java:17) call to Bean.setB in changeValues(BeanTest.java:17) new BeanTest in main(BeanTest.java:21) call to BeanTest.print in main(BeanTest.java:22) call to BeanTest.changeValues in main(BeanTest.java:23) call to BeanTest.print in main(BeanTest.java:24) Dissecting class Bean write of Bean.m_a in Bean(Bean.java:10) write of Bean.m_b in Bean(Bean.java:11) read of Bean.m_a in getA(Bean.java:15) read of Bean.m_b in getB(Bean.java:19) write of Bean.m_a in setA(Bean.java:23) write of Bean.m_b in setB(Bean.java:27) Bean values are originalA and originalB Bean values are newA and newB
|
I could easily add support for reporting casts, instanceof
tests, and catch
blocks by implementing the appropriate methods in VerboseEditor
. But just listing out information about these component items gets boring, so let's instead look into actually modifying the items.
Vivisection in progress
The Listing 4 dissection of classes lists basic component operations. It's easy to see how these would be useful to work with when implementing aspect-oriented features. For example, a logger that reports all write accesses to selected fields would be a useful aspect to apply in many applications. That is the sort of thing I've been promising to show you how to do, after all.
Fortunately for the theme of this article, ExprEditor
not only lets me know what operations are present in the code, it also lets me modify the operations being reported. The parameter types passed in the various ExprEditor.edit()
method calls each defines a replace()
method. If I pass this method a statement in the usual Javassist source code form (covered in Part 4), that statement will be compiled to bytecode and used to replace the original operation. This makes slicing and dicing your bytecode easy.
Listing 5 shows an application of code replacement. Rather than just logging operations, I've chosen here to actually modify the String
value being stored to a selected field. In FieldSetEditor
, I implement the method signature that matches field accesses. Within this method, I just check two things: that the field name is the one I'm looking for, and that the operation is a store. When I find a match, I replace the original store with one that uses the result of a call to the reverse()
method within the actual TranslateEditor
application class. The reverse()
method just reverses the order of the characters in the original string and prints out a message to show that it has been used.
Listing 5. Reversing string sets
public class TranslateEditor { public static void main(String[] args) { if (args.length >= 3) { try { // set up class loader with translator EditorTranslator xlat = new EditorTranslator(args[0], new FieldSetEditor(args[1])); ClassPool pool = ClassPool.getDefault(xlat); Loader loader = new Loader(pool); // invoke the "main" method of the application 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: TranslateEditor clas-name " + "field-name main-class args..."); } } public static String reverse(String value) { int length = value.length(); StringBuffer buff = new StringBuffer(length); for (int i = length-1; i >= 0; i--) { buff.append(value.charAt(i)); } System.out.println("TranslateEditor.reverse returning " + buff); return buff.toString(); } public static class EditorTranslator implements Translator { private String m_className; private ExprEditor m_editor; private EditorTranslator(String cname, ExprEditor editor) { m_className = cname; m_editor = editor; } 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); clas.instrument(m_editor); } } } public static class FieldSetEditor extends ExprEditor { private String m_fieldName; private FieldSetEditor(String fname) { m_fieldName = fname; } public void edit(FieldAccess arg) throws CannotCompileException { if (arg.getFieldName().equals(m_fieldName) && arg.isWriter()) { StringBuffer code = new StringBuffer(); code.append("$0."); code.append(arg.getFieldName()); code.append("=TranslateEditor.reverse($1);"); arg.replace(code.toString()); } } } }
|
Here's what happens if I run this on the Listing 2 BeanTest
program:
[dennis]$ java -cp .:javassist.jar TranslateEditor Bean m_a BeanTest TranslateEditor.reverse returning Alanigiro Bean values are Alanigiro and originalB TranslateEditor.reverse returning Awen Bean values are Awen and newB
|
I've successfully grafted in a call to the added code at each store into the Bean.m_a
field (one in the constructor and one in the set method). I could counteract this effect by implementing a similar modification on the loads from the field, but personally I find the reversed values a lot more interesting than what we started with, so I'll choose to stay with these.
Wrapping up Javassist
In this article, you've seen how systematic bytecode transformations can easily be done using Javassist. Combining it with the last two articles, you should have a solid basis for implementing your own aspect-oriented transformations of Java applications, either as a separate build step or at runtime.
To get a better idea of the power of this approach, you may also want to look at the JBoss Aspect Oriented Programming project (JBossAOP) that's been built around Javassist. JBossAOP uses an XML configuration file to define any of a variety of different operations to be done to your application classes. These include using interceptors on field accesses or method calls, adding mix-in interface implementations to existing classes, and more. JBossAOP is built into the version of the JBoss application server now under development, but is also available as a standalone tool for use with your applications outside of JBoss.
Next up for this series is a look at the Byte Code Engineering Library (BCEL), a part of the Apache Software Foundation's Jakarta Project. BCEL is one of the most widely used frameworks for Java classworking. It gives a very different way of working with bytecode from the Javassist approach we've seen in the last three articles, focusing on the individual bytecode instructions rather than the source-level work that's Javassist's strength. Check back for the full details on working at the bytecode assembler level next month.
Download
Name | Size | Download method |
---|
j-dyn0302-source.zip | | FTP |
Resources
- Check out the rest of Dennis Sosnoski's Java programming dynamics series.
- Download the example code for this article.
- Javassist was originated by Shigeru Chiba of the Department of Mathematics and Computing Sciences, Tokyo Institute of Technology. It has recently joined the open source JBoss application server project where it's the basis for the addition of new aspect-oriented programming features. Download the current release of Javassist from the JBoss project Files page on Sourceforge.
- Learn more about the Java bytecode design in "Java bytecode: Understanding bytecode makes you a better programmer" (developerWorks, July 2001) by Peter Haggar.
- Want to find out more about aspect-oriented programming? Try "Improve modularity with aspect-oriented programming" (developerWorks. January 2002) by Nicholas Lesiecki for an overview of working with the AspectJ language. A more recent article, "AOP banishes the tight-coupling blues" (developerWorks, February 2004) by Andrew Glover shows how one of AOP's functional design concepts -- static crosscutting -- can turn what might be a tangled mass of tightly coupled code into a powerful, extensible enterprise application.
- The open source Jikes Project provides a very fast and highly compliant compiler for the Java programming language. Use it to generate your bytecode the old fashioned way -- from Java source code.
- Browse for books on these and other technical topics.
- Find hundreds more Java technology resources on the developerWorks Java technology zone.
![](https://i-blog.csdnimg.cn/blog_migrate/05ac63f463130bd1876c1af927ae88a3.gif) |
About the author
![](https://i-blog.csdnimg.cn/blog_migrate/c6ccbc96c20bebb0d73e8e6368158ef5.gif) |
![Photo of Dennis Sosnoski](https://i-blog.csdnimg.cn/blog_migrate/523d5c31ca7192a06b4928e656294c43.jpeg) | ![](https://i-blog.csdnimg.cn/blog_migrate/c6ccbc96c20bebb0d73e8e6368158ef5.gif) | 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 |