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.state;
7   
8   import edu.umd.cs.findbugs.annotations.NonNull;
9   import edu.umd.cs.findbugs.annotations.Nullable;
10  
11  import java.lang.instrument.ClassFileTransformer;
12  import java.security.ProtectionDomain;
13  import java.util.HashMap;
14  import java.util.Map;
15  import java.util.WeakHashMap;
16  
17  import mockit.internal.startup.Startup;
18  
19  /**
20   * Holds a map of internal class names to the corresponding class files (bytecode arrays), for the classes that have
21   * already been loaded during the test run. These classfiles are not necessarily the same as those stored in the
22   * corresponding ".class" files available from the runtime classpath. If any third-party {@link ClassFileTransformer}s
23   * are active, those original classfiles may have been modified before being loaded by the JVM. JMockit installs a
24   * <code>ClassFileTransformer</code> of its own which saves all potentially modified classfiles here.
25   * <p>
26   * This bytecode cache allows classes to be mocked and un-mocked correctly, even in the presence of other bytecode
27   * modification agents such as the AspectJ load-time weaver.
28   */
29  public final class CachedClassfiles implements ClassFileTransformer {
30      @NonNull
31      public static final CachedClassfiles INSTANCE = new CachedClassfiles();
32  
33      @NonNull
34      private final Map<ClassLoader, Map<String, byte[]>> classLoadersAndClassfiles;
35      @Nullable
36      private Class<?> classBeingCached;
37  
38      private CachedClassfiles() {
39          classLoadersAndClassfiles = new WeakHashMap<>(2);
40      }
41  
42      @Nullable
43      @Override
44      public byte[] transform(@Nullable ClassLoader loader, String classDesc,
45              @Nullable Class<?> classBeingRedefinedOrRetransformed, @Nullable ProtectionDomain protectionDomain,
46              @NonNull byte[] classfileBuffer) {
47          // can be null for Java 8 lambdas
48          if (classDesc != null && classBeingRedefinedOrRetransformed != null
49                  && classBeingRedefinedOrRetransformed == classBeingCached) {
50              addClassfile(loader, classDesc, classfileBuffer);
51              classBeingCached = null;
52          }
53  
54          return null;
55      }
56  
57      private void addClassfile(@Nullable ClassLoader loader, @NonNull String classDesc, @NonNull byte[] classfile) {
58          Map<String, byte[]> classfiles = getClassfiles(loader);
59          classfiles.put(classDesc, classfile);
60      }
61  
62      @NonNull
63      private Map<String, byte[]> getClassfiles(@Nullable ClassLoader loader) {
64          return classLoadersAndClassfiles.computeIfAbsent(loader, k -> new HashMap<>(100));
65      }
66  
67      @Nullable
68      private byte[] findClassfile(@NonNull Class<?> aClass) {
69          String className = aClass.getName();
70  
71          // Discards an invalid numerical suffix from a synthetic Java 8 class, if detected.
72          int p = className.indexOf('/');
73          if (p > 0) {
74              className = className.substring(0, p);
75          }
76  
77          Map<String, byte[]> classfiles = getClassfiles(aClass.getClassLoader());
78          return classfiles.get(className.replace('.', '/'));
79      }
80  
81      @Nullable
82      public static synchronized byte[] getClassfile(@NonNull String classDesc) {
83          return INSTANCE.findClassfile(classDesc);
84      }
85  
86      @Nullable
87      private byte[] findClassfile(@NonNull String classDesc) {
88          byte[] classfile = null;
89  
90          for (Map<String, byte[]> classfiles : classLoadersAndClassfiles.values()) {
91              classfile = classfiles.get(classDesc);
92  
93              if (classfile != null) {
94                  return classfile;
95              }
96          }
97  
98          Class<?> desiredClass = Startup.getClassIfLoaded(classDesc);
99  
100         if (desiredClass != null) {
101             classBeingCached = desiredClass;
102             Startup.retransformClass(desiredClass);
103             ClassLoader classLoader = desiredClass.getClassLoader();
104             classfile = INSTANCE.findClassfile(classLoader, classDesc);
105         }
106 
107         return classfile;
108     }
109 
110     @Nullable
111     private synchronized byte[] findClassfile(@Nullable ClassLoader loader, @NonNull String classDesc) {
112         Map<String, byte[]> classfiles = getClassfiles(loader);
113         return classfiles.get(classDesc);
114     }
115 
116     @Nullable
117     public static synchronized byte[] getClassfile(@NonNull Class<?> aClass) {
118         byte[] cached = INSTANCE.findClassfile(aClass);
119         if (cached != null) {
120             return cached;
121         }
122 
123         INSTANCE.classBeingCached = aClass;
124         Startup.retransformClass(aClass);
125         return INSTANCE.findClassfile(aClass);
126     }
127 
128     @Nullable
129     public static byte[] getClassfile(@Nullable ClassLoader loader, @NonNull String internalClassName) {
130         return INSTANCE.findClassfile(loader, internalClassName);
131     }
132 
133     public static void addClassfile(@NonNull Class<?> aClass, @NonNull byte[] classfile) {
134         INSTANCE.addClassfile(aClass.getClassLoader(), aClass.getName().replace('.', '/'), classfile);
135     }
136 }