Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

how to change field‘s initial value using javassist

Tags:

java

javassist

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.

like image 485
kingpeter Avatar asked Oct 05 '22 17:10

kingpeter


1 Answers

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

like image 179
longhua Avatar answered Oct 09 '22 20:10

longhua