Skip to content

Commit 240f254

Browse files
committed
Allow @Cacheable method to return java.util.Optional variant of cached value
Includes renaming of internal delegate to CacheOperationExpressionEvaluator. Issue: SPR-14230
1 parent 220711d commit 240f254

File tree

8 files changed

+198
-86
lines changed

8 files changed

+198
-86
lines changed

spring-context/src/main/java/org/springframework/cache/interceptor/CacheAspectSupport.java

Lines changed: 103 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2015 the original author or authors.
2+
* Copyright 2002-2016 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -23,25 +23,27 @@
2323
import java.util.LinkedList;
2424
import java.util.List;
2525
import java.util.Map;
26+
import java.util.Optional;
2627
import java.util.concurrent.Callable;
2728
import java.util.concurrent.ConcurrentHashMap;
2829

2930
import org.apache.commons.logging.Log;
3031
import org.apache.commons.logging.LogFactory;
3132

3233
import org.springframework.aop.framework.AopProxyUtils;
34+
import org.springframework.beans.factory.BeanFactory;
35+
import org.springframework.beans.factory.BeanFactoryAware;
3336
import org.springframework.beans.factory.InitializingBean;
3437
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
3538
import org.springframework.beans.factory.NoUniqueBeanDefinitionException;
3639
import org.springframework.beans.factory.SmartInitializingSingleton;
3740
import org.springframework.beans.factory.annotation.BeanFactoryAnnotationUtils;
3841
import org.springframework.cache.Cache;
3942
import org.springframework.cache.CacheManager;
40-
import org.springframework.cache.support.SimpleValueWrapper;
4143
import org.springframework.context.ApplicationContext;
42-
import org.springframework.context.ApplicationContextAware;
4344
import org.springframework.context.expression.AnnotatedElementKey;
4445
import org.springframework.expression.EvaluationContext;
46+
import org.springframework.lang.UsesJava8;
4547
import org.springframework.util.Assert;
4648
import org.springframework.util.ClassUtils;
4749
import org.springframework.util.CollectionUtils;
@@ -77,25 +79,34 @@
7779
* @since 3.1
7880
*/
7981
public abstract class CacheAspectSupport extends AbstractCacheInvoker
80-
implements InitializingBean, SmartInitializingSingleton, ApplicationContextAware {
82+
implements BeanFactoryAware, InitializingBean, SmartInitializingSingleton {
83+
84+
private static Class<?> javaUtilOptionalClass = null;
85+
86+
static {
87+
try {
88+
javaUtilOptionalClass =
89+
ClassUtils.forName("java.util.Optional", CacheAspectSupport.class.getClassLoader());
90+
}
91+
catch (ClassNotFoundException ex) {
92+
// Java 8 not available - Optional references simply not supported then.
93+
}
94+
}
8195

8296
protected final Log logger = LogFactory.getLog(getClass());
8397

84-
/**
85-
* Cache of CacheOperationMetadata, keyed by {@link CacheOperationCacheKey}.
86-
*/
8798
private final Map<CacheOperationCacheKey, CacheOperationMetadata> metadataCache =
8899
new ConcurrentHashMap<CacheOperationCacheKey, CacheOperationMetadata>(1024);
89100

90-
private final ExpressionEvaluator evaluator = new ExpressionEvaluator();
101+
private final CacheOperationExpressionEvaluator evaluator = new CacheOperationExpressionEvaluator();
91102

92103
private CacheOperationSource cacheOperationSource;
93104

94105
private KeyGenerator keyGenerator = new SimpleKeyGenerator();
95106

96107
private CacheResolver cacheResolver;
97108

98-
private ApplicationContext applicationContext;
109+
private BeanFactory beanFactory;
99110

100111
private boolean initialized = false;
101112

@@ -164,12 +175,26 @@ public CacheResolver getCacheResolver() {
164175
return this.cacheResolver;
165176
}
166177

178+
/**
179+
* Set the containing {@link BeanFactory} for {@link CacheManager} and other
180+
* service lookups.
181+
* @since 4.3
182+
*/
167183
@Override
184+
public void setBeanFactory(BeanFactory beanFactory) {
185+
this.beanFactory = beanFactory;
186+
}
187+
188+
/**
189+
* @deprecated as of 4.3, in favor of {@link #setBeanFactory}
190+
*/
191+
@Deprecated
168192
public void setApplicationContext(ApplicationContext applicationContext) {
169-
this.applicationContext = applicationContext;
193+
this.beanFactory = applicationContext;
170194
}
171195

172196

197+
@Override
173198
public void afterPropertiesSet() {
174199
Assert.state(getCacheOperationSource() != null, "The 'cacheOperationSources' property is required: " +
175200
"If there are no cacheable methods, then don't use a cache aspect.");
@@ -181,7 +206,7 @@ public void afterSingletonsInstantiated() {
181206
if (getCacheResolver() == null) {
182207
// Lazily initialize cache resolver via default cache manager...
183208
try {
184-
setCacheManager(this.applicationContext.getBean(CacheManager.class));
209+
setCacheManager(this.beanFactory.getBean(CacheManager.class));
185210
}
186211
catch (NoUniqueBeanDefinitionException ex) {
187212
throw new IllegalStateException("No CacheResolver specified, and no unique bean of type " +
@@ -282,7 +307,7 @@ else if (StringUtils.hasText(operation.getCacheManager())) {
282307
* @see CacheOperation#cacheResolver
283308
*/
284309
protected <T> T getBean(String beanName, Class<T> expectedType) {
285-
return BeanFactoryAnnotationUtils.qualifiedBeanOfType(this.applicationContext, expectedType, beanName);
310+
return BeanFactoryAnnotationUtils.qualifiedBeanOfType(this.beanFactory, expectedType, beanName);
286311
}
287312

288313
/**
@@ -294,13 +319,12 @@ protected void clearMetadataCache() {
294319
}
295320

296321
protected Object execute(CacheOperationInvoker invoker, Object target, Method method, Object[] args) {
297-
// check whether aspect is enabled
298-
// to cope with cases where the AJ is pulled in automatically
322+
// Check whether aspect is enabled (to cope with cases where the AJ is pulled in automatically)
299323
if (this.initialized) {
300324
Class<?> targetClass = getTargetClass(target);
301325
Collection<CacheOperation> operations = getCacheOperationSource().getCacheOperations(method, targetClass);
302326
if (!CollectionUtils.isEmpty(operations)) {
303-
return execute(invoker, new CacheOperationContexts(operations, method, args, target, targetClass));
327+
return execute(invoker, method, new CacheOperationContexts(operations, method, args, target, targetClass));
304328
}
305329
}
306330

@@ -329,12 +353,12 @@ private Class<?> getTargetClass(Object target) {
329353
return targetClass;
330354
}
331355

332-
private Object execute(final CacheOperationInvoker invoker, CacheOperationContexts contexts) {
356+
private Object execute(final CacheOperationInvoker invoker, Method method, CacheOperationContexts contexts) {
333357
// Special handling of synchronized invocation
334358
if (contexts.isSynchronized()) {
335359
CacheOperationContext context = contexts.get(CacheableOperation.class).iterator().next();
336-
if (isConditionPassing(context, ExpressionEvaluator.NO_RESULT)) {
337-
Object key = generateKey(context, ExpressionEvaluator.NO_RESULT);
360+
if (isConditionPassing(context, CacheOperationExpressionEvaluator.NO_RESULT)) {
361+
Object key = generateKey(context, CacheOperationExpressionEvaluator.NO_RESULT);
338362
Cache cache = context.getCaches().iterator().next();
339363
try {
340364
return cache.get(key, new Callable<Object>() {
@@ -358,50 +382,65 @@ public Object call() throws Exception {
358382

359383

360384
// Process any early evictions
361-
processCacheEvicts(contexts.get(CacheEvictOperation.class), true, ExpressionEvaluator.NO_RESULT);
385+
processCacheEvicts(contexts.get(CacheEvictOperation.class), true,
386+
CacheOperationExpressionEvaluator.NO_RESULT);
362387

363388
// Check if we have a cached item matching the conditions
364389
Cache.ValueWrapper cacheHit = findCachedItem(contexts.get(CacheableOperation.class));
365390

366391
// Collect puts from any @Cacheable miss, if no cached item is found
367392
List<CachePutRequest> cachePutRequests = new LinkedList<CachePutRequest>();
368393
if (cacheHit == null) {
369-
collectPutRequests(contexts.get(CacheableOperation.class), ExpressionEvaluator.NO_RESULT, cachePutRequests);
394+
collectPutRequests(contexts.get(CacheableOperation.class),
395+
CacheOperationExpressionEvaluator.NO_RESULT, cachePutRequests);
370396
}
371397

372-
Cache.ValueWrapper result = null;
398+
Object cacheValue;
399+
Object returnValue;
373400

374-
// If there are no put requests, just use the cache hit
375-
if (cachePutRequests.isEmpty() && !hasCachePut(contexts)) {
376-
result = cacheHit;
401+
if (cacheHit != null && cachePutRequests.isEmpty() && !hasCachePut(contexts)) {
402+
// If there are no put requests, just use the cache hit
403+
cacheValue = cacheHit.get();
404+
if (method.getReturnType() == javaUtilOptionalClass &&
405+
(cacheValue == null || cacheValue.getClass() != javaUtilOptionalClass)) {
406+
returnValue = OptionalUnwrapper.wrap(cacheValue);
407+
}
408+
else {
409+
returnValue = cacheValue;
410+
}
377411
}
378-
379-
// Invoke the method if don't have a cache hit
380-
if (result == null) {
381-
result = new SimpleValueWrapper(invokeOperation(invoker));
412+
else {
413+
// Invoke the method if we don't have a cache hit
414+
returnValue = invokeOperation(invoker);
415+
if (returnValue != null && returnValue.getClass() == javaUtilOptionalClass) {
416+
cacheValue = OptionalUnwrapper.unwrap(returnValue);
417+
}
418+
else {
419+
cacheValue = returnValue;
420+
}
382421
}
383422

384423
// Collect any explicit @CachePuts
385-
collectPutRequests(contexts.get(CachePutOperation.class), result.get(), cachePutRequests);
424+
collectPutRequests(contexts.get(CachePutOperation.class), cacheValue, cachePutRequests);
386425

387426
// Process any collected put requests, either from @CachePut or a @Cacheable miss
388427
for (CachePutRequest cachePutRequest : cachePutRequests) {
389-
cachePutRequest.apply(result.get());
428+
cachePutRequest.apply(cacheValue);
390429
}
391430

392431
// Process any late evictions
393-
processCacheEvicts(contexts.get(CacheEvictOperation.class), false, result.get());
432+
processCacheEvicts(contexts.get(CacheEvictOperation.class), false, cacheValue);
394433

395-
return result.get();
434+
return returnValue;
396435
}
397436

398437
private boolean hasCachePut(CacheOperationContexts contexts) {
399-
// Evaluate the conditions *without* the result object because we don't have it yet.
438+
// Evaluate the conditions *without* the result object because we don't have it yet...
400439
Collection<CacheOperationContext> cachePutContexts = contexts.get(CachePutOperation.class);
401440
Collection<CacheOperationContext> excluded = new ArrayList<CacheOperationContext>();
402441
for (CacheOperationContext context : cachePutContexts) {
403442
try {
404-
if (!context.isConditionPassing(ExpressionEvaluator.RESULT_UNAVAILABLE)) {
443+
if (!context.isConditionPassing(CacheOperationExpressionEvaluator.RESULT_UNAVAILABLE)) {
405444
excluded.add(context);
406445
}
407446
}
@@ -453,7 +492,7 @@ private void logInvalidating(CacheOperationContext context, CacheEvictOperation
453492
* or {@code null} if none is found
454493
*/
455494
private Cache.ValueWrapper findCachedItem(Collection<CacheOperationContext> contexts) {
456-
Object result = ExpressionEvaluator.NO_RESULT;
495+
Object result = CacheOperationExpressionEvaluator.NO_RESULT;
457496
for (CacheOperationContext context : contexts) {
458497
if (isConditionPassing(context, result)) {
459498
Object key = generateKey(context, result);
@@ -551,7 +590,7 @@ public boolean isSynchronized() {
551590

552591
private boolean determineSyncFlag(Method method) {
553592
List<CacheOperationContext> cacheOperationContexts = this.contexts.get(CacheableOperation.class);
554-
if (cacheOperationContexts == null) { // No @Cacheable operation
593+
if (cacheOperationContexts == null) { // no @Cacheable operation at all
555594
return false;
556595
}
557596
boolean syncEnabled = false;
@@ -563,18 +602,18 @@ private boolean determineSyncFlag(Method method) {
563602
}
564603
if (syncEnabled) {
565604
if (this.contexts.size() > 1) {
566-
throw new IllegalStateException("@Cacheable(sync = true) cannot be combined with other cache operations on '" + method + "'");
605+
throw new IllegalStateException("@Cacheable(sync=true) cannot be combined with other cache operations on '" + method + "'");
567606
}
568607
if (cacheOperationContexts.size() > 1) {
569-
throw new IllegalStateException("Only one @Cacheable(sync = true) entry is allowed on '" + method + "'");
608+
throw new IllegalStateException("Only one @Cacheable(sync=true) entry is allowed on '" + method + "'");
570609
}
571610
CacheOperationContext cacheOperationContext = cacheOperationContexts.iterator().next();
572611
CacheableOperation operation = (CacheableOperation) cacheOperationContext.getOperation();
573612
if (cacheOperationContext.getCaches().size() > 1) {
574-
throw new IllegalStateException("@Cacheable(sync = true) only allows a single cache on '" + operation + "'");
613+
throw new IllegalStateException("@Cacheable(sync=true) only allows a single cache on '" + operation + "'");
575614
}
576615
if (StringUtils.hasText(operation.getUnless())) {
577-
throw new IllegalStateException("@Cacheable(sync = true) does not support unless attribute on '" + operation + "'");
616+
throw new IllegalStateException("@Cacheable(sync=true) does not support unless attribute on '" + operation + "'");
578617
}
579618
return true;
580619
}
@@ -702,9 +741,8 @@ protected Object generateKey(Object result) {
702741
}
703742

704743
private EvaluationContext createEvaluationContext(Object result) {
705-
return evaluator.createEvaluationContext(
706-
this.caches, this.metadata.method, this.args, this.target, this.metadata.targetClass,
707-
result, applicationContext);
744+
return evaluator.createEvaluationContext(this.caches, this.metadata.method, this.args,
745+
this.target, this.metadata.targetClass, result, beanFactory);
708746
}
709747

710748
protected Collection<? extends Cache> getCaches() {
@@ -790,4 +828,26 @@ public int compareTo(CacheOperationCacheKey other) {
790828
}
791829
}
792830

831+
832+
/**
833+
* Inner class to avoid a hard dependency on Java 8.
834+
*/
835+
@UsesJava8
836+
private static class OptionalUnwrapper {
837+
838+
public static Object unwrap(Object optionalObject) {
839+
Optional<?> optional = (Optional<?>) optionalObject;
840+
if (!optional.isPresent()) {
841+
return null;
842+
}
843+
Object result = optional.get();
844+
Assert.isTrue(!(result instanceof Optional), "Multi-level Optional usage not supported");
845+
return result;
846+
}
847+
848+
public static Object wrap(Object value) {
849+
return Optional.ofNullable(value);
850+
}
851+
}
852+
793853
}

spring-context/src/main/java/org/springframework/cache/interceptor/ExpressionEvaluator.java renamed to spring-context/src/main/java/org/springframework/cache/interceptor/CacheOperationExpressionEvaluator.java

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2015 the original author or authors.
2+
* Copyright 2002-2016 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -27,8 +27,6 @@
2727
import org.springframework.context.expression.AnnotatedElementKey;
2828
import org.springframework.context.expression.BeanFactoryResolver;
2929
import org.springframework.context.expression.CachedExpressionEvaluator;
30-
import org.springframework.core.DefaultParameterNameDiscoverer;
31-
import org.springframework.core.ParameterNameDiscoverer;
3230
import org.springframework.expression.EvaluationContext;
3331
import org.springframework.expression.Expression;
3432

@@ -45,7 +43,7 @@
4543
* @author Stephane Nicoll
4644
* @since 3.1
4745
*/
48-
class ExpressionEvaluator extends CachedExpressionEvaluator {
46+
class CacheOperationExpressionEvaluator extends CachedExpressionEvaluator {
4947

5048
/**
5149
* Indicate that there is no result variable.
@@ -62,8 +60,6 @@ class ExpressionEvaluator extends CachedExpressionEvaluator {
6260
*/
6361
public static final String RESULT_VARIABLE = "result";
6462

65-
// shared param discoverer since it caches data internally
66-
private final ParameterNameDiscoverer paramNameDiscoverer = new DefaultParameterNameDiscoverer();
6763

6864
private final Map<ExpressionKey, Expression> keyCache = new ConcurrentHashMap<ExpressionKey, Expression>(64);
6965

@@ -100,11 +96,11 @@ public EvaluationContext createEvaluationContext(Collection<? extends Cache> cac
10096
Method method, Object[] args, Object target, Class<?> targetClass, Object result,
10197
BeanFactory beanFactory) {
10298

103-
CacheExpressionRootObject rootObject = new CacheExpressionRootObject(caches,
104-
method, args, target, targetClass);
99+
CacheExpressionRootObject rootObject = new CacheExpressionRootObject(
100+
caches, method, args, target, targetClass);
105101
Method targetMethod = getTargetMethod(targetClass, method);
106-
CacheEvaluationContext evaluationContext = new CacheEvaluationContext(rootObject,
107-
targetMethod, args, this.paramNameDiscoverer);
102+
CacheEvaluationContext evaluationContext = new CacheEvaluationContext(
103+
rootObject, targetMethod, args, getParameterNameDiscoverer());
108104
if (result == RESULT_UNAVAILABLE) {
109105
evaluationContext.addUnavailableVariable(RESULT_VARIABLE);
110106
}

spring-context/src/main/java/org/springframework/cache/interceptor/CacheOperationInvoker.java

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,8 @@
2121
*
2222
* <p>Does not provide a way to transmit checked exceptions but
2323
* provide a special exception that should be used to wrap any
24-
* exception that was thrown by the underlying invocation. Callers
25-
* are expected to handle this issue type specifically.
24+
* exception that was thrown by the underlying invocation.
25+
* Callers are expected to handle this issue type specifically.
2626
*
2727
* @author Stephane Nicoll
2828
* @since 4.1
@@ -38,8 +38,9 @@ public interface CacheOperationInvoker {
3838
*/
3939
Object invoke() throws ThrowableWrapper;
4040

41+
4142
/**
42-
* Wrap any exception thrown while invoking {@link #invoke()}
43+
* Wrap any exception thrown while invoking {@link #invoke()}.
4344
*/
4445
@SuppressWarnings("serial")
4546
class ThrowableWrapper extends RuntimeException {

0 commit comments

Comments
 (0)