Search code examples
javamockitocglib

Why does cglib fail to call the MethodInterceptor callback?


In the unittest class show below, one of the tests fails and the other succeeds. Both tests create a CGLIB object with an interceptor mock and attempt to verify that the interceptor is interacted with. The tests differ on the class that is dynamically subclassed with CGLIB. The test that succeeds subclasses a plain Java interface; the one that fails subclasses a dynamic subclass (created with Mockito). Why is that first test failing?

Thanks.

public class ATest {

    @Test
    // This test fails.
    public void cannotCallMethodOnMockWrapper() throws Throwable {
        final Class<? extends Bar> c = Mockito.mock(Bar.class).getClass();
        verifyInterceptorIsInvokedOnCallToObjectOfEnhanced(c);
    }

    @Test
    // This test succeeds.
    public void interceptorIsCalled() throws Throwable {
        final Class<? extends Bar> c = Bar.class;
        verifyInterceptorIsInvokedOnCallToObjectOfEnhanced(c);
    }

    private void verifyInterceptorIsInvokedOnCallToObjectOfEnhanced(
            final Class<? extends Bar> c) throws Throwable {
        final MethodInterceptor interceptor = Mockito.mock(
                MethodInterceptor.class);
        final Bar wrapper = (Bar) Enhancer.create(c, interceptor);

        // Here is where the failing test chokes with exception:
        // NoSuchMethodError
        wrapper.foo();

        verifyInterceptIsCalledOn(interceptor);
    }

    private void verifyInterceptIsCalledOn(
            final MethodInterceptor interceptor) throws Throwable {
        verify(interceptor).intercept(any(), any(Method.class), 
                any(Object[].class), any(MethodProxy.class));
    }

    public static interface Bar {
        void foo();
    }

}

UPDATE:

The stack trace of the failure

java.lang.NoSuchMethodError: java.lang.Object.foo()V
    at ATest$Bar$$EnhancerByMockitoWithCGLIB$$2e1d601f.foo(<generated>)
    at ATest.verifyInterceptorIsInvokedOnCallToObjectOfEnhanced(ATest.java:37)
    at ATest.cannotCallMethodOnMockWrapper(ATest.java:19)

Also, as to whether a Mockito mock is a CGLIB-enhanced class, the test below (which fails) seems to suggest that it is not:

public class ATest {

    @Test
    public void mockitoMockIsCGLIBEnhanced() {
        assertTrue(Enhancer.isEnhanced(Mockito.mock(Bar.class).getClass()));
    }

    public static interface Bar {
        void foo();
    }

}

Solution

  • Without a stacktrace I'll assume you have a net.sf.cglib.core.CodeGenerationException probably with cause like InvocationTargetException or ClassFormatError,

    And that happen because CGLIB can't enhance an already enhanced class that it created itself. As Mockito is using CGLIB internally on the JVM, you cannot enhance a Mockito class. you need to enhance the original class. Plus even if it was possible the callback is an instance you need pass to the generated class, so by creating your own enhancer you don't have the Mockito callback for the upper class. So the mockito mock functionality won't even work. Anyway, I'm diverging from the topic.

    So basically you cannot enhance an enhanced class with CGLIB, if this issue is happening at runtime because you passed a mock, I believe you should have a code like this, check if it's enhanced, if yes use the superclass (the original class) and/or the interfaces :

    public class CGLIBEnhancerEnhancer implements TypeEnhancer {
        public void Object enhance(Object objectCandidateToEnhance, MethodInterceptor interceptor) {
            Class classCandidateToEnhance = classCandidateToEnhance.getClass();
            if(Enhancer.isEnhanced(classCandidateToEnhance)
               || Mockito.mockingDetails(objectCandidateToEnhance).isMock()) {
                // safe with CGLIB (2.x) enhanced class 
                return (Bar) Enhancer.create(
                        classCandidateToEnhance.getSuperclass(),
                        classCandidateToEnhance.getInterfaces(),
                        interceptor
                    );
            } else
                return (Bar) Enhancer.create(classCandidateToEnhance, interceptor);
            }
        }
    }
    

    EDIT: I ran the given example and it indeed give me the CodeGenerationException, that you can see at the end of this answer. Though maybe depending on the environment you can see a different stacktrace like the one you posted.

    Given your exception I believe you might have an issue at runtime of how the double enhanced class is created. As the stacktrace suggest that the instance doesn't even have a foo method, it's like the object (which seems to be a real mockito mock) was generated from the wrong type. I would look at the API of the Enhancer to use correctly the type and interfaces to create the enhanced class, you could use a new instance of the Enhancer or use static methods.

    Also if you are proving the interceptor yourself, and that you want to execute the behavior of the given instance (mock or not), you should craft your interceptor such that it will hold a reference to the original object.

    EDIT 2: Actually Mockito is repackaging/jarjar/inline CGLIB due to technical reasons, this means that net.sf.cglib.proxy.Enhancer cannot detect a mockito mock. Instead one should use the newly introduced API in 1.9.5 Mockito.mockingDetails(instance).isMock() to detect a mockito mock. Or use the repackaged/jarjared/inlined org.mockito.cglib.proxy.Enhancer. In the end you'll need Mockito in your classpath to detect a Mockito mock.

    Hope that helps

    org.mockito.cglib.core.CodeGenerationException: java.lang.reflect.InvocationTargetException-->null
        at org.mockito.cglib.core.AbstractClassGenerator.create(AbstractClassGenerator.java:238)
        at org.mockito.cglib.proxy.Enhancer.createHelper(Enhancer.java:378)
        at org.mockito.cglib.proxy.Enhancer.create(Enhancer.java:286)
        at org.mockito.cglib.proxy.Enhancer.create(Enhancer.java:664)
        at ATest.verifyInterceptorIsInvokedOnCallToObjectOfEnhanced(ATest.java:32)
        at ATest.cannotCallMethodOnMockWrapper(ATest.java:18)
        ... removed
        at org.junit.runner.JUnitCore.run(JUnitCore.java:157)
        ... removed
    Caused by: java.lang.reflect.InvocationTargetException
        at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
        at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39)
        at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)
        at org.mockito.cglib.core.ReflectUtils.defineClass(ReflectUtils.java:385)
        at org.mockito.cglib.core.AbstractClassGenerator.create(AbstractClassGenerator.java:220)
        ... 31 more
    Caused by: java.lang.ClassFormatError: Duplicate method name&signature in class file ATest$Bar$$EnhancerByMockitoWithCGLIB$$58a2468b$$EnhancerByCGLIB$$9232d1df
        at java.lang.ClassLoader.defineClass1(Native Method)
        at java.lang.ClassLoader.defineClassCond(ClassLoader.java:631)
        at java.lang.ClassLoader.defineClass(ClassLoader.java:615)
        ... 37 more