Allow @Cacheable method to return java.util.Optional variant of cached value

Includes renaming of internal delegate to CacheOperationExpressionEvaluator.

Issue: SPR-14230
This commit is contained in:
Juergen Hoeller 2016-04-28 23:16:43 +02:00
parent 220711d45b
commit 240f254bfc
8 changed files with 198 additions and 86 deletions

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2002-2015 the original author or authors. * Copyright 2002-2016 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -23,6 +23,7 @@ import java.util.Collections;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Optional;
import java.util.concurrent.Callable; import java.util.concurrent.Callable;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
@ -30,6 +31,8 @@ import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory; import org.apache.commons.logging.LogFactory;
import org.springframework.aop.framework.AopProxyUtils; import org.springframework.aop.framework.AopProxyUtils;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.BeanFactoryAware;
import org.springframework.beans.factory.InitializingBean; import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.NoSuchBeanDefinitionException; import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.beans.factory.NoUniqueBeanDefinitionException; import org.springframework.beans.factory.NoUniqueBeanDefinitionException;
@ -37,11 +40,10 @@ import org.springframework.beans.factory.SmartInitializingSingleton;
import org.springframework.beans.factory.annotation.BeanFactoryAnnotationUtils; import org.springframework.beans.factory.annotation.BeanFactoryAnnotationUtils;
import org.springframework.cache.Cache; import org.springframework.cache.Cache;
import org.springframework.cache.CacheManager; import org.springframework.cache.CacheManager;
import org.springframework.cache.support.SimpleValueWrapper;
import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.expression.AnnotatedElementKey; import org.springframework.context.expression.AnnotatedElementKey;
import org.springframework.expression.EvaluationContext; import org.springframework.expression.EvaluationContext;
import org.springframework.lang.UsesJava8;
import org.springframework.util.Assert; import org.springframework.util.Assert;
import org.springframework.util.ClassUtils; import org.springframework.util.ClassUtils;
import org.springframework.util.CollectionUtils; import org.springframework.util.CollectionUtils;
@ -77,17 +79,26 @@ import org.springframework.util.StringUtils;
* @since 3.1 * @since 3.1
*/ */
public abstract class CacheAspectSupport extends AbstractCacheInvoker public abstract class CacheAspectSupport extends AbstractCacheInvoker
implements InitializingBean, SmartInitializingSingleton, ApplicationContextAware { implements BeanFactoryAware, InitializingBean, SmartInitializingSingleton {
private static Class<?> javaUtilOptionalClass = null;
static {
try {
javaUtilOptionalClass =
ClassUtils.forName("java.util.Optional", CacheAspectSupport.class.getClassLoader());
}
catch (ClassNotFoundException ex) {
// Java 8 not available - Optional references simply not supported then.
}
}
protected final Log logger = LogFactory.getLog(getClass()); protected final Log logger = LogFactory.getLog(getClass());
/**
* Cache of CacheOperationMetadata, keyed by {@link CacheOperationCacheKey}.
*/
private final Map<CacheOperationCacheKey, CacheOperationMetadata> metadataCache = private final Map<CacheOperationCacheKey, CacheOperationMetadata> metadataCache =
new ConcurrentHashMap<CacheOperationCacheKey, CacheOperationMetadata>(1024); new ConcurrentHashMap<CacheOperationCacheKey, CacheOperationMetadata>(1024);
private final ExpressionEvaluator evaluator = new ExpressionEvaluator(); private final CacheOperationExpressionEvaluator evaluator = new CacheOperationExpressionEvaluator();
private CacheOperationSource cacheOperationSource; private CacheOperationSource cacheOperationSource;
@ -95,7 +106,7 @@ public abstract class CacheAspectSupport extends AbstractCacheInvoker
private CacheResolver cacheResolver; private CacheResolver cacheResolver;
private ApplicationContext applicationContext; private BeanFactory beanFactory;
private boolean initialized = false; private boolean initialized = false;
@ -164,12 +175,26 @@ public abstract class CacheAspectSupport extends AbstractCacheInvoker
return this.cacheResolver; return this.cacheResolver;
} }
/**
* Set the containing {@link BeanFactory} for {@link CacheManager} and other
* service lookups.
* @since 4.3
*/
@Override @Override
public void setBeanFactory(BeanFactory beanFactory) {
this.beanFactory = beanFactory;
}
/**
* @deprecated as of 4.3, in favor of {@link #setBeanFactory}
*/
@Deprecated
public void setApplicationContext(ApplicationContext applicationContext) { public void setApplicationContext(ApplicationContext applicationContext) {
this.applicationContext = applicationContext; this.beanFactory = applicationContext;
} }
@Override
public void afterPropertiesSet() { public void afterPropertiesSet() {
Assert.state(getCacheOperationSource() != null, "The 'cacheOperationSources' property is required: " + Assert.state(getCacheOperationSource() != null, "The 'cacheOperationSources' property is required: " +
"If there are no cacheable methods, then don't use a cache aspect."); "If there are no cacheable methods, then don't use a cache aspect.");
@ -181,7 +206,7 @@ public abstract class CacheAspectSupport extends AbstractCacheInvoker
if (getCacheResolver() == null) { if (getCacheResolver() == null) {
// Lazily initialize cache resolver via default cache manager... // Lazily initialize cache resolver via default cache manager...
try { try {
setCacheManager(this.applicationContext.getBean(CacheManager.class)); setCacheManager(this.beanFactory.getBean(CacheManager.class));
} }
catch (NoUniqueBeanDefinitionException ex) { catch (NoUniqueBeanDefinitionException ex) {
throw new IllegalStateException("No CacheResolver specified, and no unique bean of type " + throw new IllegalStateException("No CacheResolver specified, and no unique bean of type " +
@ -282,7 +307,7 @@ public abstract class CacheAspectSupport extends AbstractCacheInvoker
* @see CacheOperation#cacheResolver * @see CacheOperation#cacheResolver
*/ */
protected <T> T getBean(String beanName, Class<T> expectedType) { protected <T> T getBean(String beanName, Class<T> expectedType) {
return BeanFactoryAnnotationUtils.qualifiedBeanOfType(this.applicationContext, expectedType, beanName); return BeanFactoryAnnotationUtils.qualifiedBeanOfType(this.beanFactory, expectedType, beanName);
} }
/** /**
@ -294,13 +319,12 @@ public abstract class CacheAspectSupport extends AbstractCacheInvoker
} }
protected Object execute(CacheOperationInvoker invoker, Object target, Method method, Object[] args) { protected Object execute(CacheOperationInvoker invoker, Object target, Method method, Object[] args) {
// check whether aspect is enabled // Check whether aspect is enabled (to cope with cases where the AJ is pulled in automatically)
// to cope with cases where the AJ is pulled in automatically
if (this.initialized) { if (this.initialized) {
Class<?> targetClass = getTargetClass(target); Class<?> targetClass = getTargetClass(target);
Collection<CacheOperation> operations = getCacheOperationSource().getCacheOperations(method, targetClass); Collection<CacheOperation> operations = getCacheOperationSource().getCacheOperations(method, targetClass);
if (!CollectionUtils.isEmpty(operations)) { if (!CollectionUtils.isEmpty(operations)) {
return execute(invoker, new CacheOperationContexts(operations, method, args, target, targetClass)); return execute(invoker, method, new CacheOperationContexts(operations, method, args, target, targetClass));
} }
} }
@ -329,12 +353,12 @@ public abstract class CacheAspectSupport extends AbstractCacheInvoker
return targetClass; return targetClass;
} }
private Object execute(final CacheOperationInvoker invoker, CacheOperationContexts contexts) { private Object execute(final CacheOperationInvoker invoker, Method method, CacheOperationContexts contexts) {
// Special handling of synchronized invocation // Special handling of synchronized invocation
if (contexts.isSynchronized()) { if (contexts.isSynchronized()) {
CacheOperationContext context = contexts.get(CacheableOperation.class).iterator().next(); CacheOperationContext context = contexts.get(CacheableOperation.class).iterator().next();
if (isConditionPassing(context, ExpressionEvaluator.NO_RESULT)) { if (isConditionPassing(context, CacheOperationExpressionEvaluator.NO_RESULT)) {
Object key = generateKey(context, ExpressionEvaluator.NO_RESULT); Object key = generateKey(context, CacheOperationExpressionEvaluator.NO_RESULT);
Cache cache = context.getCaches().iterator().next(); Cache cache = context.getCaches().iterator().next();
try { try {
return cache.get(key, new Callable<Object>() { return cache.get(key, new Callable<Object>() {
@ -358,7 +382,8 @@ public abstract class CacheAspectSupport extends AbstractCacheInvoker
// Process any early evictions // Process any early evictions
processCacheEvicts(contexts.get(CacheEvictOperation.class), true, ExpressionEvaluator.NO_RESULT); processCacheEvicts(contexts.get(CacheEvictOperation.class), true,
CacheOperationExpressionEvaluator.NO_RESULT);
// Check if we have a cached item matching the conditions // Check if we have a cached item matching the conditions
Cache.ValueWrapper cacheHit = findCachedItem(contexts.get(CacheableOperation.class)); Cache.ValueWrapper cacheHit = findCachedItem(contexts.get(CacheableOperation.class));
@ -366,42 +391,56 @@ public abstract class CacheAspectSupport extends AbstractCacheInvoker
// Collect puts from any @Cacheable miss, if no cached item is found // Collect puts from any @Cacheable miss, if no cached item is found
List<CachePutRequest> cachePutRequests = new LinkedList<CachePutRequest>(); List<CachePutRequest> cachePutRequests = new LinkedList<CachePutRequest>();
if (cacheHit == null) { if (cacheHit == null) {
collectPutRequests(contexts.get(CacheableOperation.class), ExpressionEvaluator.NO_RESULT, cachePutRequests); collectPutRequests(contexts.get(CacheableOperation.class),
CacheOperationExpressionEvaluator.NO_RESULT, cachePutRequests);
} }
Cache.ValueWrapper result = null; Object cacheValue;
Object returnValue;
// If there are no put requests, just use the cache hit if (cacheHit != null && cachePutRequests.isEmpty() && !hasCachePut(contexts)) {
if (cachePutRequests.isEmpty() && !hasCachePut(contexts)) { // If there are no put requests, just use the cache hit
result = cacheHit; cacheValue = cacheHit.get();
if (method.getReturnType() == javaUtilOptionalClass &&
(cacheValue == null || cacheValue.getClass() != javaUtilOptionalClass)) {
returnValue = OptionalUnwrapper.wrap(cacheValue);
}
else {
returnValue = cacheValue;
}
} }
else {
// Invoke the method if don't have a cache hit // Invoke the method if we don't have a cache hit
if (result == null) { returnValue = invokeOperation(invoker);
result = new SimpleValueWrapper(invokeOperation(invoker)); if (returnValue != null && returnValue.getClass() == javaUtilOptionalClass) {
cacheValue = OptionalUnwrapper.unwrap(returnValue);
}
else {
cacheValue = returnValue;
}
} }
// Collect any explicit @CachePuts // Collect any explicit @CachePuts
collectPutRequests(contexts.get(CachePutOperation.class), result.get(), cachePutRequests); collectPutRequests(contexts.get(CachePutOperation.class), cacheValue, cachePutRequests);
// Process any collected put requests, either from @CachePut or a @Cacheable miss // Process any collected put requests, either from @CachePut or a @Cacheable miss
for (CachePutRequest cachePutRequest : cachePutRequests) { for (CachePutRequest cachePutRequest : cachePutRequests) {
cachePutRequest.apply(result.get()); cachePutRequest.apply(cacheValue);
} }
// Process any late evictions // Process any late evictions
processCacheEvicts(contexts.get(CacheEvictOperation.class), false, result.get()); processCacheEvicts(contexts.get(CacheEvictOperation.class), false, cacheValue);
return result.get(); return returnValue;
} }
private boolean hasCachePut(CacheOperationContexts contexts) { private boolean hasCachePut(CacheOperationContexts contexts) {
// Evaluate the conditions *without* the result object because we don't have it yet. // Evaluate the conditions *without* the result object because we don't have it yet...
Collection<CacheOperationContext> cachePutContexts = contexts.get(CachePutOperation.class); Collection<CacheOperationContext> cachePutContexts = contexts.get(CachePutOperation.class);
Collection<CacheOperationContext> excluded = new ArrayList<CacheOperationContext>(); Collection<CacheOperationContext> excluded = new ArrayList<CacheOperationContext>();
for (CacheOperationContext context : cachePutContexts) { for (CacheOperationContext context : cachePutContexts) {
try { try {
if (!context.isConditionPassing(ExpressionEvaluator.RESULT_UNAVAILABLE)) { if (!context.isConditionPassing(CacheOperationExpressionEvaluator.RESULT_UNAVAILABLE)) {
excluded.add(context); excluded.add(context);
} }
} }
@ -453,7 +492,7 @@ public abstract class CacheAspectSupport extends AbstractCacheInvoker
* or {@code null} if none is found * or {@code null} if none is found
*/ */
private Cache.ValueWrapper findCachedItem(Collection<CacheOperationContext> contexts) { private Cache.ValueWrapper findCachedItem(Collection<CacheOperationContext> contexts) {
Object result = ExpressionEvaluator.NO_RESULT; Object result = CacheOperationExpressionEvaluator.NO_RESULT;
for (CacheOperationContext context : contexts) { for (CacheOperationContext context : contexts) {
if (isConditionPassing(context, result)) { if (isConditionPassing(context, result)) {
Object key = generateKey(context, result); Object key = generateKey(context, result);
@ -551,7 +590,7 @@ public abstract class CacheAspectSupport extends AbstractCacheInvoker
private boolean determineSyncFlag(Method method) { private boolean determineSyncFlag(Method method) {
List<CacheOperationContext> cacheOperationContexts = this.contexts.get(CacheableOperation.class); List<CacheOperationContext> cacheOperationContexts = this.contexts.get(CacheableOperation.class);
if (cacheOperationContexts == null) { // No @Cacheable operation if (cacheOperationContexts == null) { // no @Cacheable operation at all
return false; return false;
} }
boolean syncEnabled = false; boolean syncEnabled = false;
@ -563,18 +602,18 @@ public abstract class CacheAspectSupport extends AbstractCacheInvoker
} }
if (syncEnabled) { if (syncEnabled) {
if (this.contexts.size() > 1) { if (this.contexts.size() > 1) {
throw new IllegalStateException("@Cacheable(sync = true) cannot be combined with other cache operations on '" + method + "'"); throw new IllegalStateException("@Cacheable(sync=true) cannot be combined with other cache operations on '" + method + "'");
} }
if (cacheOperationContexts.size() > 1) { if (cacheOperationContexts.size() > 1) {
throw new IllegalStateException("Only one @Cacheable(sync = true) entry is allowed on '" + method + "'"); throw new IllegalStateException("Only one @Cacheable(sync=true) entry is allowed on '" + method + "'");
} }
CacheOperationContext cacheOperationContext = cacheOperationContexts.iterator().next(); CacheOperationContext cacheOperationContext = cacheOperationContexts.iterator().next();
CacheableOperation operation = (CacheableOperation) cacheOperationContext.getOperation(); CacheableOperation operation = (CacheableOperation) cacheOperationContext.getOperation();
if (cacheOperationContext.getCaches().size() > 1) { if (cacheOperationContext.getCaches().size() > 1) {
throw new IllegalStateException("@Cacheable(sync = true) only allows a single cache on '" + operation + "'"); throw new IllegalStateException("@Cacheable(sync=true) only allows a single cache on '" + operation + "'");
} }
if (StringUtils.hasText(operation.getUnless())) { if (StringUtils.hasText(operation.getUnless())) {
throw new IllegalStateException("@Cacheable(sync = true) does not support unless attribute on '" + operation + "'"); throw new IllegalStateException("@Cacheable(sync=true) does not support unless attribute on '" + operation + "'");
} }
return true; return true;
} }
@ -702,9 +741,8 @@ public abstract class CacheAspectSupport extends AbstractCacheInvoker
} }
private EvaluationContext createEvaluationContext(Object result) { private EvaluationContext createEvaluationContext(Object result) {
return evaluator.createEvaluationContext( return evaluator.createEvaluationContext(this.caches, this.metadata.method, this.args,
this.caches, this.metadata.method, this.args, this.target, this.metadata.targetClass, this.target, this.metadata.targetClass, result, beanFactory);
result, applicationContext);
} }
protected Collection<? extends Cache> getCaches() { protected Collection<? extends Cache> getCaches() {
@ -790,4 +828,26 @@ public abstract class CacheAspectSupport extends AbstractCacheInvoker
} }
} }
/**
* Inner class to avoid a hard dependency on Java 8.
*/
@UsesJava8
private static class OptionalUnwrapper {
public static Object unwrap(Object optionalObject) {
Optional<?> optional = (Optional<?>) optionalObject;
if (!optional.isPresent()) {
return null;
}
Object result = optional.get();
Assert.isTrue(!(result instanceof Optional), "Multi-level Optional usage not supported");
return result;
}
public static Object wrap(Object value) {
return Optional.ofNullable(value);
}
}
} }

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2002-2015 the original author or authors. * Copyright 2002-2016 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -27,8 +27,6 @@ import org.springframework.cache.Cache;
import org.springframework.context.expression.AnnotatedElementKey; import org.springframework.context.expression.AnnotatedElementKey;
import org.springframework.context.expression.BeanFactoryResolver; import org.springframework.context.expression.BeanFactoryResolver;
import org.springframework.context.expression.CachedExpressionEvaluator; import org.springframework.context.expression.CachedExpressionEvaluator;
import org.springframework.core.DefaultParameterNameDiscoverer;
import org.springframework.core.ParameterNameDiscoverer;
import org.springframework.expression.EvaluationContext; import org.springframework.expression.EvaluationContext;
import org.springframework.expression.Expression; import org.springframework.expression.Expression;
@ -45,7 +43,7 @@ import org.springframework.expression.Expression;
* @author Stephane Nicoll * @author Stephane Nicoll
* @since 3.1 * @since 3.1
*/ */
class ExpressionEvaluator extends CachedExpressionEvaluator { class CacheOperationExpressionEvaluator extends CachedExpressionEvaluator {
/** /**
* Indicate that there is no result variable. * Indicate that there is no result variable.
@ -62,8 +60,6 @@ class ExpressionEvaluator extends CachedExpressionEvaluator {
*/ */
public static final String RESULT_VARIABLE = "result"; public static final String RESULT_VARIABLE = "result";
// shared param discoverer since it caches data internally
private final ParameterNameDiscoverer paramNameDiscoverer = new DefaultParameterNameDiscoverer();
private final Map<ExpressionKey, Expression> keyCache = new ConcurrentHashMap<ExpressionKey, Expression>(64); private final Map<ExpressionKey, Expression> keyCache = new ConcurrentHashMap<ExpressionKey, Expression>(64);
@ -100,11 +96,11 @@ class ExpressionEvaluator extends CachedExpressionEvaluator {
Method method, Object[] args, Object target, Class<?> targetClass, Object result, Method method, Object[] args, Object target, Class<?> targetClass, Object result,
BeanFactory beanFactory) { BeanFactory beanFactory) {
CacheExpressionRootObject rootObject = new CacheExpressionRootObject(caches, CacheExpressionRootObject rootObject = new CacheExpressionRootObject(
method, args, target, targetClass); caches, method, args, target, targetClass);
Method targetMethod = getTargetMethod(targetClass, method); Method targetMethod = getTargetMethod(targetClass, method);
CacheEvaluationContext evaluationContext = new CacheEvaluationContext(rootObject, CacheEvaluationContext evaluationContext = new CacheEvaluationContext(
targetMethod, args, this.paramNameDiscoverer); rootObject, targetMethod, args, getParameterNameDiscoverer());
if (result == RESULT_UNAVAILABLE) { if (result == RESULT_UNAVAILABLE) {
evaluationContext.addUnavailableVariable(RESULT_VARIABLE); evaluationContext.addUnavailableVariable(RESULT_VARIABLE);
} }

View File

@ -21,8 +21,8 @@ package org.springframework.cache.interceptor;
* *
* <p>Does not provide a way to transmit checked exceptions but * <p>Does not provide a way to transmit checked exceptions but
* provide a special exception that should be used to wrap any * provide a special exception that should be used to wrap any
* exception that was thrown by the underlying invocation. Callers * exception that was thrown by the underlying invocation.
* are expected to handle this issue type specifically. * Callers are expected to handle this issue type specifically.
* *
* @author Stephane Nicoll * @author Stephane Nicoll
* @since 4.1 * @since 4.1
@ -38,8 +38,9 @@ public interface CacheOperationInvoker {
*/ */
Object invoke() throws ThrowableWrapper; Object invoke() throws ThrowableWrapper;
/** /**
* Wrap any exception thrown while invoking {@link #invoke()} * Wrap any exception thrown while invoking {@link #invoke()}.
*/ */
@SuppressWarnings("serial") @SuppressWarnings("serial")
class ThrowableWrapper extends RuntimeException { class ThrowableWrapper extends RuntimeException {

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2002-2015 the original author or authors. * Copyright 2002-2016 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -27,8 +27,6 @@ import org.springframework.context.expression.AnnotatedElementKey;
import org.springframework.context.expression.BeanFactoryResolver; import org.springframework.context.expression.BeanFactoryResolver;
import org.springframework.context.expression.CachedExpressionEvaluator; import org.springframework.context.expression.CachedExpressionEvaluator;
import org.springframework.context.expression.MethodBasedEvaluationContext; import org.springframework.context.expression.MethodBasedEvaluationContext;
import org.springframework.core.DefaultParameterNameDiscoverer;
import org.springframework.core.ParameterNameDiscoverer;
import org.springframework.expression.EvaluationContext; import org.springframework.expression.EvaluationContext;
import org.springframework.expression.Expression; import org.springframework.expression.Expression;
@ -42,13 +40,11 @@ import org.springframework.expression.Expression;
*/ */
class EventExpressionEvaluator extends CachedExpressionEvaluator { class EventExpressionEvaluator extends CachedExpressionEvaluator {
// shared param discoverer since it caches data internally
private final ParameterNameDiscoverer paramNameDiscoverer = new DefaultParameterNameDiscoverer();
private final Map<ExpressionKey, Expression> conditionCache = new ConcurrentHashMap<ExpressionKey, Expression>(64); private final Map<ExpressionKey, Expression> conditionCache = new ConcurrentHashMap<ExpressionKey, Expression>(64);
private final Map<AnnotatedElementKey, Method> targetMethodCache = new ConcurrentHashMap<AnnotatedElementKey, Method>(64); private final Map<AnnotatedElementKey, Method> targetMethodCache = new ConcurrentHashMap<AnnotatedElementKey, Method>(64);
/** /**
* Create the suitable {@link EvaluationContext} for the specified event handling * Create the suitable {@link EvaluationContext} for the specified event handling
* on the specified method. * on the specified method.
@ -58,8 +54,8 @@ class EventExpressionEvaluator extends CachedExpressionEvaluator {
Method targetMethod = getTargetMethod(targetClass, method); Method targetMethod = getTargetMethod(targetClass, method);
EventExpressionRootObject root = new EventExpressionRootObject(event, args); EventExpressionRootObject root = new EventExpressionRootObject(event, args);
MethodBasedEvaluationContext evaluationContext = MethodBasedEvaluationContext evaluationContext = new MethodBasedEvaluationContext(
new MethodBasedEvaluationContext(root, targetMethod, args, this.paramNameDiscoverer); root, targetMethod, args, getParameterNameDiscoverer());
if (beanFactory != null) { if (beanFactory != null) {
evaluationContext.setBeanResolver(new BeanFactoryResolver(beanFactory)); evaluationContext.setBeanResolver(new BeanFactoryResolver(beanFactory));
} }

View File

@ -18,6 +18,8 @@ package org.springframework.context.expression;
import java.util.Map; import java.util.Map;
import org.springframework.core.DefaultParameterNameDiscoverer;
import org.springframework.core.ParameterNameDiscoverer;
import org.springframework.expression.Expression; import org.springframework.expression.Expression;
import org.springframework.expression.spel.standard.SpelExpressionParser; import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.util.Assert; import org.springframework.util.Assert;
@ -35,12 +37,14 @@ public abstract class CachedExpressionEvaluator {
private final SpelExpressionParser parser; private final SpelExpressionParser parser;
private final ParameterNameDiscoverer parameterNameDiscoverer = new DefaultParameterNameDiscoverer();
/** /**
* Create a new instance with the specified {@link SpelExpressionParser}. * Create a new instance with the specified {@link SpelExpressionParser}.
*/ */
protected CachedExpressionEvaluator(SpelExpressionParser parser) { protected CachedExpressionEvaluator(SpelExpressionParser parser) {
Assert.notNull(parser, "Parser must not be null"); Assert.notNull(parser, "SpelExpressionParser must not be null");
this.parser = parser; this.parser = parser;
} }
@ -59,6 +63,14 @@ public abstract class CachedExpressionEvaluator {
return this.parser; return this.parser;
} }
/**
* Return a shared parameter name discoverer which caches data internally.
* @since 4.3
*/
protected ParameterNameDiscoverer getParameterNameDiscoverer() {
return this.parameterNameDiscoverer;
}
/** /**
* Return the {@link Expression} for the specified SpEL value * Return the {@link Expression} for the specified SpEL value

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2002-2015 the original author or authors. * Copyright 2002-2016 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -20,12 +20,14 @@ import java.util.Arrays;
import java.util.Collection; import java.util.Collection;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Optional;
import org.junit.Rule; import org.junit.Rule;
import org.junit.Test; import org.junit.Test;
import org.junit.rules.ExpectedException; import org.junit.rules.ExpectedException;
import org.mockito.Mockito; import org.mockito.Mockito;
import org.springframework.cache.annotation.CachePut;
import org.springframework.cache.annotation.Cacheable; import org.springframework.cache.annotation.Cacheable;
import org.springframework.cache.annotation.Caching; import org.springframework.cache.annotation.Caching;
import org.springframework.cache.annotation.CachingConfigurerSupport; import org.springframework.cache.annotation.CachingConfigurerSupport;
@ -39,6 +41,7 @@ import org.springframework.cache.support.SimpleCacheManager;
import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.tests.sample.beans.TestBean;
import static org.junit.Assert.*; import static org.junit.Assert.*;
import static org.mockito.Mockito.*; import static org.mockito.Mockito.*;
@ -83,11 +86,11 @@ public class CacheReproTests {
String key = "1"; String key = "1";
Object result = bean.getSimple("1"); Object result = bean.getSimple("1");
verify(cache, times(1)).get(key); // first call: cache miss verify(cache, times(1)).get(key); // first call: cache miss
Object cachedResult = bean.getSimple("1"); Object cachedResult = bean.getSimple("1");
assertSame(result, cachedResult); assertSame(result, cachedResult);
verify(cache, times(2)).get(key); // second call: cache hit verify(cache, times(2)).get(key); // second call: cache hit
context.close(); context.close();
} }
@ -100,11 +103,11 @@ public class CacheReproTests {
String key = "1"; String key = "1";
Object result = bean.getNeverCache("1"); Object result = bean.getNeverCache("1");
verify(cache, times(0)).get(key); // no cache hit at all, caching disabled verify(cache, times(0)).get(key); // no cache hit at all, caching disabled
Object cachedResult = bean.getNeverCache("1"); Object cachedResult = bean.getNeverCache("1");
assertNotSame(result, cachedResult); assertNotSame(result, cachedResult);
verify(cache, times(0)).get(key); // caching disabled verify(cache, times(0)).get(key); // caching disabled
context.close(); context.close();
} }
@ -114,8 +117,9 @@ public class CacheReproTests {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(Spr13081Config.class); AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(Spr13081Config.class);
MyCacheResolver cacheResolver = context.getBean(MyCacheResolver.class); MyCacheResolver cacheResolver = context.getBean(MyCacheResolver.class);
Spr13081Service bean = context.getBean(Spr13081Service.class); Spr13081Service bean = context.getBean(Spr13081Service.class);
assertNull(cacheResolver.getCache("foo").get("foo")); assertNull(cacheResolver.getCache("foo").get("foo"));
Object result = bean.getSimple("foo"); // cache name = id Object result = bean.getSimple("foo"); // cache name = id
assertEquals(result, cacheResolver.getCache("foo").get("foo").get()); assertEquals(result, cacheResolver.getCache("foo").get("foo").get());
} }
@ -124,12 +128,21 @@ public class CacheReproTests {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(Spr13081Config.class); AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(Spr13081Config.class);
Spr13081Service bean = context.getBean(Spr13081Service.class); Spr13081Service bean = context.getBean(Spr13081Service.class);
thrown.expect(IllegalStateException.class); thrown.expect(IllegalStateException.class);
thrown.expectMessage(MyCacheResolver.class.getName()); thrown.expectMessage(MyCacheResolver.class.getName());
bean.getSimple(null); bean.getSimple(null);
} }
@Test
public void spr14230AdaptsToOptional() {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(Spr14230Config.class);
Spr14230Service bean = context.getBean(Spr14230Service.class);
TestBean tb = new TestBean("tb1");
bean.insertItem(tb);
assertSame(tb, bean.findById("tb1").get());
}
@Configuration @Configuration
@EnableCaching @EnableCaching
@ -245,6 +258,7 @@ public class CacheReproTests {
} }
} }
@Configuration @Configuration
@EnableCaching @EnableCaching
public static class Spr13081Config extends CachingConfigurerSupport { public static class Spr13081Config extends CachingConfigurerSupport {
@ -259,9 +273,9 @@ public class CacheReproTests {
public Spr13081Service service() { public Spr13081Service service() {
return new Spr13081Service(); return new Spr13081Service();
} }
} }
public static class MyCacheResolver extends AbstractCacheResolver { public static class MyCacheResolver extends AbstractCacheResolver {
public MyCacheResolver() { public MyCacheResolver() {
@ -282,6 +296,7 @@ public class CacheReproTests {
} }
} }
public static class Spr13081Service { public static class Spr13081Service {
@Cacheable @Cacheable
@ -290,4 +305,34 @@ public class CacheReproTests {
} }
} }
public static class Spr14230Service {
@Cacheable("itemCache")
public Optional<TestBean> findById(String id) {
return Optional.of(new TestBean(id));
}
@CachePut(cacheNames = "itemCache", key = "#item.name")
public TestBean insertItem(TestBean item) {
return item;
}
}
@Configuration
@EnableCaching
public static class Spr14230Config {
@Bean
public CacheManager cacheManager() {
return new ConcurrentMapCacheManager();
}
@Bean
public Spr14230Service service() {
return new Spr14230Service();
}
}
} }

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2002-2015 the original author or authors. * Copyright 2002-2016 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -67,35 +67,35 @@ public class CacheSyncFailureTests {
@Test @Test
public void unlessSync() { public void unlessSync() {
thrown.expect(IllegalStateException.class); thrown.expect(IllegalStateException.class);
thrown.expectMessage("@Cacheable(sync = true) does not support unless attribute"); thrown.expectMessage("@Cacheable(sync=true) does not support unless attribute");
this.simpleService.unlessSync("key"); this.simpleService.unlessSync("key");
} }
@Test @Test
public void severalCachesSync() { public void severalCachesSync() {
thrown.expect(IllegalStateException.class); thrown.expect(IllegalStateException.class);
thrown.expectMessage("@Cacheable(sync = true) only allows a single cache"); thrown.expectMessage("@Cacheable(sync=true) only allows a single cache");
this.simpleService.severalCachesSync("key"); this.simpleService.severalCachesSync("key");
} }
@Test @Test
public void severalCachesWithResolvedSync() { public void severalCachesWithResolvedSync() {
thrown.expect(IllegalStateException.class); thrown.expect(IllegalStateException.class);
thrown.expectMessage("@Cacheable(sync = true) only allows a single cache"); thrown.expectMessage("@Cacheable(sync=true) only allows a single cache");
this.simpleService.severalCachesWithResolvedSync("key"); this.simpleService.severalCachesWithResolvedSync("key");
} }
@Test @Test
public void syncWithAnotherOperation() { public void syncWithAnotherOperation() {
thrown.expect(IllegalStateException.class); thrown.expect(IllegalStateException.class);
thrown.expectMessage("@Cacheable(sync = true) cannot be combined with other cache operations"); thrown.expectMessage("@Cacheable(sync=true) cannot be combined with other cache operations");
this.simpleService.syncWithAnotherOperation("key"); this.simpleService.syncWithAnotherOperation("key");
} }
@Test @Test
public void syncWithTwoGetOperations() { public void syncWithTwoGetOperations() {
thrown.expect(IllegalStateException.class); thrown.expect(IllegalStateException.class);
thrown.expectMessage("Only one @Cacheable(sync = true) entry is allowed"); thrown.expectMessage("Only one @Cacheable(sync=true) entry is allowed");
this.simpleService.syncWithTwoGetOperations("key"); this.simpleService.syncWithTwoGetOperations("key");
} }

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2002-2015 the original author or authors. * Copyright 2002-2016 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -47,15 +47,17 @@ import static org.junit.Assert.*;
*/ */
public class ExpressionEvaluatorTests { public class ExpressionEvaluatorTests {
private ExpressionEvaluator eval = new ExpressionEvaluator(); private final CacheOperationExpressionEvaluator eval = new CacheOperationExpressionEvaluator();
private final AnnotationCacheOperationSource source = new AnnotationCacheOperationSource();
private AnnotationCacheOperationSource source = new AnnotationCacheOperationSource();
private Collection<CacheOperation> getOps(String name) { private Collection<CacheOperation> getOps(String name) {
Method method = ReflectionUtils.findMethod(AnnotatedClass.class, name, Object.class, Object.class); Method method = ReflectionUtils.findMethod(AnnotatedClass.class, name, Object.class, Object.class);
return source.getCacheOperations(method, AnnotatedClass.class); return this.source.getCacheOperations(method, AnnotatedClass.class);
} }
@Test @Test
public void testMultipleCachingSource() throws Exception { public void testMultipleCachingSource() throws Exception {
Collection<CacheOperation> ops = getOps("multipleCaching"); Collection<CacheOperation> ops = getOps("multipleCaching");
@ -110,14 +112,14 @@ public class ExpressionEvaluatorTests {
@Test @Test
public void withoutReturnValue() throws Exception { public void withoutReturnValue() throws Exception {
EvaluationContext context = createEvaluationContext(ExpressionEvaluator.NO_RESULT); EvaluationContext context = createEvaluationContext(CacheOperationExpressionEvaluator.NO_RESULT);
Object value = new SpelExpressionParser().parseExpression("#result").getValue(context); Object value = new SpelExpressionParser().parseExpression("#result").getValue(context);
assertThat(value, nullValue()); assertThat(value, nullValue());
} }
@Test @Test
public void unavailableReturnValue() throws Exception { public void unavailableReturnValue() throws Exception {
EvaluationContext context = createEvaluationContext(ExpressionEvaluator.RESULT_UNAVAILABLE); EvaluationContext context = createEvaluationContext(CacheOperationExpressionEvaluator.RESULT_UNAVAILABLE);
try { try {
new SpelExpressionParser().parseExpression("#result").getValue(context); new SpelExpressionParser().parseExpression("#result").getValue(context);
fail("Should have failed to parse expression, result not available"); fail("Should have failed to parse expression, result not available");
@ -134,7 +136,7 @@ public class ExpressionEvaluatorTests {
applicationContext.registerBeanDefinition("myBean", beanDefinition); applicationContext.registerBeanDefinition("myBean", beanDefinition);
applicationContext.refresh(); applicationContext.refresh();
EvaluationContext context = createEvaluationContext(ExpressionEvaluator.NO_RESULT, applicationContext); EvaluationContext context = createEvaluationContext(CacheOperationExpressionEvaluator.NO_RESULT, applicationContext);
Object value = new SpelExpressionParser().parseExpression("@myBean.class.getName()").getValue(context); Object value = new SpelExpressionParser().parseExpression("@myBean.class.getName()").getValue(context);
assertThat(value, is(String.class.getName())); assertThat(value, is(String.class.getName()));
} }