Skip to content

Commit 6c563a1

Browse files
Fixes mockito#2398 : Implement global suppress static initialization leveraging javaagent
1 parent 82998ff commit 6c563a1

8 files changed

Lines changed: 498 additions & 3 deletions

File tree

‎mockito-core/src/main/java/org/mockito/Mockito.java‎

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,7 @@
119119
* <a href="#55">55. Verification with assertions (Since 5.3.0)</a><br/>
120120
* <a href="#56">56. Mocking singletons (like Java enums) (Since 5.22.0)</a><br/>
121121
* <a href="#57">57. Spying on static methods (Since 5.23.0)</a><br/>
122+
* <a href="#58">58. Suppressing static initializers (Since 5.24.0)</a><br/>
122123
* </b>
123124
*
124125
* <h3 id="0">0. <a class="meaningful_link" href="#mockito2" name="mockito2">Migrating to Mockito 2</a></h3>
@@ -1823,6 +1824,34 @@
18231824
* // Original behavior is restored
18241825
* assertEquals("foo", Foo.method());
18251826
* </code></pre>
1827+
*
1828+
* <h3 id="58">58. <a class="meaningful_link" href="#suppress_clinit" name="suppress_clinit">
1829+
* Suppressing static initializers</a></h3>
1830+
*
1831+
* When Mockito is used as a Java agent ({@code -javaagent}), you can suppress static initializers
1832+
* ({@code <clinit>}) of specific classes via the system property {@code mockito.suppress.clinit}.
1833+
* This causes all static fields in those classes to retain their JVM zero-value defaults
1834+
* ({@code null}, {@code 0}, {@code false}, etc.) instead of running the static initialization block.
1835+
*
1836+
* <p>This is useful when a class static initializer performs expensive or environment-dependent
1837+
* operations (e.g. loading native libraries, connecting to databases) that are undesirable in tests.
1838+
*
1839+
* <p>Because suppression is configured once per JVM at agent startup, it is consistent across all
1840+
* tests, which means <b>tests can safely run in parallel</b>.
1841+
*
1842+
* <p><b>Maven configuration:</b>
1843+
* <pre class="code"><code class="xml">
1844+
* &lt;plugin&gt;
1845+
* &lt;groupId&gt;org.apache.maven.plugins&lt;/groupId&gt;
1846+
* &lt;artifactId&gt;maven-surefire-plugin&lt;/artifactId&gt;
1847+
* &lt;configuration&gt;
1848+
* &lt;argLine&gt;
1849+
* -javaagent:${org.mockito:mockito-core:jar}
1850+
* -Dmockito.suppress.clinit=com.example.ClassA,com.example.ClassB
1851+
* &lt;/argLine&gt;
1852+
* &lt;/configuration&gt;
1853+
* &lt;/plugin&gt;
1854+
* </code></pre>
18261855
*/
18271856
@CheckReturnValue
18281857
@SuppressWarnings("unchecked")

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

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,12 @@
44
*/
55
package org.mockito.internal;
66

7+
import org.mockito.internal.creation.bytebuddy.ClinitSuppressionTransformer;
8+
79
import java.lang.instrument.Instrumentation;
10+
import java.util.Collections;
11+
import java.util.HashSet;
12+
import java.util.Set;
813

914
/**
1015
* Allows users to specify Mockito as a Java agent where the {@link Instrumentation}
@@ -17,15 +22,38 @@
1722
public class PremainAttach {
1823

1924
private static volatile Instrumentation instrumentation;
25+
private static final String SUPPRESS_CLINIT_PROPERTY = "mockito.suppress.clinit";
2026

2127
public static void premain(String arg, Instrumentation instrumentation) {
2228
if (PremainAttach.instrumentation != null) {
2329
return;
2430
}
31+
Set<String> classes =
32+
parseSuppressClinitProperty(System.getProperty(SUPPRESS_CLINIT_PROPERTY));
33+
if (!classes.isEmpty()) {
34+
instrumentation.addTransformer(new ClinitSuppressionTransformer(classes), false);
35+
}
2536
PremainAttach.instrumentation = instrumentation;
2637
}
2738

28-
public static Instrumentation getInstrumentation() {
29-
return instrumentation;
39+
/**
40+
* Parses a comma-separated list of class names into a set, trimming whitespace from each
41+
* entry and ignoring empty entries.
42+
*
43+
* @param property the property value, may be {@code null}
44+
* @return an unmodifiable set of class names
45+
*/
46+
static Set<String> parseSuppressClinitProperty(String property) {
47+
if (property == null || property.trim().isEmpty()) {
48+
return Collections.emptySet();
49+
}
50+
Set<String> result = new HashSet<>();
51+
for (String name : property.split(",")) {
52+
String trimmed = name.trim();
53+
if (!trimmed.isEmpty()) {
54+
result.add(trimmed);
55+
}
56+
}
57+
return Collections.unmodifiableSet(result);
3058
}
3159
}
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
/*
2+
* Copyright (c) 2026 Mockito contributors
3+
* This program is made available under the terms of the MIT License.
4+
*/
5+
package org.mockito.internal.creation.bytebuddy;
6+
7+
import net.bytebuddy.jar.asm.ClassReader;
8+
import net.bytebuddy.jar.asm.ClassVisitor;
9+
import net.bytebuddy.jar.asm.ClassWriter;
10+
import net.bytebuddy.jar.asm.MethodVisitor;
11+
import net.bytebuddy.jar.asm.Opcodes;
12+
import net.bytebuddy.utility.OpenedClassReader;
13+
14+
import java.lang.instrument.ClassFileTransformer;
15+
import java.security.ProtectionDomain;
16+
import java.util.Set;
17+
18+
/**
19+
* A {@link ClassFileTransformer} that suppresses static initializers ({@code <clinit>}) for
20+
* specified classes at class-load time.
21+
*
22+
* <p>When a class is first loaded (not retransformed) and its fully-qualified name appears in the
23+
* configured set, the transformer replaces the {@code <clinit>} method with an empty method body.
24+
* This causes all static fields to retain their JVM zero-value defaults ({@code null},
25+
* {@code 0}, {@code false}, etc.).
26+
*
27+
* <p>This transformer is registered at premain time when the system property
28+
* {@code mockito.suppress.clinit} is set. See {@link org.mockito.internal.PremainAttach} for
29+
* details.
30+
*
31+
* @since 5.+
32+
*/
33+
public class ClinitSuppressionTransformer implements ClassFileTransformer {
34+
35+
private final Set<String> classNames;
36+
37+
/**
38+
* Creates a new transformer.
39+
*
40+
* @param classNames fully-qualified class names whose static initializers should be suppressed
41+
* (e.g. {@code "com.example.MyClass"})
42+
*/
43+
public ClinitSuppressionTransformer(Set<String> classNames) {
44+
this.classNames = classNames;
45+
}
46+
47+
@Override
48+
public byte[] transform(
49+
ClassLoader loader,
50+
String className,
51+
Class<?> classBeingRedefined,
52+
ProtectionDomain protectionDomain,
53+
byte[] classfileBuffer) {
54+
if (classBeingRedefined != null) {
55+
return null;
56+
}
57+
if (className == null) {
58+
return null;
59+
}
60+
String dotName = className.replace('/', '.');
61+
if (!classNames.contains(dotName)) {
62+
return null;
63+
}
64+
return suppressClinit(classfileBuffer);
65+
}
66+
67+
/**
68+
* Replaces the {@code <clinit>} method in the given class bytecode with an empty method body.
69+
* If the class has no {@code <clinit>}, the bytecode is returned unchanged.
70+
*
71+
* <p>The transformation uses ASM (shaded inside ByteBuddy) to visit all methods. When
72+
* {@code <clinit>} is encountered, a new empty method body containing only a {@code RETURN}
73+
* instruction is emitted. All other methods and class structure are preserved as-is.
74+
*
75+
* @param classfileBuffer the original class bytecode
76+
* @return the transformed bytecode with an empty {@code <clinit>}, or the original bytecode
77+
* if no {@code <clinit>} was found
78+
*/
79+
static byte[] suppressClinit(byte[] classfileBuffer) {
80+
ClassReader reader = new ClassReader(classfileBuffer);
81+
ClassWriter writer = new ClassWriter(reader, 0);
82+
ClinitSuppressionClassVisitor classVisitor = new ClinitSuppressionClassVisitor(writer);
83+
84+
reader.accept(classVisitor, 0);
85+
86+
return classVisitor.found ? writer.toByteArray() : classfileBuffer;
87+
}
88+
89+
private static class ClinitSuppressionClassVisitor extends ClassVisitor {
90+
boolean found;
91+
92+
ClinitSuppressionClassVisitor(ClassVisitor classVisitor) {
93+
super(OpenedClassReader.ASM_API, classVisitor);
94+
}
95+
96+
@Override
97+
public MethodVisitor visitMethod(
98+
int access, String name, String descriptor, String signature, String[] exceptions) {
99+
if ("<clinit>".equals(name)) {
100+
found = true;
101+
MethodVisitor mv =
102+
super.visitMethod(access, name, descriptor, signature, exceptions);
103+
mv.visitCode();
104+
mv.visitInsn(Opcodes.RETURN);
105+
mv.visitMaxs(0, 0);
106+
mv.visitEnd();
107+
return null;
108+
}
109+
return super.visitMethod(access, name, descriptor, signature, exceptions);
110+
}
111+
}
112+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
/*
2+
* Copyright (c) 2026 Mockito contributors
3+
* This program is made available under the terms of the MIT License.
4+
*/
5+
package org.mockito.internal;
6+
7+
import org.junit.Test;
8+
9+
import java.util.Set;
10+
11+
import static org.assertj.core.api.Assertions.assertThat;
12+
13+
public class PremainAttachTest {
14+
15+
@Test
16+
public void parse_suppress_clinit_property_with_multiple_classes() {
17+
Set<String> result =
18+
PremainAttach.parseSuppressClinitProperty(
19+
"com.example.ClassA,com.example.ClassB,com.example.ClassC");
20+
21+
assertThat(result)
22+
.containsExactlyInAnyOrder(
23+
"com.example.ClassA", "com.example.ClassB", "com.example.ClassC");
24+
}
25+
26+
@Test
27+
public void parse_suppress_clinit_property_with_whitespace_trimming() {
28+
Set<String> result =
29+
PremainAttach.parseSuppressClinitProperty(
30+
" com.example.ClassA , com.example.ClassB ");
31+
32+
assertThat(result).containsExactlyInAnyOrder("com.example.ClassA", "com.example.ClassB");
33+
}
34+
35+
@Test
36+
public void parse_suppress_clinit_property_returns_empty_when_null() {
37+
Set<String> result = PremainAttach.parseSuppressClinitProperty(null);
38+
39+
assertThat(result).isEmpty();
40+
}
41+
42+
@Test
43+
public void parse_suppress_clinit_property_returns_empty_when_blank() {
44+
Set<String> result = PremainAttach.parseSuppressClinitProperty(" ");
45+
46+
assertThat(result).isEmpty();
47+
}
48+
}

0 commit comments

Comments
 (0)