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