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
Pages: 1, 2

Reading J2SE 5.0 Annotations

As shown above, annotations can be generated and accessed from Java 5 code; however, it would be interesting to access these annotations from older JVMs. Let's see how an adapter class, similar to the Java 5 reflection API, could use the ASM toolkit to access this information.



Here is the public part of the AnnotatedClass adapter.

public class AnnotatedClass {
  private AnnReader r;

  public AnnotatedClass(Class c) {
    try {
      URL u = c.getResource("/"+
          c.getName().replace('.', '/')+".class");
      r = new AnnReader(u.openStream());
    } catch(IOException ex) {
      throw new RuntimeException(ex.toString());
    }
  }
  
  public AnnotatedClass(InputStream is) {
    try {
      r = new AnnotationReader(is);
    } catch(IOException ex) {
      throw new RuntimeException(ex.toString());
    }
  }

  public Ann[] getAnnotations() {
    List anns = r.getClassAnnotations();
    return (Ann[]) anns.toArray(new Ann[0]);
  }

  ... 
}

The method getAnnotations() substitutes for a new method with the same name in the Java 5 API. However, because java.lang.annotation.Annotation class can't be used, our method return the marker interface Ann.

public static interface Ann {
}

Client code that uses the above class would cast the received Ann instance into the corresponding interface.

Class c =  Calculator2.class;
AnnotatedClass ac = new AnnotatedClass(c);
Ann[] anns = ac.getAnnotations();
if( anns[0] instanceof Marker) {
    String value = ((Marker)anns[0]).value();
    ...
}

The tricky part is that the Marker annotation class can't be used directly with older JREs, because its bytecode version is only accepted by Java 5 VM and it contains a few additional flags not recognized by the older JVMs. However, it is easy to transform it on the fly and make it a plain Java interface by comparing the results produced by the ASMifierClassVisitor utility or just manually creating and compiling such an interface to be used with old JREs.

Annotation data is loaded by the AnnReader class, which extends ASM's ClassAdapter and redefines the visitAttribute(), visitField(), and visitMethod() methods.

public class AnnReader 
      extends ClassAdapter {
  private List classAnns = new ArrayList();
  private Map fieldAnns = new HashMap();
  private Map methodAnns = new HashMap();
  private Map methodParamAnns = new HashMap();


  public AnnReader(InputStream is) 
      throws IOException {
    super(null);
    ClassReader r = new ClassReader(is);
    r.accept(this, 
        Attributes.getDefaultAttributes(), true);
  }

  public void visitAttribute(Attribute attr) {
    classAnns.addAll(loadAnns(attr));
  }
  
  public void visitField(int access, 
        String name, String desc, Object value, 
        Attribute attrs) {
    fieldAnns.put(name+desc, loadAnns(attrs));
  }
  
  public CodeVisitor visitMethod(int access, 
        String name, String desc, 
        String[] exceptions, Attribute attrs) {
    methodAnns.put(name+desc, loadAnns(attrs));
    methodParamAnns.put(name+desc, 
        loadParamAnns(attrs));
    return null;
  }

  ... 

The loadAnns() and loadParamAnns() methods are very straightforward. They just iterate through annotations and collect all values into a List, using the loadAnn() method. Each element in the List would be a dynamic proxy that implements the Ann interface and the interface declared by the annotation (e.g., Marker).

  private List loadAnns(Attribute a) {
    List anns = new ArrayList();
    while(a!=null) {
      if(a instanceof 
          RuntimeVisibleAnnotations) {
        RuntimeVisibleAnnotations ra = 
            (RuntimeVisibleAnnotations) a;
        addAnns(anns, ra.annotations);
      } else if(a instanceof 
          RuntimeInvisibleAnnotations) {
        ...
      }
      a = a.next;
    }
    return anns;
  }

  private List loadParamAnns(Attribute a) {
    List anns = new ArrayList();
    while(a!=null) {
      if(a instanceof 
          RuntimeVisibleParameterAnnotations) {
        RuntimeVisibleParameterAnnotations ra = 
          (RuntimeVisibleParameterAnnotations) a;
        addParamAnns( anns, ra.parameters);
      } else if(a instanceof 
          RuntimeInvisibleParameterAnnotations) {
        ...
      }
      a = a.next;
    }
    return anns;
  }

  private void addParamAnns( List anns, List params) {
    for(Iterator it = params.iterator(); it.hasNext();) {
      List paramAttrs = (List) it.next();
      List paramAnns = new ArrayList();
      addAnns(paramAnns, paramAttrs);
      anns.add(paramAnns);
    }
  }

  private void addAnns(List anns, List attr) {
    for(int i = 0; i<attr.size(); i++) {
      anns.add(loadAnn((Annotation) attr.get(i)));
    }
  }

Method loadAnn() is responsible for creating a dynamic proxy from the values retrieved from an Annotation object. The proxy is created using AnnInvocationHandler, which tries to find a value in the map with the same key as the method name. It is also creates a summary in case toString() is called, and throws a RuntimeException otherwise.

  private Object loadAnn(Annotation annotation) {
    String type = annotation.type;
    List vals = annotation.elementValues;
    List nvals = new ArrayList(vals.size());
    for(int i = 0; i < vals.size(); i++) {
      Object[] element = (Object[]) vals.get(i);
      String name = (String) element[0];
      Object value = getValue(element[1]);
      nvals.add(new Object[] { name, value});
    }
    
    try {
      Type t = Type.getType(type);
      String cname = t.getClassName();
      Class typeClass = Class.forName(cname);
      ClassLoader cl = getClass().getClassLoader();
      return Proxy.newProxyInstance(cl, 
          new Class[] { Ann.class, typeClass}, 
          new AnnInvocationHandler(type, nvals));
    
    } catch(ClassNotFoundException ex) {
      throw new RuntimeException(ex.toString());
    
    }
  }

Finally, the getValue() method recursively converts annotation values into Java types. It also wraps nested annotations into dynamic proxies using the loadAnn() method.

  private Object getValue(Object value) {
    if (value instanceof EnumConstValue) {
      // TODO convert to java.lang.Enum adapter
      return value;
    }
    if (value instanceof Type) {
      String cname = ((Type)value).getClassName();
      try {
        return Class.forName(cname);
      } catch(ClassNotFoundException e) {
        throw new RuntimeException(e.toString());
      }
    }
    if (value instanceof Annotation) {
      return loadAnn(((Annotation) value));
    }
    if (value instanceof Object[]) {
      Object[] values = (Object[]) value;
      Object[] o = new Object[ values.length];
      for(int i = 0; i < values.length; i++) {
        o[ i] = getValue(values[ i]);
      }
      return o;
    }

    return value;
  }

In fact, the above code allows you to read annotation data that is not available through the Java 5 reflection API. For example, you can retrieve annotations with RetentionPolicy.CLASS.

Conclusion

J2SE 5's annotation facility opens new possibilities for declarative component configuration. Some scenarios may require the dynamic manipulation of annotations in the runtime, and this is provided by the ASM toolkit, which offers complete support for bytecode attributes used to persist Java 5 annotation data. It also allows access to those attributes from older JREs and can even read non-visible annotations at runtime.

Resources

Eugene Kuleshov is an independent consultant with over 15 years of experience in software design and development.


Return to ONJava.com.