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]
[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.
|