View Javadoc
1   package emissary.util;
2   
3   import jakarta.annotation.Nullable;
4   
5   import java.lang.ref.SoftReference;
6   
7   /**
8    * This implements a simple caching mechanism for {@link Class#forName(String)}. For example if the same class name is
9    * looked up repeatedly, using this cache may be able to avoid a lot of JVM reflection overhead.
10   *
11   * <p>
12   * To use this, just call {@link ClassLookupCache#lookup(String)} where you would normally use
13   * {@link Class#forName(String)}. There are also methods for directly manipulating the cache but most uses can avoid
14   * those.
15   *
16   * <p>
17   * Note that the cache implementation may have a small capacity and/or be thread-specific, so storing something in the
18   * cache does not <i>guarantee</i> that it will be indefinitely cached or that the cached value will be visible to other
19   * threads.
20   */
21  public final class ClassLookupCache {
22  
23      /**
24       * A binding from a name string to a {@link Class} object.
25       *
26       * @param <T> The object type that will be produced by the class.
27       */
28      private static final class NamedClass<T> {
29  
30          /** The class name. */
31          private final String className;
32  
33          /** A class object that matches {@link #className}. */
34          private final Class<T> clazz;
35  
36          /**
37           * Create a new binding between a class name and a matching object.
38           *
39           * @param className the class name.
40           * @param clazz A class object that matches {@code className}.
41           */
42          private NamedClass(final String className, final Class<T> clazz) {
43              this.className = className;
44              this.clazz = clazz;
45          }
46  
47          /**
48           * Get the class object, if it has the given name.
49           *
50           * @param desiredName The class name to check.
51           * @return If this instance has the given {@code desiredName}, this returns its {@link Class} object. Otherwise
52           *         {@code null}.
53           */
54          @Nullable
55          public Class<T> getClass(final String desiredName) {
56              return this.className.equals(desiredName) ? this.clazz : null;
57          }
58  
59          /**
60           * Get a binding between a class name and a matching object.
61           *
62           * <p>
63           * The reason we use a static builder method is that it can handle wildcards such as {@code <?>} more easily than when
64           * calling the constructor directly.
65           *
66           * @param className the class name.
67           * @param clazz A class object that matches {@code className}.
68           * @return A binding between the given name and class object.
69           */
70          public static <T> NamedClass<T> getInstance(final String className, final Class<T> clazz) {
71              return new NamedClass<>(className, clazz);
72          }
73      }
74  
75      /**
76       * A cached class object. If a thread is asked to repeatedly construct the same type of object, we can cache the class
77       * name lookup so that subsequent constructions can get to the {@link Class} object without doing a full lookup in the
78       * JVM.
79       */
80      private static final ThreadLocal<SoftReference<NamedClass<?>>> cachedLookupResult = new ThreadLocal<>();
81  
82      /**
83       * Look up a class in the cache.
84       *
85       * @param className The class name to find.
86       * @return If the class name is currently known to the cache, the corresponding {@link Class} object is returned.
87       *         Otherwise {@code null}.
88       */
89      @Nullable
90      public static Class<?> get(final String className) {
91          // Currently there is at most one cached lookup per thread, so
92          // we can just check if the current thread knows about the
93          // given class name.
94          final SoftReference<NamedClass<?>> softResult = cachedLookupResult.get();
95          if (softResult == null) {
96              return null; // Nothing is currently cached in this thread.
97          }
98  
99          final NamedClass<?> actualResult = softResult.get();
100         if (actualResult == null) {
101             return null; // There was something cached but it's been lost.
102         }
103 
104         // We do have a cached lookup. It can be used iff it matches
105         // the given name.
106         return actualResult.getClass(className);
107     }
108 
109     /**
110      * Store a class lookup in the cache.
111      *
112      * @param className The class name.
113      * @param clazz The class. Assumed to match {@code className}.
114      */
115     public static void put(final String className, final Class<?> clazz) {
116         cachedLookupResult.set(new SoftReference<>(NamedClass.getInstance(className, clazz)));
117     }
118 
119     /**
120      * Look up a class by name. This is basically a utility method that can be called instead of
121      * {@link Class#forName(String)}, and will try to use the cache to speed up the lookups.
122      *
123      * @param className The class name to get.
124      * @return The {@link Class} object corresponding to {@code className}.
125      * @throws ClassNotFoundException If the class name could not be resolved.
126      */
127     public static Class<?> lookup(final String className) throws ClassNotFoundException {
128         final Class<?> cachedResult = get(className);
129         if (cachedResult != null) {
130             // We found the class in the cache.
131             return cachedResult;
132         } else {
133             // The given class name is not currently cached, so look
134             // it up directly.
135             final Class<?> uncachedResult = Class.forName(className);
136 
137             // If we reach here, the class was found. Cache the
138             // result before returning.
139             put(className, uncachedResult);
140             return uncachedResult;
141         }
142     }
143 
144     /**
145      * Destroy the ThreadLocal cache object
146      */
147     public static void unload() {
148         cachedLookupResult.remove();
149     }
150 
151     /** This is a static utility class, so prevent instantiation. */
152     private ClassLookupCache() {}
153 }