Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow for installing a Java agent within the Mockito jar, without exposing Byte Buddy's attach mechanism. #3437

Merged
merged 10 commits into from
Sep 16, 2024
4 changes: 4 additions & 0 deletions gradle/mockito-core/inline-mock.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,8 @@ sourceSets.main {
tasks.named("jar", Jar) {
exclude("org/mockito/internal/creation/bytebuddy/inject/package-info.class")
exclude("org/mockito/internal/creation/bytebuddy/inject/MockMethodDispatcher.class")
manifest {
attributes 'Premain-Class': 'org.mockito.internal.PremainAttach'
attributes 'Can-Retransform-Classes': 'true'
}
}
47 changes: 47 additions & 0 deletions src/main/java/org/mockito/Mockito.java
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@
* <a href="#0">0. Migrating to Mockito 2</a><br/>
* <a href="#0.1">0.1 Mockito Android support</a><br/>
* <a href="#0.2">0.2 Configuration-free inline mock making</a><br/>
* <a href="#0.3">0.3 Explicitly enabling instrumentation for inline mocking (Java 21+)</a><br/>
* <a href="#1">1. Let's verify some behaviour! </a><br/>
* <a href="#2">2. How about some stubbing? </a><br/>
* <a href="#3">3. Argument matchers </a><br/>
Expand Down Expand Up @@ -168,6 +169,52 @@
* <p>
* For more information about inline mock making, see <a href="#39">section 39</a>.
*
* <h3 id="0.3">0.3. <a class="meaningful_link" href="#mockito-instrumentation" name="mockito-instrumentation">Explicitly setting up instrumentation for inline mocking (Java 21+)</a></h3>
*
* Starting from Java 21, the <a href="https://openjdk.org/jeps/451">JDK restricts the ability of libraries
* to attach a Java agent to their own JVM</a>. As a result, the inline-mock-maker might not be able
* to function without an explicit setup to enable instrumentation, and the JVM will always display a warning.
*
* <p>
* To explicitly attach Mockito during test execution, the library's jar file needs to be specified as `-javaagent`
* as an argument to the executing JVM. To enable this in Gradle, the following example adds Mockito to all test
* tasks:
*
* <pre class="code"><code class="kotlin">
* val mockitoAgent = configurations.create("mockitoAgent")
* dependencies {
* mockitoAgent(libs.mockito)
* }
* tasks {
* test {
* jvmArgs("-javaagent:${mockitoAgent.asPath}")
* }
* }
* </code></pre>
*
* To add Mockito as an agent to Maven's surefire plugin, the following configuration is needed:
*
* <pre class="code"><code class="xml">
* &lt;plugin&gt;
* &lt;groupId&gt;org.apache.maven.plugins&lt;/groupId&gt;
* &lt;artifactId&gt;maven-dependency-plugin&lt;/artifactId&gt;
* &lt;executions&gt;
* &lt;execution&gt;
* &lt;goals&gt;
* &lt;goal&gt;properties&lt;/goal&gt;
* &lt;/goals&gt;
* &lt;/execution&gt;
* &lt;/executions&gt;
* &lt;/plugin&gt;
* &lt;plugin&gt;
* &lt;groupId&gt;org.apache.maven.plugins&lt;/groupId&gt;
* &lt;artifactId&gt;maven-surefire-plugin&lt;/artifactId&gt;
* &lt;configuration&gt;
* &lt;argLine&gt;@{argLine} -javaagent:${org.mockito:mockito-core:jar}&lt;/argLine&gt;
* &lt;/configuration&gt;
* &lt;/plugin&gt;
* </code></pre>
*
* <h3 id="1">1. <a class="meaningful_link" href="#verification" name="verification">Let's verify some behaviour!</a></h3>
*
* The following examples mock a List, because most people are familiar with the interface (such as the
Expand Down
47 changes: 47 additions & 0 deletions src/main/java/org/mockito/internal/PremainAttach.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/*
* Copyright (c) 2024 Mockito contributors
* This program is made available under the terms of the MIT License.
*/
package org.mockito.internal;

import java.lang.instrument.Instrumentation;

/**
* Allows users to specify Mockito as a Java agent where the {@link Instrumentation}
* instance is stored for use by the inline mock maker.
raphw marked this conversation as resolved.
Show resolved Hide resolved
*
* The <a href="https://openjdk.org/jeps/451">JEP 451</a>, delivered with JDK 21,
* is the first milestone to disallow dynamic loading of by default which will happen
* in a future version of the JDK.
*/
public class PremainAttach {

private static volatile Instrumentation instrumentation;

public static void premain(String arg, Instrumentation instrumentation) {
if (PremainAttach.instrumentation != null) {
return;
}
PremainAttach.instrumentation = instrumentation;
}

public static Instrumentation getInstrumentation() {
// A Java agent is always added to the system class loader. If Mockito is executed from a
// different class loader we need to make sure to resolve the instrumentation instance
// from there, or fail the resolution, if this class does not exist on the system class
// loader.
ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
if (PremainAttach.class.getClassLoader() == systemClassLoader) {
return instrumentation;
} else {
try {
return (Instrumentation)
Class.forName(PremainAttach.class.getName(), true, systemClassLoader)
.getMethod("getInstrumentation")
.invoke(null);
} catch (Exception ignored) {
return null;
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,15 @@
*/
package org.mockito.internal.creation.bytebuddy;

import net.bytebuddy.ClassFileVersion;
import net.bytebuddy.agent.ByteBuddyAgent;
import org.mockito.MockedConstruction;
import org.mockito.creation.instance.InstantiationException;
import org.mockito.creation.instance.Instantiator;
import org.mockito.exceptions.base.MockitoException;
import org.mockito.exceptions.base.MockitoInitializationException;
import org.mockito.exceptions.misusing.MockitoConfigurationException;
import org.mockito.internal.PremainAttach;
import org.mockito.internal.SuppressSignatureCheck;
import org.mockito.internal.configuration.plugins.Plugins;
import org.mockito.internal.creation.instance.ConstructorInstantiator;
Expand Down Expand Up @@ -111,11 +113,14 @@ class InlineDelegateByteBuddyMockMaker
Instrumentation instrumentation;
Throwable initializationError = null;

// ByteBuddy internally may attempt to fork a subprocess. In Java 11 and Java 19, the Java
// process class observes the os.name system property to determine the OS and thus determine
// ByteBuddy internally may attempt to fork a subprocess. In Java 11 and Java 19, the
// Java
// process class observes the os.name system property to determine the OS and thus
// determine
// how to fork a new process. If the user is stubbing System properties, they may clear
// the existing System properties, which will cause this to fail. This is very much an
// implementation detail, but it will result in Mockito failing to load with an error that
// implementation detail, but it will result in Mockito failing to load with an error
// that
// is not overly clear, so let's attempt to detect this issue ahead of time instead.
if (System.getProperty("os.name") == null) {
throw new IllegalStateException(
Expand All @@ -130,18 +135,28 @@ class InlineDelegateByteBuddyMockMaker

try {
try {
instrumentation = ByteBuddyAgent.install();
instrumentation = PremainAttach.getInstrumentation();
if (instrumentation == null) {
if (ClassFileVersion.ofThisVm().isAtLeast(ClassFileVersion.JAVA_V21)) {
System.out.println(
"Mockito is currently self-attaching to enable the inline-mock-maker. This "
+ "will no longer work in future releases of the JDK. Please add Mockito as an agent to your "
+ "build what is described in Mockito's documentation: "
+ "https://javadoc.io/doc/org.mockito/mockito-core/latest/org/mockito/Mockito.html#0.3");
}
instrumentation = ByteBuddyAgent.install();
}
if (!instrumentation.isRetransformClassesSupported()) {
throw new IllegalStateException(
join(
"Byte Buddy requires retransformation for creating inline mocks. This feature is unavailable on the current VM.",
"Mockito requires retransformation for creating inline mocks. This feature is unavailable on the current VM.",
"",
"You cannot use this mock maker on this VM"));
}
File boot = File.createTempFile("mockitoboot", ".jar");
boot.deleteOnExit();
JarOutputStream outputStream = new JarOutputStream(new FileOutputStream(boot));
try {
try (JarOutputStream outputStream =
new JarOutputStream(new FileOutputStream(boot))) {
String source =
"org/mockito/internal/creation/bytebuddy/inject/MockMethodDispatcher";
InputStream inputStream =
Expand All @@ -159,19 +174,15 @@ class InlineDelegateByteBuddyMockMaker
+ InlineDelegateByteBuddyMockMaker.class
.getClassLoader()));
}
outputStream.putNextEntry(new JarEntry(source + ".class"));
try {
try (inputStream) {
outputStream.putNextEntry(new JarEntry(source + ".class"));
int length;
byte[] buffer = new byte[1024];
while ((length = inputStream.read(buffer)) != -1) {
outputStream.write(buffer, 0, length);
}
} finally {
inputStream.close();
}
outputStream.closeEntry();
} finally {
outputStream.close();
}
try (JarFile jarfile = new JarFile(boot)) {
instrumentation.appendToBootstrapClassLoaderSearch(jarfile);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,12 @@
package org.mockito.internal.util.reflection;

import net.bytebuddy.ByteBuddy;
import net.bytebuddy.ClassFileVersion;
import net.bytebuddy.agent.ByteBuddyAgent;
import net.bytebuddy.dynamic.loading.ClassLoadingStrategy;
import net.bytebuddy.implementation.MethodCall;
import org.mockito.exceptions.base.MockitoInitializationException;
import org.mockito.internal.PremainAttach;
import org.mockito.internal.SuppressSignatureCheck;
import org.mockito.plugins.MemberAccessor;

Expand Down Expand Up @@ -46,7 +48,24 @@ class InstrumentationMemberAccessor implements MemberAccessor {
Dispatcher dispatcher;
Throwable throwable;
try {
instrumentation = ByteBuddyAgent.install();
instrumentation = PremainAttach.getInstrumentation();
if (instrumentation == null) {
if (ClassFileVersion.ofThisVm().isAtLeast(ClassFileVersion.JAVA_V21)) {
System.out.println(
"Mockito is currently self-attaching to enable the inline-mock-maker. This "
+ "will no longer work in future releases of the JDK. Please add Mockito as an agent to your "
+ "build what is described in Mockito's documentation: "
+ "https://javadoc.io/doc/org.mockito/mockito-core/latest/org/mockito/Mockito.html#0.3");
}
instrumentation = ByteBuddyAgent.install();
}
if (!instrumentation.isRetransformClassesSupported()) {
throw new IllegalStateException(
join(
"Mockito requires retransformation for creating inline mocks. This feature is unavailable on the current VM.",
"",
"You cannot use this mock maker on this VM"));
}
// We need to generate a dispatcher instance that is located in a distinguished class
// loader to create a unique (unnamed) module to which we can open other packages to.
// This way, we assure that classes within Mockito's module (which might be a shared,
Expand Down
Loading