org.springframework.cache.interceptor.CacheAspectSupport Maven / Gradle / Ivy
Show all versions of spring-context Show documentation
/*
* 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.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.aop.framework.AopProxyUtils;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.beans.factory.NoUniqueBeanDefinitionException;
import org.springframework.beans.factory.SmartInitializingSingleton;
import org.springframework.beans.factory.annotation.BeanFactoryAnnotationUtils;
import org.springframework.cache.Cache;
import org.springframework.cache.CacheManager;
import org.springframework.cache.support.SimpleValueWrapper;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.expression.AnnotatedElementKey;
import org.springframework.expression.EvaluationContext;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.CollectionUtils;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
/**
* Base class for caching aspects, such as the {@link CacheInterceptor}
* or an AspectJ aspect.
*
* This enables the underlying Spring caching infrastructure to be
* used easily to implement an aspect for any aspect system.
*
*
Subclasses are responsible for calling methods in this class in
* the correct order.
*
*
Uses the Strategy design pattern. A {@link CacheResolver}
* implementation will resolve the actual cache(s) to use, and a
* {@link CacheOperationSource} is used for determining caching
* operations.
*
*
A cache aspect is serializable if its {@code CacheResolver} and
* {@code CacheOperationSource} are serializable.
*
* @author Costin Leau
* @author Juergen Hoeller
* @author Chris Beams
* @author Phillip Webb
* @author Sam Brannen
* @author Stephane Nicoll
* @since 3.1
*/
public abstract class CacheAspectSupport extends AbstractCacheInvoker
implements InitializingBean, SmartInitializingSingleton, ApplicationContextAware {
protected final Log logger = LogFactory.getLog(getClass());
/**
* Cache of CacheOperationMetadata, keyed by {@link CacheOperationCacheKey}.
*/
private final Map metadataCache =
new ConcurrentHashMap(1024);
private final ExpressionEvaluator evaluator = new ExpressionEvaluator();
private CacheOperationSource cacheOperationSource;
private KeyGenerator keyGenerator = new SimpleKeyGenerator();
private CacheResolver cacheResolver;
private ApplicationContext applicationContext;
private boolean initialized = false;
/**
* Set one or more cache operation sources which are used to find the cache
* attributes. If more than one source is provided, they will be aggregated
* using a {@link CompositeCacheOperationSource}.
*/
public void setCacheOperationSources(CacheOperationSource... cacheOperationSources) {
Assert.notEmpty(cacheOperationSources, "At least 1 CacheOperationSource needs to be specified");
this.cacheOperationSource = (cacheOperationSources.length > 1 ?
new CompositeCacheOperationSource(cacheOperationSources) : cacheOperationSources[0]);
}
/**
* Return the CacheOperationSource for this cache aspect.
*/
public CacheOperationSource getCacheOperationSource() {
return this.cacheOperationSource;
}
/**
* Set the default {@link KeyGenerator} that this cache aspect should delegate to
* if no specific key generator has been set for the operation.
* The default is a {@link SimpleKeyGenerator}
*/
public void setKeyGenerator(KeyGenerator keyGenerator) {
this.keyGenerator = keyGenerator;
}
/**
* Return the default {@link KeyGenerator} that this cache aspect delegates to.
*/
public KeyGenerator getKeyGenerator() {
return this.keyGenerator;
}
/**
* Set the {@link CacheManager} to use to create a default {@link CacheResolver}.
* Replace the current {@link CacheResolver}, if any.
* @see #setCacheResolver(CacheResolver)
* @see SimpleCacheResolver
*/
public void setCacheManager(CacheManager cacheManager) {
this.cacheResolver = new SimpleCacheResolver(cacheManager);
}
/**
* Set the default {@link CacheResolver} that this cache aspect should delegate
* to if no specific cache resolver has been set for the operation.
*
The default resolver resolves the caches against their names and the
* default cache manager.
* @see #setCacheManager(org.springframework.cache.CacheManager)
* @see SimpleCacheResolver
*/
public void setCacheResolver(CacheResolver cacheResolver) {
Assert.notNull(cacheResolver, "CacheResolver must not be null");
this.cacheResolver = cacheResolver;
}
/**
* Return the default {@link CacheResolver} that this cache aspect delegates to.
*/
public CacheResolver getCacheResolver() {
return this.cacheResolver;
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) {
this.applicationContext = applicationContext;
}
public void afterPropertiesSet() {
Assert.state(getCacheOperationSource() != null, "The 'cacheOperationSources' property is required: " +
"If there are no cacheable methods, then don't use a cache aspect.");
Assert.state(getErrorHandler() != null, "The 'errorHandler' property is required");
}
@Override
public void afterSingletonsInstantiated() {
if (getCacheResolver() == null) {
// Lazily initialize cache resolver via default cache manager...
try {
setCacheManager(this.applicationContext.getBean(CacheManager.class));
}
catch (NoUniqueBeanDefinitionException ex) {
throw new IllegalStateException("No CacheResolver specified, and no unique bean of type " +
"CacheManager found. Mark one as primary (or give it the name 'cacheManager') or " +
"declare a specific CacheManager to use, that serves as the default one.");
}
catch (NoSuchBeanDefinitionException ex) {
throw new IllegalStateException("No CacheResolver specified, and no bean of type CacheManager found. " +
"Register a CacheManager bean or remove the @EnableCaching annotation from your configuration.");
}
}
this.initialized = true;
}
/**
* Convenience method to return a String representation of this Method
* for use in logging. Can be overridden in subclasses to provide a
* different identifier for the given method.
* @param method the method we're interested in
* @param targetClass class the method is on
* @return log message identifying this method
* @see org.springframework.util.ClassUtils#getQualifiedMethodName
*/
protected String methodIdentification(Method method, Class> targetClass) {
Method specificMethod = ClassUtils.getMostSpecificMethod(method, targetClass);
return ClassUtils.getQualifiedMethodName(specificMethod);
}
protected Collection extends Cache> getCaches(
CacheOperationInvocationContext context, CacheResolver cacheResolver) {
Collection extends Cache> caches = cacheResolver.resolveCaches(context);
if (caches.isEmpty()) {
throw new IllegalStateException("No cache could be resolved for '" +
context.getOperation() + "' using resolver '" + cacheResolver +
"'. At least one cache should be provided per cache operation.");
}
return caches;
}
protected CacheOperationContext getOperationContext(
CacheOperation operation, Method method, Object[] args, Object target, Class> targetClass) {
CacheOperationMetadata metadata = getCacheOperationMetadata(operation, method, targetClass);
return new CacheOperationContext(metadata, args, target);
}
/**
* Return the {@link CacheOperationMetadata} for the specified operation.
* Resolve the {@link CacheResolver} and the {@link KeyGenerator} to be
* used for the operation.
* @param operation the operation
* @param method the method on which the operation is invoked
* @param targetClass the target type
* @return the resolved metadata for the operation
*/
protected CacheOperationMetadata getCacheOperationMetadata(
CacheOperation operation, Method method, Class> targetClass) {
CacheOperationCacheKey cacheKey = new CacheOperationCacheKey(operation, method, targetClass);
CacheOperationMetadata metadata = this.metadataCache.get(cacheKey);
if (metadata == null) {
KeyGenerator operationKeyGenerator;
if (StringUtils.hasText(operation.getKeyGenerator())) {
operationKeyGenerator = getBean(operation.getKeyGenerator(), KeyGenerator.class);
}
else {
operationKeyGenerator = getKeyGenerator();
}
CacheResolver operationCacheResolver;
if (StringUtils.hasText(operation.getCacheResolver())) {
operationCacheResolver = getBean(operation.getCacheResolver(), CacheResolver.class);
}
else if (StringUtils.hasText(operation.getCacheManager())) {
CacheManager cacheManager = getBean(operation.getCacheManager(), CacheManager.class);
operationCacheResolver = new SimpleCacheResolver(cacheManager);
}
else {
operationCacheResolver = getCacheResolver();
}
metadata = new CacheOperationMetadata(operation, method, targetClass,
operationKeyGenerator, operationCacheResolver);
this.metadataCache.put(cacheKey, metadata);
}
return metadata;
}
/**
* Return a bean with the specified name and type. Used to resolve services that
* are referenced by name in a {@link CacheOperation}.
* @param beanName the name of the bean, as defined by the operation
* @param expectedType type type for the bean
* @return the bean matching that name
* @throws org.springframework.beans.factory.NoSuchBeanDefinitionException if such bean does not exist
* @see CacheOperation#keyGenerator
* @see CacheOperation#cacheManager
* @see CacheOperation#cacheResolver
*/
protected T getBean(String beanName, Class expectedType) {
return BeanFactoryAnnotationUtils.qualifiedBeanOfType(this.applicationContext, expectedType, beanName);
}
/**
* Clear the cached metadata.
*/
protected void clearMetadataCache() {
this.metadataCache.clear();
this.evaluator.clear();
}
protected Object execute(CacheOperationInvoker invoker, Object target, Method method, Object[] args) {
// check whether aspect is enabled
// to cope with cases where the AJ is pulled in automatically
if (this.initialized) {
Class> targetClass = getTargetClass(target);
Collection operations = getCacheOperationSource().getCacheOperations(method, targetClass);
if (!CollectionUtils.isEmpty(operations)) {
return execute(invoker, new CacheOperationContexts(operations, method, args, target, targetClass));
}
}
return invoker.invoke();
}
/**
* Execute the underlying operation (typically in case of cache miss) and return
* the result of the invocation. If an exception occurs it will be wrapped in
* a {@link CacheOperationInvoker.ThrowableWrapper}: the exception can be handled
* or modified but it must be wrapped in a
* {@link CacheOperationInvoker.ThrowableWrapper} as well.
* @param invoker the invoker handling the operation being cached
* @return the result of the invocation
* @see CacheOperationInvoker#invoke()
*/
protected Object invokeOperation(CacheOperationInvoker invoker) {
return invoker.invoke();
}
private Class> getTargetClass(Object target) {
Class> targetClass = AopProxyUtils.ultimateTargetClass(target);
if (targetClass == null && target != null) {
targetClass = target.getClass();
}
return targetClass;
}
private Object execute(CacheOperationInvoker invoker, CacheOperationContexts contexts) {
// Process any early evictions
processCacheEvicts(contexts.get(CacheEvictOperation.class), true, ExpressionEvaluator.NO_RESULT);
// Check if we have a cached item matching the conditions
Cache.ValueWrapper cacheHit = findCachedItem(contexts.get(CacheableOperation.class));
// Collect puts from any @Cacheable miss, if no cached item is found
List cachePutRequests = new LinkedList();
if (cacheHit == null) {
collectPutRequests(contexts.get(CacheableOperation.class), ExpressionEvaluator.NO_RESULT, cachePutRequests);
}
Cache.ValueWrapper result = null;
// If there are no put requests, just use the cache hit
if (cachePutRequests.isEmpty() && !hasCachePut(contexts)) {
result = cacheHit;
}
// Invoke the method if don't have a cache hit
if (result == null) {
result = new SimpleValueWrapper(invokeOperation(invoker));
}
// Collect any explicit @CachePuts
collectPutRequests(contexts.get(CachePutOperation.class), result.get(), cachePutRequests);
// Process any collected put requests, either from @CachePut or a @Cacheable miss
for (CachePutRequest cachePutRequest : cachePutRequests) {
cachePutRequest.apply(result.get());
}
// Process any late evictions
processCacheEvicts(contexts.get(CacheEvictOperation.class), false, result.get());
return result.get();
}
private boolean hasCachePut(CacheOperationContexts contexts) {
// Evaluate the conditions *without* the result object because we don't have it yet.
Collection cachePutContexts = contexts.get(CachePutOperation.class);
Collection excluded = new ArrayList();
for (CacheOperationContext context : cachePutContexts) {
try {
if (!context.isConditionPassing(ExpressionEvaluator.RESULT_UNAVAILABLE)) {
excluded.add(context);
}
}
catch (VariableNotAvailableException e) {
// Ignoring failure due to missing result, consider the cache put has
// to proceed
}
}
// check if all puts have been excluded by condition
return cachePutContexts.size() != excluded.size();
}
private void processCacheEvicts(Collection contexts, boolean beforeInvocation, Object result) {
for (CacheOperationContext context : contexts) {
CacheEvictOperation operation = (CacheEvictOperation) context.metadata.operation;
if (beforeInvocation == operation.isBeforeInvocation() && isConditionPassing(context, result)) {
performCacheEvict(context, operation, result);
}
}
}
private void performCacheEvict(CacheOperationContext context, CacheEvictOperation operation, Object result) {
Object key = null;
for (Cache cache : context.getCaches()) {
if (operation.isCacheWide()) {
logInvalidating(context, operation, null);
doClear(cache);
}
else {
if (key == null) {
key = context.generateKey(result);
}
logInvalidating(context, operation, key);
doEvict(cache, key);
}
}
}
private void logInvalidating(CacheOperationContext context, CacheEvictOperation operation, Object key) {
if (logger.isTraceEnabled()) {
logger.trace("Invalidating " + (key != null ? "cache key [" + key + "]" : "entire cache") +
" for operation " + operation + " on method " + context.metadata.method);
}
}
/**
* Find a cached item only for {@link CacheableOperation} that passes the condition.
* @param contexts the cacheable operations
* @return a {@link Cache.ValueWrapper} holding the cached item,
* or {@code null} if none is found
*/
private Cache.ValueWrapper findCachedItem(Collection contexts) {
Object result = ExpressionEvaluator.NO_RESULT;
for (CacheOperationContext context : contexts) {
if (isConditionPassing(context, result)) {
Object key = generateKey(context, result);
Cache.ValueWrapper cached = findInCaches(context, key);
if (cached != null) {
return cached;
}
else {
if (logger.isTraceEnabled()) {
logger.trace("No cache entry for key '" + key + "' in cache(s) " + context.getCacheNames());
}
}
}
}
return null;
}
/**
* Collect the {@link CachePutRequest} for all {@link CacheOperation} using
* the specified result item.
* @param contexts the contexts to handle
* @param result the result item (never {@code null})
* @param putRequests the collection to update
*/
private void collectPutRequests(Collection contexts,
Object result, Collection putRequests) {
for (CacheOperationContext context : contexts) {
if (isConditionPassing(context, result)) {
Object key = generateKey(context, result);
putRequests.add(new CachePutRequest(context, key));
}
}
}
private Cache.ValueWrapper findInCaches(CacheOperationContext context, Object key) {
for (Cache cache : context.getCaches()) {
Cache.ValueWrapper wrapper = doGet(cache, key);
if (wrapper != null) {
if (logger.isTraceEnabled()) {
logger.trace("Cache entry for key '" + key + "' found in cache '" + cache.getName() + "'");
}
return wrapper;
}
}
return null;
}
private boolean isConditionPassing(CacheOperationContext context, Object result) {
boolean passing = context.isConditionPassing(result);
if (!passing && logger.isTraceEnabled()) {
logger.trace("Cache condition failed on method " + context.metadata.method +
" for operation " + context.metadata.operation);
}
return passing;
}
private Object generateKey(CacheOperationContext context, Object result) {
Object key = context.generateKey(result);
if (key == null) {
throw new IllegalArgumentException("Null key returned for cache operation (maybe you are " +
"using named params on classes without debug info?) " + context.metadata.operation);
}
if (logger.isTraceEnabled()) {
logger.trace("Computed cache key '" + key + "' for operation " + context.metadata.operation);
}
return key;
}
private class CacheOperationContexts {
private final MultiValueMap, CacheOperationContext> contexts =
new LinkedMultiValueMap, CacheOperationContext>();
public CacheOperationContexts(Collection extends CacheOperation> operations, Method method,
Object[] args, Object target, Class> targetClass) {
for (CacheOperation operation : operations) {
this.contexts.add(operation.getClass(), getOperationContext(operation, method, args, target, targetClass));
}
}
public Collection get(Class extends CacheOperation> operationClass) {
Collection result = this.contexts.get(operationClass);
return (result != null ? result : Collections.emptyList());
}
}
/**
* Metadata of a cache operation that does not depend on a particular invocation
* which makes it a good candidate for caching.
*/
protected static class CacheOperationMetadata {
private final CacheOperation operation;
private final Method method;
private final Class> targetClass;
private final KeyGenerator keyGenerator;
private final CacheResolver cacheResolver;
public CacheOperationMetadata(CacheOperation operation, Method method, Class> targetClass,
KeyGenerator keyGenerator, CacheResolver cacheResolver) {
this.operation = operation;
this.method = method;
this.targetClass = targetClass;
this.keyGenerator = keyGenerator;
this.cacheResolver = cacheResolver;
}
}
protected class CacheOperationContext implements CacheOperationInvocationContext {
private final CacheOperationMetadata metadata;
private final Object[] args;
private final Object target;
private final Collection extends Cache> caches;
private final Collection cacheNames;
private final AnnotatedElementKey methodCacheKey;
public CacheOperationContext(CacheOperationMetadata metadata, Object[] args, Object target) {
this.metadata = metadata;
this.args = extractArgs(metadata.method, args);
this.target = target;
this.caches = CacheAspectSupport.this.getCaches(this, metadata.cacheResolver);
this.cacheNames = createCacheNames(this.caches);
this.methodCacheKey = new AnnotatedElementKey(metadata.method, metadata.targetClass);
}
@Override
public CacheOperation getOperation() {
return this.metadata.operation;
}
@Override
public Object getTarget() {
return this.target;
}
@Override
public Method getMethod() {
return this.metadata.method;
}
@Override
public Object[] getArgs() {
return this.args;
}
private Object[] extractArgs(Method method, Object[] args) {
if (!method.isVarArgs()) {
return args;
}
Object[] varArgs = ObjectUtils.toObjectArray(args[args.length - 1]);
Object[] combinedArgs = new Object[args.length - 1 + varArgs.length];
System.arraycopy(args, 0, combinedArgs, 0, args.length - 1);
System.arraycopy(varArgs, 0, combinedArgs, args.length - 1, varArgs.length);
return combinedArgs;
}
protected boolean isConditionPassing(Object result) {
if (StringUtils.hasText(this.metadata.operation.getCondition())) {
EvaluationContext evaluationContext = createEvaluationContext(result);
return evaluator.condition(this.metadata.operation.getCondition(),
this.methodCacheKey, evaluationContext);
}
return true;
}
protected boolean canPutToCache(Object value) {
String unless = "";
if (this.metadata.operation instanceof CacheableOperation) {
unless = ((CacheableOperation) this.metadata.operation).getUnless();
}
else if (this.metadata.operation instanceof CachePutOperation) {
unless = ((CachePutOperation) this.metadata.operation).getUnless();
}
if (StringUtils.hasText(unless)) {
EvaluationContext evaluationContext = createEvaluationContext(value);
return !evaluator.unless(unless, this.methodCacheKey, evaluationContext);
}
return true;
}
/**
* Compute the key for the given caching operation.
* @return the generated key, or {@code null} if none can be generated
*/
protected Object generateKey(Object result) {
if (StringUtils.hasText(this.metadata.operation.getKey())) {
EvaluationContext evaluationContext = createEvaluationContext(result);
return evaluator.key(this.metadata.operation.getKey(), this.methodCacheKey, evaluationContext);
}
return this.metadata.keyGenerator.generate(this.target, this.metadata.method, this.args);
}
private EvaluationContext createEvaluationContext(Object result) {
return evaluator.createEvaluationContext(
this.caches, this.metadata.method, this.args, this.target, this.metadata.targetClass, result);
}
protected Collection extends Cache> getCaches() {
return this.caches;
}
protected Collection getCacheNames() {
return this.cacheNames;
}
private Collection createCacheNames(Collection extends Cache> caches) {
Collection names = new ArrayList();
for (Cache cache : caches) {
names.add(cache.getName());
}
return names;
}
}
private class CachePutRequest {
private final CacheOperationContext context;
private final Object key;
public CachePutRequest(CacheOperationContext context, Object key) {
this.context = context;
this.key = key;
}
public void apply(Object result) {
if (this.context.canPutToCache(result)) {
for (Cache cache : this.context.getCaches()) {
doPut(cache, this.key, result);
}
}
}
}
private static class CacheOperationCacheKey {
private final CacheOperation cacheOperation;
private final AnnotatedElementKey methodCacheKey;
private CacheOperationCacheKey(CacheOperation cacheOperation, Method method, Class> targetClass) {
this.cacheOperation = cacheOperation;
this.methodCacheKey = new AnnotatedElementKey(method, targetClass);
}
@Override
public boolean equals(Object other) {
if (this == other) {
return true;
}
if (!(other instanceof CacheOperationCacheKey)) {
return false;
}
CacheOperationCacheKey otherKey = (CacheOperationCacheKey) other;
return (this.cacheOperation.equals(otherKey.cacheOperation) &&
this.methodCacheKey.equals(otherKey.methodCacheKey));
}
@Override
public int hashCode() {
return (this.cacheOperation.hashCode() * 31 + this.methodCacheKey.hashCode());
}
}
}