Skip to content

Commit c81be5d

Browse files
authored
Add support for including module-info in Mockito. (#3597)
Both `mockito-core` and `mockito-junit-jupiter` now ship a `module-info` declaration. All internal packages are not exported.
1 parent d01ac9d commit c81be5d

File tree

14 files changed

+221
-41
lines changed

14 files changed

+221
-41
lines changed

buildSrc/src/main/kotlin/mockito.javadoc-conventions.gradle.kts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@ tasks.named<Javadoc>("javadoc") {
1515
inputs.dir(javadocConfigDir)
1616
description = "Creates javadoc html for ${project.name}."
1717

18+
// Work-around as suggested in https://github.com/gradle/gradle/issues/19726
19+
val sourceSetDirectories = sourceSets.main.get().java.sourceDirectories.joinToString(":")
20+
val coreOptions = options as CoreJavadocOptions
21+
coreOptions.addStringOption("-source-path", sourceSetDirectories)
1822
exclude("**/internal/**")
1923

2024
// For more details on the format

mockito-core/build.gradle.kts

Lines changed: 39 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,9 @@
11
import com.android.build.gradle.internal.tasks.factory.dependsOn
2+
import org.objectweb.asm.ClassReader
3+
import org.objectweb.asm.ClassVisitor
4+
import org.objectweb.asm.ClassWriter
5+
import org.objectweb.asm.ModuleVisitor
6+
import org.objectweb.asm.Opcodes
27

38
plugins {
49
id("mockito.library-conventions")
@@ -70,11 +75,40 @@ tasks {
7075

7176
from(sourceSets.main.flatMap { it.java.classesDirectory }
7277
.map { it.file("org/mockito/internal/creation/bytebuddy/inject/MockMethodDispatcher.class") })
73-
into(generatedInlineResource.map { it.dir("org/mockito/internal/creation/bytebuddy/inject") })
78+
into(generatedInlineResource.map { it.dir("org/mockito/internal/creation/bytebuddy") })
7479

75-
rename("(.+)\\.class", "$1.raw")
80+
rename(".*", "inject-MockMethodDispatcher.raw")
7681
}
82+
83+
val removeInjectionPackageFromModuleInfo by registering(DefaultTask::class) {
84+
dependsOn(compileJava)
85+
86+
doLast {
87+
val moduleInfo = sourceSets.main.get().output.classesDirs.first().resolve("module-info.class")
88+
89+
val reader = ClassReader(moduleInfo.readBytes())
90+
val writer = ClassWriter(reader, 0)
91+
reader.accept(object : ClassVisitor(Opcodes.ASM9, writer) {
92+
override fun visitModule(name: String?, access: Int, version: String?): ModuleVisitor {
93+
return object : ModuleVisitor(
94+
Opcodes.ASM9,
95+
super.visitModule(name, access, version)
96+
) {
97+
override fun visitPackage(packaze: String) {
98+
if (packaze != "org/mockito/internal/creation/bytebuddy/inject") {
99+
super.visitPackage(packaze)
100+
}
101+
}
102+
}
103+
}
104+
}, 0)
105+
106+
moduleInfo.writeBytes(writer.toByteArray())
107+
}
108+
}
109+
77110
classes.dependsOn(copyMockMethodDispatcher)
111+
classes.dependsOn(removeInjectionPackageFromModuleInfo)
78112

79113

80114
jar {
@@ -112,8 +146,8 @@ tasks {
112146
# Export rules for public and internal packages
113147
# https://bnd.bndtools.org/heads/export_package.html
114148
Export-Package: \
115-
org.mockito.internal.*;status=INTERNAL;mandatory:=status;version=${archiveVersion.get()}, \
116-
org.mockito.*;version=${archiveVersion.get()}
149+
!org.mockito.internal.creation.bytebuddy.inject,org.mockito.internal.*;status=INTERNAL;mandatory:=status;version=${archiveVersion.get()}, \
150+
!org.mockito.internal.creation.bytebuddy.inject,org.mockito.*;version=${archiveVersion.get()}
117151
118152
# General rules for package import
119153
# https://bnd.bndtools.org/heads/import_package.html
@@ -132,15 +166,12 @@ tasks {
132166
org.hamcrest;resolution:=optional, \
133167
org.objenesis;version="[3.1,4.0)", \
134168
org.opentest4j.*;resolution:=optional, \
135-
org.mockito.*
169+
!org.mockito.internal.creation.bytebuddy.inject,org.mockito.*
136170
137171
# Don't add the Private-Package header.
138172
# See https://bnd.bndtools.org/instructions/removeheaders.html
139173
-removeheaders: Private-Package
140174
141-
# Configures the automatic module name for Java 9+.
142-
Automatic-Module-Name: org.mockito
143-
144175
# Don't add all the extra headers bnd normally adds.
145176
# See https://bnd.bndtools.org/instructions/noextraheaders.html
146177
-noextraheaders: true
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
/*
2+
* Copyright (c) 2025 Mockito contributors
3+
* This program is made available under the terms of the MIT License.
4+
*/
5+
module org.mockito {
6+
requires java.instrument;
7+
requires jdk.attach;
8+
requires net.bytebuddy;
9+
requires net.bytebuddy.agent;
10+
requires static junit;
11+
requires static org.hamcrest;
12+
requires static org.opentest4j;
13+
requires static jdk.unsupported;
14+
15+
exports org.mockito;
16+
exports org.mockito.configuration;
17+
exports org.mockito.creation.instance;
18+
exports org.mockito.exceptions.base;
19+
exports org.mockito.exceptions.misusing;
20+
exports org.mockito.exceptions.verification;
21+
exports org.mockito.exceptions.verification.junit;
22+
exports org.mockito.exceptions.verification.opentest4j;
23+
exports org.mockito.hamcrest;
24+
exports org.mockito.invocation;
25+
exports org.mockito.junit;
26+
exports org.mockito.listeners;
27+
exports org.mockito.mock;
28+
exports org.mockito.plugins;
29+
exports org.mockito.quality;
30+
exports org.mockito.session;
31+
exports org.mockito.stubbing;
32+
exports org.mockito.verification;
33+
exports org.mockito.internal.configuration to
34+
org.mockito.junit.jupiter;
35+
exports org.mockito.internal.session to
36+
org.mockito.junit.jupiter;
37+
exports org.mockito.internal.configuration.plugins to
38+
org.mockito.junit.jupiter;
39+
exports org.mockito.internal.util to
40+
org.mockito.junit.jupiter;
41+
}

mockito-core/src/main/java/org/mockito/internal/configuration/plugins/PluginInitializer.java

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
package org.mockito.internal.configuration.plugins;
66

77
import java.io.IOException;
8+
import java.lang.reflect.Method;
89
import java.net.URL;
910
import java.util.ArrayList;
1011
import java.util.Enumeration;
@@ -48,6 +49,7 @@ public <T> T loadImpl(Class<T> service) {
4849
classOrAlias = DefaultMockitoPlugins.getDefaultPluginClass(classOrAlias);
4950
}
5051
Class<?> pluginClass = loader.loadClass(classOrAlias);
52+
addReads(pluginClass);
5153
Object plugin = pluginClass.getDeclaredConstructor().newInstance();
5254
return service.cast(plugin);
5355
}
@@ -80,6 +82,7 @@ public <T> List<T> loadImpls(Class<T> service) {
8082
classOrAlias = DefaultMockitoPlugins.getDefaultPluginClass(classOrAlias);
8183
}
8284
Class<?> pluginClass = loader.loadClass(classOrAlias);
85+
addReads(pluginClass);
8386
Object plugin = pluginClass.getDeclaredConstructor().newInstance();
8487
impls.add(service.cast(plugin));
8588
}
@@ -89,4 +92,17 @@ public <T> List<T> loadImpls(Class<T> service) {
8992
"Failed to load " + service + " implementation declared in " + resources, e);
9093
}
9194
}
95+
96+
private static void addReads(Class<?> pluginClass) {
97+
try {
98+
Method getModule = Class.class.getMethod("getModule");
99+
Method addReads =
100+
getModule.getReturnType().getMethod("addReads", getModule.getReturnType());
101+
addReads.invoke(
102+
getModule.invoke(PluginInitializer.class), getModule.invoke(pluginClass));
103+
} catch (NoSuchMethodException ignored) {
104+
} catch (Exception e) {
105+
throw new IllegalStateException(e);
106+
}
107+
}
92108
}

mockito-core/src/main/java/org/mockito/internal/creation/bytebuddy/InlineDelegateByteBuddyMockMaker.java

Lines changed: 34 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -138,25 +138,29 @@ class InlineDelegateByteBuddyMockMaker
138138
boot.deleteOnExit();
139139
try (JarOutputStream outputStream =
140140
new JarOutputStream(new FileOutputStream(boot))) {
141-
String source =
142-
"org/mockito/internal/creation/bytebuddy/inject/MockMethodDispatcher";
143141
InputStream inputStream =
144-
InlineDelegateByteBuddyMockMaker.class
145-
.getClassLoader()
146-
.getResourceAsStream(source + ".raw");
142+
InlineDelegateByteBuddyMockMaker.class.getResourceAsStream(
143+
"inject-MockMethodDispatcher.raw");
147144
if (inputStream == null) {
148145
throw new IllegalStateException(
149146
join(
150147
"The MockMethodDispatcher class file is not locatable: "
151-
+ source
152-
+ ".raw",
148+
+ "inject-MockMethodDispatcher.raw"
149+
+ " in context of "
150+
+ InlineDelegateByteBuddyMockMaker.class.getName(),
153151
"",
154152
"The class loader responsible for looking up the resource: "
155153
+ InlineDelegateByteBuddyMockMaker.class
156-
.getClassLoader()));
154+
.getClassLoader(),
155+
"",
156+
"The module responsible for looking up the resource: "
157+
+ InlineDelegateByteBuddyMockMaker.class
158+
.getModule()));
157159
}
158160
try (inputStream) {
159-
outputStream.putNextEntry(new JarEntry(source + ".class"));
161+
outputStream.putNextEntry(
162+
new JarEntry(
163+
"org/mockito/internal/creation/bytebuddy/inject/MockMethodDispatcher.class"));
160164
int length;
161165
byte[] buffer = new byte[1024];
162166
while ((length = inputStream.read(buffer)) != -1) {
@@ -168,11 +172,13 @@ class InlineDelegateByteBuddyMockMaker
168172
try (JarFile jarfile = new JarFile(boot)) {
169173
instrumentation.appendToBootstrapClassLoaderSearch(jarfile);
170174
}
175+
Class<?> dispatcher;
171176
try {
172-
Class.forName(
173-
"org.mockito.internal.creation.bytebuddy.inject.MockMethodDispatcher",
174-
false,
175-
null);
177+
dispatcher =
178+
Class.forName(
179+
"org.mockito.internal.creation.bytebuddy.inject.MockMethodDispatcher",
180+
false,
181+
null);
176182
} catch (ClassNotFoundException cnfe) {
177183
throw new IllegalStateException(
178184
join(
@@ -181,6 +187,21 @@ class InlineDelegateByteBuddyMockMaker
181187
"It seems like your current VM does not support the instrumentation API correctly."),
182188
cnfe);
183189
}
190+
try {
191+
InlineDelegateByteBuddyMockMaker.class
192+
.getModule()
193+
.addReads(dispatcher.getModule());
194+
} catch (Exception e) {
195+
throw new IllegalStateException(
196+
join(
197+
"Mockito failed to adjust the module graph to read the dispatcher module",
198+
"",
199+
"Dispatcher: "
200+
+ dispatcher
201+
+ " is loaded by "
202+
+ dispatcher.getClassLoader()),
203+
e);
204+
}
184205
} catch (IOException ioe) {
185206
throw new IllegalStateException(
186207
join(

mockito-core/src/main/java/org/mockito/internal/creation/bytebuddy/inject/package-info.java

Lines changed: 0 additions & 11 deletions
This file was deleted.

mockito-core/src/main/java/org/mockito/internal/util/reflection/InstrumentationMemberAccessor.java

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@
55
package org.mockito.internal.util.reflection;
66

77
import net.bytebuddy.ByteBuddy;
8-
import net.bytebuddy.dynamic.loading.ClassLoadingStrategy;
8+
import net.bytebuddy.dynamic.loading.ByteArrayClassLoader;
9+
import net.bytebuddy.dynamic.loading.InjectionClassLoader;
910
import net.bytebuddy.implementation.MethodCall;
1011
import org.mockito.exceptions.base.MockitoInitializationException;
1112
import org.mockito.internal.PremainAttachAccess;
@@ -52,6 +53,20 @@ class InstrumentationMemberAccessor implements MemberAccessor {
5253
// This way, we assure that classes within Mockito's module (which might be a shared,
5354
// unnamed module) do not face escalated privileges where tests might pass that would
5455
// otherwise fail without Mockito's opening.
56+
InjectionClassLoader classLoader =
57+
new ByteArrayClassLoader(
58+
InstrumentationMemberAccessor.class.getClassLoader(),
59+
false,
60+
Collections.emptyMap());
61+
instrumentation.redefineModule(
62+
Dispatcher.class.getModule(),
63+
Collections.emptySet(),
64+
Collections.singletonMap(
65+
Dispatcher.class.getPackageName(),
66+
Collections.singleton(classLoader.getUnnamedModule())),
67+
Collections.emptyMap(),
68+
Collections.emptySet(),
69+
Collections.emptyMap());
5570
dispatcher =
5671
new ByteBuddy()
5772
.subclass(Dispatcher.class)
@@ -78,12 +93,11 @@ class InstrumentationMemberAccessor implements MemberAccessor {
7893
.onArgument(0)
7994
.withArgument(1))
8095
.make()
81-
.load(
82-
InstrumentationMemberAccessor.class.getClassLoader(),
83-
ClassLoadingStrategy.Default.WRAPPER)
96+
.load(classLoader, InjectionClassLoader.Strategy.INSTANCE)
8497
.getLoaded()
8598
.getConstructor()
8699
.newInstance();
100+
classLoader.seal();
87101
throwable = null;
88102
} catch (Throwable t) {
89103
instrumentation = null;

mockito-core/src/test/java/org/mockito/internal/creation/bytebuddy/InlineDelegateByteBuddyMockMakerTest.java

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -528,13 +528,20 @@ protected static <T> MockCreationSettings<T> settingsFor(
528528
}
529529

530530
@Test
531-
public void testMockDispatcherIsRelocated() throws Exception {
531+
public void testMockDispatcherIsRelocated() {
532532
assertThat(
533533
InlineByteBuddyMockMaker.class
534534
.getClassLoader()
535535
.getResource(
536-
"org/mockito/internal/creation/bytebuddy/inject/MockMethodDispatcher.raw"))
536+
"org/mockito/internal/creation/bytebuddy/inject-MockMethodDispatcher.raw"))
537537
.isNotNull();
538+
539+
assertThat(
540+
InlineByteBuddyMockMaker.class
541+
.getClassLoader()
542+
.getResource(
543+
"org/mockito/internal/creation/bytebuddy/inject/MockMethodDispatcher.class"))
544+
.isNull();
538545
}
539546

540547
private static final class FinalClass {

mockito-extensions/mockito-junit-jupiter/build.gradle.kts

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -53,9 +53,6 @@ tasks {
5353
# See https://bnd.bndtools.org/instructions/removeheaders.html
5454
-removeheaders: Private-Package
5555
56-
# Configures the automatic module name for Java 9+.
57-
Automatic-Module-Name: org.mockito.junit.jupiter
58-
5956
# Don't add all the extra headers bnd normally adds.
6057
# See https://bnd.bndtools.org/instructions/noextraheaders.html
6158
-noextraheaders: true
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
/*
2+
* Copyright (c) 2025 Mockito contributors
3+
* This program is made available under the terms of the MIT License.
4+
*/
5+
module org.mockito.junit.jupiter {
6+
requires transitive org.mockito;
7+
requires transitive org.junit.jupiter.api;
8+
9+
exports org.mockito.junit.jupiter;
10+
exports org.mockito.junit.jupiter.resolver;
11+
}

0 commit comments

Comments
 (0)