![]() |
|
||||||||
| Consortium Activities Projects Forge Events | |||||||||
ASM |
by Eugene Kuleshov |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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);
}
}
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.
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.