Ärgernis beim Testen
Beim Testen mit Unittests verwendet man meist Mock-Objecte, die die “Umgebung” der Klasse, die zu Testen ist “simulieren”. Dumm nur, dass diese Umgebung manchmal aus Klassen besteht, die kein Interface implementieren. Wenn dann auch noch einer dieser Klassen “final” ist (oder eine Methode), dann ist man selbst mit so schönen Mock “Generatoren” wie EasyMock am Ende.
EasyMock Classextensions
Zwar kann EasyMock mit den ClassExtensions auch Mock-Objecte von Klassen erzeugen, aber dazu dürfen diese nicht “final” sein. Das hat damit zu tun, dass unter der Haube mit Hilfe der CGLIB eine Ableitung der betreffenden Klasse erzeugt wird, die als Mock-Object dient. Das aber kann nur funktionieren, wann Ableiten erlaubt ist. Hier kann GLUONJ Abhilfe schaffen …
AOP mit GLUONJ
Gluonj ist ein kleines aber feines AOP-Framework, das auf Javaassist aufbaut. Mit Hilfe von Annotationen werden sogenannte “Glues” definiert, die bestimmen, wie der Bytecode verändert werden soll.
Ein Gluon ist eine Java-Klasse, die mit bestimmten Annotation “verziert” wird und so den Code-Weaver von Gluonj steuert. Normalerweise wird der Code Weaver als JavaAgent in die VM eingebunden, was aber nur geht, wenn man einen Startparameter der VM selbst angeben kann. Beim Testen ist sowas eher unpraktisch. Man kann jedoch ganz leicht auch einen ClassLoader schreiben, der eine mit Gluonj-gewobene Klasse zur Laufzeit laden kann.
Ein solcher ClassLoader sieht in etwa so aus:
package com.rinke.solutions.gluonj;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.HashMap;import javassist.gluonj.WeaveException;
import javassist.gluonj.weave.Weaver;/**
* a class loader that uses a gluonj weaver to transform classes.
* @author ster
*/
public class GluonClassloader extends ClassLoader {/**
* the gluonj weaver used by this class loader.
*/
private Weaver weaver = null;
/**
* the parent loader used by the weaver.
*/
private ClassLoader loader;
/**
* the name of the gluon class.
*/
private String glueName;
/**
* cache for all classes already loaded by this class loader.
*/
private HashMap<String,Class> classes = new HashMap<String,Class>();/**
* constructor
* @param loader the class used by the weaver (normally the parent class loader / system class loader )
* @param glueName the class name of the "glue" class (see gluonj documentation)
*/
public GluonClassloader(ClassLoader loader, String glueName) {
super();
this.loader = loader;
this.glueName = glueName;
}/**
* load and transform a the given class.
* @param classname the name of the class to load
* @param resolveIt flag wether to call resolve or not
*/
@Override
public synchronized Class<?> loadClass(String classname, boolean resolveIt) throws ClassNotFoundException {
try {
// init the code weaver, if null
if( weaver == null ) {
weaver = new Weaver(glueName, loader);
}// check cache
Class result = (Class)classes.get(classname);
if( result != null ) { // if cache hit, return it
return result;
}// exclude system classes (wont work)
if( classname.startsWith("java.") || classname.startsWith("javax.")) {
return super.loadClass(classname, resolveIt);
}// load bytes from class path of parent class loader (normal classpath, set by VM)
byte[] orgBytes = loadClassFromClassPath(classname);// if found try to transform it
if( orgBytes != null ) {
byte[] classBytes = weaver.transform(classname, orgBytes);// transformation needed?
if( classBytes == null ) {
// no, do it the normal way
return super.loadClass(classname, resolveIt);
}// generate class from the transformed class
result = defineClass(classname, classBytes, 0, classBytes.length);// if successful
if( result != null ) {
// resolve if, if requested
if( resolveIt ) {
resolveClass(result);
}
// store in my cache
classes.put(classname, result);
// and return it
return result;
}
}
// else there is not such class
throw new ClassNotFoundException(classname + " not found");
} catch (WeaveException e) {
// the weaver has a problem ???
throw new ClassNotFoundException("weaving problems",e);
}
}/**
* load class bytes from system class path using getSystemResourceAsStream
* @param name the name of the class
* @return the classes bytes or null if not found
*/
private byte[] loadClassFromClassPath(String name) {
String filename = name.replace ('.', '/') + ".class";
try {
byte[] buffer = new byte[10000];
ByteArrayOutputStream bos = new ByteArrayOutputStream();
InputStream i = getSystemResourceAsStream(filename);
if( i != null ) {
while(i.available() > 0 ) {
int r = i.read(buffer);
if( r < 0) {
break;
}
bos.write(buffer, 0, r);
}
i.close();
bos.close();
return bos.toByteArray();
}
} catch (IOException e) {
}
return null;
}/**
* convenience method
*/
@Override
public Class<?> loadClass(String classname) throws ClassNotFoundException {
return loadClass(classname, true);
}
}
Hat man nun eine Final-Klasse, die für einen Test verändert werden soll …
package test;
public final class FinalTest {
public String getHello() {
return "this class is final";
}}
dann kann man mit Gluonj eine Gluon-Klasse definieren, die die FinalTest Klasse wie gewünscht ändert:
package com.rinke.solutions.gluonj;
import javassist.gluonj.Glue;
import javassist.gluonj.Privileged;
import javassist.gluonj.Refine;/**
* just a test glue, that refines a final class
* @author ster
*/
@Glue public class MyGlue {
@Privileged @Refine("test.FinalTest")
public static class Diff {
public String getHello() {
return "hello";
}
}}
So wird zwar nicht von der finalen Klassen abgeleitet (das ist ja verboten 😉 ) aber der GluonClassloader ändert die Klasse beim Laden wie gewünscht ab.
Jetzt braucht man nur noch ein Test-Programm, welches das Ganze aufruft und den GluonClassloader nutzt:
package com.rinke.solutions.gluonj;
import java.lang.reflect.Method;
public class Main {
@SuppressWarnings("unused")
private String[] args;public Main(String[] args) {
this.args = args;
}/**
* @param args
*/
public static void main(String[] args) {
Main r = new Main(args);
r.run();
}public void run() {
GluonClassloader myLoader =
new GluonClassloader(this.getClass().getClassLoader(),
"com.rinke.solutions.gluonj.MyGlue");try {
Class<?> c = myLoader.loadClass("test.FinalTest");// do not cast to FinalTest, because its loaded by a different class loader
Object ft = c.newInstance();
Method method = c.getMethod("getHello", new Class[] {});
Object result = method.invoke(ft, new Object[]{});if( result.equals("hello") ) {
System.out.println("Test succeeded");
}} catch (Exception e) {
e.printStackTrace();
}
}}
Zu beachten ist hierbei, dass die FinalTest-Klassen nicht auch im Main benutzt wird, weil die Klasse, ja dann schon vom SystemClassLoader geladen würde. Ein
FinalTest ft = (FinalTest) c.newInstance();
würde z.B. eine ClassCast-Exception auslösen, weil c vom GluonClassloader geladen wurde und “FinalTest” in Main aber vom SystemClassLoader.
Mit Gluonj lassen sich noch weitere “hübsche” Dinge anstellen (siehe Gluonj-Dokumentation), die sich nicht nur für Tests nutzen lassen.