Java programming dynamics, Part 6: Aspect-oriented changes with Javassist

Java programming dynamics, Part 6: Aspect-oriented changes with Javassist

Using Javassist for bytecode search-and-replace transformations

developerWorks
Document options
<script type="text/javascript" language="JavaScript"> </script>
Set printer orientation to landscape mode

Print this page

<script type="text/javascript" language="JavaScript"> </script>
Email this page

E-mail this page

Sample code


Using XML, but need to do more?

Download DB2 Express-C 9


Rate this page

Help us improve this content


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.



Back to top


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.

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 5, "Transforming classes on-the-fly" (February 2004)

Part 7, "Bytecode engineering with BCEL" (April 2004)

Part 8, "Replacing reflection with code generation" (June 2004)

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.

Ask the expert: Dennis Sosnoski on JVM and bytecode issues
For comments or questions about the material covered in this article series, as well as anything else that pertains to Java bytecode, the Java binary class format, or general JVM issues, visit the JVM and Bytecode discussion forum, moderated by Dennis Sosnoski.

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.



Back to top


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.



Back to top


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.




Back to top


Download

NameSizeDownload method
j-dyn0302-source.zip FTP
Information about download methodsGet Adobe® Reader®


Back to top


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.


Back to top


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

 
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值