ConstructorLookupCache.java
package emissary.util;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.lang.ref.SoftReference;
import java.lang.reflect.Constructor;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import javax.annotation.Nullable;
/**
* This implements a simple caching mechanism for {@link Constructor} lookups. For example if the same class constructor
* is looked up repeatedly, using this cache may be able to avoid a lot of JVM reflection overhead.
*
* <p>
* Note that the cache implementation may have a small capacity and/or be thread-specific, so storing something in the
* cache does not <i>guarantee</i> that it will be indefinitely cached or that the cached value will be visible to other
* threads.
*/
@SuppressWarnings("AvoidObjectArrays")
public final class ConstructorLookupCache {
private static final Logger logger = LoggerFactory.getLogger(ConstructorLookupCache.class);
/**
* Represents a known result for looking up a class constructor that can handle a specific set of argument types.
*/
private static final class KnownConstructor {
/** The class being constructed. */
private final Class<?> clazz;
/** The argument types that will be passed to the constructor. */
private final Class<?>[] argTypes;
/**
* A constructor for {@link #clazz} that accepts the argument types from {@link #argTypes}.
*/
private final Constructor<?> constructor;
/**
* Create a binding between a class, some constructor argument types, and a constructor.
*
* @param clazz The class being constructed.
* @param argTypes The argument types that will be passed to the constructor.
* @param constructor A constructor for {@code clazz} that can accept the argument types specified in {@code argTypes}.
*/
public KnownConstructor(final Class<?> clazz, final Class<?>[] argTypes, final Constructor<?> constructor) {
this.clazz = clazz;
this.argTypes = argTypes;
this.constructor = constructor;
}
/**
* Get the stored constructor if it matches the given class and argument types.
*
* @param desiredClazz The type of class being constructed.
* @param desiredArgTypes The argument types that will be passed to the constructor.
* @return If the class and argument types match the ones that are stored, this returns the stored constructor that is
* assumed to be compatible with those argument types and create an object of the given type; otherwise
* {@code null}.
*/
@Nullable
public Constructor<?> getConstructor(final Class<?> desiredClazz, final Class<?>[] desiredArgTypes) {
// The stored class has to match the desired class.
if (!this.clazz.equals(desiredClazz)) {
return null; // Non-matching class.
}
// The stored argument count has to match the desired
// argument count.
final Class<?>[] myArgTypes = this.argTypes;
if (myArgTypes.length != desiredArgTypes.length) {
return null; // The number of arguments doesn't match.
}
// The stored argument types have to match the desired
// argument types.
for (int i = 0; i < myArgTypes.length; i++) {
if (myArgTypes[i] == null) {
if (desiredArgTypes[i] != null) {
return null; // Non-matching null argument type.
}
} else if (!myArgTypes[i].equals(desiredArgTypes[i])) {
return null; // Non-matching non-null argument type.
}
}
// At this point we know that we have the desired class
// and arguments.
return this.constructor;
}
}
/**
* A cached constructor lookup result. If a thread is asked to invoke the same constructor repeatedly, we can cache the
* result of the lookup to avoid some costly reflection calls.
*/
private static final ThreadLocal<SoftReference<KnownConstructor>> cachedConstructorLookup = new ThreadLocal<>();
/** A table mapping boxed classes to their primitive types. */
private static final Map<Class<?>, Class<?>> primClass = new HashMap<>();
// Initialize the mappings.
static {
primClass.put(Integer.class, Integer.TYPE);
primClass.put(Boolean.class, Boolean.TYPE);
primClass.put(Float.class, Float.TYPE);
primClass.put(Character.class, Character.TYPE);
primClass.put(Long.class, Long.TYPE);
primClass.put(Double.class, Double.TYPE);
primClass.put(Byte.class, Byte.TYPE);
}
/**
* Convert a boxed type into its primitive type.
*
* @param clazz The type of interest.
* @return If {@code clazz} is a boxed primitive, return the primitive type; otherwise just return {@code clazz}.
*/
private static Class<?> getPrim(final Class<?> clazz) {
final Class<?> prim = primClass.get(clazz);
return (prim != null) ? prim : clazz;
}
/**
* Look for a constructor for the given class type which can accept the given argument types, without using the lookup
* cache.
*
* @param clazz The type of object that will be constructed.
* @param argTypes The types of the arguments that will be passed to the class constructor.
* @return If {@code clazz} has a constructor that can accept the argument types in {@code argTypes}, this returns the
* matching constructor; otherwise {@code null}.
*/
@Nullable
private static Constructor<?> directConstructorLookup(final Class<?> clazz, final Class<?>[] argTypes) {
// Look for an exact match.
try {
return clazz.getConstructor(argTypes);
} catch (NoSuchMethodException e) {
logger.debug("No constructor for [{}] in Factory.create())", clazz.getName());
}
// There was no exact match, so look through the existing
// constructors for an assignable match.
NEXT_CANDIDATE_CONSTRUCTOR: for (final Constructor<?> candidate : clazz.getConstructors()) {
final Class<?>[] ctypes = candidate.getParameterTypes();
if (logger.isDebugEnabled()) {
logger.debug("Checking:{}, {}", clazz.getName(), clazz);
logger.debug(" types :{}", Arrays.toString(ctypes));
logger.debug(" numParms:{} =? {}", ctypes.length, argTypes.length);
}
// If the candidate constructor doesn't have the same
// number of arguments, it definitely isn't compatible
// with the desired argument types.
if (ctypes.length != argTypes.length) {
continue NEXT_CANDIDATE_CONSTRUCTOR;
}
// The candidate takes the right number of arguments, so
// compare its expected types to the types that will be
// passed. If the given type is not assignable to the
// expected type, then this constructor is not compatible.
for (int j = 0; j < argTypes.length; j++) {
final Class<?> a = ctypes[j];
final Class<?> b = argTypes[j];
if ((a != null) && (b != null)) {
if (a.isAssignableFrom(b)) {
logger.debug(" param={} assignable {}", a, b);
} else {
final Class<?> bPrim = getPrim(b);
if (a.isAssignableFrom(bPrim)) {
logger.debug(" param={} assignable {}", a, b);
} else {
logger.debug(" param={} !assignable {}", a, b);
continue NEXT_CANDIDATE_CONSTRUCTOR;
}
}
}
}
// If we reach here, the current candidate constructor is
// a compatible match.
return candidate;
}
// None of the class's available constructors are compatible
// with the given argument types.
return null;
}
/**
* Look for a constructor in the cache.
*
* @param clazz The class to be constructed.
* @param argTypes The argument types that the caller intends to pass to the constructor.
* @return If a matching constructor is in the cache, return it. Otherwise {@code null}.
*/
@Nullable
public static Constructor<?> get(final Class<?> clazz, final Class<?>[] argTypes) {
// Currently there is at most one cached lookup per thread, so
// we can just check if the current thread knows about the
// given class and constructor arguments.
final SoftReference<KnownConstructor> softResult = cachedConstructorLookup.get();
if (softResult == null) {
return null; // Nothing is currently cached in this thread.
}
final KnownConstructor knownResult = softResult.get();
if (knownResult == null) {
return null; // There was something cached but it's been lost.
}
// We do have a cached lookup. It can be used iff it matches the
// given class and constructor arguments.
return knownResult.getConstructor(clazz, argTypes);
}
/**
* Store a constructor lookup in the cache.
*
* @param clazz The class to be constructed.
* @param argTypes The argument types that the caller intends to pass to the constructor.
* @param constructor A constructor for {@code clazz} that can accept the argument types specified in {@code argTypes}.
*/
public static void put(final Class<?> clazz, final Class<?>[] argTypes, final Constructor<?> constructor) {
cachedConstructorLookup.set(new SoftReference<>(new KnownConstructor(clazz, argTypes, constructor)));
}
/**
* Look for a constructor for the given class type which can accept the given argument types.
*
* @param clazz The class to be constructed.
* @param argTypes The argument types that the caller intends to pass to the constructor.
* @return A matching constructor for the specified class, or {@code null} if no such constructor was found.
*/
public static Constructor<?> lookup(final Class<?> clazz, final Class<?>[] argTypes) {
final Constructor<?> cachedConstructor = get(clazz, argTypes);
if (cachedConstructor != null) {
// We found the constructor in the cache.
return cachedConstructor;
} else {
// The desired constructor is not currently cached, so
// look it up directly.
final Constructor<?> uncachedConstructor = directConstructorLookup(clazz, argTypes);
// If we got a result, cache it before returning.
if (uncachedConstructor != null) {
put(clazz, argTypes, uncachedConstructor);
}
return uncachedConstructor;
}
}
/**
* Destroy the ThreadLocal cache object
*/
public static void unload() {
cachedConstructorLookup.remove();
}
/** This is a static utility class, so prevent instantiation. */
private ConstructorLookupCache() {}
}