8000 Avoid attach warning if Byte Buddy is configured for command-line att… · mockito/mockito@9942641 · GitHub
[go: up one dir, main page]

Skip to content

Commit 9942641

Browse files
raphwbric3
andauthored
Avoid attach warning if Byte Buddy is configured for command-line attach. (#3459)
* Avoid warning when attaching instrumentation through Byte Buddy on startup. * Avoid warning when attaching instrumentation through Byte Buddy on startup. Avoid duplication of code. * Remove unused imports. * Test whether dynamic attach generates JVM warnings (#3462) For implementation of #3459 * Use Mockito logger instead of standard out. * Fix java 21 test config * Revert suggested Plugins.getMockitoLogger to System.err.println The suggestions I made in #3459, cannot work currently due to a circular dependency on `Plugins.registry`. --------- Co-authored-by: Brice Dutheil <brice.dutheil@gmail.com>
1 parent 560d855 commit 9942641

File tree

7 files changed

+157
-63
lines changed

7 files changed

+157
-63
lines changed

mockito-core/src/main/java/org/mockito/internal/PremainAttach.java

Lines changed: 1 addition & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -26,22 +26,6 @@ public static void premain(String arg, Instrumentation instrumentation) {
2626
}
2727

2828
public static Instrumentation getInstrumentation() {
29-
// A Java agent is always added to the system class loader. If Mockito is executed from a
30-
// different class loader we need to make sure to resolve the instrumentation instance
31-
// from there, or fail the resolution, if this class does not exist on the system class
32-
// loader.
33-
ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
34-
if (PremainAttach.class.getClassLoader() == systemClassLoader) {
35-
return instrumentation;
36-
} else {
37-
try {
38-
return (Instrumentation)
39-
Class.forName(PremainAttach.class.getName(), true, systemClassLoader)
40-
.getMethod("getInstrumentation")
41-
.invoke(null);
42-
} catch (Exception ignored) {
43-
return null;
44-
}
45-
}
29+
return instrumentation;
4630
}
4731
}
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
/*
2+
* Copyright (c) 2024 Mockito contributors
3+
* This program is made available under the terms of the MIT License.
4+
*/
5+
package org.mockito.internal;
6+
7+
import net.bytebuddy.ClassFileVersion;
8+
import net.bytebuddy.agent.ByteBuddyAgent;
9+
import net.bytebuddy.agent.Installer;
10+
11+
import java.lang.instrument.Instrumentation;
12+
13+
import static org.mockito.internal.util.StringUtil.join;
14+
15+
public class PremainAttachAccess {
16+
17+
public static Instrumentation getInstrumentation() {
18+
// A Java agent is always added to the system class loader. If Mockito is executed from a
19+
// different class loader we need to make sure to resolve the instrumentation instance
20+
// from there, or fail the resolution, if this class does not exist on the system class
21+
// loader.
22+
ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
23+
Instrumentation instrumentation =
24+
doGetInstrumentation(PremainAttach.class, "getInstrumentation", systemClassLoader);
25+
if (instrumentation == null) {
26+
try {
27+
// At this point, we do not want to attempt dynamic attach but check the Byte Buddy
28+
// agent as a fallback if the Mockito agent is not attached.
29+
instrumentation =
30+
doGetInstrumentation(
31+
Installer.class, "getInstrumentation", systemClassLoader);
32+
} catch (IllegalStateException ignored) {
33+
}
34+
}
35+
if (instrumentation == null) {
36+
if (ClassFileVersion.ofThisVm().isAtLeast(ClassFileVersion.JAVA_V21)) {
37+
// Cannot use `Plugins.getMockitoLogger().warn(...)` at this time due to a circular
38+
// dependency on `Plugins.registry`.
39+
// The `PluginRegistry` is not yet fully initialized (in `Plugins`), because it is
40+
// currently initializing the `MockMaker` which is a InlineByteBuddyMockMaker, and
41+
// it is later calling this method to access the instrumentation.
42+
System.err.println(
43+
"Mockito is currently self-attaching to enable the inline-mock-maker. This "
44+
+ "will no longer work in future releases of the JDK. Please add Mockito as an agent to your "
45+
+ "build what is described in Mockito's documentation: "
46+
+ "https://javadoc.io/doc/org.mockito/mockito-core/latest/org/mockito/Mockito.html#0.3");
47+
}
48+
// Attempt to dynamically attach, as a last resort.
49+
instrumentation = ByteBuddyAgent.install();
50+
}
51+
if (!instrumentation.isRetransformClassesSupported()) {
52+
throw new IllegalStateException(
53+
join(
54+
"Mockito requires retransformation for creating inline mocks. This feature is unavailable on the current VM.",
55+
"",
56+
"You cannot use this mock maker on this VM"));
57+
}
58+
return instrumentation;
59+
}
60+
61+
private static Instrumentation doGetInstrumentation(
62+
Class<?> type, String method, ClassLoader systemClassLoader) {
63+
try {
64+
return (Instrumentation)
65+
Class.forName(type.getName(), true, systemClassLoader)
66+
.getMethod(method)
67+
.invoke(null);
68+
} catch (Exception ignored) {
69+
return null;
70+
}
71+
}
72+
}

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

Lines changed: 2 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,13 @@
44
*/
55
package org.mockito.internal.creation.bytebuddy;
66

7-
import net.bytebuddy.ClassFileVersion;
8-
import net.bytebuddy.agent.ByteBuddyAgent;
97
import org.mockito.MockedConstruction;
108
import org.mockito.creation.instance.InstantiationException;
119
import org.mockito.creation.instance.Instantiator;
1210
import org.mockito.exceptions.base.MockitoException;
1311
import org.mockito.exceptions.base.MockitoInitializationException;
1412
import org.mockito.exceptions.misusing.MockitoConfigurationException;
15-
import org.mockito.internal.PremainAttach;
13+
import org.mockito.internal.PremainAttachAccess;
1614
import org.mockito.internal.SuppressSignatureCheck;
1715
import org.mockito.internal.configuration.plugins.Plugins;
1816
import org.mockito.internal.creation.instance.ConstructorInstantiator;
@@ -137,24 +135,7 @@ class InlineDelegateByteBuddyMockMaker
137135

138136
try {
139137
try {
140-
instrumentation = PremainAttach.getInstrumentation();
141-
if (instrumentation == null) {
142-
if (ClassFileVersion.ofThisVm().isAtLeast(ClassFileVersion.JAVA_V21)) {
143-
System.out.println(
144-
"Mockito is currently self-attaching to enable the inline-mock-maker. This "
145-
+ "will no longer work in future releases of the JDK. Please add Mockito as an agent to your "
146-
+ "build what is described in Mockito's documentation: "
147-
+ "https://javadoc.io/doc/org.mockito/mockito-core/latest/org/mockito/Mockito.html#0.3");
148-
}
149-
instrumentation = ByteBuddyAgent.install();
150-
}
151-
if (!instrumentation.isRetransformClassesSupported()) {
152-
throw new IllegalStateException(
153-
join(
154-
"Mockito requires retransformation for creating inline mocks. This feature is unavailable on the current VM.",
155-
"",
156-
"You cannot use this mock maker on this VM"));
157-
}
138+
instrumentation = PremainAttachAccess.getInstrumentation();
158139
File boot = File.createTempFile("mockitoboot", ".jar");
159140
boot.deleteOnExit();
160141
try (JarOutputStream outputStream =

mockito-core/src/main/java/org/mockito/internal/util/ConsoleMockitoLogger.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,13 @@
88

99
public class ConsoleMockitoLogger implements MockitoLogger {
1010

11+
@Override
1112
public void log(Object what) {
1213
System.out.println(what);
1314
}
15+
16+
@Override
17+
public void warn(Object what) {
18+
System.err.println(what);
19+
}
1420
}

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

Lines changed: 2 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,10 @@
55
package org.mockito.internal.util.reflection;
66

77
import net.bytebuddy.ByteBuddy;
8-
import net.bytebuddy.ClassFileVersion;
9-
import net.bytebuddy.agent.ByteBuddyAgent;
108
import net.bytebuddy.dynamic.loading.ClassLoadingStrategy;
119
import net.bytebuddy.implementation.MethodCall;
1210
import org.mockito.exceptions.base.MockitoInitializationException;
13-
import org.mockito.internal.PremainAttach;
11+
import org.mockito.internal.PremainAttachAccess;
1412
import org.mockito.internal.SuppressSignatureCheck;
1513
import org.mockito.plugins.MemberAccessor;
1614

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

mockito-core/src/main/java/org/mockito/plugins/MockitoLogger.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,4 +40,13 @@ public interface MockitoLogger {
4040
* @param what to be logged
4141
*/
4242
void log(Object what);
43+
44+
/**
45+
* Log specified object as a warning, if supported.
46+
*
47+
* @param what to be logged.
48+
*/
49+
default void warn(Object what) {
50+
log(what);
51+
}
4352
}

mockito-integration-tests/java-21-tests/build.gradle

Lines changed: 65 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,14 @@ apply from: "$rootDir/gradle/java-test.gradle"
22

33
description = "Test suite for Java 21 Mockito"
44

5+
def bytebuddyAgentConf = configurations.create("bytebuddyAgent")
6+
57
dependencies {
68
implementation project(":mockito-core")
79
testImplementation libs.junit4
810
testImplementation libs.assertj
11+
testImplementation project(path: ':mockito-core', configuration: 'testUtil')
12+
bytebuddyAgent(libs.bytebuddy.agent) { setTransitive(false) }
913
}
1014

1115
java {
@@ -16,12 +20,69 @@ java {
1620
}
1721
}
1822

19-
tasks.named('test', Test) {
23+
tasks.register("test-with-bytebuddy-agent", Test) {
24+
if (!JavaVersion.current().isCompatibleWith(JavaVersion.VERSION_21)) {
25+
enabled = false
26+
} else {
27+
jvmArgs(
28+
"-javaagent:${bytebuddyAgentConf.asPath}",
29+
// "-Djdk.instrument.traceUsage",
30+
)
31+
}
32+
}
33+
34+
tasks.register("test-with-mockito-agent", Test) {
35+
dependsOn(':mockito-core:jar')
2036
if (!JavaVersion.current().isCompatibleWith(JavaVersion.VERSION_21)) {
2137
enabled = false
2238
} else {
23-
dependsOn(':mockito-core:jar')
24-
jvmArgs('-javaagent:' + project(":mockito-core").tasks.jar.outputs.files.singleFile)
25-
// jvmArgs('-Djdk.instrument.traceUsage')
39+
jvmArgs(
40+
"-javaagent:${project(":mockito-core").tasks.jar.outputs.files.singleFile}",
41+
// "-Djdk.instrument.traceUsage",
42+
)
43+
}
44+
failIfStdErrWarningOnDynamicallyLoadedAgent(it)
45+
}
46+
47+
tasks.named('test', Test) {
48+
// dependsOn(
49+
// "test-with-bytebuddy-agent",
50+
// "test-with-mockito-agent",
51+
// )
52+
enabled JavaVersion.current().isCompatibleWith(JavaVersion.VERSION_21)
53+
54+
expectStdErrWhenDynamicallyLoadedAgent(it)
55+
}
56+
57+
private static void failIfStdErrWarningOnDynamicallyLoadedAgent(Test testTask) {
58+
def stderrMessages = new ArrayList<String>()
59+
testTask.addTestOutputListener { descriptor, event ->
60+
if (event.destination == TestOutputEvent.Destination.StdErr) {
61+
stderrMessages.add(event.message)
62+
}
63+
}
64+
testTask.doLast {
65+
def stderrText = stderrMessages.join '\n'
66+
if (stderrText.contains("A Java agent has been loaded dynamically")
67+
&& stderrText.contains("net.bytebuddy/byte-buddy-agent")
68+
&& stderrText.contains("If a serviceability tool is in use, please run with -XX:+EnableDynamicAgentLoading to hide this warning")
69+
) {
70+
throw new GradleException("JDK should no emit warning when mockito is set as agent and -XX:+EnableDynamicAgentLoading is not set")
71+
}
72+
}
73+
}
74+
75+
private static void expectStdErrWhenDynamicallyLoadedAgent(Test testTask) {
76+
def stderrMessages = new ArrayList<String>()
77+
testTask.addTestOutputListener { descriptor, event ->
78+
if (event.destination == TestOutputEvent.Destination.StdErr) {
79+
stderrMessages.add(event.message)
80+
}
81+
}
82+
testTask.doLast {
83+
def stdoutText = stderrMessages.join '\n'
84+
if (!stdoutText.contains("Mockito is currently self-attaching to enable the inline-mock-maker.")) {
85+
throw new GradleException("Mockito should emit a message when self-attaching")
86+
}
2687
}
2788
}

0 commit comments

Comments
 (0)
0