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
Prev Previous commit
Next Next commit
Adjust use of Mockito premain class to resolve from the system class …
…loader and to resolve consistently.
  • Loading branch information
raphw committed Sep 5, 2024
commit e2ad121d3177d2c79b4af0fe918f4d9290a63ef4
23 changes: 22 additions & 1 deletion src/main/java/org/mockito/internal/PremainAttach.java
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@
/**
* 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">JET 451</a>, delivered with JDK 21,
raphw marked this conversation as resolved.
Show resolved Hide resolved
* is the first milestone to disallow dynamic loading of by default which will happen
* in a future version of the JDK.
*/
public class PremainAttach {

Expand All @@ -22,6 +26,23 @@ public static void premain(String arg, Instrumentation instrumentation) {
}

public static Instrumentation getInstrumentation() {
return instrumentation;
// 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 @@ -109,111 +109,107 @@ class InlineDelegateByteBuddyMockMaker
private static final Throwable INITIALIZATION_ERROR;

static {
Instrumentation instrumentation = PremainAttach.getInstrumentation();
if (instrumentation != null && instrumentation.isRetransformClassesSupported()) {
INSTRUMENTATION = instrumentation;
INITIALIZATION_ERROR = null;
} else {
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
// 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
// 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(
join(
"The Byte Buddy agent cannot be loaded.",
"",
"To initialise the Byte Buddy agent, a subprocess may need to be created. To do this, the JVM requires "
+ "knowledge of the 'os.name' System property in most JRE implementations. This property is not present, "
+ "which means this operation will fail to complete. Please first make sure you are not clearing this "
+ "property anywhere, and failing that, raise a bug with your JVM vendor."));
}
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
// 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
// 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(
join(
"The Byte Buddy agent cannot be loaded.",
"",
"To initialise the Byte Buddy agent, a subprocess may need to be created. To do this, the JVM requires "
+ "knowledge of the 'os.name' System property in most JRE implementations. This property is not present, "
+ "which means this operation will fail to complete. Please first make sure you are not clearing this "
+ "property anywhere, and failing that, raise a bug with your JVM vendor."));
}

try {
try {
try {
instrumentation = PremainAttach.getInstrumentation();
if (instrumentation == null) {
instrumentation = ByteBuddyAgent.install();
if (!instrumentation.isRetransformClassesSupported()) {
}
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"));
}
File boot = File.createTempFile("mockitoboot", ".jar");
boot.deleteOnExit();
try (JarOutputStream outputStream =
new JarOutputStream(new FileOutputStream(boot))) {
String source =
"org/mockito/internal/creation/bytebuddy/inject/MockMethodDispatcher";
InputStream inputStream =
InlineDelegateByteBuddyMockMaker.class
.getClassLoader()
.getResourceAsStream(source + ".raw");
if (inputStream == null) {
throw new IllegalStateException(
join(
"Byte Buddy requires retransformation for creating inline mocks. This feature is unavailable on the current VM.",
"The MockMethodDispatcher class file is not locatable: "
+ source
+ ".raw",
"",
"You cannot use this mock maker on this VM"));
"The class loader responsible for looking up the resource: "
+ InlineDelegateByteBuddyMockMaker.class
.getClassLoader()));
}
File boot = File.createTempFile("mockitoboot", ".jar");
boot.deleteOnExit();
JarOutputStream outputStream = new JarOutputStream(new FileOutputStream(boot));
try {
String source =
"org/mockito/internal/creation/bytebuddy/inject/MockMethodDispatcher";
InputStream inputStream =
InlineDelegateByteBuddyMockMaker.class
.getClassLoader()
.getResourceAsStream(source + ".raw");
if (inputStream == null) {
throw new IllegalStateException(
join(
"The MockMethodDispatcher class file is not locatable: "
+ source
+ ".raw",
"",
"The class loader responsible for looking up the resource: "
+ InlineDelegateByteBuddyMockMaker.class
.getClassLoader()));
}
try (inputStream) {
outputStream.putNextEntry(new JarEntry(source + ".class"));
try {
int length;
byte[] buffer = new byte[1024];
while ((length = inputStream.read(buffer)) != -1) {
outputStream.write(buffer, 0, length);
}
} finally {
inputStream.close();
int length;
byte[] buffer = new byte[1024];
while ((length = inputStream.read(buffer)) != -1) {
outputStream.write(buffer, 0, length);
}
outputStream.closeEntry();
} finally {
outputStream.close();
}
try (JarFile jarfile = new JarFile(boot)) {
instrumentation.appendToBootstrapClassLoaderSearch(jarfile);
inputStream.close();
raphw marked this conversation as resolved.
Show resolved Hide resolved
}
try {
Class.forName(
"org.mockito.internal.creation.bytebuddy.inject.MockMethodDispatcher",
false,
null);
} catch (ClassNotFoundException cnfe) {
throw new IllegalStateException(
join(
"Mockito failed to inject the MockMethodDispatcher class into the bootstrap class loader",
"",
"It seems like your current VM does not support the instrumentation API correctly."),
cnfe);
}
} catch (IOException ioe) {
outputStream.closeEntry();
}
try (JarFile jarfile = new JarFile(boot)) {
instrumentation.appendToBootstrapClassLoaderSearch(jarfile);
}
try {
Class.forName(
"org.mockito.internal.creation.bytebuddy.inject.MockMethodDispatcher",
false,
null);
} catch (ClassNotFoundException cnfe) {
throw new IllegalStateException(
join(
"Mockito could not self-attach a Java agent to the current VM. This feature is required for inline mocking.",
"This error occured due to an I/O error during the creation of this agent: "
+ ioe,
"Mockito failed to inject the MockMethodDispatcher class into the bootstrap class loader",
"",
"Potentially, the current VM does not support the instrumentation API correctly"),
ioe);
"It seems like your current VM does not support the instrumentation API correctly."),
cnfe);
}
} catch (Throwable throwable) {
instrumentation = null;
initializationError = throwable;
} catch (IOException ioe) {
throw new IllegalStateException(
join(
"Mockito could not self-attach a Java agent to the current VM. This feature is required for inline mocking.",
"This error occured due to an I/O error during the creation of this agent: "
+ ioe,
"",
"Potentially, the current VM does not support the instrumentation API correctly"),
ioe);
}
INSTRUMENTATION = instrumentation;
INITIALIZATION_ERROR = initializationError;
} catch (Throwable throwable) {
instrumentation = null;
initializationError = throwable;
}
INSTRUMENTATION = instrumentation;
INITIALIZATION_ERROR = initializationError;
}

private final BytecodeGenerator bytecodeGenerator;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
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 +47,17 @@ class InstrumentationMemberAccessor implements MemberAccessor {
Dispatcher dispatcher;
Throwable throwable;
try {
instrumentation = ByteBuddyAgent.install();
instrumentation = PremainAttach.getInstrumentation();
if (instrumentation == null) {
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