Question

I'm trying to mock some services in my developing env. The serviceFactory code is something like:

public class ApiFacadeImpl implements ApiFacade {
private OneService oneService = null;
    public OneService getOneService(){
        if(oneService==null) {//some initialization steps }
        return oneService;
    }
}

I know this factory is not well coded and designed. However I can't modify its code. my idea is to change the bytecode, so I can redefine it to something like:

public class ApiFacadeImpl implements ApiFacade {
private OneService oneService = new MyMockOneService();
    ....
}

My 1st is: is this possible using javassist? And how?

Since I can't find anything like reinitialize a field using javassist using google, I tried myself by deleting it and recreate it:

        CtField oneServiceField = cc.getDeclaredField("oneService");
    cc.removeField(oneServiceField);
    CtField f = CtField.make(String.format("private %s %s=new %s();",
            oneServiceField.getType().getName(), "oneService",
            mockClass.getCanonicalName()), cc);
    cc.addField(f);
    cc.toClass();

then I got exception:

javassist.CannotCompileException: by java.lang.ClassFormatError: Invalid length 99 in LocalVariableTable in class file com/Test
at javassist.ClassPool.toClass(ClassPool.java:1051)
at javassist.ClassPool.toClass(ClassPool.java:994)
at javassist.ClassPool.toClass(ClassPool.java:952)
at javassist.CtClass.toClass(CtClass.java:1079)

My 2nd question is why this exception? Which step disobey java class's definition? And when I delete the field does javassist help to delete field reference in:

public OneService getOneService(){
        if(oneService==null) {//some initialization steps }
        return oneService;
    }

Many thanks.

Was it helpful?

Solution

For question #1, yes, you can achieve this via modify ApiFacadeImpl's constructor. Remember to use CtConstructor#insertAfter to append the assignment statement.

ClassPool pool = ClassPool.getDefault();
CtClass factoryClass = pool.getCtClass("ServiceFactory");
CtConstructor constructor = factoryClass.getDeclaredConstructor(null);
String setMockStatement = String.format("service = new %s();",
        MockServiceImpl.class.getCanonicalName());
constructor.insertAfter(setMockStatement);
factoryClass.toClass();
new ServiceFactory().getService().say();

I tried the way you proposed. However, it didn't work. After some debuggings, I found that if we remove a field, it won't remove the initialization statement for this field. If we add the field again with our expected initialization statement, it will executed before the original initialization statement. So the service field is assigned to MockServiceImpl at first and then assigned to null. Please refer to the following javassist bug.

javassist-3.14.0-GA - Problm in default initializer of class variables when they are deleted and created using removeField and addField, CtField.make

For question #2, I am not sure why this happens. What is your javassist version? How does your mockClass look like? Could you please refer to the following javassist bug?

Javassist causes java.lang.ClassFormatError: Invalid length 561 in LocalVariableTable in class file

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top