Java – metaprogramming is possible?

January 14, 2011 at 6:37 pm (JRuby, Programming)

Don’t worry, the answer for this question is yeah it’s possible, but there isn’t a natural way to do, the language for itself doesn’t offer options, but we can do through bytecode manipulation, in my opinion this way isn’t exactly a invite to use metaprogramming.
For bytecode manipulation we have two popular options: ASM and javassist, the first is faster but bring complexity in api, the second is more easily to use but is considerably slower than first.

Imagine that we have:

public class SomeClass {
...
}

and:

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface Marker {
    String value() default "";
}

I would like to add Marker annotation to SomeClass in runtime, so let’s see a helper with two implementations one using ASM and the other Javassist:

ASM

public class AnnotationHelper {

	private static final AsmUtilClassLoader classLoader = new AsmUtilClassLoader();


	public static <T> Class<T> addClassAnnotation(Class<T> clazz, Class<? extends Annotation> annotation) {
		try {
			ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS);
			new ClassReader(clazz.getName()).accept(new AnnotationClassAdapter(cw, annotation), 0);
			return (Class<T>) classLoader.defineAnnotatedClass(cw, clazz.getName());
		} catch (Exception e) {
			throw new IllegalArgumentException();
		}
	}

	private static class AnnotationClassAdapter extends ClassAdapter {
		private Class<? extends Annotation> annotation;

		private ClassWriter cw;

		public AnnotationClassAdapter(ClassWriter cw, Class<? extends Annotation> annotation) {
			super(cw);
			this.cw = cw;
			this.annotation = annotation;
		}

		@Override
		public void visit(int version, int access, String name,
				String signature, String superName, String[] interfaces) {
			super.visit(version, access, name, signature, superName, interfaces);
			AnnotationVisitor av0;

			av0 = cw.visitAnnotation("L" + annotation.getName().replaceAll("\\.", "/") + ";", true);
			av0.visit("value", "Class");
			av0.visitEnd();
		}
	}

	private static class AsmUtilClassLoader extends ClassLoader {
		private Class<?> defineAnnotatedClass(ClassWriter cw, final String name)  throws ClassNotFoundException {
			byte[] b = cw.toByteArray();
			return defineClass(name, b, 0, b.length);
		}
	}

}

Javassist

public class AnnotationHelper {

	private static final JavassistUtilClassLoader classLoader = new JavassistUtilClassLoader();

	public static <T> Class<T> addClassAnnotation(Class<?> clazz, Class<? extends Annotation> annotation) {
		try {
			CtClass ctClass = ClassPool.getDefault().get(clazz.getName());

			ClassFile classFile = ctClass.getClassFile();
			ConstPool constantPool = classFile.getConstPool();
			AnnotationsAttribute attr = new AnnotationsAttribute(constantPool, AnnotationsAttribute.visibleTag);
			javassist.bytecode.annotation.Annotation a = new javassist.bytecode.annotation.Annotation(annotation.getName(), constantPool);
			attr.setAnnotation(a);
			classFile.addAttribute(attr);
			classFile.setVersionToJava5();
			return (Class<T>) classLoader.defineAnnotatedClass(ctClass);
		} catch (Exception e) {
			throw new IllegalArgumentException();
		}

	}

	private static class JavassistUtilClassLoader extends ClassLoader {

		private Class<?> defineAnnotatedClass(CtClass ctClass)  throws ClassNotFoundException, IOException, CannotCompileException {
			byte[] b = ctClass.toBytecode();
			return defineClass(ctClass.getName(), b, 0, b.length);
		}
	}
}

testing:

Class<SomeClass> c = AnnotationHelper.addClassAnnotation(SomeClass.class, Marker.class);
Annotation a = c.getAnnotation(Marker.class);
System.out.println(a instanceof Marker); //#=> true

On this helper (two implementations) we have to be careful about custom classloader holding the changed class while de default classloader hold the original class, we should have a ClassCastException.

JRuby

It isn’t really a java option, but Jruby has a great java interoperabillity with some facilities to do what we want, in my opinion this is a approach to invite to use metaprogramming:

SomeClass.add_class_annotation({ Marker => {} })
SomeClass.become_java!

Jruby uses ASM for instrumentation, is also a helper like the others, but the essence is different, it’s a core feature, providing to you a easy way to use metaprogramming without the overhead of learn bytecode frameworks.

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: