ONJava.com -- The Independent Source for Enterprise Java
oreilly.comSafari Books Online.Conferences.

advertisement

AddThis Social Bookmark Button

Create and Read J2SE 5.0 Annotations with the ASM Bytecode Toolkit Create and Read J2SE 5.0 Annotations with the ASM Bytecode Toolkit

by Eugene Kuleshov
10/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
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 an attrs parameter of this method.
  • The ClassVisitor.visitMethod() method receives events for every method, so both method and method parameter annotations are passed as an attrs parameter 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.

  ...
  ClassWriter cw = new ClassWriter(false);
  CodeVisitor cv;

  cw.visit(V1_5, ACC_PUBLIC + ACC_SUPER,
-     "Calculator1", "java/lang/Object", null, 
-     "Calculator1.java");
+     "Calculator2", "java/lang/Object", null, 
+     "Calculator2.java");
+ // FIELD ATTRIBUTES
+ RuntimeInvisibleAnnotations fAtt1 = 
+   	new RuntimeInvisibleAnnotations();
+ Annotation fAtt1ann0 = new Annotation("LMarker;");
+ fAtt1ann0.add( "value", "Field");
+ fAtt1.annotations.add( fAtt1ann0);
  cw.visitField(ACC_PRIVATE, "result", "I", null,
-     null);
+     fAtt1);

  {
  cv = cw.visitMethod(ACC_PUBLIC, "<init>", "()V", 
      null, null);
  cv.visitVarInsn(ALOAD, 0);
  cv.visitMethodInsn(INVOKESPECIAL, 
      "java/lang/Object", "<init>", "()V");
  cv.visitInsn(RETURN);
  cv.visitMaxs(1, 1);
  }
  {
+ // METHOD ATTRIBUTES
+ RuntimeInvisibleParameterAnnotations mAtt1 = 
+   	new RuntimeInvisibleParameterAnnotations();
+ List mAtt1p0 = new ArrayList();
+ mAtt1.parameters.add( mAtt1p0);
+
+ List mAtt1p1 = new ArrayList();
+ Annotation mAtt1p1a0 = new Annotation("LMarker;");
+ mAtt1p1a0.add( "value", "Parameter");
+ mAtt1p1.add( mAtt1p1a0);
+ mAtt1.parameters.add( mAtt1p1);
+
+ RuntimeInvisibleAnnotations mAtt2 = 
+   	new RuntimeInvisibleAnnotations();
+ Annotation mAtt2a0 = new Annotation("LMarker;");
+ mAtt2a0.add( "value", "Method");
+ mAtt2.annotations.add( mAtt2a0);
+
+ mAtt1.next = mAtt2;
+
  cv = cw.visitMethod(ACC_PRIVATE, "sum", "(II)V",
-     null, null);
+     null, mAtt1);
  cv.visitVarInsn(ALOAD, 0);
  cv.visitVarInsn(ILOAD, 1);
  cv.visitVarInsn(ILOAD, 2);
  cv.visitInsn(IADD);
- cv.visitFieldInsn(PUTFIELD, "Calculator1",
+ cv.visitFieldInsn(PUTFIELD, "Calculator2",
      "result", "I");
  cv.visitInsn(RETURN);
  cv.visitMaxs(3, 3);
  }
+ {
+ // CLASS ATRIBUTE
+ RuntimeInvisibleAnnotations attr = 
+     new RuntimeInvisibleAnnotations();
+ Annotation attrann0 = new Annotation("LMarker;");
+ attrann0.add( "value", "Class");
+ attr.annotations.add( attrann0);
+ cw.visitAttribute(attr);
+ }
  cw.visitEnd();
  ...

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

Next Pagearrow