// =================================================================================================
// Copyright 2011 Twitter, Inc.
// -------------------------------------------------------------------------------------------------
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this work except in compliance with the License.
// You may obtain a copy of the License in the LICENSE file, or at:
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// =================================================================================================
package com.twitter.common.util.caching;
import com.google.common.base.Function;
import com.google.common.base.Preconditions;
import com.google.common.base.Predicate;
import com.google.common.base.Predicates;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Maps;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
/**
* A proxy class that handles caching of return values for method calls to a wrapped object.
*
* Example usage:
*
* Foo uncached = new Foo();
* CachingMethodProxy<Foo> methodProxy = CachingMethodProxy.proxyFor(uncached, Foo.class);
* Foo foo = methodProxy.getCachingProxy();
* methodProxy.cache(foo.doBar(), lruCache1)
* .cache(foo.doBaz(), lruCache2)
* .prepare();
*
* @author William Farner
*/
public class CachingMethodProxy<T> {
// Dummy return values to return when in recording state.
private static final Map<Class<?>, Object> EMPTY_RETURN_VALUES =
ImmutableMap.<Class<?>, Object>builder()
.put(Boolean.TYPE, Boolean.FALSE)
.put(Byte.TYPE, Byte.valueOf((byte) 0))
.put(Short.TYPE, Short.valueOf((short) 0))
.put(Character.TYPE, Character.valueOf((char)0))
.put(Integer.TYPE, Integer.valueOf(0))
.put(Long.TYPE, Long.valueOf(0))
.put(Float.TYPE, Float.valueOf(0))
.put(Double.TYPE, Double.valueOf(0))
.build();
private static final Map<Class<?>, Class<?>> AUTO_BOXING_MAP =
ImmutableMap.<Class<?>, Class<?>>builder()
.put(Boolean.TYPE, Boolean.class)
.put(Byte.TYPE, Byte.class)
.put(Short.TYPE, Short.class)
.put(Character.TYPE, Character.class)
.put(Integer.TYPE, Integer.class)
.put(Long.TYPE, Long.class)
.put(Float.TYPE, Float.class)
.put(Double.TYPE, Double.class)
.build();
// The uncached resource, whose method calls are deemed to be expensive and cacheable.
private final T uncached;
// The methods that are cached, and the caches themselves.
private final Map<Method, MethodCache> methodCaches = Maps.newHashMap();
private final Class<T> type;
private Method lastMethodCall = null;
private boolean recordMode = true;
/**
* Creates a new caching method proxy that will wrap an object and cache for the provided methods.
*
* @param uncached The uncached object that will be reverted to when a cache entry is not present.
*/
private CachingMethodProxy(T uncached, Class<T> type) {
this.uncached = Preconditions.checkNotNull(uncached);
this.type = Preconditions.checkNotNull(type);
Preconditions.checkArgument(type.isInterface(), "The proxied type must be an interface.");
}
private static Object invokeMethod(Object subject, Method method, Object[] args)
throws Throwable {
try {
return method.invoke(subject, args);
} catch (IllegalAccessException e) {
throw new RuntimeException("Cannot access " + subject.getClass() + "." + method, e);
} catch (InvocationTargetException e) {
throw e.getCause();
}
}
/**
* A cached method and its caching control structures.
*
* @param <K> Cache key type.
* @param <V> Cache value type, expected to match the return type of the method.
*/
private static class MethodCache<K, V> {
private final Method method;
private final Cache<K, V> cache;
private final Function<Object[], K> keyBuilder;
private final Predicate<V> entryFilter;
MethodCache(Method method, Cache<K, V> cache, Function<Object[], K> keyBuilder,
Predicate<V> entryFilter) {
this.method = method;
this.cache = cache;
this.keyBuilder = keyBuilder;
this.entryFilter = entryFilter;
}
V doInvoke(Object uncached, Object[] args) throws Throwable {
K key = keyBuilder.apply(args);
V cachedValue = cache.get(key);
if (cachedValue != null) return cachedValue;
Object fetched = invokeMethod(uncached, method, args);
if (fetched == null) return null;
@SuppressWarnings("unchecked")
V typedValue = (V) fetched;
if (entryFilter.apply(typedValue)) cache.put(key, typedValue);
return typedValue;
}
}
/**
* Creates a new builder for the given type.
*
* @param uncached The uncached object that should be insulated by caching.
* @param type The interface that a proxy should be created for.
* @param <T> Type parameter to the proxied class.
* @return A new builder.
*/
public static <T> CachingMethodProxy<T> proxyFor(T uncached, Class<T> type) {
return new CachingMethodProxy<T>(uncached, type);
}
@SuppressWarnings("unchecked")
public T getCachingProxy() {
return (T) Proxy.newProxyInstance(type.getClassLoader(), new Class[] { type },
new InvocationHandler() {
@Override public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable {
return doInvoke(method, args);
}
});
}
private Object doInvoke(Method method, Object[] args) throws Throwable {
return recordMode ? recordCall(method) : cacheRequest(method, args);
}
private Object recordCall(Method method) {
Preconditions.checkArgument(method.getReturnType() != Void.TYPE,
"Void return methods cannot be cached: " + method);
Preconditions.checkArgument(method.getParameterTypes().length > 0,
"Methods with zero arguments cannot be cached: " + method);
Preconditions.checkState(lastMethodCall == null,
"No cache instructions provided for call to: " + lastMethodCall);
lastMethodCall = method;
Class<?> returnType = method.getReturnType();
return returnType.isPrimitive() ? EMPTY_RETURN_VALUES.get(returnType) : null;
}
private Object cacheRequest(Method method, Object[] args) throws Throwable {
MethodCache cache = methodCaches.get(method);
// Check if we are caching for this method.
if (cache == null) return invokeMethod(uncached, method, args);
return cache.doInvoke(uncached, args);
}
/**
* Instructs the proxy that cache setup is complete, and the proxy instance should begin caching
* and delegating uncached calls. After this is called, any subsequent calls to any of the
* cache setup methods will result in an {@link IllegalStateException}.
*/
public void prepare() {
Preconditions.checkState(!methodCaches.isEmpty(), "At least one method must be cached.");
Preconditions.checkState(recordMode, "prepare() may only be invoked once.");
recordMode = false;
}
public <V> CachingMethodProxy<T> cache(V value, Cache<List, V> cache) {
return cache(value, cache, Predicates.<V>alwaysTrue());
}
public <V> CachingMethodProxy<T> cache(V value, Cache<List, V> cache,
Predicate<V> valueFilter) {
return cache(value, cache, DEFAULT_KEY_BUILDER, valueFilter);
}
public <K, V> CachingMethodProxy<T> cache(V value, Cache<K, V> cache,
Function<Object[], K> keyBuilder) {
// Get the last method call and declare it the cached method.
return cache(value, cache, keyBuilder, Predicates.<V>alwaysTrue());
}
public <K, V> CachingMethodProxy<T> cache(V value, Cache<K, V> cache,
Function<Object[], K> keyBuilder, Predicate<V> valueFilter) {
Preconditions.checkNotNull(cache);
Preconditions.checkNotNull(keyBuilder);
Preconditions.checkNotNull(valueFilter);
Preconditions.checkState(recordMode, "Cache setup is not allowed after prepare() is called.");
// Get the last method call and declare it the cached method.
Preconditions.checkState(lastMethodCall != null, "No method call captured to be cached.");
Class<?> returnType = lastMethodCall.getReturnType();
Preconditions.checkArgument(returnType != Void.TYPE,
"Cannot cache results from void method: " + lastMethodCall);
if (returnType.isPrimitive()) {
// If a primitive type is returned, we need to make sure that the cache holds the boxed
// type for the primitive.
returnType = AUTO_BOXING_MAP.get(returnType);
}
// TODO(William Farner): Figure out a simple way to make this possible. Right now, since the proxy
// objects return null, we get a null here and can't check the type.
//Preconditions.checkArgument(value.getClass() == returnType,
// String.format("Cache value type '%s' does not match method return type '%s'",
// value.getClass(), lastMethodCall.getReturnType()));
methodCaches.put(lastMethodCall, new MethodCache<K, V>(lastMethodCall, cache, keyBuilder,
valueFilter));
lastMethodCall = null;
return this;
}
private static final Function<Object[], List> DEFAULT_KEY_BUILDER =
new Function<Object[], List>() {
@Override public List apply(Object[] args) {
return Arrays.asList(args);
}
};
}