ByteBuddy-MockMaker uses wrong ClassLoader on Java 21, if mocked type and additional interfaces are from different classloaders
beatbrot opened this issue · comments
Describe the bug
Given the following conditions:
- ByteBuddy is available
- We are running on Java 21
- and the following classloader hierarchy:
When mocking type iam.Mocked
with additionalInterfaces: [foo.Bar]
and if the specification is loaded by MySpecClassLoader
, we will get the following exception:
Stacktrace:
java.lang.IllegalArgumentException: Could not create type
at net.bytebuddy.TypeCache.findOrInsert(TypeCache.java:170)
at net.bytebuddy.TypeCache$WithInlineExpunction.findOrInsert(TypeCache.java:399)
at net.bytebuddy.TypeCache.findOrInsert(TypeCache.java:190)
at net.bytebuddy.TypeCache$WithInlineExpunction.findOrInsert(TypeCache.java:410)
at org.spockframework.mock.runtime.ByteBuddyMockFactory.createMock(ByteBuddyMockFactory.java:97)
at org.spockframework.mock.runtime.ByteBuddyMockMaker.makeMock(ByteBuddyMockMaker.java:63)
at org.spockframework.mock.runtime.MockMakerRegistry.createWithAppropriateMockMaker(MockMakerRegistry.java:194)
at org.spockframework.mock.runtime.MockMakerRegistry.makeMockInternal(MockMakerRegistry.java:153)
at org.spockframework.mock.runtime.MockMakerRegistry.makeMock(MockMakerRegistry.java:125)
at org.spockframework.mock.runtime.JavaMockFactory.createInternal(JavaMockFactory.java:56)
at org.spockframework.mock.runtime.JavaMockFactory.create(JavaMockFactory.java:42)
at org.spockframework.mock.runtime.CompositeMockFactory.create(CompositeMockFactory.java:44)
at org.spockframework.lang.SpecInternals.createMock(SpecInternals.java:55)
at org.spockframework.lang.SpecInternals.createMockImpl(SpecInternals.java:330)
at org.spockframework.lang.SpecInternals.createMockImpl(SpecInternals.java:320)
at org.spockframework.lang.SpecInternals.MockImpl(SpecInternals.java:123)
at apackage.ASpec.a feature(Script_2ea152aea2eb5fd1885101de83bd8e60.groovy:1)
Caused by: java.lang.NoClassDefFoundError: foo/Bar
at java.base/java.lang.System$2.defineClass(System.java:2394)
at net.bytebuddy.utility.dispatcher.JavaDispatcher$Dispatcher$ForNonStaticMethod.invoke(JavaDispatcher.java:1033)
at net.bytebuddy.utility.dispatcher.JavaDispatcher$ProxiedInvocationHandler.invoke(JavaDispatcher.java:1163)
at net.bytebuddy.dynamic.loading.ClassInjector$UsingLookup.injectRaw(ClassInjector.java:1639)
at net.bytebuddy.dynamic.loading.ClassInjector$AbstractBase.inject(ClassInjector.java:119)
at net.bytebuddy.dynamic.loading.ClassLoadingStrategy$UsingLookup.load(ClassLoadingStrategy.java:519)
at net.bytebuddy.dynamic.TypeResolutionStrategy$Passive.initialize(TypeResolutionStrategy.java:101)
at net.bytebuddy.dynamic.DynamicType$Default$Unloaded.load(DynamicType.java:6367)
at org.spockframework.mock.runtime.ByteBuddyMockFactory.lambda$createMock$2(ByteBuddyMockFactory.java:137)
at net.bytebuddy.TypeCache.findOrInsert(TypeCache.java:168)
... 16 more
Caused by: java.lang.ClassNotFoundException: foo.Bar
at java.base/java.lang.ClassLoader.findClass(ClassLoader.java:733)
at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:593)
at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:526)
... 26 more
Spock will pick the ByteBuddy MockMaker and then use the net.bytebuddy.dynamic.loading.ClassLoadingStrategy.UsingLookup
classloading strategy. This strategy receives the lookup of the to-be-mocked class. It then uses that lookup to define the mocked class. Internally, this just means that the classloader of the to-be-mocked is used. In our example, this would be MockedClassClassLoader
. This classloader is not able to see foo.Bar
and therefore loading the mocked class fails.
To Reproduce
Paste the following testcase in spock.util.EmbeddedSpecRunnerClassLoaderSpec
:
def "ByteBuddy Mocks with interfaces that are not visible to the mocked type's classloader"() {
def testCl1 = new ByteBuddyTestClassLoader()
def testCl2 = new ByteBuddyTestClassLoader()
testCl1.defineInterface("foo.Bar")
testCl2.defineClass("iam.Mocked")
def cl = new MultipleParentClassLoader([getClass().classLoader, testCl1, testCl2])
def runner = new EmbeddedSpecRunner(cl)
when:
def r = runner.runFeatureBody("""
def mock = Mock(iam.Mocked, additionalInterfaces: [foo.Bar])
expect:
true
""")
then:
noExceptionThrown()
}
and run gradlew :spock-specs:test --tests "spock.util.EmbeddedSpecRunnerClassLoaderSpec.ByteBuddy Mocks with interfaces that are not visible to the mocked type's classloader" "-Dvariant=4.0" "-DjavaVersion=21"
Expected behavior
The mocking succeeds
Actual behavior
See exception above
Java version
openjdk version "21.0.2" 2024-01-16
Buildtool version
Gradle 8.10.2
What operating system are you using
Windows
Dependencies
Reproducer works in spock repo
Additional context
Mockito has a similar problem. They only use the Lookup strategy for "local mocks" (see here). Local mocks are mocks where the mocked type and all its additional interfaces are loadable by the classloader of the mocked type. I'd suggest implementing the same behavior for Spock and falling back to another loading strategy for non-local mocks. I have a simple version of this change ready and would love to open a PR if we can agree on this solution :)
Also FYI: I discovered this bug together with @AndreasTu
For additional context and as a quick workaround, if you manually select the MockMakers.mockito
for the sample above, it will succeed. Showing that the Mockito handling of such a case resolves the issue.
@beatbrot Thanks for the issue report and I would appreciate, if you could open up a PR for that.