Create and Read J2SE 5.0 Annotations with the ASM Bytecode Toolkit
by Eugene Kuleshov10/20/2004
The previous article in this series showed how the ASM toolkit can be used to generate new code, and modify existing classes by adding or changing code. This is suitable for many cases, but there are times when it is necessary to stick some meta-information into the class bytecode and then access it later on. A typical example of such metadata is the annotation facility introduced in J2SE 5.0.
Bytecode Attributes
Annotations are actually stored in bytecode with several special attributes. The
binary format for these and all other standard attributes is described in the
Java Virtual Machine (JVM) specification
(which has been updated for JDK 1.5). Here is a short outline of attributes supported by ASM's
org.objectweb.asm.attrs package.
EnclosingMethod
Used for anonymous or local classes.LocalVariableTypeTable
Used by debuggers to determine the value of a given local variable during the execution of a method.
The following attributes have been introduced in the J2SE 5.0 VM.
Signature
Introduced in JSR-14 ("Adding Generics to the Java Programming Language") and used for classes, fields, and methods to carry generic type information in a backwards-compatible way.SourceDebugExtension
Defined in JSR-45 ("Debugging Support for Other Languages") and used for classes only. This attribute allows debuggers keep a reference to the original non-Java source.RuntimeInvisibleAnnotations,RuntimeInvisibleParameterAnnotations,RuntimeVisibleAnnotations,RuntimeVisibleParameterAnnotations,AnnotationDefault
Used to store annotations as defined in JSR-175 ("A Metadata Facility for the Java Programming Language").
There is also an attribute that hasn't been included in the J2SE 5.0 release but may be added in the future. For now this attribute is used for J2ME MIDlets and is generated by the CLDC preverifier tool.
StackMap
Contains information for the two-step bytecode verifier used by CDLC; its definition is given in the appendix "CLDC Byte Code Typechecker Specification" in the CLDC 1.1 specification.
All other nonstandard attributes will be ignored by Sun's JVM, although vendors may use proprietary
attributes to implement additional features without breaking bytecode compatibility.
For example, Microsoft's JVM used the attributes ActualAccessFlags,
Hidden, LinkUnsafe, NAT_L, and NAT_L_DCTS
to enable bindings to native libraries without using JNI. These attributes were
generated by the MS jvc java compiler, based on instructions in its JavaDoc. For example, the code
below will create the attribute NAT_L:
/**
* @dll.import("SHELL32",auto)
*/
private native static boolean
ShellExecuteEx(SHELLEXECUTEINFO pshex);
Nonstandard attributes could be also used by some application containers to persist proprietary metadata in the bytecode. However, the metadata attributes introduced in Java 5, as mentioned above, eliminate the need for such custom attributes.
Java 5 Annotations Support in ASM
ASM provides a generic API for bytecode attributes.
All attributes supported by ASM extend the Attribute class
and override the read() and write()
methods in order to load and save attribute structures. Concrete attribute classes
used to represent annotations are shown in Figure 1.
All of them use the Annotation data object to store
the actual values for annotations.

Figure 1. UML class diagram for ASM annotation attributes
RuntimeVisibleAnnotations and RuntimeInvisibleAnnotations
contain Lists of Annotations.
RuntimeVisibleParameterAnnotations and RuntimeInvisibleParameterAnnotations
contain Lists of Lists of Annotations in order to handle
multiple annotations for each method parameter.
When parsing existing bytecode, ClassReader builds
concrete attributes from the bytecode data and sends them as
parameters to the appropriate visit... methods of ClassVisitor and
CodeVisitor. The same events can be generated manually,
as we will see later.
- The
ClassVisitor.visitAttribute()method receives events for class-level attributes, and it is where annotations for class will be passed. - The
ClassVisitor.visitField()method receives events for fields, so all field annotations are passed as anattrsparameter of this method. - The
ClassVisitor.visitMethod()method receives events for every method, so both method and method parameter annotations are passed as anattrsparameter of this method.
Notice that the ClassVisitor.visitAttribute() and CodeVisitor.visitAttribute()
methods are called for every attribute. Attributes in ClassVisitor.visitField()
and ClassVisitor.visitMethod() are represented as a linked list.
The Java Virtual Machine specification defines structures and restrictions
for all attributes, and I recommend keeping the "Class File Format" chapter handy.
However, the ASMifier utility can help to implement required
transformations with minimal knowledge of bytecode.
Let's pick a simple class and apply a custom Marker
annotation to see how it will be handled by the ASM API. Here's
a trivial Calculator1 class:
public class Calculator1 {
private int result;
private void sum( int i1, int i2) {
result = i1 + i2;
}
}
Here is the definition of the Marker annotation.
@Retention(RetentionPolicy.RUNTIME)
public @interface Marker {
String value();
}
And this is an annotated version of Calculator class, called Calculator2.
@Marker("Class")
public class Calculator2 {
@Marker("Field")
private int result;
@Marker("Method")
private void sum( int i1, @Marker("Parameter") int i2) {
result = i1 + i2;
}
}
Now we can compile Calculator1 and Calculator2
and run ASMifierClassVisitor on both compiled classes,
and then compare the results to see the ASM API calls required to add annotation
attributes into bytecode.
The comparison result is below.
Red lines represent code without annotations and green lines represent
code that has been added to generate annotation attributes
in bytecode for the Calculator2 class.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
It's common practice to generate or transform classes at runtime using a custom
ClassLoader. We can also use this technique to add Java 5 annotations.
A ClassLoader implementation may use the following code to do the required
transformation on loaded classes.
ClassWriter cw = new ClassWriter(false);
try {
ClassReader cr =
new ClassReader(url.openStream());
cr.accept(new MarkerClassVisitor(cw),
Attributes.getDefaultAttributes(), false);
byte[] b = cw.toByteArray();
return defineClass( name, b, 0, b.length);
} catch( Exception ex) {
throw new ClassNotFoundException(
"Unable to load class "+name);
}
The actual transformation is done by MarkerClassVisitor.
It changes the bytecode version in the visit() method and
adds a class-level Marker annotation using the code
from the above comparison, before delegating the call to the
visitEnd() method of the chained ClassVisitor.
public static class MarkerClassVisitor
extends ClassAdapter {
public MarkerClassVisitor(ClassVisitor cv) {
super(cv);
}
public void visit( int version, int access,
String name, String superName,
String[] interfaces, String sourceFile) {
super.visit(Constants.V1_5, access, name,
superName, interfaces, sourceFile);
}
public void visitEnd() {
String t = Type.getDescriptor(Marker.class);
Annotation ann = new Annotation(t);
ann.add("value", "Class");
RuntimeVisibleAnnotations attr =
new RuntimeVisibleAnnotations();
attr.annotations.add(ann);
cv.visitAttribute(attr);
super.visitEnd();
}
}
Below is a simple JUnit test case that uses the Java 5 reflection API to
verify that the Marker annotation has been created. You can find the
complete source code in the Resources section below.
public class MarkerClassLoaderTest extends TestCase {
public void testLoadClass() throws Exception {
MarkerClassLoader cl =
new MarkerClassLoader(getClass());
Class c = cl.loadClass( "asm.Calculator1");
Annotation a = c.getAnnotation(Marker.class);
assertNotNull( "Expecting Marker", a);
}
}
Pages: 1, 2 |