All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.springframework.cache.interceptor.CacheAspectSupport Maven / Gradle / Ivy

/*
 * 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 getCaches( CacheOperationInvocationContext context, CacheResolver cacheResolver) { Collection 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 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 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 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 getCaches() { return this.caches; } protected Collection getCacheNames() { return this.cacheNames; } private Collection createCacheNames(Collection 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()); } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy