Support for multi-threaded cache access

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-9254
This commit is contained in:
Stephane Nicoll
2015-11-05 10:42:07 +01:00
parent 15c7dcd11a
commit 19d97c4253
33 changed files with 938 additions and 146 deletions

View File

@@ -67,11 +67,22 @@ public abstract aspect AbstractCacheAspect extends CacheAspectSupport implements
CacheOperationInvoker aspectJInvoker = new CacheOperationInvoker() {
public Object invoke() {
return proceed(cachedObject);
try {
return proceed(cachedObject);
}
catch (Throwable ex) {
throw new ThrowableWrapper(ex);
}
}
};
return execute(aspectJInvoker, thisJoinPoint.getTarget(), method, thisJoinPoint.getArgs());
try {
return execute(aspectJInvoker, thisJoinPoint.getTarget(), method, thisJoinPoint.getArgs());
}
catch (CacheOperationInvoker.ThrowableWrapper th) {
AnyThrow.throwUnchecked(th.getOriginal());
return null; // never reached
}
}
/**

View File

@@ -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;
}
}

View File

@@ -109,16 +109,4 @@ public aspect JCacheCacheAspect extends JCacheAspectSupport {
execution(@CacheRemoveAll * *(..));
private static class AnyThrow {
private 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;
}
}
}