|
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
then we add the bytecodes for the following statement at the beginning of the method:
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:
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.
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 Example 13-7. Item.java1. 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 Modifying Bytecodes at Load TimeIn 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:
To launch a Java program together with the agent, use the following command-line options:
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.java1. 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
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. |
|