Bytecode Engineering


You have seen how annotations can be processed at run time or at the source code level. There is a third possibility: processing at the bytecode level. Unless annotations are removed at the source level, they are present in the class files. The class file format is documented (see http://java.sun.com/docs/books/vmspec). The format is rather complex, and it would be challenging to process class files without special libraries. One such library is BCEL, the Bytecode Engineering Library, available at http://jakarta.apache.org/bcel.

In this section, we use BCEL to add logging messages to annotated methods. If a method is annotated with


@LogEntry(logger=loggerName)

then we add the bytecodes for the following statement at the beginning of the method:


Logger.getLogger(loggerName).entering(className, methodName);

For example, if you annotate the hashCode method of the Item class as

 @LogEntry(logger="global") public int hashCode() 

then a message similar to the following is printed whenever the method is called:

 Aug 17, 2004 9:32:59 PM Item hashCode FINER: ENTRY 

To achieve this task, we do the following:

  • Load the bytecodes in the class file.

  • Locate all methods.

  • For each method, check whether it has a LogEntry annotation.

  • If it does, add the bytecodes for the following instructions at the beginning of the method:


    ldc loggerName
    invokestatic java/util/logging/Logger.getLogger:(Ljava/lang/String;)Ljava/util/logging/Logger;
    ldc className
    ldc methodName
    invokevirtual java/util/logging/Logger.entering:(Ljava/lang/String;Ljava/lang/String;)V

Inserting these bytecodes sounds tricky, but BCEL makes it fairly straightforward. However, at the time that this chapter was written, BCEL 5.1 had no support for processing annotations. Annotations are stored as "attribute" sections in the class file. Those sections store a wide variety of information, such as the settings of public, private, protected, and static modifiers and line numbers for debugging support. The class file format is extensible, and compilers are free to add custom attribute sections. Therefore, BCEL makes it possible to plug in readers for new attribute sections.

We supplied a class AnnotationsAttributeReader that processes the attribute section containing annotations. To keep the code simple, we handle only annotations whose elements have type String, and we don't handle defaults. Future versions of BCEL should include support for handling attributes.

We don't describe the process of analyzing and inserting bytecodes in detail. The important point is that the program in Example 13-6 edits a class file and inserts a logging call at the beginning of the methods that are annotated with the LogEntry annotation. If you are interested in the details of bytecode engineering, we suggest that you read through the BCEL manual at http://jakarta.apache.org/bcel/manual.html.

You need the BCEL library to compile and run the EntryLogger program. For example, here is how you add the logging instructions to the file Item.java in Example 13-7.

 javac Item.java javac -classpath .:bcel-5.1.jar EntryLogger.java java -classpath .:bcel-5.1.jar EntryLogger Item 

Try running

 javap -c Item 

before and after modifying the Item class file. You can see the inserted instructions at the beginning of the hashCode, equals, and compareTo methods.

[View full width]

public int hashCode(); Code: 0: ldc #85; //String global 2: invokestatic #80; //Method java/util/logging/Logger.getLogger:(Ljava/lang /String;)Ljava/util/logging/Logger; 5: ldc #86; //String Item 7: ldc #88; //String hashCode 9: invokevirtual #84; //Method java/util/logging/Logger.entering:(Ljava/lang /String;Ljava/lang/String;)V 12: bipush 13 14: aload_0 15: getfield #2; //Field description:Ljava/lang/String; 18: invokevirtual #15; //Method java/lang/String.hashCode:()I 21: imul 22: bipush 17 24: aload_0 25: getfield #3; //Field partNumber:I 28: imul 29: iadd 30: ireturn

The SetTest program in Example 13-8 inserts Item objects into a hash set. When you run it with the modified class file, you will see the logging messages.

 Aug 18, 2004 10:57:59 AM Item hashCode FINER: ENTRY Aug 18, 2004 10:57:59 AM Item hashCode FINER: ENTRY Aug 18, 2004 10:57:59 AM Item hashCode FINER: ENTRY Aug 18, 2004 10:57:59 AM Item equals FINER: ENTRY [[descripion=Toaster, partNumber=1729], [descripion=Microwave, partNumber=4562]] 

Note the call to equals when we insert the same item twice.

This example shows the power of bytecode engineering. Annotations are used to add directives to a program. A bytecode editing tool picks up the directives and modifies the virtual machine instructions.

Example 13-6. EntryLogger.java

[View full width]

   1. import java.lang.annotation.*;   2. import java.lang.reflect.Proxy;   3. import java.lang.reflect.InvocationHandler;   4. import java.io.*;   5. import java.util.*;   6.   7. import org.apache.bcel.*;   8. import org.apache.bcel.Repository;   9. import org.apache.bcel.classfile.*;  10. import org.apache.bcel.classfile.FieldOrMethod;  11. import org.apache.bcel.generic.*;  12. import org.apache.bcel.util.*;  13.  14. /**  15.    Adds "entering" logs to all methods of a class that have the LogEntry annotation.  16. */  17. public class EntryLogger  18. {  19.    /**  20.       Adds entry logging code to the given class  21.       @param args the name of the class file to patch  22.    */  23.    public static void main(String[] args)  24.    {  25.       try  26.       {  27.          if (args.length == 0)  28.             System.out.println("USAGE: java EntryLogger classname");  29.          else  30.          {  31.             Attribute.addAttributeReader("RuntimeVisibleAnnotations",  32.                new AnnotationsAttributeReader());  33.             JavaClass jc = Repository.lookupClass(args[0]);  34.             ClassGen cg = new ClassGen(jc);  35.             EntryLogger el = new EntryLogger(cg);  36.             el.convert();  37.             File f = new File(Repository.lookupClassFile(cg.getClassName()).getPath());  38.             cg.getJavaClass().dump(f.getPath());  39.          }  40.       }  41.       catch (Exception e)  42.       {  43.          e.printStackTrace();  44.       }  45.    }  46.  47.    /**  48.       Constructs an EntryLogger that inserts logging into annotated methods of a  given class  49.       @param cg the class  50.    */  51.    public EntryLogger(ClassGen cg)  52.    {  53.       this.cg = cg;  54.       cpg = cg.getConstantPool();  55.    }  56.  57.    /**  58.       converts the class by inserting the logging calls.  59.    */  60.    public void convert() throws IOException  61.    {  62.       for (Method m : cg.getMethods())  63.       {  64.          AnnotationsAttribute attr  65.             = (AnnotationsAttribute) getAttribute(m, "RuntimeVisibleAnnotations");  66.          if (attr != null)  67.          {  68.             LogEntry logEntry = attr.getAnnotation(LogEntry.class);  69.             if (logEntry != null)  70.             {  71.                String loggerName = logEntry.logger();  72.                if (loggerName == null) loggerName = "";  73.                cg.replaceMethod(m, insertLogEntry(m, loggerName));  74.             }  75.          }  76.       }  77.    }  78.  79.    /**  80.       Adds an "entering" call to the beginning of a method.  81.       @param m the method  82.       @param loggerName the name of the logger to call  83.    */  84.    private Method insertLogEntry(Method m, String loggerName)  85.    {  86.       MethodGen mg = new MethodGen(m, cg.getClassName(), cpg);  87.       String className = cg.getClassName();  88.       String methodName = mg.getMethod().getName();  89.       System.out.printf("Adding logging instructions to %s.%s%n", className, methodName);  90.  91.  92.       int getLoggerIndex = cpg.addMethodref(  93.             "java.util.logging.Logger",  94.             "getLogger",  95.             "(Ljava/lang/String;)Ljava/util/logging/Logger;");  96.       int enteringIndex = cpg.addMethodref(  97.             "java.util.logging.Logger",  98.             "entering",  99.             "(Ljava/lang/String;Ljava/lang/String;)V"); 100. 101.       InstructionList il = mg.getInstructionList(); 102.       InstructionList patch = new InstructionList(); 103.       patch.append(new PUSH(cpg, loggerName)); 104.       patch.append(new INVOKESTATIC(getLoggerIndex)); 105.       patch.append(new PUSH(cpg, className)); 106.       patch.append(new PUSH(cpg, methodName)); 107.       patch.append(new INVOKEVIRTUAL(enteringIndex)); 108.       InstructionHandle[] ihs = il.getInstructionHandles(); 109.       il.insert(ihs[0], patch); 110. 111.       mg.setMaxStack(); 112.       return mg.getMethod(); 113.    } 114. 115.    /** 116.       Gets the attribute of a field or method with the given name. 117.       @param fm the field or method 118.       @param name the attribute name 119.       @return the attribute, or null, if no attribute with the given name was found 120.    */ 121.    public static Attribute getAttribute(FieldOrMethod fm, String name) 122.    { 123.       for (Attribute attr : fm.getAttributes()) 124.       { 125.          int nameIndex = attr.getNameIndex(); 126.          ConstantPool cp = attr.getConstantPool(); 127.          String attrName = cp.constantToString(cp.getConstant(nameIndex)); 128.          if (attrName.equals(name)) 129.             return attr; 130.       } 131.       return null; 132.    } 133. 134.    private ClassGen cg; 135.    private ConstantPoolGen cpg; 136. } 137. 138. /** 139.    This is a pluggable reader for an annotations attribute for the BCEL framework. 140. */ 141. class AnnotationsAttributeReader implements org.apache.bcel.classfile.AttributeReader 142. { 143.    public Attribute createAttribute(int nameIndex, int length, DataInputStream in, 144.       ConstantPool constantPool) 145.    { 146.       AnnotationsAttribute attribute = new AnnotationsAttribute(nameIndex, length,  constantPool); 147.       try 148.       { 149.          attribute.read(in, constantPool); 150.          return attribute; 151.       } 152.       catch (IOException e) 153.       { 154.          e.printStackTrace(); 155.          return null; 156.       } 157.    } 158. } 159. 160. /** 161.    This attribute describes a set of annotations. 162.    Only String-valued annotation attributes are supported. 163. */ 164. class AnnotationsAttribute extends Attribute 165. { 166.    /** 167.       Reads this annotation. 168.       @param nameIndex the index for the name of this attribute 169.       @param length the number of bytes in this attribute 170.       @param cp the constant pool 171.    */ 172.    public AnnotationsAttribute (int nameIndex, int length, ConstantPool cp) 173.    { 174.       super(Constants.ATTR_UNKNOWN, nameIndex, length, cp); 175.       annotations = new HashMap<String, Map<String, String>>(); 176.    } 177. 178.    /** 179.       Reads this annotation. 180.       @param in the input stream 181.       @param cp the constant pool 182.    */ 183.    public void read(DataInputStream in, ConstantPool cp) 184.       throws IOException 185.    { 186.       short numAnnotations = in.readShort(); 187.       for (int i = 0; i < numAnnotations; i++) 188.       { 189.          short typeIndex = in.readShort(); 190.          String type = cp.constantToString(cp.getConstant(typeIndex)); 191.          Map<String, String> nvPairs = new HashMap<String, String>(); 192.          annotations.put(type, nvPairs); 193.          short numElementValuePairs = in.readShort(); 194.          for (int j = 0; j < numElementValuePairs; j++) 195.          { 196.             short nameIndex = in.readShort(); 197.             String name = cp.constantToString(cp.getConstant(nameIndex)); 198.             byte tag = in.readByte(); 199.             if (tag == 's') 200.             { 201.                short constValueIndex = in.readShort(); 202.                String value = cp.constantToString(cp.getConstant(constValueIndex)); 203.                nvPairs.put(name, value); 204.             } 205.             else 206.                throw new UnsupportedOperationException("Can only handle String  attributes"); 207.          } 208.       } 209.    } 210. 211.    public void dump(DataOutputStream out) 212.       throws IOException 213.    { 214.       ConstantPoolGen cpg = new ConstantPoolGen(getConstantPool()); 215. 216.       out.writeShort(getNameIndex()); 217.       out.writeInt(getLength()); 218.       out.writeShort(annotations.size()); 219.       for (Map.Entry<String, Map<String, String>> entry : annotations.entrySet()) 220.       { 221.          String type = entry.getKey(); 222.          Map<String, String> nvPairs = entry.getValue(); 223.          out.writeShort(cpg.lookupUtf8(type)); 224.          out.writeShort(nvPairs.size()); 225.          for (Map.Entry<String, String> nv : nvPairs.entrySet()) 226.          { 227.             out.writeShort(cpg.lookupUtf8(nv.getKey())); 228.             out.writeByte('s'); 229.             out.writeShort(cpg.lookupUtf8(nv.getValue())); 230.          } 231.       } 232.    } 233. 234.    /** 235.       Gets an annotation from this set of annotations. 236.       @param annotationClass the class of the annotation to get 237.       @return the annotation object, or null if no matching annotation is present 238.    */ 239.    public <A extends Annotation> A getAnnotation(Class<A> annotationClass) 240.    { 241.       String key = "L" + annotationClass.getName() + ";"; 242.       final Map<String, String> nvPairs = annotations.get(key); 243.       if (nvPairs == null) return null; 244. 245.       InvocationHandler handler = new 246.          InvocationHandler() 247.          { 248.             public Object invoke(Object proxy, java.lang.reflect.Method m, Object[] args) 249.                throws Throwable 250.             { 251.                return nvPairs.get(m.getName()); 252.             } 253.          }; 254. 255.       return (A) Proxy.newProxyInstance( 256.          getClass().getClassLoader(), 257.          new Class[] { annotationClass }, 258.          handler); 259.    } 260. 261.    public void accept(org.apache.bcel.classfile.Visitor v) 262.    { 263.       throw new UnsupportedOperationException(); 264.    } 265. 266.    public Attribute copy(ConstantPool cp) 267.    { 268.       throw new UnsupportedOperationException(); 269.    } 270. 271.    public String toString () 272.    { 273.       return annotations.toString(); 274.    } 275. 276.    private Map<String, Map<String, String>> annotations; 277. } 

Example 13-7. Item.java
  1. public class Item  2. {  3.    /**  4.       Constructs an item.  5.       @param aDescription the item's description  6.       @param aPartNumber the item's part number  7.    */  8.    public Item(String aDescription, int aPartNumber)  9.    { 10.       description = aDescription; 11.       partNumber = aPartNumber; 12.    } 13. 14.    /** 15.       Gets the description of this item. 16.       @return the description 17.    */ 18.    public String getDescription() 19.    { 20.       return description; 21.    } 22. 23.    public String toString() 24.    { 25.       return "[description=" + description 26.          + ", partNumber=" + partNumber + "]"; 27.    } 28. 29.    @LogEntry(logger="global") public boolean equals(Object otherObject) 30.    { 31.       if (this == otherObject) return true; 32.       if (otherObject == null) return false; 33.       if (getClass() != otherObject.getClass()) return false; 34.       Item other = (Item) otherObject; 35.       return description.equals(other.description) 36.          && partNumber == other.partNumber; 37.    } 38. 39.    @LogEntry(logger="global") public int hashCode() 40.    { 41.       return 13 * description.hashCode() + 17 * partNumber; 42.    } 43. 44.    private String description; 45.    private int partNumber; 46. } 

Example 13-8. SetTest.java

[View full width]

  1. import java.util.*;  2. import java.util.logging.*;  3.  4. /**  5.    This program logs the equals and hashCode method calls when inserting items into a  hash set.  6. */  7. public class SetTest  8. {  9.    public static void main(String[] args) 10.    { 11.       Logger.global.setLevel(Level.FINEST); 12.       Handler handler = new ConsoleHandler(); 13.       handler.setLevel(Level.FINEST); 14.       Logger.global.addHandler(handler); 15. 16.       Set<Item> parts = new HashSet<Item>(); 17.       parts.add(new Item("Toaster", 1279)); 18.       parts.add(new Item("Microwave", 4562)); 19.       parts.add(new Item("Toaster", 1279)); 20.       System.out.println(parts); 21.    } 22. } 

Modifying Bytecodes at Load Time

In the preceding section, you saw a tool that edits class files. However, it can be cumbersome to add yet another tool into the build process. An attractive alternative is to defer the bytecode engineering until load time, when the class loader loads the class.

Before JDK 5.0, you had to write a custom classloader to achieve this task. Now, the instrumentation API has a hook for installing a bytecode transformer. The transformer must be installed before the main method of the program is called. You handle this requirement by defining an agent, a library that is loaded to monitor a program in some way. The agent code can carry out initializations in a premain method.

Here are the steps required to build an agent:

  • Implement a class with a method

     public static void premain(String arg, Instrumentation instr) 

    This method is called when the agent is loaded. The agent can get a single command-line argument, which is passed in the arg parameter. The instr parameter can be used to install various hooks.

  • Make a manifest file that sets the Premain-Class attribute, for example:

     Premain-Class: EntryLoggingAgent 

  • Package the agent code and the manifest into a JAR file, for example:

     jar cvfm EntryLoggingAgent.jar EntryLoggingAgent.mf *.class 

To launch a Java program together with the agent, use the following command-line options:


java -javaagent:AgentJARFile=agentArgument . . .

For example, to run the SetTest program with the entry logging agent, call

 java -javaagent:EntryLoggingAgent.jar=Item -classpath .:bcel-5.1.jar SetTest 

The Item argument is the name of the class that the agent should modify.

Example 13-9 shows the agent code. The agent installs a class file transformer. The transformer first checks whether the class name matches the agent argument. If so, it uses the EntryLogger class from the preceding section to modify the bytecodes. However, the modified bytecodes are not saved to a file. Instead, the transformer returns them for loading into the virtual machine. In other words, this technique carries out "just in time" modification of the bytecodes.

Example 13-9. EntryLoggingAgent.java
  1. import java.lang.instrument.*;  2. import java.io.*;  3. import java.security.*;  4.  5. import org.apache.bcel.classfile.*;  6. import org.apache.bcel.generic.*;  7.  8. public class EntryLoggingAgent  9. { 10.    public static void premain(final String arg, Instrumentation instr) 11.    { 12.       System.out.println(instr); 13.       instr.addTransformer(new 14.          ClassFileTransformer() 15.          { 16.             public byte[] transform(ClassLoader loader, String className, Class cl, 17.                ProtectionDomain pd, byte[] data) 18.             { 19.                if (!className.equals(arg)) return null; 20.                try 21.                { 22.                   Attribute.addAttributeReader("RuntimeVisibleAnnotations", 23.                      new AnnotationsAttributeReader()); 24.                   ClassParser parser = new ClassParser( 25.                      new ByteArrayInputStream(data), className + ".java"); 26.                   JavaClass jc = parser.parse(); 27.                   ClassGen cg = new ClassGen(jc); 28.                   EntryLogger el = new EntryLogger(cg); 29.                   el.convert(); 30.                   return cg.getJavaClass().getBytes(); 31.                } 32.                catch (Exception e) 33.                { 34.                   e.printStackTrace(); 35.                   return null; 36.                } 37.             } 38.          }); 39.    } 40. } 

In this chapter, you have learned

  • how to add annotations to Java programs,

  • how to design your own annotation interfaces, and

  • how to implement tools that make use of the annotations.

It is easy to use annotations that someone else designed. It is also easy to design an annotation interface. But annotations are useless without tools. Building annotation tools is undeniably complex. The examples that we presented in this chapter give you an idea of the possibilities, and we expect many far more sophisticated tools to emerge in the future. This chapter has given you background knowledge for evaluating annotation tools, and perhaps has piqued your interest in developing your own tools.



    Core JavaT 2 Volume II - Advanced Features
    Building an On Demand Computing Environment with IBM: How to Optimize Your Current Infrastructure for Today and Tomorrow (MaxFacts Guidebook series)
    ISBN: 193164411X
    EAN: 2147483647
    Year: 2003
    Pages: 156
    Authors: Jim Hoskins

    flylib.com © 2008-2017.
    If you may any questions please contact us: flylib@qtcs.net