diff --git a/config/spotbugs/exclude.xml b/config/spotbugs/exclude.xml
index d097bb0293..e0ed5ec959 100644
--- a/config/spotbugs/exclude.xml
+++ b/config/spotbugs/exclude.xml
@@ -259,6 +259,11 @@
+
+
+
+
+
diff --git a/guava/src/main/java/com/github/benmanes/caffeine/guava/CaffeinatedGuava.java b/guava/src/main/java/com/github/benmanes/caffeine/guava/CaffeinatedGuava.java
index a1441b7f5e..42ea69f394 100644
--- a/guava/src/main/java/com/github/benmanes/caffeine/guava/CaffeinatedGuava.java
+++ b/guava/src/main/java/com/github/benmanes/caffeine/guava/CaffeinatedGuava.java
@@ -15,6 +15,8 @@
*/
package com.github.benmanes.caffeine.guava;
+import static java.util.Objects.requireNonNull;
+
import java.lang.reflect.Method;
import com.github.benmanes.caffeine.cache.Caffeine;
@@ -26,7 +28,7 @@
import com.google.errorprone.annotations.CheckReturnValue;
/**
- * An adapter to expose a Caffeine cache through the Guava interfaces.
+ * Static utility methods pertaining to adapting between Caffeine and Guava cache interfaces.
*
* @author ben.manes@gmail.com (Ben Manes)
*/
@@ -57,9 +59,7 @@ public static LoadingCache build(
Caffeine builder, CacheLoader super K1, V1> loader) {
@SuppressWarnings("unchecked")
CacheLoader castedLoader = (CacheLoader) loader;
- return build(builder, hasLoadAll(castedLoader)
- ? new BulkLoader<>(castedLoader)
- : new SingleLoader<>(castedLoader));
+ return build(builder, caffeinate(castedLoader, /* externalized */ false));
}
/**
@@ -76,6 +76,27 @@ public static LoadingCache build(
return new CaffeinatedGuavaLoadingCache<>(builder.build(loader));
}
+ /**
+ * Returns a Caffeine cache loader that delegates to a Guava cache loader.
+ *
+ * @param loader the cache loader used to obtain new values
+ * @return a cache loader exposed under the Caffeine APIs
+ */
+ @CheckReturnValue
+ public static com.github.benmanes.caffeine.cache.CacheLoader caffeinate(
+ CacheLoader loader) {
+ requireNonNull(loader);
+ return caffeinate(loader, /* externalized */ true);
+ }
+
+ private static com.github.benmanes.caffeine.cache.CacheLoader caffeinate(
+ CacheLoader loader, boolean externalized) {
+ requireNonNull(loader);
+ return hasLoadAll(loader)
+ ? new BulkLoader<>(loader, externalized)
+ : new SingleLoader<>(loader, externalized);
+ }
+
static boolean hasLoadAll(CacheLoader, ?> cacheLoader) {
return hasMethod(cacheLoader, "loadAll", Iterable.class);
}
diff --git a/guava/src/main/java/com/github/benmanes/caffeine/guava/CaffeinatedGuavaLoadingCache.java b/guava/src/main/java/com/github/benmanes/caffeine/guava/CaffeinatedGuavaLoadingCache.java
index dfbd6b6b28..9dce0284a6 100644
--- a/guava/src/main/java/com/github/benmanes/caffeine/guava/CaffeinatedGuavaLoadingCache.java
+++ b/guava/src/main/java/com/github/benmanes/caffeine/guava/CaffeinatedGuavaLoadingCache.java
@@ -21,15 +21,20 @@
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
+import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Executor;
+
+import org.checkerframework.checker.nullness.qual.Nullable;
import com.github.benmanes.caffeine.cache.CacheLoader;
-import com.google.common.base.Throwables;
import com.google.common.cache.CacheLoader.InvalidCacheLoadException;
import com.google.common.cache.LoadingCache;
import com.google.common.collect.ImmutableMap;
import com.google.common.util.concurrent.ExecutionError;
+import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.UncheckedExecutionException;
/**
@@ -127,13 +132,15 @@ static class SingleLoader implements CacheLoader, Serializable {
private static final long serialVersionUID = 1L;
final com.google.common.cache.CacheLoader cacheLoader;
+ final boolean externalized;
- SingleLoader(com.google.common.cache.CacheLoader cacheLoader) {
+ SingleLoader(com.google.common.cache.CacheLoader cacheLoader, boolean external) {
this.cacheLoader = requireNonNull(cacheLoader);
+ this.externalized = external;
}
@Override
- public V load(K key) {
+ public V load(K key) throws Exception {
try {
V value = cacheLoader.load(key);
if (value == null) {
@@ -143,40 +150,43 @@ public V load(K key) {
} catch (RuntimeException | Error e) {
throw e;
} catch (InterruptedException e) {
- Thread.currentThread().interrupt();
- throw new CacheLoaderException(e);
+ if (externalized) {
+ throw e;
+ } else {
+ Thread.currentThread().interrupt();
+ throw new CacheLoaderException(e);
+ }
} catch (Exception e) {
- throw new CacheLoaderException(e);
+ throw externalized ? e : new CacheLoaderException(e);
}
}
@Override
- public V reload(K key, V oldValue) {
+ public CompletableFuture asyncReload(K key, V oldValue, Executor executor) {
+ var future = new CompletableFuture();
try {
- V value = Futures.getUnchecked(cacheLoader.reload(key, oldValue));
- if (value == null) {
- throw new InvalidCacheLoadException("null value");
+ ListenableFuture reload = cacheLoader.reload(key, oldValue);
+ if (reload == null) {
+ future.completeExceptionally(new InvalidCacheLoadException("null value"));
+ } else {
+ Futures.addCallback(reload, new FutureCompleter<>(future), Runnable::run);
}
- return value;
- } catch (InterruptedException e) {
- Thread.currentThread().interrupt();
- throw new CacheLoaderException(e);
- } catch (Exception e) {
- Throwables.throwIfUnchecked(e);
- throw new RuntimeException(e);
+ } catch (Throwable t) {
+ future.completeExceptionally(t);
}
+ return future;
}
}
static final class BulkLoader extends SingleLoader {
private static final long serialVersionUID = 1L;
- BulkLoader(com.google.common.cache.CacheLoader cacheLoader) {
- super(cacheLoader);
+ BulkLoader(com.google.common.cache.CacheLoader cacheLoader, boolean externalized) {
+ super(cacheLoader, externalized);
}
@Override
- public Map loadAll(Set extends K> keys) {
+ public Map loadAll(Set extends K> keys) throws Exception {
try {
Map loaded = cacheLoader.loadAll(keys);
if (loaded == null) {
@@ -185,7 +195,9 @@ public Map loadAll(Set extends K> keys) {
Map result = new HashMap<>(loaded.size(), /* load factor */ 1.0f);
loaded.forEach((key, value) -> {
if ((key == null) || (value == null)) {
- nullBulkLoad.set(true);
+ if (!externalized) {
+ nullBulkLoad.set(true);
+ }
} else {
result.put(key, value);
}
@@ -194,11 +206,36 @@ public Map loadAll(Set extends K> keys) {
} catch (RuntimeException | Error e) {
throw e;
} catch (InterruptedException e) {
+ if (externalized) {
+ throw e;
+ }
Thread.currentThread().interrupt();
throw new CacheLoaderException(e);
} catch (Exception e) {
+ if (externalized) {
+ throw e;
+ }
throw new CacheLoaderException(e);
}
}
}
+
+ static final class FutureCompleter implements FutureCallback {
+ final CompletableFuture future;
+
+ FutureCompleter(CompletableFuture future) {
+ this.future = future;
+ }
+
+ @Override public void onSuccess(@Nullable V value) {
+ if (value == null) {
+ future.completeExceptionally(new InvalidCacheLoadException("null value"));
+ } else {
+ future.complete(value);
+ }
+ }
+ @Override public void onFailure(Throwable t) {
+ future.completeExceptionally(t);
+ }
+ }
}
diff --git a/guava/src/test/java/com/github/benmanes/caffeine/guava/CaffeinatedGuavaTest.java b/guava/src/test/java/com/github/benmanes/caffeine/guava/CaffeinatedGuavaTest.java
index 609eb3cc4e..fab81fccb6 100644
--- a/guava/src/test/java/com/github/benmanes/caffeine/guava/CaffeinatedGuavaTest.java
+++ b/guava/src/test/java/com/github/benmanes/caffeine/guava/CaffeinatedGuavaTest.java
@@ -15,13 +15,26 @@
*/
package com.github.benmanes.caffeine.guava;
+import static com.google.common.truth.Truth.assertThat;
+
import java.lang.reflect.Constructor;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.CompletionException;
+
+import org.junit.Assert;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.github.benmanes.caffeine.guava.CaffeinatedGuavaCache.CacheLoaderException;
+import com.github.benmanes.caffeine.guava.CaffeinatedGuavaLoadingCache.BulkLoader;
+import com.github.benmanes.caffeine.guava.CaffeinatedGuavaLoadingCache.SingleLoader;
import com.github.benmanes.caffeine.guava.compatibility.TestingCacheLoaders;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Maps;
import com.google.common.testing.SerializableTester;
import com.google.common.util.concurrent.MoreExecutors;
@@ -82,6 +95,121 @@ public void testReload_throwable() {
}
}
+ public void testCacheLoader_null() {
+ try {
+ CaffeinatedGuava.caffeinate(null);
+ Assert.fail();
+ } catch (NullPointerException expected) {}
+ }
+
+ public void testCacheLoader_exception() throws Exception {
+ runCacheLoaderExceptionTest(new InterruptedException());
+ runCacheLoaderExceptionTest(new RuntimeException());
+ runCacheLoaderExceptionTest(new Exception());
+ }
+
+ public void runCacheLoaderExceptionTest(Exception error) throws Exception {
+ var guava = new CacheLoader() {
+ @Override public Integer load(Integer key) throws Exception {
+ throw error;
+ }
+ @Override public ImmutableMap loadAll(
+ Iterable extends Integer> keys) throws Exception {
+ throw error;
+ }
+ };
+ var caffeine = CaffeinatedGuava.caffeinate(guava);
+ try {
+ caffeine.load(1);
+ Assert.fail();
+ } catch (Exception e) {
+ assertThat(e).isSameInstanceAs(error);
+ }
+ try {
+ caffeine.loadAll(Set.of(1, 2));
+ Assert.fail();
+ } catch (Exception e) {
+ assertThat(e).isSameInstanceAs(error);
+ }
+ try {
+ caffeine.asyncReload(1, 2, Runnable::run).join();
+ Assert.fail();
+ } catch (CompletionException e) {
+ assertThat(e).hasCauseThat().isSameInstanceAs(error);
+ }
+ }
+
+ public void testCacheLoader_single() throws Exception {
+ var error = new Exception();
+ var guava = new CacheLoader() {
+ @Override public Integer load(Integer key) throws Exception {
+ if (key > 0) {
+ return -key;
+ }
+ throw error;
+ }
+ };
+ var caffeine = CaffeinatedGuava.caffeinate(guava);
+ assertThat(caffeine).isNotInstanceOf(BulkLoader.class);
+ checkSingleLoader(error, guava, caffeine);
+ }
+
+ public void testCacheLoader_bulk() throws Exception {
+ var error = new Exception();
+ var guava = new CacheLoader() {
+ @Override public Integer load(Integer key) throws Exception {
+ if (key > 0) {
+ return -key;
+ }
+ throw error;
+ }
+ @Override public ImmutableMap loadAll(
+ Iterable extends Integer> keys) throws Exception {
+ if (Iterables.all(keys, key -> key > 0)) {
+ return Maps.toMap(ImmutableSet.copyOf(keys), key -> -key);
+ }
+ throw error;
+ }
+ };
+ var caffeine = CaffeinatedGuava.caffeinate(guava);
+ checkSingleLoader(error, guava, caffeine);
+ checkBulkLoader(error, caffeine);
+ }
+
+ private static void checkSingleLoader(Exception error, CacheLoader guava,
+ com.github.benmanes.caffeine.cache.CacheLoader caffeine) throws Exception {
+ assertThat(caffeine).isInstanceOf(SingleLoader.class);
+ assertThat(((SingleLoader, ?>) caffeine).cacheLoader).isSameInstanceAs(guava);
+
+ assertThat(caffeine.load(1)).isEqualTo(-1);
+ try {
+ caffeine.load(-1);
+ Assert.fail();
+ } catch (Exception e) {
+ assertThat(e).isSameInstanceAs(error);
+ }
+
+ assertThat(caffeine.asyncReload(1, 2, Runnable::run).join()).isEqualTo(-1);
+ try {
+ caffeine.asyncReload(-1, 2, Runnable::run).join();
+ Assert.fail();
+ } catch (CompletionException e) {
+ assertThat(e).hasCauseThat().isSameInstanceAs(error);
+ }
+ }
+
+ private static void checkBulkLoader(Exception error,
+ com.github.benmanes.caffeine.cache.CacheLoader caffeine) throws Exception {
+ assertThat(caffeine).isInstanceOf(BulkLoader.class);
+ assertThat(caffeine.loadAll(Set.of(1, 2, 3))).isEqualTo(Map.of(1, -1, 2, -2, 3, -3));
+ try {
+ caffeine.loadAll(Set.of(1, -1));
+ Assert.fail();
+ } catch (Exception e) {
+ assertThat(e).isSameInstanceAs(error);
+ }
+ }
+
enum IdentityLoader implements com.github.benmanes.caffeine.cache.CacheLoader