View Javadoc
1   /*
2    * MIT License
3    * Copyright (c) 2006-2025 JMockit developers
4    * See LICENSE file for full license text.
5    */
6   package mockit.internal.startup;
7   
8   import static mockit.internal.startup.ClassLoadingBridgeFields.createSyntheticFieldsInJREClassToHoldClassLoadingBridges;
9   
10  import edu.umd.cs.findbugs.annotations.NonNull;
11  import edu.umd.cs.findbugs.annotations.Nullable;
12  
13  import java.lang.instrument.ClassDefinition;
14  import java.lang.instrument.Instrumentation;
15  import java.lang.instrument.UnmodifiableClassException;
16  
17  import mockit.internal.ClassIdentification;
18  import mockit.internal.expectations.transformation.ExpectationsTransformer;
19  import mockit.internal.state.CachedClassfiles;
20  
21  /**
22   * This is the "agent class" that initializes the JMockit "Java agent", provided the JVM is initialized with
23   *
24   * <pre>{@code -javaagent:&lt;properPathTo>/jmockit-1-x.jar }</pre>
25   *
26   * .
27   *
28   * @see #premain(String, Instrumentation)
29   */
30  public final class Startup {
31      @Nullable
32      private static Instrumentation instrumentation;
33      public static boolean initializing;
34  
35      private static final String UNMOCKABLE_CLASS_SUGGESTION = "This typically occurs with classes from restricted JDK"
36              + " modules (such as java.net, java.nio) in JDK 9+. Consider wrapping the class in a testable abstraction"
37              + " or interface that can be mocked instead. For example, define an interface for the operations you need"
38              + " to test and inject it into the class under test.";
39  
40      private Startup() {
41      }
42  
43      /**
44       * User-specified fakes will applied at this time, if the "fakes" system property is set to the fully qualified
45       * class names.
46       *
47       * @param agentArgs
48       *            if "coverage", the coverage tool is activated
49       * @param inst
50       *            the instrumentation service provided by the JVM
51       */
52      public static void premain(@Nullable String agentArgs, @NonNull Instrumentation inst) {
53          createSyntheticFieldsInJREClassToHoldClassLoadingBridges(inst);
54  
55          instrumentation = inst;
56          inst.addTransformer(CachedClassfiles.INSTANCE, true);
57  
58          initializing = true;
59          try {
60              JMockitInitialization.initialize(inst, "coverage".equals(agentArgs));
61          } finally {
62              initializing = false;
63          }
64  
65          inst.addTransformer(new ExpectationsTransformer());
66      }
67  
68      @NonNull
69      @SuppressWarnings("ConstantConditions")
70      public static Instrumentation instrumentation() {
71          return instrumentation;
72      }
73  
74      public static void verifyInitialization() {
75          if (instrumentation == null) {
76              throw new IllegalStateException(
77                      "JMockit didn't get initialized; please check the -javaagent JVM initialization parameter was used");
78          }
79      }
80  
81      @SuppressWarnings("ConstantConditions")
82      public static void retransformClass(@NonNull Class<?> aClass) {
83          try {
84              instrumentation.retransformClasses(aClass);
85          } catch (UnmodifiableClassException ignore) {
86          }
87      }
88  
89      public static void redefineMethods(@NonNull ClassIdentification classToRedefine,
90              @NonNull byte[] modifiedClassfile) {
91          Class<?> loadedClass = classToRedefine.getLoadedClass();
92          redefineMethods(loadedClass, modifiedClassfile);
93      }
94  
95      public static void redefineMethods(@NonNull Class<?> classToRedefine, @NonNull byte[] modifiedClassfile) {
96          redefineMethods(new ClassDefinition(classToRedefine, modifiedClassfile));
97      }
98  
99      public static void redefineMethods(@NonNull ClassDefinition... classDefs) {
100         for (ClassDefinition classDef : classDefs) {
101             checkClassIsModifiable(classDef.getDefinitionClass());
102         }
103 
104         try {
105             // noinspection ConstantConditions
106             instrumentation.redefineClasses(classDefs);
107         } catch (ClassNotFoundException e) {
108             throw new RuntimeException(e); // should never happen
109         } catch (UnmodifiableClassException e) {
110             // This can happen if a class is in a restricted JDK module that the JVM does not allow to be modified.
111             // Provide a clear error to help the user understand what to do.
112             throw new IllegalArgumentException(buildUnmockableErrorMessage(classDefs), e);
113         } catch (InternalError ignore) {
114             // If a class to be redefined hasn't been loaded yet, the JVM may get a NoClassDefFoundError during
115             // redefinition. Unfortunately, it then throws a plain InternalError instead.
116             for (ClassDefinition classDef : classDefs) {
117                 detectMissingDependenciesIfAny(classDef.getDefinitionClass());
118             }
119 
120             // If the above didn't throw upon detecting a NoClassDefFoundError, then ignore the original error and
121             // continue, in order to prevent secondary failures.
122         }
123     }
124 
125     private static void checkClassIsModifiable(@NonNull Class<?> classToRedefine) {
126         // noinspection ConstantConditions
127         if (!instrumentation.isModifiableClass(classToRedefine)) {
128             throw new IllegalArgumentException("Class " + classToRedefine.getName()
129                     + " cannot be mocked/faked because the JVM does not allow it to be modified. "
130                     + UNMOCKABLE_CLASS_SUGGESTION);
131         }
132     }
133 
134     @NonNull
135     private static String buildUnmockableErrorMessage(@NonNull ClassDefinition[] classDefs) {
136         StringBuilder sb = new StringBuilder("The JVM prevented modification of class(es):");
137         for (ClassDefinition classDef : classDefs) {
138             sb.append(' ').append(classDef.getDefinitionClass().getName());
139         }
140         sb.append(". ").append(UNMOCKABLE_CLASS_SUGGESTION);
141         return sb.toString();
142     }
143 
144     private static void detectMissingDependenciesIfAny(@NonNull Class<?> mockedClass) {
145         try {
146             Class.forName(mockedClass.getName(), false, mockedClass.getClassLoader());
147         } catch (NoClassDefFoundError e) {
148             throw new RuntimeException("Unable to mock " + mockedClass + " due to a missing dependency", e);
149         } catch (ClassNotFoundException ignore) {
150             // Shouldn't happen since the mocked class would already have been found in the classpath.
151         }
152     }
153 
154     @Nullable
155     public static Class<?> getClassIfLoaded(@NonNull String classDescOrName) {
156         String className = classDescOrName.replace('/', '.');
157         @SuppressWarnings("ConstantConditions")
158         Class<?>[] loadedClasses = instrumentation.getAllLoadedClasses();
159 
160         for (Class<?> aClass : loadedClasses) {
161             if (aClass.getName().equals(className)) {
162                 return aClass;
163             }
164         }
165 
166         return null;
167     }
168 }