diff --git a/spring-context-support/src/main/java/org/springframework/cache/ehcache/EhCacheCache.java b/spring-context-support/src/main/java/org/springframework/cache/ehcache/EhCacheCache.java index c730c637e8..31bfd2f530 100644 --- a/spring-context-support/src/main/java/org/springframework/cache/ehcache/EhCacheCache.java +++ b/spring-context-support/src/main/java/org/springframework/cache/ehcache/EhCacheCache.java @@ -29,6 +29,7 @@ import org.springframework.util.Assert; * * @author Costin Leau * @author Juergen Hoeller + * @author Stephane Nicoll * @since 3.1 */ public class EhCacheCache implements Cache { @@ -62,7 +63,7 @@ public class EhCacheCache implements Cache { @Override public ValueWrapper get(Object key) { Element element = this.cache.get(key); - return (element != null ? new SimpleValueWrapper(element.getObjectValue()) : null); + return toWrapper(element); } @Override @@ -81,6 +82,12 @@ public class EhCacheCache implements Cache { this.cache.put(new Element(key, value)); } + @Override + public ValueWrapper putIfAbsent(Object key, Object value) { + Element existingElement = this.cache.putIfAbsent(new Element(key, value)); + return toWrapper(existingElement); + } + @Override public void evict(Object key) { this.cache.remove(key); @@ -91,4 +98,8 @@ public class EhCacheCache implements Cache { this.cache.removeAll(); } + private ValueWrapper toWrapper(Element element) { + return (element != null ? new SimpleValueWrapper(element.getObjectValue()) : null); + } + } 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 index 6a9cf4cbe7..6acc2aad0b 100644 --- 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 @@ -17,6 +17,8 @@ package org.springframework.cache.guava; import java.io.Serializable; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutionException; import org.springframework.cache.Cache; import org.springframework.cache.support.SimpleValueWrapper; @@ -29,6 +31,7 @@ import org.springframework.util.Assert; *

Requires Google Guava 12.0 or higher. * * @author Juergen Hoeller + * @author Stephane Nicoll * @since 4.0 */ public class GuavaCache implements Cache { @@ -83,7 +86,7 @@ public class GuavaCache implements Cache { @Override public ValueWrapper get(Object key) { Object value = this.cache.getIfPresent(key); - return (value != null ? new SimpleValueWrapper(fromStoreValue(value)) : null); + return toWrapper(value); } @Override @@ -101,6 +104,17 @@ public class GuavaCache implements Cache { this.cache.put(key, toStoreValue(value)); } + @Override + public ValueWrapper putIfAbsent(Object key, final Object value) { + try { + PutIfAbsentCallable callable = new PutIfAbsentCallable(value); + Object result = this.cache.get(key, callable); + return (callable.called ? null : toWrapper(result)); + } catch (ExecutionException e) { + throw new IllegalArgumentException(e); + } + } + @Override public void evict(Object key) { this.cache.invalidate(key); @@ -138,9 +152,29 @@ public class GuavaCache implements Cache { return userValue; } + private ValueWrapper toWrapper(Object value) { + return (value != null ? new SimpleValueWrapper(fromStoreValue(value)) : null); + } + @SuppressWarnings("serial") private static class NullHolder implements Serializable { } + private class PutIfAbsentCallable implements Callable { + private boolean called; + + private final Object value; + + private PutIfAbsentCallable(Object value) { + this.value = value; + } + + @Override + public Object call() throws Exception { + called = true; + return toStoreValue(value); + } + } + } diff --git a/spring-context-support/src/main/java/org/springframework/cache/jcache/JCacheCache.java b/spring-context-support/src/main/java/org/springframework/cache/jcache/JCacheCache.java index a0b34470f1..372e754c9f 100644 --- a/spring-context-support/src/main/java/org/springframework/cache/jcache/JCacheCache.java +++ b/spring-context-support/src/main/java/org/springframework/cache/jcache/JCacheCache.java @@ -29,6 +29,7 @@ import org.springframework.util.Assert; *

Note: This class has been updated for JCache 1.0, as of Spring 4.0. * * @author Juergen Hoeller + * @author Stephane Nicoll * @since 3.2 */ public class JCacheCache implements Cache { @@ -95,6 +96,12 @@ public class JCacheCache implements Cache { this.cache.put(key, toStoreValue(value)); } + @Override + public ValueWrapper putIfAbsent(Object key, Object value) { + boolean set = this.cache.putIfAbsent(key, toStoreValue(value)); + return (set ? null : get(key)); + } + @Override public void evict(Object key) { this.cache.remove(key); diff --git a/spring-context-support/src/main/java/org/springframework/cache/transaction/TransactionAwareCacheDecorator.java b/spring-context-support/src/main/java/org/springframework/cache/transaction/TransactionAwareCacheDecorator.java index adc931abe3..e239b96d1e 100644 --- a/spring-context-support/src/main/java/org/springframework/cache/transaction/TransactionAwareCacheDecorator.java +++ b/spring-context-support/src/main/java/org/springframework/cache/transaction/TransactionAwareCacheDecorator.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2013 the original author or authors. + * 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. @@ -28,7 +28,11 @@ import org.springframework.util.Assert; * successful transaction. If no transaction is active, {@link #put} and {@link #evict} * operations will be performed immediately, as usual. * + *

Use of more aggressive operations such as {@link #putIfAbsent} cannot be deferred + * to the after-commit phase of a running transaction. Use these with care. + * * @author Juergen Hoeller + * @author Stephane Nicoll * @since 3.2 * @see TransactionAwareCacheManagerProxy */ @@ -82,6 +86,11 @@ public class TransactionAwareCacheDecorator implements Cache { } } + @Override + public ValueWrapper putIfAbsent(final Object key, final Object value) { + return this.targetCache.putIfAbsent(key, value); + } + @Override public void evict(final Object key) { if (TransactionSynchronizationManager.isSynchronizationActive()) { diff --git a/spring-context-support/src/test/java/org/springframework/cache/AbstractCacheTests.java b/spring-context-support/src/test/java/org/springframework/cache/AbstractCacheTests.java new file mode 100644 index 0000000000..ba41bfa180 --- /dev/null +++ b/spring-context-support/src/test/java/org/springframework/cache/AbstractCacheTests.java @@ -0,0 +1,105 @@ +/* + * 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; + +import static org.junit.Assert.*; + +import org.junit.Test; + +/** + * @author Stephane Nicoll + */ +public abstract class AbstractCacheTests { + + protected final static String CACHE_NAME = "testCache"; + + protected abstract T getCache(); + + protected abstract Object getNativeCache(); + + @Test + public void testCacheName() throws Exception { + assertEquals(CACHE_NAME, getCache().getName()); + } + + @Test + public void testNativeCache() throws Exception { + assertSame(getNativeCache(), getCache().getNativeCache()); + } + + @Test + public void testCachePut() throws Exception { + T cache = getCache(); + + Object key = "enescu"; + Object value = "george"; + + assertNull(cache.get(key)); + assertNull(cache.get(key, String.class)); + assertNull(cache.get(key, Object.class)); + + cache.put(key, value); + assertEquals(value, cache.get(key).get()); + assertEquals(value, cache.get(key, String.class)); + assertEquals(value, cache.get(key, Object.class)); + assertEquals(value, cache.get(key, null)); + + cache.put(key, null); + assertNotNull(cache.get(key)); + assertNull(cache.get(key).get()); + assertNull(cache.get(key, String.class)); + assertNull(cache.get(key, Object.class)); + } + + @Test + public void testCachePutIfAbsent() throws Exception { + T cache = getCache(); + + Object key = new Object(); + Object value = "initialValue"; + + assertNull(cache.get(key)); + assertNull(cache.putIfAbsent(key, value)); + assertEquals(value, cache.get(key).get()); + assertEquals("initialValue", cache.putIfAbsent(key, "anotherValue").get()); + assertEquals(value, cache.get(key).get()); // not changed + } + + @Test + public void testCacheRemove() throws Exception { + T cache = getCache(); + + Object key = "enescu"; + Object value = "george"; + + assertNull(cache.get(key)); + cache.put(key, value); + } + + @Test + public void testCacheClear() throws Exception { + T cache = getCache(); + + assertNull(cache.get("enescu")); + cache.put("enescu", "george"); + assertNull(cache.get("vlaicu")); + cache.put("vlaicu", "aurel"); + cache.clear(); + assertNull(cache.get("vlaicu")); + assertNull(cache.get("enescu")); + } +} diff --git a/spring-context-support/src/test/java/org/springframework/cache/ehcache/EhCacheCacheTests.java b/spring-context-support/src/test/java/org/springframework/cache/ehcache/EhCacheCacheTests.java index 587eb95a1f..b6d58d214d 100644 --- a/spring-context-support/src/test/java/org/springframework/cache/ehcache/EhCacheCacheTests.java +++ b/spring-context-support/src/test/java/org/springframework/cache/ehcache/EhCacheCacheTests.java @@ -16,56 +16,55 @@ package org.springframework.cache.ehcache; +import static org.junit.Assert.*; + import net.sf.ehcache.CacheManager; import net.sf.ehcache.Ehcache; import net.sf.ehcache.Element; import net.sf.ehcache.config.CacheConfiguration; +import net.sf.ehcache.config.Configuration; +import org.junit.After; import org.junit.Before; import org.junit.Test; - -import org.springframework.cache.Cache; +import org.springframework.cache.AbstractCacheTests; import org.springframework.tests.Assume; import org.springframework.tests.TestGroup; -import static org.junit.Assert.*; - /** * @author Costin Leau + * @author Stephane Nicoll * @author Juergen Hoeller */ -public class EhCacheCacheTests { - - protected final static String CACHE_NAME = "testCache"; - - protected Ehcache nativeCache; - - protected Cache cache; +public class EhCacheCacheTests extends AbstractCacheTests { + private CacheManager cacheManager; + private Ehcache nativeCache; + private EhCacheCache cache; @Before - public void setUp() throws Exception { - if (CacheManager.getInstance().cacheExists(CACHE_NAME)) { - nativeCache = CacheManager.getInstance().getEhcache(CACHE_NAME); - } - else { - nativeCache = new net.sf.ehcache.Cache(new CacheConfiguration(CACHE_NAME, 100)); - CacheManager.getInstance().addCache(nativeCache); - } + public void setUp() { + cacheManager = new CacheManager(new Configuration().name("EhCacheCacheTests") + .defaultCache(new CacheConfiguration("default", 100))); + nativeCache = new net.sf.ehcache.Cache(new CacheConfiguration(CACHE_NAME, 100)); + cacheManager.addCache(nativeCache); + cache = new EhCacheCache(nativeCache); - cache.clear(); } - - @Test - public void testCacheName() throws Exception { - assertEquals(CACHE_NAME, cache.getName()); + @After + public void tearDown() { + cacheManager.shutdown(); } - @Test - public void testNativeCache() throws Exception { - assertSame(nativeCache, cache.getNativeCache()); + @Override + protected EhCacheCache getCache() { + return cache; } + @Override + protected Ehcache getNativeCache() { + return nativeCache; + } @Test public void testCachePut() throws Exception { Object key = "enescu"; @@ -88,26 +87,6 @@ public class EhCacheCacheTests { assertNull(cache.get(key, Object.class)); } - @Test - public void testCacheRemove() throws Exception { - Object key = "enescu"; - Object value = "george"; - - assertNull(cache.get(key)); - cache.put(key, value); - } - - @Test - public void testCacheClear() throws Exception { - assertNull(cache.get("enescu")); - cache.put("enescu", "george"); - assertNull(cache.get("vlaicu")); - cache.put("vlaicu", "aurel"); - cache.clear(); - assertNull(cache.get("vlaicu")); - assertNull(cache.get("enescu")); - } - @Test public void testExpiredElements() throws Exception { Assume.group(TestGroup.LONG_RUNNING); @@ -123,5 +102,4 @@ public class EhCacheCacheTests { Thread.sleep(5 * 1000); assertNull(cache.get(key)); } - } diff --git a/spring-context-support/src/test/java/org/springframework/cache/guava/GuavaCacheTests.java b/spring-context-support/src/test/java/org/springframework/cache/guava/GuavaCacheTests.java new file mode 100644 index 0000000000..763e56abb2 --- /dev/null +++ b/spring-context-support/src/test/java/org/springframework/cache/guava/GuavaCacheTests.java @@ -0,0 +1,67 @@ +/* + * 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.guava; + +import static org.junit.Assert.*; + +import com.google.common.cache.CacheBuilder; +import org.junit.Before; +import org.junit.Test; + +import org.springframework.cache.AbstractCacheTests; +import org.springframework.cache.Cache; + +/** + * @author Stephane Nicoll + */ +public class GuavaCacheTests extends AbstractCacheTests { + + private com.google.common.cache.Cache nativeCache; + private GuavaCache cache; + + @Before + public void setUp() { + nativeCache = CacheBuilder.newBuilder().build(); + cache = new GuavaCache(CACHE_NAME, nativeCache); + } + + @Override + protected GuavaCache getCache() { + return cache; + } + + @Override + protected Object getNativeCache() { + return nativeCache; + } + + @Test + public void putIfAbsentNullValue() throws Exception { + GuavaCache 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-support/src/test/java/org/springframework/cache/transaction/TransactionAwareCacheDecoratorTests.java b/spring-context-support/src/test/java/org/springframework/cache/transaction/TransactionAwareCacheDecoratorTests.java index 553aa74b16..e43c721bd3 100644 --- a/spring-context-support/src/test/java/org/springframework/cache/transaction/TransactionAwareCacheDecoratorTests.java +++ b/spring-context-support/src/test/java/org/springframework/cache/transaction/TransactionAwareCacheDecoratorTests.java @@ -90,6 +90,18 @@ public class TransactionAwareCacheDecoratorTests { assertEquals("123", target.get(key, String.class)); } + @Test + public void putIfAbsent() { // no transactional support for putIfAbsent + Cache target = new ConcurrentMapCache("testCache"); + Cache cache = new TransactionAwareCacheDecorator(target); + + Object key = new Object(); + assertNull(cache.putIfAbsent(key, "123")); + assertEquals("123", target.get(key, String.class)); + assertEquals("123", cache.putIfAbsent(key, "456").get()); + assertEquals("123", target.get(key, String.class)); // unchanged + } + @Test public void evictNonTransactional() { Cache target = new ConcurrentMapCache("testCache"); diff --git a/spring-context/src/main/java/org/springframework/cache/Cache.java b/spring-context/src/main/java/org/springframework/cache/Cache.java index bfe32443d1..61058eace1 100644 --- a/spring-context/src/main/java/org/springframework/cache/Cache.java +++ b/spring-context/src/main/java/org/springframework/cache/Cache.java @@ -80,6 +80,33 @@ public interface Cache { */ void put(Object key, Object value); + /** + * Atomically associate the specified value with the specified key in this cache if + * it is not set already. + *

This is equivalent to: + *


+	 * Object existingValue = cache.get(key);
+	 * if (existingValue == null) {
+	 *     cache.put(key, value);
+	 *     return null;
+	 * } else {
+	 *     return existingValue;
+	 * }
+	 * 
+ * except that the action is performed atomically. While all known providers are + * able to perform the put atomically, the returned value may be retrieved after + * the attempt to put (i.e. in a non atomic way). Check the documentation of + * the native cache implementation that you are using for more details. + * @param key the key with which the specified value is to be associated + * @param value the value to be associated with the specified key + * @return the value to which this cache maps the specified key (which may + * be {@code null} itself), or also {@code null} if the cache did not contain + * any mapping for that key prior to this call. Returning {@code null} is + * therefore an indicator that the given {@code value} has been associated + * with the key + */ + ValueWrapper putIfAbsent(Object key, Object value); + /** * Evict the mapping for this key from this cache if it is present. * @param key the key whose mapping is to be removed from the cache 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 0cf8264dd0..8601245ea8 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 @@ -103,7 +103,7 @@ public class ConcurrentMapCache implements Cache { @Override public ValueWrapper get(Object key) { Object value = this.store.get(key); - return (value != null ? new SimpleValueWrapper(fromStoreValue(value)) : null); + return toWrapper(value); } @Override @@ -121,6 +121,12 @@ public class ConcurrentMapCache implements Cache { this.store.put(key, toStoreValue(value)); } + @Override + public ValueWrapper putIfAbsent(Object key, Object value) { + Object existing = this.store.putIfAbsent(key, value); + return toWrapper(existing); + } + @Override public void evict(Object key) { this.store.remove(key); @@ -158,6 +164,9 @@ public class ConcurrentMapCache implements Cache { return userValue; } + private ValueWrapper toWrapper(Object value) { + return (value != null ? new SimpleValueWrapper(fromStoreValue(value)) : null); + } @SuppressWarnings("serial") private static class NullHolder implements Serializable { diff --git a/spring-context/src/main/java/org/springframework/cache/support/NoOpCacheManager.java b/spring-context/src/main/java/org/springframework/cache/support/NoOpCacheManager.java index c76c79731d..0b486804a6 100644 --- a/spring-context/src/main/java/org/springframework/cache/support/NoOpCacheManager.java +++ b/spring-context/src/main/java/org/springframework/cache/support/NoOpCacheManager.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2013 the original author or authors. + * 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. @@ -34,6 +34,7 @@ import org.springframework.cache.CacheManager; *

Will simply accept any items into the cache not actually storing them. * * @author Costin Leau + * @author Stephane Nicoll * @since 3.1 * @see CompositeCacheManager */ @@ -111,6 +112,11 @@ public class NoOpCacheManager implements CacheManager { @Override public void put(Object key, Object value) { } + + @Override + public ValueWrapper putIfAbsent(Object key, Object value) { + return null; + } } } diff --git a/spring-context/src/test/java/org/springframework/cache/concurrent/ConcurrentCacheTests.java b/spring-context/src/test/java/org/springframework/cache/concurrent/ConcurrentCacheTests.java index a80bc9560b..cba7db7d51 100644 --- a/spring-context/src/test/java/org/springframework/cache/concurrent/ConcurrentCacheTests.java +++ b/spring-context/src/test/java/org/springframework/cache/concurrent/ConcurrentCacheTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2010-2014 the original author or authors. + * 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. @@ -29,6 +29,7 @@ import static org.junit.Assert.*; /** * @author Costin Leau * @author Juergen Hoeller + * @author Stephane Nicoll */ public class ConcurrentCacheTests { @@ -79,6 +80,18 @@ public class ConcurrentCacheTests { assertNull(cache.get(key, Object.class)); } + @Test + public void testCachePutIfAbsent() throws Exception { + Object key = new Object(); + Object value = "initialValue"; + + assertNull(cache.get(key)); + assertNull(cache.putIfAbsent(key, value)); + assertEquals(value, cache.get(key).get()); + assertEquals("initialValue", cache.putIfAbsent(key, "anotherValue").get()); + assertEquals(value, cache.get(key).get()); // not changed + } + @Test public void testCacheRemove() throws Exception { Object key = "enescu";