diff --git a/build.gradle b/build.gradle index 57c343d04e..293206a5f1 100644 --- a/build.gradle +++ b/build.gradle @@ -507,6 +507,7 @@ project("spring-context-support") { optional(project(":spring-tx")) // for Quartz support optional("javax.mail:mail:1.4.7") optional("javax.cache:cache-api:1.0.0-PFD") + optional("com.google.guava:guava:14.0.1") optional("net.sf.ehcache:ehcache-core:2.6.5") optional("org.quartz-scheduler:quartz:1.8.6") { exclude group: "org.slf4j", module: "slf4j-log4j12" diff --git a/spring-context-support/src/main/java/org/springframework/cache/guava/GuavaCache.java b/spring-context-support/src/main/java/org/springframework/cache/guava/GuavaCache.java new file mode 100644 index 0000000000..edf76d2602 --- /dev/null +++ b/spring-context-support/src/main/java/org/springframework/cache/guava/GuavaCache.java @@ -0,0 +1,147 @@ +/* + * Copyright 2002-2013 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.guava; + +import java.io.Serializable; + +import org.springframework.cache.Cache; +import org.springframework.cache.support.SimpleValueWrapper; +import org.springframework.util.Assert; + +/** + * Spring {@link Cache} adapter implementation on top of a + * Guava {@link com.google.common.cache.Cache} instance. + * + *

Requires Google Guava 12.0 or higher. + * + * @author Costin Leau + * @author Juergen Hoeller + * @since 3.1 + */ +public class GuavaCache implements Cache { + + private static final Object NULL_HOLDER = new NullHolder(); + + private final String name; + + private final com.google.common.cache.Cache cache; + + private final boolean allowNullValues; + + + /** + * Create a {@link GuavaCache} instance. + * @param name the name of the cache + * @param cache backing Guava Cache instance + */ + public GuavaCache(String name, com.google.common.cache.Cache cache) { + this(name, cache, true); + } + + /** + * Create a {@link GuavaCache} instance. + * @param name the name of the cache + * @param cache backing Guava Cache instance + * @param allowNullValues whether to accept and convert null values for this cache + */ + public GuavaCache(String name, com.google.common.cache.Cache cache, boolean allowNullValues) { + Assert.notNull(name, "Name must not be null"); + Assert.notNull(cache, "Cache must not be null"); + this.name = name; + this.cache = cache; + this.allowNullValues = allowNullValues; + } + + + @Override + public String getName() { + return this.name; + } + + @Override + public com.google.common.cache.Cache getNativeCache() { + return this.cache; + } + + public boolean isAllowNullValues() { + return this.allowNullValues; + } + + @Override + public ValueWrapper get(Object key) { + Object value = this.cache.getIfPresent(key); + return (value != null ? new SimpleValueWrapper(fromStoreValue(value)) : null); + } + + @Override + @SuppressWarnings("unchecked") + public T get(Object key, Class type) { + Object value = fromStoreValue(this.cache.getIfPresent(key)); + if (type != null && !type.isInstance(value)) { + throw new IllegalStateException("Cached value is not of required type [" + type.getName() + "]: " + value); + } + return (T) value; + } + + @Override + public void put(Object key, Object value) { + this.cache.put(key, toStoreValue(value)); + } + + @Override + public void evict(Object key) { + this.cache.invalidate(key); + } + + @Override + public void clear() { + this.cache.invalidateAll(); + } + + + /** + * Convert the given value from the internal store to a user value + * returned from the get method (adapting {@code null}). + * @param storeValue the store value + * @return the value to return to the user + */ + protected Object fromStoreValue(Object storeValue) { + if (this.allowNullValues && storeValue == NULL_HOLDER) { + return null; + } + return storeValue; + } + + /** + * Convert the given user value, as passed into the put method, + * to a value in the internal store (adapting {@code null}). + * @param userValue the given user value + * @return the value to store + */ + protected Object toStoreValue(Object userValue) { + if (this.allowNullValues && userValue == null) { + return NULL_HOLDER; + } + return userValue; + } + + + @SuppressWarnings("serial") + private static class NullHolder implements Serializable { + } + +} diff --git a/spring-context-support/src/main/java/org/springframework/cache/guava/GuavaCacheManager.java b/spring-context-support/src/main/java/org/springframework/cache/guava/GuavaCacheManager.java new file mode 100644 index 0000000000..cc08695006 --- /dev/null +++ b/spring-context-support/src/main/java/org/springframework/cache/guava/GuavaCacheManager.java @@ -0,0 +1,188 @@ +/* + * Copyright 2002-2013 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.guava; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +import com.google.common.cache.CacheBuilder; +import com.google.common.cache.CacheBuilderSpec; +import com.google.common.cache.CacheLoader; + +import org.springframework.beans.factory.DisposableBean; +import org.springframework.cache.Cache; +import org.springframework.cache.CacheManager; +import org.springframework.util.Assert; + +/** + * {@link CacheManager} implementation that lazily builds {@link GuavaCache} + * 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 + * Guava {@link CacheBuilder} or {@link CacheBuilderSpec}, passed into this + * CacheManager through {@link #setCacheBuilder}/{@link #setCacheBuilderSpec}. + * A {@link CacheBuilderSpec}-compliant expression value can also be applied + * via the {@link #setCacheSpecification "cacheSpecification"} bean property. + * + *

Requires Google Guava 12.0 or higher. + * + * @author Juergen Hoeller + * @since 4.0 + * @see GuavaCache + */ +public class GuavaCacheManager implements CacheManager, DisposableBean { + + private final ConcurrentMap cacheMap = new ConcurrentHashMap(16); + + private boolean dynamic = true; + + private CacheBuilder cacheBuilder = CacheBuilder.newBuilder(); + + private CacheLoader cacheLoader; + + + /** + * Construct a dynamic GuavaCacheManager, + * lazily creating cache instances as they are being requested. + */ + public GuavaCacheManager() { + } + + /** + * Construct a static GuavaCacheManager, + * managing caches for the specified cache names only. + */ + public GuavaCacheManager(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. + */ + public void setCacheNames(Collection cacheNames) { + if (cacheNames != null) { + for (String name : cacheNames) { + this.cacheMap.put(name, createGuavaCache(name)); + } + this.dynamic = false; + } + } + + /** + * Set the Guava CacheBuilder to use for building each individual + * {@link GuavaCache} instance. + * @see #createNativeGuavaCache + * @see com.google.common.cache.CacheBuilder#build() + */ + public void setCacheBuilder(CacheBuilder cacheBuilder) { + Assert.notNull(cacheBuilder, "CacheBuilder must not be null"); + this.cacheBuilder = cacheBuilder; + } + + /** + * Set the Guava CacheBuilderSpec to use for building each individual + * {@link GuavaCache} instance. + * @see #createNativeGuavaCache + * @see com.google.common.cache.CacheBuilder#from(CacheBuilderSpec) + */ + public void setCacheBuilderSpec(CacheBuilderSpec cacheBuilderSpec) { + this.cacheBuilder = CacheBuilder.from(cacheBuilderSpec); + } + + /** + * Set the Guava cache specification String to use for building each + * individual {@link GuavaCache} instance. The given value needs to + * comply with Guava's {@link CacheBuilderSpec} (see its javadoc). + * @see #createNativeGuavaCache + * @see com.google.common.cache.CacheBuilder#from(String) + */ + public void setCacheSpecification(String cacheSpecification) { + this.cacheBuilder = CacheBuilder.from(cacheSpecification); + } + + /** + * Set the Guava CacheLoader to use for building each individual + * {@link GuavaCache} instance, turning it into a LoadingCache. + * @see #createNativeGuavaCache + * @see com.google.common.cache.CacheBuilder#build(CacheLoader) + * @see com.google.common.cache.LoadingCache + */ + public void setCacheLoader(CacheLoader cacheLoader) { + this.cacheLoader = cacheLoader; + } + + + @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 = createGuavaCache(name); + this.cacheMap.put(name, cache); + } + } + } + return cache; + } + + /** + * Create a new GuavaCache instance for the specified cache name. + * @param name the name of the cache + * @return the Spring GuavaCache adapter (or a decorator thereof) + */ + protected Cache createGuavaCache(String name) { + return new GuavaCache(name, createNativeGuavaCache(name)); + } + + /** + * Create a native Guava Cache instance for the specified cache name. + * @param name the name of the cache + * @return the native Guava Cache instance + */ + protected com.google.common.cache.Cache createNativeGuavaCache(String name) { + if (this.cacheLoader != null) { + return this.cacheBuilder.build(this.cacheLoader); + } + else { + return this.cacheBuilder.build(); + } + } + + + @Override + public void destroy() { + for (Cache cache : this.cacheMap.values()) { + + } + } + +} diff --git a/spring-context-support/src/main/java/org/springframework/cache/guava/package-info.java b/spring-context-support/src/main/java/org/springframework/cache/guava/package-info.java new file mode 100644 index 0000000000..6f892820a9 --- /dev/null +++ b/spring-context-support/src/main/java/org/springframework/cache/guava/package-info.java @@ -0,0 +1,6 @@ +/** + * Support classes for the open source cache in Google's + * Guava library, + * allowing to set up Guava caches within Spring's cache abstraction. + */ +package org.springframework.cache.guava; diff --git a/spring-context-support/src/test/java/org/springframework/cache/guava/GuavaCacheManagerTests.java b/spring-context-support/src/test/java/org/springframework/cache/guava/GuavaCacheManagerTests.java new file mode 100644 index 0000000000..0820d12f95 --- /dev/null +++ b/spring-context-support/src/test/java/org/springframework/cache/guava/GuavaCacheManagerTests.java @@ -0,0 +1,81 @@ +/* + * Copyright 2002-2013 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.guava; + +import org.junit.Test; + +import org.springframework.cache.Cache; +import org.springframework.cache.CacheManager; + +import static org.junit.Assert.*; + +/** + * @author Juergen Hoeller + */ +public class GuavaCacheManagerTests { + + @Test + public void testDynamicMode() { + CacheManager cm = new GuavaCacheManager(); + Cache cache1 = cm.getCache("c1"); + assertTrue(cache1 instanceof GuavaCache); + Cache cache1again = cm.getCache("c1"); + assertSame(cache1again, cache1); + Cache cache2 = cm.getCache("c2"); + assertTrue(cache2 instanceof GuavaCache); + Cache cache2again = cm.getCache("c2"); + assertSame(cache2again, cache2); + Cache cache3 = cm.getCache("c3"); + assertTrue(cache3 instanceof GuavaCache); + 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() { + CacheManager cm = new GuavaCacheManager("c1", "c2"); + Cache cache1 = cm.getCache("c1"); + assertTrue(cache1 instanceof GuavaCache); + Cache cache1again = cm.getCache("c1"); + assertSame(cache1again, cache1); + Cache cache2 = cm.getCache("c2"); + assertTrue(cache2 instanceof GuavaCache); + 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")); + } + +} diff --git a/spring-context/src/main/java/org/springframework/cache/concurrent/ConcurrentMapCache.java b/spring-context/src/main/java/org/springframework/cache/concurrent/ConcurrentMapCache.java index 4dc8af0d7a..7294b5bd63 100644 --- a/spring-context/src/main/java/org/springframework/cache/concurrent/ConcurrentMapCache.java +++ b/spring-context/src/main/java/org/springframework/cache/concurrent/ConcurrentMapCache.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 the original author or authors. + * Copyright 2002-2013 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. @@ -22,6 +22,7 @@ import java.util.concurrent.ConcurrentMap; import org.springframework.cache.Cache; import org.springframework.cache.support.SimpleValueWrapper; +import org.springframework.util.Assert; /** * Simple {@link Cache} implementation based on the core JDK @@ -77,6 +78,8 @@ public class ConcurrentMapCache implements Cache { * (adapting them to an internal null holder value) */ public ConcurrentMapCache(String name, ConcurrentMap store, boolean allowNullValues) { + Assert.notNull(name, "Name must not be null"); + Assert.notNull(store, "Store must not be null"); this.name = name; this.store = store; this.allowNullValues = allowNullValues; 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 35371d92d4..6c87d7832b 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 the original author or authors. + * Copyright 2002-2013 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. @@ -31,8 +31,15 @@ import org.springframework.cache.CacheManager; * the set of cache names is pre-defined through {@link #setCacheNames}, with no * dynamic creation of further cache regions at runtime. * + *

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 org.springframework.cache.ehcache.EhCacheCacheManager}. + * * @author Juergen Hoeller * @since 3.1 + * @see ConcurrentMapCache */ public class ConcurrentMapCacheManager implements CacheManager { diff --git a/spring-context/src/test/java/org/springframework/cache/concurrent/ConcurrentMapCacheManagerTests.java b/spring-context/src/test/java/org/springframework/cache/concurrent/ConcurrentMapCacheManagerTests.java index cc0c963070..661e4a3d35 100644 --- a/spring-context/src/test/java/org/springframework/cache/concurrent/ConcurrentMapCacheManagerTests.java +++ b/spring-context/src/test/java/org/springframework/cache/concurrent/ConcurrentMapCacheManagerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2011 the original author or authors. + * Copyright 2002-2013 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. @@ -43,11 +43,20 @@ public class ConcurrentMapCacheManagerTests { assertTrue(cache3 instanceof ConcurrentMapCache); 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() { - ConcurrentMapCacheManager cm = new ConcurrentMapCacheManager("c1", "c2"); + CacheManager cm = new ConcurrentMapCacheManager("c1", "c2"); Cache cache1 = cm.getCache("c1"); assertTrue(cache1 instanceof ConcurrentMapCache); Cache cache1again = cm.getCache("c1"); @@ -58,6 +67,15 @@ public class ConcurrentMapCacheManagerTests { 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")); } }