domenica, aprile 16, 2006

Dynamic Pojo generation with ASM

At last, i decided to post something about dynamic pojo generation using a bytecode manipulation tool, ASM.
My need was to be able to declare in an easy and fashionable way the fields, and associated getXXX/setXXX of a java pojo, whitout requiring the programmer to hard-code them into the source code. At this time, I ended up with a solution which still requires the presence of a base, emtpy. java class, which is enhanced at load-time with bytecode instrumentation. In future works, this base class can be generated on the fly, providing at least a name for it.

So, consider the simplest java class you can imagine, BasePojo.java:

public class Empty {

}


Given that class, we want to introduce into it a field, author, without modifying its source code. This is possible through ASM, as stated also in the faqs (unfortunatly, that code didn't worked out of the box, and this is the main reason i'm blogging this).

So, I ended up with the following code, which also includes another portion of the code which actually loads the modified bytecode into the jvm.


import java.io.IOException;

import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.FieldVisitor;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;

public class EntityTransformer implements Opcodes {
/*a class for writing fields and methods*/
ClassWriter cw;
/*the name of the target class*/
String className;

private ClassReader cr;

/**
* The default constructor: it generates bytecode for 1.5 JVM,
* creating a default constructor in the target class.
*/
EntityTransformer(String className) throws IOException {
this.className = className;
this.cr = new ClassReader(className);
this.cw = new ClassWriter(cr, true);

this.cw.visit(V1_5, ACC_PUBLIC + ACC_SUPER,
className.replace('.', '/'), null, "java/lang/Object", null);

MethodVisitor mv = cw.visitMethod(ACC_PUBLIC, "", "()V", null,
null);
mv.visitCode();
mv.visitVarInsn(ALOAD, 0);
mv.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "", "()V");
mv.visitInsn(RETURN);
mv.visitMaxs(1, 1);
mv.visitEnd();
}
....

}


Then, here comes the real interesting stuff, the code that actually introduces setter and getter methods:


void createSetter(String propertyName, String type) {
/*
* Field First
*
* Remind yourself: Object Types are in the form L; <-
* Notice the Comma
*/
FieldVisitor fv = cw.visitField(ACC_PRIVATE, propertyName, "L"
+ type.replace('.', '/') + ";", null, null);
fv.visitEnd();

String methodName = "set" + propertyName.substring(0, 1).toUpperCase()
+ propertyName.substring(1);
MethodVisitor mv = cw.visitMethod(ACC_PUBLIC, methodName, "(L"
+ type.replace('.', '/') + ";)V", null, null);
mv.visitCode();
mv.visitVarInsn(ALOAD, 0);
mv.visitVarInsn(ALOAD, 1);
mv.visitFieldInsn(PUTFIELD, className.replace('.', '/'), propertyName,
"L" + type.replace('.', '/') + ";");

mv.visitInsn(RETURN);
mv.visitMaxs(2, 2);
mv.visitEnd();
}

void createGetter(String propertyName, String returnType) {

String methodName = "get" + propertyName.substring(0, 1).toUpperCase()
+ propertyName.substring(1);

MethodVisitor mv = cw.visitMethod(ACC_PUBLIC, methodName, "()L"
+ returnType.replace('.', '/')+";", null, null);
mv.visitCode();
mv.visitVarInsn(ALOAD, 0);
mv.visitFieldInsn(GETFIELD, className.replace('.', '/'), propertyName,
"L"+returnType.replace('.', '/')+";");
mv.visitInsn(ARETURN);

mv.visitMaxs(0, 0);
mv.visitEnd();
}


There can be better ways to do that, especially looking at stuff like "L"+returnType.replace('.', '/')+";": it's almost unreadable, and I could have used the Type class to facilitate that.

Then, the code that loads the produced bytecode into the classloader in-memory repository of loaded classes:

private Class loadClass(byte[] b) {
// override classDefine (as it is protected) and define the class.
Class clazz = null;
try {
ClassLoader loader = ClassLoader.getSystemClassLoader();
Class cls = Class.forName("java.lang.ClassLoader");
java.lang.reflect.Method method = cls.getDeclaredMethod(
"defineClass", new Class[]{String.class, byte[].class,
int.class, int.class});

// protected method invocaton
method.setAccessible(true);
try {
Object[] args = new Object[]{className, b, new Integer(0),
new Integer(b.length)};
clazz = (Class) method.invoke(loader, args);
} finally {
method.setAccessible(false);
}
} catch (Exception e) {
e.printStackTrace();
System.exit(1);
}
return clazz;
}


At last, the easy part:

public Class transform(String[] properties) throws IOException {
for (String prop : properties) {
//property name, type parameter
createSetter(prop, "java.lang.String");
//property name, return type
createGetter(prop, "java.lang.String");
}
return loadClass(cw.toByteArray());
}


Take care not to load the class you want to transform before invoking the transform method, or you'll probably encounter a ClassLinkageError or stuff like that.
That's all for my first techy-post. I'll post some new stuff while coding on it.

Nessun commento: