Search code examples
javajunitjavassist

Remove final modifier from 3rd party class


I need to test write a JUnit test which tests the following line:

CSVRecord csvRecord = csvReader.readCsv(filename);

with CSVRecord from org.apache.commons.csv being a final class. If I try to tests this using EasyMock I get the following error:

java.lang.IllegalArgumentException: Cannot subclass final class pathname.FinalClass
at org.easymock.cglib.proxy.Enhancer.generateClass(Enhancer.java:565)
at org.easymock.cglib.core.DefaultGeneratorStrategy.generate(DefaultGeneratorStrategy.java:25)
at ...

So I need to detach the "final" modifier from the CSVRecord. I tried this with javassist. However, I am running into an error. Have a look at this minimalistic example:

public class MyTestClass extends EasyMockSupport {

    @Mock
    private MockedClass mockedClass;

    @TestSubject
    private MyClass classUnderTest = new AmountConverter();

    @Test
    public void testName() throws Exception {
        ClassPool pool = ClassPool.getDefault();
        CtClass ctClass = pool.get(FinalClass.class.getName());
        ctClass.defrost();
        removeFinal(ctClass);
        FinalClass finalClass = (FinalClass) EasyMock.createMock(ctClass.toClass());
        expect(mockedClass.foo()).andReturn(finalClass);

        replayAll();

        classUnderTest.foo();
    }

        static void removeFinal(CtClass clazz) throws Exception {
        int modifiers = clazz.getModifiers();
        if(Modifier.isFinal(modifiers)) {
            System.out.println("Removing Final");
            int notFinalModifier = Modifier.clear(modifiers, Modifier.FINAL);
            clazz.setModifiers(notFinalModifier);
        }
    }
}

with

public class MyClass {

    @Inject
    private MockedClass mockedClass;

    public void foo() {
        mockedClass.foo();
    }

    class MockedClass {

        FinalClass foo() {
            return null;
        }

    }
}

and in it's own class file

public final class FinalClass {

}

I get the following error

javassist.CannotCompileException: by java.lang.LinkageError: loader (instance of  sun/misc/Launcher$AppClassLoader): attempted  duplicate class definition for name: "pathname/FinalClass"
at javassist.ClassPool.toClass(ClassPool.java:1099)
at javassist.ClassPool.toClass(ClassPool.java:1042)
at javassist.ClassPool.toClass(ClassPool.java:1000)
at javassist.CtClass.toClass(CtClass.java:1224)
...

Solution

  • You can not change the definition of an already loaded class this way.

    The problem is that the construct FinalClass.class.getName() or more specific, the class literal FinalClass.class, does already load the class to produce the associated Class object, the runtime representation of the loaded class.

    Assuming that you are not using the class in any other way before that, you simply have to change the code to

    ClassPool pool = ClassPool.getDefault();
    CtClass ctClass = pool.get("qualified.name.of.FinalClass");
    ctClass.defrost();
    removeFinal(ctClass);
    FinalClass finalClass = (FinalClass) EasyMock.createMock(ctClass.toClass());
    

    to change the definition of the class before its runtime representation is created.