Skip to content

Mock() does not intercept package-private methods in OSGi on Java 25+ #2384

Description

@jhonnen

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

Metadata

Metadata

Assignees

Labels

Type

Fields

No fields configured for Bug.

Projects

No projects

Relationships

None yet

Development

No branches or pull requests

Issue actions