Browse Source
Previously, if a `@Cacheable` method was accessed with the same key by multiple threads, the underlying method was invoked several times instead of blocking the threads while the value is computed. This scenario typically affects users that enable caching to avoid calling a costly method too often. When said method can be invoked by an arbitrary number of clients on startup, caching has close to no effect. This commit adds a new method on `Cache` that implements the read-through pattern: ``` <T> T get(Object key, Callable<T> valueLoader); ``` If an entry for a given key is not found, the specified `Callable` is invoked to "load" the value and cache it before returning it to the caller. Because the entire operation is managed by the underlying cache provider, it is much more easier to guarantee that the loader (e.g. the annotated method) will be called only once in case of concurrent access. A new `sync` attribute to the `@Cacheable` annotation has been addded. When this flag is enabled, the caching abstraction invokes the new `Cache` method define above. This new mode bring a set of limitations: * It can't be combined with other cache operations * Only one `@Cacheable` operation can be specified * Only one cache is allowed * `condition` and `unless` attribute are not supported The rationale behind those limitations is that the underlying Cache is taking care of the actual caching operation so we can't really apply any SpEL or multiple caches handling there. Issue: SPR-9254pull/925/merge
33 changed files with 938 additions and 146 deletions
@ -0,0 +1,35 @@ |
|||||||
|
/* |
||||||
|
* 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.aspectj; |
||||||
|
|
||||||
|
/** |
||||||
|
* Utility to trick the compiler to throw a valid checked |
||||||
|
* exceptions within the interceptor. |
||||||
|
* |
||||||
|
* @author Stephane Nicoll |
||||||
|
*/ |
||||||
|
class AnyThrow { |
||||||
|
|
||||||
|
static void throwUnchecked(Throwable e) { |
||||||
|
AnyThrow.<RuntimeException>throwAny(e); |
||||||
|
} |
||||||
|
|
||||||
|
@SuppressWarnings("unchecked") |
||||||
|
private static <E extends Throwable> void throwAny(Throwable e) throws E { |
||||||
|
throw (E) e; |
||||||
|
} |
||||||
|
} |
||||||
@ -1,115 +0,0 @@ |
|||||||
/* |
|
||||||
* 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.concurrent; |
|
||||||
|
|
||||||
import java.util.concurrent.ConcurrentHashMap; |
|
||||||
import java.util.concurrent.ConcurrentMap; |
|
||||||
|
|
||||||
import org.junit.Before; |
|
||||||
import org.junit.Test; |
|
||||||
|
|
||||||
import org.springframework.cache.Cache; |
|
||||||
|
|
||||||
import static org.junit.Assert.*; |
|
||||||
|
|
||||||
/** |
|
||||||
* @author Costin Leau |
|
||||||
* @author Juergen Hoeller |
|
||||||
* @author Stephane Nicoll |
|
||||||
*/ |
|
||||||
public class ConcurrentCacheTests { |
|
||||||
|
|
||||||
protected final static String CACHE_NAME = "testCache"; |
|
||||||
|
|
||||||
protected ConcurrentMap<Object, Object> nativeCache; |
|
||||||
|
|
||||||
protected Cache cache; |
|
||||||
|
|
||||||
|
|
||||||
@Before |
|
||||||
public void setUp() throws Exception { |
|
||||||
nativeCache = new ConcurrentHashMap<Object, Object>(); |
|
||||||
cache = new ConcurrentMapCache(CACHE_NAME, nativeCache, true); |
|
||||||
cache.clear(); |
|
||||||
} |
|
||||||
|
|
||||||
|
|
||||||
@Test |
|
||||||
public void testCacheName() throws Exception { |
|
||||||
assertEquals(CACHE_NAME, cache.getName()); |
|
||||||
} |
|
||||||
|
|
||||||
@Test |
|
||||||
public void testNativeCache() throws Exception { |
|
||||||
assertSame(nativeCache, cache.getNativeCache()); |
|
||||||
} |
|
||||||
|
|
||||||
@Test |
|
||||||
public void testCachePut() throws Exception { |
|
||||||
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 { |
|
||||||
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"; |
|
||||||
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")); |
|
||||||
} |
|
||||||
|
|
||||||
} |
|
||||||
@ -0,0 +1,56 @@ |
|||||||
|
/* |
||||||
|
* 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.concurrent; |
||||||
|
|
||||||
|
import java.util.concurrent.ConcurrentHashMap; |
||||||
|
import java.util.concurrent.ConcurrentMap; |
||||||
|
|
||||||
|
import org.junit.Before; |
||||||
|
import org.junit.Ignore; |
||||||
|
|
||||||
|
import org.springframework.cache.AbstractCacheTests; |
||||||
|
|
||||||
|
/** |
||||||
|
* @author Costin Leau |
||||||
|
* @author Juergen Hoeller |
||||||
|
* @author Stephane Nicoll |
||||||
|
*/ |
||||||
|
public class ConcurrentMapCacheTests extends AbstractCacheTests<ConcurrentMapCache> { |
||||||
|
|
||||||
|
protected ConcurrentMap<Object, Object> nativeCache; |
||||||
|
|
||||||
|
protected ConcurrentMapCache cache; |
||||||
|
|
||||||
|
|
||||||
|
@Before |
||||||
|
public void setUp() throws Exception { |
||||||
|
nativeCache = new ConcurrentHashMap<Object, Object>(); |
||||||
|
cache = new ConcurrentMapCache(CACHE_NAME, nativeCache, true); |
||||||
|
cache.clear(); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
protected ConcurrentMapCache getCache() { |
||||||
|
return this.cache; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
protected ConcurrentMap<Object, Object> getNativeCache() { |
||||||
|
return this.nativeCache; |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
@ -0,0 +1,158 @@ |
|||||||
|
/* |
||||||
|
* 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.interceptor; |
||||||
|
|
||||||
|
import java.util.concurrent.atomic.AtomicLong; |
||||||
|
|
||||||
|
import org.junit.After; |
||||||
|
import org.junit.Before; |
||||||
|
import org.junit.Rule; |
||||||
|
import org.junit.Test; |
||||||
|
import org.junit.rules.ExpectedException; |
||||||
|
|
||||||
|
import org.springframework.cache.CacheManager; |
||||||
|
import org.springframework.cache.CacheTestUtils; |
||||||
|
import org.springframework.cache.annotation.CacheEvict; |
||||||
|
import org.springframework.cache.annotation.Cacheable; |
||||||
|
import org.springframework.cache.annotation.Caching; |
||||||
|
import org.springframework.cache.annotation.CachingConfigurerSupport; |
||||||
|
import org.springframework.cache.annotation.EnableCaching; |
||||||
|
import org.springframework.context.ConfigurableApplicationContext; |
||||||
|
import org.springframework.context.annotation.AnnotationConfigApplicationContext; |
||||||
|
import org.springframework.context.annotation.Bean; |
||||||
|
import org.springframework.context.annotation.Configuration; |
||||||
|
|
||||||
|
/** |
||||||
|
* Provides various failure scenario linked to the use of {@link Cacheable#sync()}. |
||||||
|
* |
||||||
|
* @author Stephane Nicoll |
||||||
|
* @since 4.3 |
||||||
|
*/ |
||||||
|
public class CacheSyncFailureTests { |
||||||
|
|
||||||
|
@Rule |
||||||
|
public final ExpectedException thrown = ExpectedException.none(); |
||||||
|
|
||||||
|
private ConfigurableApplicationContext context; |
||||||
|
|
||||||
|
private SimpleService simpleService; |
||||||
|
|
||||||
|
@Before |
||||||
|
public void setUp() { |
||||||
|
this.context = new AnnotationConfigApplicationContext(Config.class); |
||||||
|
this.simpleService = context.getBean(SimpleService.class); |
||||||
|
} |
||||||
|
|
||||||
|
@After |
||||||
|
public void closeContext() { |
||||||
|
if (this.context != null) { |
||||||
|
this.context.close(); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void unlessSync() { |
||||||
|
thrown.expect(IllegalStateException.class); |
||||||
|
thrown.expectMessage("@Cacheable(sync = true) does not support unless attribute"); |
||||||
|
this.simpleService.unlessSync("key"); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void severalCachesSync() { |
||||||
|
thrown.expect(IllegalStateException.class); |
||||||
|
thrown.expectMessage("@Cacheable(sync = true) only allows a single cache"); |
||||||
|
this.simpleService.severalCachesSync("key"); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void severalCachesWithResolvedSync() { |
||||||
|
thrown.expect(IllegalStateException.class); |
||||||
|
thrown.expectMessage("@Cacheable(sync = true) only allows a single cache"); |
||||||
|
this.simpleService.severalCachesWithResolvedSync("key"); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void syncWithAnotherOperation() { |
||||||
|
thrown.expect(IllegalStateException.class); |
||||||
|
thrown.expectMessage("@Cacheable(sync = true) cannot be combined with other cache operations"); |
||||||
|
this.simpleService.syncWithAnotherOperation("key"); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void syncWithTwoGetOperations() { |
||||||
|
thrown.expect(IllegalStateException.class); |
||||||
|
thrown.expectMessage("Only one @Cacheable(sync = true) entry is allowed"); |
||||||
|
this.simpleService.syncWithTwoGetOperations("key"); |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
static class SimpleService { |
||||||
|
|
||||||
|
private final AtomicLong counter = new AtomicLong(); |
||||||
|
|
||||||
|
@Cacheable(cacheNames = "testCache", sync = true, unless = "#result > 10") |
||||||
|
public Object unlessSync(Object arg1) { |
||||||
|
return this.counter.getAndIncrement(); |
||||||
|
} |
||||||
|
|
||||||
|
@Cacheable(cacheNames = {"testCache", "anotherTestCache"}, sync = true) |
||||||
|
public Object severalCachesSync(Object arg1) { |
||||||
|
return this.counter.getAndIncrement(); |
||||||
|
} |
||||||
|
|
||||||
|
@Cacheable(cacheResolver = "testCacheResolver", sync = true) |
||||||
|
public Object severalCachesWithResolvedSync(Object arg1) { |
||||||
|
return this.counter.getAndIncrement(); |
||||||
|
} |
||||||
|
|
||||||
|
@Cacheable(cacheNames = "testCache", sync = true) |
||||||
|
@CacheEvict(cacheNames = "anotherTestCache", key = "#arg1") |
||||||
|
public Object syncWithAnotherOperation(Object arg1) { |
||||||
|
return this.counter.getAndIncrement(); |
||||||
|
} |
||||||
|
|
||||||
|
@Caching(cacheable = { |
||||||
|
@Cacheable(cacheNames = "testCache", sync = true), |
||||||
|
@Cacheable(cacheNames = "anotherTestCache", sync = true) |
||||||
|
}) |
||||||
|
public Object syncWithTwoGetOperations(Object arg1) { |
||||||
|
return this.counter.getAndIncrement(); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
@Configuration |
||||||
|
@EnableCaching |
||||||
|
static class Config extends CachingConfigurerSupport { |
||||||
|
|
||||||
|
@Override |
||||||
|
@Bean |
||||||
|
public CacheManager cacheManager() { |
||||||
|
return CacheTestUtils.createSimpleCacheManager("testCache", "anotherTestCache"); |
||||||
|
} |
||||||
|
|
||||||
|
@Bean |
||||||
|
public CacheResolver testCacheResolver() { |
||||||
|
return new NamedCacheResolver(cacheManager(), "testCache", "anotherTestCache"); |
||||||
|
} |
||||||
|
|
||||||
|
@Bean |
||||||
|
public SimpleService simpleService() { |
||||||
|
return new SimpleService(); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
Loading…
Reference in new issue