From 13aabeef37018e9315a6b041b1b83907dc6c500f Mon Sep 17 00:00:00 2001 From: Ben Manes Date: Sat, 14 Nov 2015 21:44:45 -0800 Subject: [PATCH] Add caching support for Caffeine Issue: SPR-13690 --- build.gradle | 2 + .../cache/caffeine/CaffeineCache.java | 136 ++++++++++++ .../cache/caffeine/CaffeineCacheManager.java | 206 ++++++++++++++++++ .../cache/caffeine/package-info.java | 6 + .../caffeine/CaffeineCacheManagerTests.java | 186 ++++++++++++++++ .../cache/caffeine/CaffeineCacheTests.java | 68 ++++++ .../concurrent/ConcurrentMapCacheManager.java | 3 +- 7 files changed, 606 insertions(+), 1 deletion(-) create mode 100644 spring-context-support/src/main/java/org/springframework/cache/caffeine/CaffeineCache.java create mode 100644 spring-context-support/src/main/java/org/springframework/cache/caffeine/CaffeineCacheManager.java create mode 100644 spring-context-support/src/main/java/org/springframework/cache/caffeine/package-info.java create mode 100644 spring-context-support/src/test/java/org/springframework/cache/caffeine/CaffeineCacheManagerTests.java create mode 100644 spring-context-support/src/test/java/org/springframework/cache/caffeine/CaffeineCacheTests.java diff --git a/build.gradle b/build.gradle index 559df1172d5..400b0ae53df 100644 --- a/build.gradle +++ b/build.gradle @@ -28,6 +28,7 @@ configure(allprojects) { project -> version = qualifyVersionIfNecessary(version) ext.aspectjVersion = "1.8.7" + ext.caffeineVersion = "2.0.1" ext.eclipselinkVersion = "2.4.2" ext.ehcacheVersion = "2.10.1" ext.ehcachejcacheVersion = "1.0.1" @@ -647,6 +648,7 @@ project("spring-context-support") { optional("javax.mail:javax.mail-api:${javamailVersion}") optional("javax.cache:cache-api:1.0.0") optional("com.google.guava:guava:${guavaVersion}") + optional("com.github.ben-manes.caffeine:caffeine:${caffeineVersion}") optional("net.sf.ehcache:ehcache:${ehcacheVersion}") optional("org.quartz-scheduler:quartz:2.2.2") optional("org.codehaus.fabric3.api:commonj:1.1.0") diff --git a/spring-context-support/src/main/java/org/springframework/cache/caffeine/CaffeineCache.java b/spring-context-support/src/main/java/org/springframework/cache/caffeine/CaffeineCache.java new file mode 100644 index 00000000000..3623e300357 --- /dev/null +++ b/spring-context-support/src/main/java/org/springframework/cache/caffeine/CaffeineCache.java @@ -0,0 +1,136 @@ +/* + * Copyright 2002-2015 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License 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 org.springframework.cache.caffeine; + +import java.util.function.Function; + +import org.springframework.cache.support.AbstractValueAdaptingCache; +import org.springframework.util.Assert; + +import com.github.benmanes.caffeine.cache.LoadingCache; + +/** + * Spring {@link org.springframework.cache.Cache} adapter implementation + * on top of a Caffeine {@link com.github.benmanes.caffeine.cache.Cache} instance. + * + *

Requires Caffeine 2.0 or higher. + * + * @author Juergen Hoeller + * @author Stephane Nicoll + * @author Ben Manes + * @since 4.0 + */ +public class CaffeineCache extends AbstractValueAdaptingCache { + + private final String name; + + private final com.github.benmanes.caffeine.cache.Cache cache; + + + /** + * Create a {@link CaffeineCache} instance with the specified name and the + * given internal {@link com.github.benmanes.caffeine.cache.Cache} to use. + * @param name the name of the cache + * @param cache the backing Caffeine Cache instance + */ + public CaffeineCache(String name, com.github.benmanes.caffeine.cache.Cache cache) { + this(name, cache, true); + } + + /** + * Create a {@link CaffeineCache} instance with the specified name and the + * given internal {@link com.github.benmanes.caffeine.cache.Cache} to use. + * @param name the name of the cache + * @param cache the backing Caffeine Cache instance + * @param allowNullValues whether to accept and convert {@code null} + * values for this cache + */ + public CaffeineCache(String name, com.github.benmanes.caffeine.cache.Cache cache, + boolean allowNullValues) { + super(allowNullValues); + Assert.notNull(name, "Name must not be null"); + Assert.notNull(cache, "Cache must not be null"); + this.name = name; + this.cache = cache; + } + + + @Override + public final String getName() { + return this.name; + } + + @Override + public final com.github.benmanes.caffeine.cache.Cache getNativeCache() { + return this.cache; + } + + @Override + public ValueWrapper get(Object key) { + if (this.cache instanceof LoadingCache) { + Object value = ((LoadingCache) this.cache).get(key); + return toValueWrapper(value); + } + return super.get(key); + } + + @Override + protected Object lookup(Object key) { + return this.cache.getIfPresent(key); + } + + @Override + public void put(Object key, Object value) { + this.cache.put(key, toStoreValue(value)); + } + + @Override + public ValueWrapper putIfAbsent(Object key, final Object value) { + PutIfAbsentFunction callable = new PutIfAbsentFunction(value); + Object result = this.cache.get(key, callable); + return (callable.called ? null : toValueWrapper(result)); + } + + @Override + public void evict(Object key) { + this.cache.invalidate(key); + } + + @Override + public void clear() { + this.cache.invalidateAll(); + } + + + private class PutIfAbsentFunction implements Function { + + private final Object value; + + private boolean called; + + public PutIfAbsentFunction(Object value) { + this.value = value; + } + + @Override + public Object apply(Object key) { + this.called = true; + return toStoreValue(this.value); + } + } + +} diff --git a/spring-context-support/src/main/java/org/springframework/cache/caffeine/CaffeineCacheManager.java b/spring-context-support/src/main/java/org/springframework/cache/caffeine/CaffeineCacheManager.java new file mode 100644 index 00000000000..74ceca77fc9 --- /dev/null +++ b/spring-context-support/src/main/java/org/springframework/cache/caffeine/CaffeineCacheManager.java @@ -0,0 +1,206 @@ +/* + * Copyright 2002-2014 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License 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 org.springframework.cache.caffeine; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +import org.springframework.cache.Cache; +import org.springframework.cache.CacheManager; +import org.springframework.util.Assert; +import org.springframework.util.ObjectUtils; + +import com.github.benmanes.caffeine.cache.CacheLoader; +import com.github.benmanes.caffeine.cache.Caffeine; + +/** + * {@link CacheManager} implementation that lazily builds {@link CaffeineCache} + * instances for each {@link #getCache} request. Also supports a 'static' mode + * where the set of cache names is pre-defined through {@link #setCacheNames}, + * with no dynamic creation of further cache regions at runtime. + * + *

The configuration of the underlying cache can be fine-tuned through a + * {@link Caffeine} builder, passed into this CacheManager through + * {@link #setCaffeine}. + * + *

Requires Caffeine 2.0 or higher. + * + * @author Juergen Hoeller + * @author Stephane Nicoll + * @author Ben Manes + * @since 4.0 + * @see CaffeineCache + */ +public class CaffeineCacheManager implements CacheManager { + + private final ConcurrentMap cacheMap = new ConcurrentHashMap(16); + + private boolean dynamic = true; + + private Caffeine cacheBuilder = Caffeine.newBuilder(); + + private CacheLoader cacheLoader; + + private boolean allowNullValues = true; + + + /** + * Construct a dynamic CaffeineCacheManager, + * lazily creating cache instances as they are being requested. + */ + public CaffeineCacheManager() { + } + + /** + * Construct a static CaffeineCacheManager, + * managing caches for the specified cache names only. + */ + public CaffeineCacheManager(String... cacheNames) { + setCacheNames(Arrays.asList(cacheNames)); + } + + + /** + * Specify the set of cache names for this CacheManager's 'static' mode. + *

The number of caches and their names will be fixed after a call to this method, + * with no creation of further cache regions at runtime. + *

Calling this with a {@code null} collection argument resets the + * mode to 'dynamic', allowing for further creation of caches again. + */ + public void setCacheNames(Collection cacheNames) { + if (cacheNames != null) { + for (String name : cacheNames) { + this.cacheMap.put(name, createCaffeineCache(name)); + } + this.dynamic = false; + } + else { + this.dynamic = true; + } + } + + /** + * Set the Caffeine to use for building each individual + * {@link CaffeineCache} instance. + * @see #createNativeCaffeineCache + * @see com.github.benmanes.caffeine.cache.CacheBuilder#build() + */ + public void setCaffeine(Caffeine cacheBuilder) { + Assert.notNull(cacheBuilder, "Caffeine must not be null"); + doSetCaffeine(cacheBuilder); + } + + /** + * Set the Caffeine CacheLoader to use for building each individual + * {@link CaffeineCache} instance, turning it into a LoadingCache. + * @see #createNativeCaffeineCache + * @see com.github.benmanes.caffeine.cache.Caffeine#build(CacheLoader) + * @see com.github.benmanes.caffeine.cache.LoadingCache + */ + public void setCacheLoader(CacheLoader cacheLoader) { + if (!ObjectUtils.nullSafeEquals(this.cacheLoader, cacheLoader)) { + this.cacheLoader = cacheLoader; + refreshKnownCaches(); + } + } + + /** + * Specify whether to accept and convert {@code null} values for all caches + * in this cache manager. + *

Default is "true", despite Caffeine itself not supporting {@code null} values. + * An internal holder object will be used to store user-level {@code null}s. + */ + public void setAllowNullValues(boolean allowNullValues) { + if (this.allowNullValues != allowNullValues) { + this.allowNullValues = allowNullValues; + refreshKnownCaches(); + } + } + + /** + * Return whether this cache manager accepts and converts {@code null} values + * for all of its caches. + */ + public boolean isAllowNullValues() { + return this.allowNullValues; + } + + + @Override + public Collection getCacheNames() { + return Collections.unmodifiableSet(this.cacheMap.keySet()); + } + + @Override + public Cache getCache(String name) { + Cache cache = this.cacheMap.get(name); + if (cache == null && this.dynamic) { + synchronized (this.cacheMap) { + cache = this.cacheMap.get(name); + if (cache == null) { + cache = createCaffeineCache(name); + this.cacheMap.put(name, cache); + } + } + } + return cache; + } + + /** + * Create a new CaffeineCache instance for the specified cache name. + * @param name the name of the cache + * @return the Spring CaffeineCache adapter (or a decorator thereof) + */ + protected Cache createCaffeineCache(String name) { + return new CaffeineCache(name, createNativeCaffeineCache(name), isAllowNullValues()); + } + + /** + * Create a native Caffeine Cache instance for the specified cache name. + * @param name the name of the cache + * @return the native Caffeine Cache instance + */ + protected com.github.benmanes.caffeine.cache.Cache createNativeCaffeineCache(String name) { + if (this.cacheLoader != null) { + return this.cacheBuilder.build(this.cacheLoader); + } + else { + return this.cacheBuilder.build(); + } + } + + private void doSetCaffeine(Caffeine cacheBuilder) { + if (!ObjectUtils.nullSafeEquals(this.cacheBuilder, cacheBuilder)) { + this.cacheBuilder = cacheBuilder; + refreshKnownCaches(); + } + } + + /** + * Create the known caches again with the current state of this manager. + */ + private void refreshKnownCaches() { + for (Map.Entry entry : this.cacheMap.entrySet()) { + entry.setValue(createCaffeineCache(entry.getKey())); + } + } + +} diff --git a/spring-context-support/src/main/java/org/springframework/cache/caffeine/package-info.java b/spring-context-support/src/main/java/org/springframework/cache/caffeine/package-info.java new file mode 100644 index 00000000000..786880a748c --- /dev/null +++ b/spring-context-support/src/main/java/org/springframework/cache/caffeine/package-info.java @@ -0,0 +1,6 @@ +/** + * Support classes for the open source cache in + * Caffeine library, + * allowing to set up Caffeine caches within Spring's cache abstraction. + */ +package org.springframework.cache.caffeine; diff --git a/spring-context-support/src/test/java/org/springframework/cache/caffeine/CaffeineCacheManagerTests.java b/spring-context-support/src/test/java/org/springframework/cache/caffeine/CaffeineCacheManagerTests.java new file mode 100644 index 00000000000..0298e717f7a --- /dev/null +++ b/spring-context-support/src/test/java/org/springframework/cache/caffeine/CaffeineCacheManagerTests.java @@ -0,0 +1,186 @@ +/* + * Copyright 2002-2015 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License 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 org.springframework.cache.caffeine; + +import com.github.benmanes.caffeine.cache.Caffeine; +import com.github.benmanes.caffeine.cache.CacheLoader; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +import org.springframework.cache.Cache; +import org.springframework.cache.CacheManager; + +import static org.junit.Assert.*; +import static org.mockito.Mockito.*; + +/** + * @author Juergen Hoeller + * @author Stephane Nicoll + * @author Ben Manes + */ +public class CaffeineCacheManagerTests { + + @Rule + public final ExpectedException thrown = ExpectedException.none(); + + @Test + public void testDynamicMode() { + CacheManager cm = new CaffeineCacheManager(); + Cache cache1 = cm.getCache("c1"); + assertTrue(cache1 instanceof CaffeineCache); + Cache cache1again = cm.getCache("c1"); + assertSame(cache1again, cache1); + Cache cache2 = cm.getCache("c2"); + assertTrue(cache2 instanceof CaffeineCache); + Cache cache2again = cm.getCache("c2"); + assertSame(cache2again, cache2); + Cache cache3 = cm.getCache("c3"); + assertTrue(cache3 instanceof CaffeineCache); + Cache cache3again = cm.getCache("c3"); + assertSame(cache3again, cache3); + + cache1.put("key1", "value1"); + assertEquals("value1", cache1.get("key1").get()); + cache1.put("key2", 2); + assertEquals(2, cache1.get("key2").get()); + cache1.put("key3", null); + assertNull(cache1.get("key3").get()); + cache1.evict("key3"); + assertNull(cache1.get("key3")); + } + + @Test + public void testStaticMode() { + CaffeineCacheManager cm = new CaffeineCacheManager("c1", "c2"); + Cache cache1 = cm.getCache("c1"); + assertTrue(cache1 instanceof CaffeineCache); + Cache cache1again = cm.getCache("c1"); + assertSame(cache1again, cache1); + Cache cache2 = cm.getCache("c2"); + assertTrue(cache2 instanceof CaffeineCache); + Cache cache2again = cm.getCache("c2"); + assertSame(cache2again, cache2); + Cache cache3 = cm.getCache("c3"); + assertNull(cache3); + + cache1.put("key1", "value1"); + assertEquals("value1", cache1.get("key1").get()); + cache1.put("key2", 2); + assertEquals(2, cache1.get("key2").get()); + cache1.put("key3", null); + assertNull(cache1.get("key3").get()); + cache1.evict("key3"); + assertNull(cache1.get("key3")); + + cm.setAllowNullValues(false); + Cache cache1x = cm.getCache("c1"); + assertTrue(cache1x instanceof CaffeineCache); + assertTrue(cache1x != cache1); + Cache cache2x = cm.getCache("c2"); + assertTrue(cache2x instanceof CaffeineCache); + assertTrue(cache2x != cache2); + Cache cache3x = cm.getCache("c3"); + assertNull(cache3x); + + cache1x.put("key1", "value1"); + assertEquals("value1", cache1x.get("key1").get()); + cache1x.put("key2", 2); + assertEquals(2, cache1x.get("key2").get()); + try { + cache1x.put("key3", null); + fail("Should have thrown NullPointerException"); + } + catch (NullPointerException ex) { + // expected + } + + cm.setAllowNullValues(true); + Cache cache1y = cm.getCache("c1"); + + cache1y.put("key3", null); + assertNull(cache1y.get("key3").get()); + cache1y.evict("key3"); + assertNull(cache1y.get("key3")); + } + + @Test + public void changeCacheSpecificationRecreateCache() { + CaffeineCacheManager cm = new CaffeineCacheManager("c1"); + Cache cache1 = cm.getCache("c1"); + + Caffeine caffeine = Caffeine.newBuilder().maximumSize(10); + cm.setCaffeine(caffeine); + Cache cache1x = cm.getCache("c1"); + assertTrue(cache1x != cache1); + + cm.setCaffeine(caffeine); // Set same instance + Cache cache1xx = cm.getCache("c1"); + assertSame(cache1x, cache1xx); + } + + @Test + public void changeCacheLoaderRecreateCache() { + CaffeineCacheManager cm = new CaffeineCacheManager("c1"); + Cache cache1 = cm.getCache("c1"); + + CacheLoader loader = mockCacheLoader(); + cm.setCacheLoader(loader); + Cache cache1x = cm.getCache("c1"); + assertTrue(cache1x != cache1); + + cm.setCacheLoader(loader); // Set same instance + Cache cache1xx = cm.getCache("c1"); + assertSame(cache1x, cache1xx); + } + + @Test + public void setCacheNameNullRestoreDynamicMode() { + CaffeineCacheManager cm = new CaffeineCacheManager("c1"); + assertNull(cm.getCache("someCache")); + cm.setCacheNames(null); + assertNotNull(cm.getCache("someCache")); + } + + @Test + public void cacheLoaderUseLoadingCache() { + CaffeineCacheManager cm = new CaffeineCacheManager("c1"); + cm.setCacheLoader(new CacheLoader() { + @Override + public Object load(Object key) throws Exception { + if ("ping".equals(key)) { + return "pong"; + } + throw new IllegalArgumentException("I only know ping"); + } + }); + Cache cache1 = cm.getCache("c1"); + Cache.ValueWrapper value = cache1.get("ping"); + assertNotNull(value); + assertEquals("pong", value.get()); + + thrown.expect(IllegalArgumentException.class); + thrown.expectMessage("I only know ping"); + assertNull(cache1.get("foo")); + } + + @SuppressWarnings("unchecked") + private CacheLoader mockCacheLoader() { + return mock(CacheLoader.class); + } + +} diff --git a/spring-context-support/src/test/java/org/springframework/cache/caffeine/CaffeineCacheTests.java b/spring-context-support/src/test/java/org/springframework/cache/caffeine/CaffeineCacheTests.java new file mode 100644 index 00000000000..34671acd461 --- /dev/null +++ b/spring-context-support/src/test/java/org/springframework/cache/caffeine/CaffeineCacheTests.java @@ -0,0 +1,68 @@ +/* + * Copyright 2002-2014 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License 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 org.springframework.cache.caffeine; + +import com.github.benmanes.caffeine.cache.Caffeine; +import org.junit.Before; +import org.junit.Test; + +import org.springframework.cache.AbstractCacheTests; +import org.springframework.cache.Cache; + +import static org.junit.Assert.*; + +/** + * @author Stephane Nicoll + * @author Ben Manes + */ +public class CaffeineCacheTests extends AbstractCacheTests { + + private com.github.benmanes.caffeine.cache.Cache nativeCache; + private CaffeineCache cache; + + @Before + public void setUp() { + nativeCache = Caffeine.newBuilder().build(); + cache = new CaffeineCache(CACHE_NAME, nativeCache); + } + + @Override + protected CaffeineCache getCache() { + return cache; + } + + @Override + protected Object getNativeCache() { + return nativeCache; + } + + @Test + public void putIfAbsentNullValue() throws Exception { + CaffeineCache cache = getCache(); + + Object key = new Object(); + Object value = null; + + assertNull(cache.get(key)); + assertNull(cache.putIfAbsent(key, value)); + assertEquals(value, cache.get(key).get()); + Cache.ValueWrapper wrapper = cache.putIfAbsent(key, "anotherValue"); + assertNotNull(wrapper); // A value is set but is 'null' + assertEquals(null, wrapper.get()); + assertEquals(value, cache.get(key).get()); // not changed + } +} diff --git a/spring-context/src/main/java/org/springframework/cache/concurrent/ConcurrentMapCacheManager.java b/spring-context/src/main/java/org/springframework/cache/concurrent/ConcurrentMapCacheManager.java index 14c6956bf58..efe43fe3925 100644 --- a/spring-context/src/main/java/org/springframework/cache/concurrent/ConcurrentMapCacheManager.java +++ b/spring-context/src/main/java/org/springframework/cache/concurrent/ConcurrentMapCacheManager.java @@ -35,7 +35,8 @@ import org.springframework.cache.CacheManager; *

Note: This is by no means a sophisticated CacheManager; it comes with no * cache configuration options. However, it may be useful for testing or simple * caching scenarios. For advanced local caching needs, consider - * {@link org.springframework.cache.guava.GuavaCacheManager} or + * {@link com.github.benmanes.caffeine.cache.CaffeineCacheManager}, + * {@link org.springframework.cache.guava.GuavaCacheManager}, or * {@link org.springframework.cache.ehcache.EhCacheCacheManager}. * * @author Juergen Hoeller