Remove final modifier from 3rd party classes
I need to test write a JUnit test to test the following line:
CSVRecord csvRecord = csvReader.readCsv(filename);
CSVRecord
from org.apache.commons.csv is the final class. If I try to test this with 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 separate the “final” modifier from CSVRecord
. I tried it with javassist. However, I encountered an error. Take a look at this minimalist 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;
}
}
}
In its own class file
public final class FinalClass {
}
The following error occurred
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 cannot change the definition of a loaded class in this way.
The problem is constructing FinalClass.class.getName()
or more specifically, the class literal FinalClass.class
which has indeed loaded the class to generate the associated Class object, loading the runtime representation of the class.
Assuming you haven’t used the class in any other way before that, you can simply 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());
Change the definition of a class before creating a runtime representation.