Describe the bug
After updating to Java 25 and byte-buddy 1.18.9, some tests show surprising behavior where a mocked method is not intercepted anymore.
The issue occurs in an OSGi environment when mocking a package-private method of a class.
It worked before Java 25 and on Java 25 with byte-buddy 1.18.8. The difference is the class loading strategy returned by ByteBuddyMockFactory.determineBestClassLoadingStrategy():
- in the old working scenario
Default.INJECTION is used
- in the new failing scenario
Default.WRAPPER is used -- unless -Dnet.bytebuddy.safe=false is supplied
- --> the mock is created with a different classloader
A solution might be to improve the ByteBuddyMockFactory#isLocalMock logic specifically for OSGi: In my scenario the test is implemented in an OSGi fragment and the mocked class is contained in the fragment's host.
So in reality it would be a local mock because the target class's classloader has access to ISpockMockObject.
I.e. the solution would be to implement
A mock is considered local, if all additional interfaces of the mock (including ISpockMockObject) are loadable by the target class' classloader.
literally rather than determining it via the MultipleParentClassLoader.
To Reproduce
In an OSGi environment:
Production bundle P containing
package com.example;
public class SomeClass {
public record InvocationFromJava(SomeClass cls) {
public Object invoke() {
return cls.packagePrivate();
}
}
String packagePrivate() {
return "original";
}
}
Test fragment on P containing
package com.example
import spock.lang.Specification
class MockPackagePrivateSpec extends Specification {
def mock = Mock(SomeClass) {
packagePrivate() >> "mocked"
}
def "mock is called when invoked from Java code"() {
when:
def result = new SomeClass.InvocationFromJava(mock).invoke()
then:
result == "mocked" // fails
}
def "mock is called when invoked directly from Groovy"() {
when:
def result = mock.packagePrivate()
then:
result == "mocked" // passes
}
}
Expected behavior
The mocked package-private method is invoked when called from Java.
Or if that is just not possible: An appropriate exception is thrown explaining that the mock is not possible with that mock maker.
Actual behavior
When the mocked method is called from Java code, the original method is invoked instead.
Java version
openjdk 25.0.2 2026-01-20 LTS
Buildtool version
Gradle 9.6.0
What operating system are you using
Windows
Dependencies
+--- org.spockframework:spock-core:2.4-groovy-4.0
| +--- org.apache.groovy:groovy:4.0.29
| | \--- org.apache.groovy:groovy-bom:4.0.29
| | \--- org.apache.groovy:groovy:4.0.29 (c)
| +--- io.leangen.geantyref:geantyref:1.3.16
| +--- org.spockframework:spock-bom:2.4-groovy-4.0
| | \--- org.spockframework:spock-core:2.4-groovy-4.0 (c)
| +--- org.junit:junit-bom:5.14.1
| | +--- org.junit.platform:junit-platform-engine:1.14.1 (c)
| | +--- org.junit.platform:junit-platform-launcher:1.14.1 (c)
| | \--- org.junit.platform:junit-platform-commons:1.14.1 (c)
| +--- org.junit.platform:junit-platform-engine -> 1.14.1
| | +--- org.junit:junit-bom:5.14.1 (*)
| | +--- org.opentest4j:opentest4j:1.3.0
| | \--- org.junit.platform:junit-platform-commons:1.14.1
| | \--- org.junit:junit-bom:5.14.1 (*)
| \--- org.hamcrest:hamcrest:3.0
+--- org.apache.groovy:groovy:4.0.24 -> 4.0.29 (*)
+--- net.bytebuddy:byte-buddy:1.18.9
+--- net.bytebuddy:byte-buddy-agent:1.18.9
\--- org.junit.platform:junit-platform-launcher -> 1.14.1
+--- org.junit:junit-bom:5.14.1 (*)
\--- org.junit.platform:junit-platform-engine:1.14.1 (*)
Additional context
Workaround:
- use
-Dnet.bytebuddy.safe=false while the Unsafe API is still available
- explicitly use the
mockito mock maker for this mock
Describe the bug
After updating to Java 25 and byte-buddy 1.18.9, some tests show surprising behavior where a mocked method is not intercepted anymore.
The issue occurs in an OSGi environment when mocking a package-private method of a class.
It worked before Java 25 and on Java 25 with byte-buddy 1.18.8. The difference is the class loading strategy returned by
ByteBuddyMockFactory.determineBestClassLoadingStrategy():Default.INJECTIONis usedDefault.WRAPPERis used -- unless-Dnet.bytebuddy.safe=falseis suppliedA solution might be to improve the
ByteBuddyMockFactory#isLocalMocklogic specifically for OSGi: In my scenario the test is implemented in an OSGi fragment and the mocked class is contained in the fragment's host.So in reality it would be a local mock because the target class's classloader has access to
ISpockMockObject.I.e. the solution would be to implement
literally rather than determining it via the MultipleParentClassLoader.
To Reproduce
In an OSGi environment:
Production bundle
PcontainingTest fragment on
PcontainingExpected behavior
The mocked package-private method is invoked when called from Java.
Or if that is just not possible: An appropriate exception is thrown explaining that the mock is not possible with that mock maker.
Actual behavior
When the mocked method is called from Java code, the original method is invoked instead.
Java version
openjdk 25.0.2 2026-01-20 LTS
Buildtool version
Gradle 9.6.0
What operating system are you using
Windows
Dependencies
Additional context
Workaround:
-Dnet.bytebuddy.safe=falsewhile the Unsafe API is still availablemockitomock maker for this mock