001/*
002 * Copyright 2002-2020 the original author or authors.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *      https://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016
017package org.springframework.cache.interceptor;
018
019import java.lang.reflect.Method;
020import java.lang.reflect.Proxy;
021import java.util.ArrayList;
022import java.util.Collection;
023import java.util.Collections;
024import java.util.LinkedList;
025import java.util.List;
026import java.util.Map;
027import java.util.Optional;
028import java.util.concurrent.ConcurrentHashMap;
029import java.util.function.Supplier;
030
031import org.apache.commons.logging.Log;
032import org.apache.commons.logging.LogFactory;
033
034import org.springframework.aop.framework.AopProxyUtils;
035import org.springframework.aop.support.AopUtils;
036import org.springframework.beans.factory.BeanFactory;
037import org.springframework.beans.factory.BeanFactoryAware;
038import org.springframework.beans.factory.InitializingBean;
039import org.springframework.beans.factory.NoSuchBeanDefinitionException;
040import org.springframework.beans.factory.NoUniqueBeanDefinitionException;
041import org.springframework.beans.factory.SmartInitializingSingleton;
042import org.springframework.beans.factory.annotation.BeanFactoryAnnotationUtils;
043import org.springframework.cache.Cache;
044import org.springframework.cache.CacheManager;
045import org.springframework.context.expression.AnnotatedElementKey;
046import org.springframework.core.BridgeMethodResolver;
047import org.springframework.expression.EvaluationContext;
048import org.springframework.lang.Nullable;
049import org.springframework.util.Assert;
050import org.springframework.util.ClassUtils;
051import org.springframework.util.CollectionUtils;
052import org.springframework.util.LinkedMultiValueMap;
053import org.springframework.util.MultiValueMap;
054import org.springframework.util.ObjectUtils;
055import org.springframework.util.ReflectionUtils;
056import org.springframework.util.StringUtils;
057import org.springframework.util.function.SingletonSupplier;
058import org.springframework.util.function.SupplierUtils;
059
060/**
061 * Base class for caching aspects, such as the {@link CacheInterceptor} or an
062 * AspectJ aspect.
063 *
064 * <p>This enables the underlying Spring caching infrastructure to be used easily
065 * to implement an aspect for any aspect system.
066 *
067 * <p>Subclasses are responsible for calling relevant methods in the correct order.
068 *
069 * <p>Uses the <b>Strategy</b> design pattern. A {@link CacheOperationSource} is
070 * used for determining caching operations, a {@link KeyGenerator} will build the
071 * cache keys, and a {@link CacheResolver} will resolve the actual cache(s) to use.
072 *
073 * <p>Note: A cache aspect is serializable but does not perform any actual caching
074 * after deserialization.
075 *
076 * @author Costin Leau
077 * @author Juergen Hoeller
078 * @author Chris Beams
079 * @author Phillip Webb
080 * @author Sam Brannen
081 * @author Stephane Nicoll
082 * @since 3.1
083 */
084public abstract class CacheAspectSupport extends AbstractCacheInvoker
085                implements BeanFactoryAware, InitializingBean, SmartInitializingSingleton {
086
087        protected final Log logger = LogFactory.getLog(getClass());
088
089        private final Map<CacheOperationCacheKey, CacheOperationMetadata> metadataCache = new ConcurrentHashMap<>(1024);
090
091        private final CacheOperationExpressionEvaluator evaluator = new CacheOperationExpressionEvaluator();
092
093        @Nullable
094        private CacheOperationSource cacheOperationSource;
095
096        private SingletonSupplier<KeyGenerator> keyGenerator = SingletonSupplier.of(SimpleKeyGenerator::new);
097
098        @Nullable
099        private SingletonSupplier<CacheResolver> cacheResolver;
100
101        @Nullable
102        private BeanFactory beanFactory;
103
104        private boolean initialized = false;
105
106
107        /**
108         * Configure this aspect with the given error handler, key generator and cache resolver/manager
109         * suppliers, applying the corresponding default if a supplier is not resolvable.
110         * @since 5.1
111         */
112        public void configure(
113                        @Nullable Supplier<CacheErrorHandler> errorHandler, @Nullable Supplier<KeyGenerator> keyGenerator,
114                        @Nullable Supplier<CacheResolver> cacheResolver, @Nullable Supplier<CacheManager> cacheManager) {
115
116                this.errorHandler = new SingletonSupplier<>(errorHandler, SimpleCacheErrorHandler::new);
117                this.keyGenerator = new SingletonSupplier<>(keyGenerator, SimpleKeyGenerator::new);
118                this.cacheResolver = new SingletonSupplier<>(cacheResolver,
119                                () -> SimpleCacheResolver.of(SupplierUtils.resolve(cacheManager)));
120        }
121
122
123        /**
124         * Set one or more cache operation sources which are used to find the cache
125         * attributes. If more than one source is provided, they will be aggregated
126         * using a {@link CompositeCacheOperationSource}.
127         * @see #setCacheOperationSource
128         */
129        public void setCacheOperationSources(CacheOperationSource... cacheOperationSources) {
130                Assert.notEmpty(cacheOperationSources, "At least 1 CacheOperationSource needs to be specified");
131                this.cacheOperationSource = (cacheOperationSources.length > 1 ?
132                                new CompositeCacheOperationSource(cacheOperationSources) : cacheOperationSources[0]);
133        }
134
135        /**
136         * Set the CacheOperationSource for this cache aspect.
137         * @since 5.1
138         * @see #setCacheOperationSources
139         */
140        public void setCacheOperationSource(@Nullable CacheOperationSource cacheOperationSource) {
141                this.cacheOperationSource = cacheOperationSource;
142        }
143
144        /**
145         * Return the CacheOperationSource for this cache aspect.
146         */
147        @Nullable
148        public CacheOperationSource getCacheOperationSource() {
149                return this.cacheOperationSource;
150        }
151
152        /**
153         * Set the default {@link KeyGenerator} that this cache aspect should delegate to
154         * if no specific key generator has been set for the operation.
155         * <p>The default is a {@link SimpleKeyGenerator}.
156         */
157        public void setKeyGenerator(KeyGenerator keyGenerator) {
158                this.keyGenerator = SingletonSupplier.of(keyGenerator);
159        }
160
161        /**
162         * Return the default {@link KeyGenerator} that this cache aspect delegates to.
163         */
164        public KeyGenerator getKeyGenerator() {
165                return this.keyGenerator.obtain();
166        }
167
168        /**
169         * Set the default {@link CacheResolver} that this cache aspect should delegate
170         * to if no specific cache resolver has been set for the operation.
171         * <p>The default resolver resolves the caches against their names and the
172         * default cache manager.
173         * @see #setCacheManager
174         * @see SimpleCacheResolver
175         */
176        public void setCacheResolver(@Nullable CacheResolver cacheResolver) {
177                this.cacheResolver = SingletonSupplier.ofNullable(cacheResolver);
178        }
179
180        /**
181         * Return the default {@link CacheResolver} that this cache aspect delegates to.
182         */
183        @Nullable
184        public CacheResolver getCacheResolver() {
185                return SupplierUtils.resolve(this.cacheResolver);
186        }
187
188        /**
189         * Set the {@link CacheManager} to use to create a default {@link CacheResolver}.
190         * Replace the current {@link CacheResolver}, if any.
191         * @see #setCacheResolver
192         * @see SimpleCacheResolver
193         */
194        public void setCacheManager(CacheManager cacheManager) {
195                this.cacheResolver = SingletonSupplier.of(new SimpleCacheResolver(cacheManager));
196        }
197
198        /**
199         * Set the containing {@link BeanFactory} for {@link CacheManager} and other
200         * service lookups.
201         * @since 4.3
202         */
203        @Override
204        public void setBeanFactory(BeanFactory beanFactory) {
205                this.beanFactory = beanFactory;
206        }
207
208
209        @Override
210        public void afterPropertiesSet() {
211                Assert.state(getCacheOperationSource() != null, "The 'cacheOperationSources' property is required: " +
212                                "If there are no cacheable methods, then don't use a cache aspect.");
213        }
214
215        @Override
216        public void afterSingletonsInstantiated() {
217                if (getCacheResolver() == null) {
218                        // Lazily initialize cache resolver via default cache manager...
219                        Assert.state(this.beanFactory != null, "CacheResolver or BeanFactory must be set on cache aspect");
220                        try {
221                                setCacheManager(this.beanFactory.getBean(CacheManager.class));
222                        }
223                        catch (NoUniqueBeanDefinitionException ex) {
224                                throw new IllegalStateException("No CacheResolver specified, and no unique bean of type " +
225                                                "CacheManager found. Mark one as primary or declare a specific CacheManager to use.");
226                        }
227                        catch (NoSuchBeanDefinitionException ex) {
228                                throw new IllegalStateException("No CacheResolver specified, and no bean of type CacheManager found. " +
229                                                "Register a CacheManager bean or remove the @EnableCaching annotation from your configuration.");
230                        }
231                }
232                this.initialized = true;
233        }
234
235
236        /**
237         * Convenience method to return a String representation of this Method
238         * for use in logging. Can be overridden in subclasses to provide a
239         * different identifier for the given method.
240         * @param method the method we're interested in
241         * @param targetClass class the method is on
242         * @return log message identifying this method
243         * @see org.springframework.util.ClassUtils#getQualifiedMethodName
244         */
245        protected String methodIdentification(Method method, Class<?> targetClass) {
246                Method specificMethod = ClassUtils.getMostSpecificMethod(method, targetClass);
247                return ClassUtils.getQualifiedMethodName(specificMethod);
248        }
249
250        protected Collection<? extends Cache> getCaches(
251                        CacheOperationInvocationContext<CacheOperation> context, CacheResolver cacheResolver) {
252
253                Collection<? extends Cache> caches = cacheResolver.resolveCaches(context);
254                if (caches.isEmpty()) {
255                        throw new IllegalStateException("No cache could be resolved for '" +
256                                        context.getOperation() + "' using resolver '" + cacheResolver +
257                                        "'. At least one cache should be provided per cache operation.");
258                }
259                return caches;
260        }
261
262        protected CacheOperationContext getOperationContext(
263                        CacheOperation operation, Method method, Object[] args, Object target, Class<?> targetClass) {
264
265                CacheOperationMetadata metadata = getCacheOperationMetadata(operation, method, targetClass);
266                return new CacheOperationContext(metadata, args, target);
267        }
268
269        /**
270         * Return the {@link CacheOperationMetadata} for the specified operation.
271         * <p>Resolve the {@link CacheResolver} and the {@link KeyGenerator} to be
272         * used for the operation.
273         * @param operation the operation
274         * @param method the method on which the operation is invoked
275         * @param targetClass the target type
276         * @return the resolved metadata for the operation
277         */
278        protected CacheOperationMetadata getCacheOperationMetadata(
279                        CacheOperation operation, Method method, Class<?> targetClass) {
280
281                CacheOperationCacheKey cacheKey = new CacheOperationCacheKey(operation, method, targetClass);
282                CacheOperationMetadata metadata = this.metadataCache.get(cacheKey);
283                if (metadata == null) {
284                        KeyGenerator operationKeyGenerator;
285                        if (StringUtils.hasText(operation.getKeyGenerator())) {
286                                operationKeyGenerator = getBean(operation.getKeyGenerator(), KeyGenerator.class);
287                        }
288                        else {
289                                operationKeyGenerator = getKeyGenerator();
290                        }
291                        CacheResolver operationCacheResolver;
292                        if (StringUtils.hasText(operation.getCacheResolver())) {
293                                operationCacheResolver = getBean(operation.getCacheResolver(), CacheResolver.class);
294                        }
295                        else if (StringUtils.hasText(operation.getCacheManager())) {
296                                CacheManager cacheManager = getBean(operation.getCacheManager(), CacheManager.class);
297                                operationCacheResolver = new SimpleCacheResolver(cacheManager);
298                        }
299                        else {
300                                operationCacheResolver = getCacheResolver();
301                                Assert.state(operationCacheResolver != null, "No CacheResolver/CacheManager set");
302                        }
303                        metadata = new CacheOperationMetadata(operation, method, targetClass,
304                                        operationKeyGenerator, operationCacheResolver);
305                        this.metadataCache.put(cacheKey, metadata);
306                }
307                return metadata;
308        }
309
310        /**
311         * Return a bean with the specified name and type. Used to resolve services that
312         * are referenced by name in a {@link CacheOperation}.
313         * @param beanName the name of the bean, as defined by the operation
314         * @param expectedType type for the bean
315         * @return the bean matching that name
316         * @throws org.springframework.beans.factory.NoSuchBeanDefinitionException if such bean does not exist
317         * @see CacheOperation#getKeyGenerator()
318         * @see CacheOperation#getCacheManager()
319         * @see CacheOperation#getCacheResolver()
320         */
321        protected <T> T getBean(String beanName, Class<T> expectedType) {
322                if (this.beanFactory == null) {
323                        throw new IllegalStateException(
324                                        "BeanFactory must be set on cache aspect for " + expectedType.getSimpleName() + " retrieval");
325                }
326                return BeanFactoryAnnotationUtils.qualifiedBeanOfType(this.beanFactory, expectedType, beanName);
327        }
328
329        /**
330         * Clear the cached metadata.
331         */
332        protected void clearMetadataCache() {
333                this.metadataCache.clear();
334                this.evaluator.clear();
335        }
336
337        @Nullable
338        protected Object execute(CacheOperationInvoker invoker, Object target, Method method, Object[] args) {
339                // Check whether aspect is enabled (to cope with cases where the AJ is pulled in automatically)
340                if (this.initialized) {
341                        Class<?> targetClass = getTargetClass(target);
342                        CacheOperationSource cacheOperationSource = getCacheOperationSource();
343                        if (cacheOperationSource != null) {
344                                Collection<CacheOperation> operations = cacheOperationSource.getCacheOperations(method, targetClass);
345                                if (!CollectionUtils.isEmpty(operations)) {
346                                        return execute(invoker, method,
347                                                        new CacheOperationContexts(operations, method, args, target, targetClass));
348                                }
349                        }
350                }
351
352                return invoker.invoke();
353        }
354
355        /**
356         * Execute the underlying operation (typically in case of cache miss) and return
357         * the result of the invocation. If an exception occurs it will be wrapped in a
358         * {@link CacheOperationInvoker.ThrowableWrapper}: the exception can be handled
359         * or modified but it <em>must</em> be wrapped in a
360         * {@link CacheOperationInvoker.ThrowableWrapper} as well.
361         * @param invoker the invoker handling the operation being cached
362         * @return the result of the invocation
363         * @see CacheOperationInvoker#invoke()
364         */
365        protected Object invokeOperation(CacheOperationInvoker invoker) {
366                return invoker.invoke();
367        }
368
369        private Class<?> getTargetClass(Object target) {
370                return AopProxyUtils.ultimateTargetClass(target);
371        }
372
373        @Nullable
374        private Object execute(final CacheOperationInvoker invoker, Method method, CacheOperationContexts contexts) {
375                // Special handling of synchronized invocation
376                if (contexts.isSynchronized()) {
377                        CacheOperationContext context = contexts.get(CacheableOperation.class).iterator().next();
378                        if (isConditionPassing(context, CacheOperationExpressionEvaluator.NO_RESULT)) {
379                                Object key = generateKey(context, CacheOperationExpressionEvaluator.NO_RESULT);
380                                Cache cache = context.getCaches().iterator().next();
381                                try {
382                                        return wrapCacheValue(method, handleSynchronizedGet(invoker, key, cache));
383                                }
384                                catch (Cache.ValueRetrievalException ex) {
385                                        // Directly propagate ThrowableWrapper from the invoker,
386                                        // or potentially also an IllegalArgumentException etc.
387                                        ReflectionUtils.rethrowRuntimeException(ex.getCause());
388                                }
389                        }
390                        else {
391                                // No caching required, only call the underlying method
392                                return invokeOperation(invoker);
393                        }
394                }
395
396
397                // Process any early evictions
398                processCacheEvicts(contexts.get(CacheEvictOperation.class), true,
399                                CacheOperationExpressionEvaluator.NO_RESULT);
400
401                // Check if we have a cached item matching the conditions
402                Cache.ValueWrapper cacheHit = findCachedItem(contexts.get(CacheableOperation.class));
403
404                // Collect puts from any @Cacheable miss, if no cached item is found
405                List<CachePutRequest> cachePutRequests = new LinkedList<>();
406                if (cacheHit == null) {
407                        collectPutRequests(contexts.get(CacheableOperation.class),
408                                        CacheOperationExpressionEvaluator.NO_RESULT, cachePutRequests);
409                }
410
411                Object cacheValue;
412                Object returnValue;
413
414                if (cacheHit != null && !hasCachePut(contexts)) {
415                        // If there are no put requests, just use the cache hit
416                        cacheValue = cacheHit.get();
417                        returnValue = wrapCacheValue(method, cacheValue);
418                }
419                else {
420                        // Invoke the method if we don't have a cache hit
421                        returnValue = invokeOperation(invoker);
422                        cacheValue = unwrapReturnValue(returnValue);
423                }
424
425                // Collect any explicit @CachePuts
426                collectPutRequests(contexts.get(CachePutOperation.class), cacheValue, cachePutRequests);
427
428                // Process any collected put requests, either from @CachePut or a @Cacheable miss
429                for (CachePutRequest cachePutRequest : cachePutRequests) {
430                        cachePutRequest.apply(cacheValue);
431                }
432
433                // Process any late evictions
434                processCacheEvicts(contexts.get(CacheEvictOperation.class), false, cacheValue);
435
436                return returnValue;
437        }
438
439        @Nullable
440        private Object handleSynchronizedGet(CacheOperationInvoker invoker, Object key, Cache cache) {
441                InvocationAwareResult invocationResult = new InvocationAwareResult();
442                Object result = cache.get(key, () -> {
443                        invocationResult.invoked = true;
444                        if (logger.isTraceEnabled()) {
445                                logger.trace("No cache entry for key '" + key + "' in cache " + cache.getName());
446                        }
447                        return unwrapReturnValue(invokeOperation(invoker));
448                });
449                if (!invocationResult.invoked && logger.isTraceEnabled()) {
450                        logger.trace("Cache entry for key '" + key + "' found in cache '" + cache.getName() + "'");
451                }
452                return result;
453        }
454
455        @Nullable
456        private Object wrapCacheValue(Method method, @Nullable Object cacheValue) {
457                if (method.getReturnType() == Optional.class &&
458                                (cacheValue == null || cacheValue.getClass() != Optional.class)) {
459                        return Optional.ofNullable(cacheValue);
460                }
461                return cacheValue;
462        }
463
464        @Nullable
465        private Object unwrapReturnValue(Object returnValue) {
466                return ObjectUtils.unwrapOptional(returnValue);
467        }
468
469        private boolean hasCachePut(CacheOperationContexts contexts) {
470                // Evaluate the conditions *without* the result object because we don't have it yet...
471                Collection<CacheOperationContext> cachePutContexts = contexts.get(CachePutOperation.class);
472                Collection<CacheOperationContext> excluded = new ArrayList<>();
473                for (CacheOperationContext context : cachePutContexts) {
474                        try {
475                                if (!context.isConditionPassing(CacheOperationExpressionEvaluator.RESULT_UNAVAILABLE)) {
476                                        excluded.add(context);
477                                }
478                        }
479                        catch (VariableNotAvailableException ex) {
480                                // Ignoring failure due to missing result, consider the cache put has to proceed
481                        }
482                }
483                // Check if all puts have been excluded by condition
484                return (cachePutContexts.size() != excluded.size());
485        }
486
487        private void processCacheEvicts(
488                        Collection<CacheOperationContext> contexts, boolean beforeInvocation, @Nullable Object result) {
489
490                for (CacheOperationContext context : contexts) {
491                        CacheEvictOperation operation = (CacheEvictOperation) context.metadata.operation;
492                        if (beforeInvocation == operation.isBeforeInvocation() && isConditionPassing(context, result)) {
493                                performCacheEvict(context, operation, result);
494                        }
495                }
496        }
497
498        private void performCacheEvict(
499                        CacheOperationContext context, CacheEvictOperation operation, @Nullable Object result) {
500
501                Object key = null;
502                for (Cache cache : context.getCaches()) {
503                        if (operation.isCacheWide()) {
504                                logInvalidating(context, operation, null);
505                                doClear(cache, operation.isBeforeInvocation());
506                        }
507                        else {
508                                if (key == null) {
509                                        key = generateKey(context, result);
510                                }
511                                logInvalidating(context, operation, key);
512                                doEvict(cache, key, operation.isBeforeInvocation());
513                        }
514                }
515        }
516
517        private void logInvalidating(CacheOperationContext context, CacheEvictOperation operation, @Nullable Object key) {
518                if (logger.isTraceEnabled()) {
519                        logger.trace("Invalidating " + (key != null ? "cache key [" + key + "]" : "entire cache") +
520                                        " for operation " + operation + " on method " + context.metadata.method);
521                }
522        }
523
524        /**
525         * Find a cached item only for {@link CacheableOperation} that passes the condition.
526         * @param contexts the cacheable operations
527         * @return a {@link Cache.ValueWrapper} holding the cached item,
528         * or {@code null} if none is found
529         */
530        @Nullable
531        private Cache.ValueWrapper findCachedItem(Collection<CacheOperationContext> contexts) {
532                Object result = CacheOperationExpressionEvaluator.NO_RESULT;
533                for (CacheOperationContext context : contexts) {
534                        if (isConditionPassing(context, result)) {
535                                Object key = generateKey(context, result);
536                                Cache.ValueWrapper cached = findInCaches(context, key);
537                                if (cached != null) {
538                                        return cached;
539                                }
540                                else {
541                                        if (logger.isTraceEnabled()) {
542                                                logger.trace("No cache entry for key '" + key + "' in cache(s) " + context.getCacheNames());
543                                        }
544                                }
545                        }
546                }
547                return null;
548        }
549
550        /**
551         * Collect the {@link CachePutRequest} for all {@link CacheOperation} using
552         * the specified result item.
553         * @param contexts the contexts to handle
554         * @param result the result item (never {@code null})
555         * @param putRequests the collection to update
556         */
557        private void collectPutRequests(Collection<CacheOperationContext> contexts,
558                        @Nullable Object result, Collection<CachePutRequest> putRequests) {
559
560                for (CacheOperationContext context : contexts) {
561                        if (isConditionPassing(context, result)) {
562                                Object key = generateKey(context, result);
563                                putRequests.add(new CachePutRequest(context, key));
564                        }
565                }
566        }
567
568        @Nullable
569        private Cache.ValueWrapper findInCaches(CacheOperationContext context, Object key) {
570                for (Cache cache : context.getCaches()) {
571                        Cache.ValueWrapper wrapper = doGet(cache, key);
572                        if (wrapper != null) {
573                                if (logger.isTraceEnabled()) {
574                                        logger.trace("Cache entry for key '" + key + "' found in cache '" + cache.getName() + "'");
575                                }
576                                return wrapper;
577                        }
578                }
579                return null;
580        }
581
582        private boolean isConditionPassing(CacheOperationContext context, @Nullable Object result) {
583                boolean passing = context.isConditionPassing(result);
584                if (!passing && logger.isTraceEnabled()) {
585                        logger.trace("Cache condition failed on method " + context.metadata.method +
586                                        " for operation " + context.metadata.operation);
587                }
588                return passing;
589        }
590
591        private Object generateKey(CacheOperationContext context, @Nullable Object result) {
592                Object key = context.generateKey(result);
593                if (key == null) {
594                        throw new IllegalArgumentException("Null key returned for cache operation (maybe you are " +
595                                        "using named params on classes without debug info?) " + context.metadata.operation);
596                }
597                if (logger.isTraceEnabled()) {
598                        logger.trace("Computed cache key '" + key + "' for operation " + context.metadata.operation);
599                }
600                return key;
601        }
602
603
604        private class CacheOperationContexts {
605
606                private final MultiValueMap<Class<? extends CacheOperation>, CacheOperationContext> contexts;
607
608                private final boolean sync;
609
610                public CacheOperationContexts(Collection<? extends CacheOperation> operations, Method method,
611                                Object[] args, Object target, Class<?> targetClass) {
612
613                        this.contexts = new LinkedMultiValueMap<>(operations.size());
614                        for (CacheOperation op : operations) {
615                                this.contexts.add(op.getClass(), getOperationContext(op, method, args, target, targetClass));
616                        }
617                        this.sync = determineSyncFlag(method);
618                }
619
620                public Collection<CacheOperationContext> get(Class<? extends CacheOperation> operationClass) {
621                        Collection<CacheOperationContext> result = this.contexts.get(operationClass);
622                        return (result != null ? result : Collections.emptyList());
623                }
624
625                public boolean isSynchronized() {
626                        return this.sync;
627                }
628
629                private boolean determineSyncFlag(Method method) {
630                        List<CacheOperationContext> cacheOperationContexts = this.contexts.get(CacheableOperation.class);
631                        if (cacheOperationContexts == null) {  // no @Cacheable operation at all
632                                return false;
633                        }
634                        boolean syncEnabled = false;
635                        for (CacheOperationContext cacheOperationContext : cacheOperationContexts) {
636                                if (((CacheableOperation) cacheOperationContext.getOperation()).isSync()) {
637                                        syncEnabled = true;
638                                        break;
639                                }
640                        }
641                        if (syncEnabled) {
642                                if (this.contexts.size() > 1) {
643                                        throw new IllegalStateException(
644                                                        "@Cacheable(sync=true) cannot be combined with other cache operations on '" + method + "'");
645                                }
646                                if (cacheOperationContexts.size() > 1) {
647                                        throw new IllegalStateException(
648                                                        "Only one @Cacheable(sync=true) entry is allowed on '" + method + "'");
649                                }
650                                CacheOperationContext cacheOperationContext = cacheOperationContexts.iterator().next();
651                                CacheableOperation operation = (CacheableOperation) cacheOperationContext.getOperation();
652                                if (cacheOperationContext.getCaches().size() > 1) {
653                                        throw new IllegalStateException(
654                                                        "@Cacheable(sync=true) only allows a single cache on '" + operation + "'");
655                                }
656                                if (StringUtils.hasText(operation.getUnless())) {
657                                        throw new IllegalStateException(
658                                                        "@Cacheable(sync=true) does not support unless attribute on '" + operation + "'");
659                                }
660                                return true;
661                        }
662                        return false;
663                }
664        }
665
666
667        /**
668         * Metadata of a cache operation that does not depend on a particular invocation
669         * which makes it a good candidate for caching.
670         */
671        protected static class CacheOperationMetadata {
672
673                private final CacheOperation operation;
674
675                private final Method method;
676
677                private final Class<?> targetClass;
678
679                private final Method targetMethod;
680
681                private final AnnotatedElementKey methodKey;
682
683                private final KeyGenerator keyGenerator;
684
685                private final CacheResolver cacheResolver;
686
687                public CacheOperationMetadata(CacheOperation operation, Method method, Class<?> targetClass,
688                                KeyGenerator keyGenerator, CacheResolver cacheResolver) {
689
690                        this.operation = operation;
691                        this.method = BridgeMethodResolver.findBridgedMethod(method);
692                        this.targetClass = targetClass;
693                        this.targetMethod = (!Proxy.isProxyClass(targetClass) ?
694                                        AopUtils.getMostSpecificMethod(method, targetClass) : this.method);
695                        this.methodKey = new AnnotatedElementKey(this.targetMethod, targetClass);
696                        this.keyGenerator = keyGenerator;
697                        this.cacheResolver = cacheResolver;
698                }
699        }
700
701
702        /**
703         * A {@link CacheOperationInvocationContext} context for a {@link CacheOperation}.
704         */
705        protected class CacheOperationContext implements CacheOperationInvocationContext<CacheOperation> {
706
707                private final CacheOperationMetadata metadata;
708
709                private final Object[] args;
710
711                private final Object target;
712
713                private final Collection<? extends Cache> caches;
714
715                private final Collection<String> cacheNames;
716
717                @Nullable
718                private Boolean conditionPassing;
719
720                public CacheOperationContext(CacheOperationMetadata metadata, Object[] args, Object target) {
721                        this.metadata = metadata;
722                        this.args = extractArgs(metadata.method, args);
723                        this.target = target;
724                        this.caches = CacheAspectSupport.this.getCaches(this, metadata.cacheResolver);
725                        this.cacheNames = createCacheNames(this.caches);
726                }
727
728                @Override
729                public CacheOperation getOperation() {
730                        return this.metadata.operation;
731                }
732
733                @Override
734                public Object getTarget() {
735                        return this.target;
736                }
737
738                @Override
739                public Method getMethod() {
740                        return this.metadata.method;
741                }
742
743                @Override
744                public Object[] getArgs() {
745                        return this.args;
746                }
747
748                private Object[] extractArgs(Method method, Object[] args) {
749                        if (!method.isVarArgs()) {
750                                return args;
751                        }
752                        Object[] varArgs = ObjectUtils.toObjectArray(args[args.length - 1]);
753                        Object[] combinedArgs = new Object[args.length - 1 + varArgs.length];
754                        System.arraycopy(args, 0, combinedArgs, 0, args.length - 1);
755                        System.arraycopy(varArgs, 0, combinedArgs, args.length - 1, varArgs.length);
756                        return combinedArgs;
757                }
758
759                protected boolean isConditionPassing(@Nullable Object result) {
760                        if (this.conditionPassing == null) {
761                                if (StringUtils.hasText(this.metadata.operation.getCondition())) {
762                                        EvaluationContext evaluationContext = createEvaluationContext(result);
763                                        this.conditionPassing = evaluator.condition(this.metadata.operation.getCondition(),
764                                                        this.metadata.methodKey, evaluationContext);
765                                }
766                                else {
767                                        this.conditionPassing = true;
768                                }
769                        }
770                        return this.conditionPassing;
771                }
772
773                protected boolean canPutToCache(@Nullable Object value) {
774                        String unless = "";
775                        if (this.metadata.operation instanceof CacheableOperation) {
776                                unless = ((CacheableOperation) this.metadata.operation).getUnless();
777                        }
778                        else if (this.metadata.operation instanceof CachePutOperation) {
779                                unless = ((CachePutOperation) this.metadata.operation).getUnless();
780                        }
781                        if (StringUtils.hasText(unless)) {
782                                EvaluationContext evaluationContext = createEvaluationContext(value);
783                                return !evaluator.unless(unless, this.metadata.methodKey, evaluationContext);
784                        }
785                        return true;
786                }
787
788                /**
789                 * Compute the key for the given caching operation.
790                 */
791                @Nullable
792                protected Object generateKey(@Nullable Object result) {
793                        if (StringUtils.hasText(this.metadata.operation.getKey())) {
794                                EvaluationContext evaluationContext = createEvaluationContext(result);
795                                return evaluator.key(this.metadata.operation.getKey(), this.metadata.methodKey, evaluationContext);
796                        }
797                        return this.metadata.keyGenerator.generate(this.target, this.metadata.method, this.args);
798                }
799
800                private EvaluationContext createEvaluationContext(@Nullable Object result) {
801                        return evaluator.createEvaluationContext(this.caches, this.metadata.method, this.args,
802                                        this.target, this.metadata.targetClass, this.metadata.targetMethod, result, beanFactory);
803                }
804
805                protected Collection<? extends Cache> getCaches() {
806                        return this.caches;
807                }
808
809                protected Collection<String> getCacheNames() {
810                        return this.cacheNames;
811                }
812
813                private Collection<String> createCacheNames(Collection<? extends Cache> caches) {
814                        Collection<String> names = new ArrayList<>();
815                        for (Cache cache : caches) {
816                                names.add(cache.getName());
817                        }
818                        return names;
819                }
820        }
821
822
823        private class CachePutRequest {
824
825                private final CacheOperationContext context;
826
827                private final Object key;
828
829                public CachePutRequest(CacheOperationContext context, Object key) {
830                        this.context = context;
831                        this.key = key;
832                }
833
834                public void apply(@Nullable Object result) {
835                        if (this.context.canPutToCache(result)) {
836                                for (Cache cache : this.context.getCaches()) {
837                                        doPut(cache, this.key, result);
838                                }
839                        }
840                }
841        }
842
843
844        private static final class CacheOperationCacheKey implements Comparable<CacheOperationCacheKey> {
845
846                private final CacheOperation cacheOperation;
847
848                private final AnnotatedElementKey methodCacheKey;
849
850                private CacheOperationCacheKey(CacheOperation cacheOperation, Method method, Class<?> targetClass) {
851                        this.cacheOperation = cacheOperation;
852                        this.methodCacheKey = new AnnotatedElementKey(method, targetClass);
853                }
854
855                @Override
856                public boolean equals(@Nullable Object other) {
857                        if (this == other) {
858                                return true;
859                        }
860                        if (!(other instanceof CacheOperationCacheKey)) {
861                                return false;
862                        }
863                        CacheOperationCacheKey otherKey = (CacheOperationCacheKey) other;
864                        return (this.cacheOperation.equals(otherKey.cacheOperation) &&
865                                        this.methodCacheKey.equals(otherKey.methodCacheKey));
866                }
867
868                @Override
869                public int hashCode() {
870                        return (this.cacheOperation.hashCode() * 31 + this.methodCacheKey.hashCode());
871                }
872
873                @Override
874                public String toString() {
875                        return this.cacheOperation + " on " + this.methodCacheKey;
876                }
877
878                @Override
879                public int compareTo(CacheOperationCacheKey other) {
880                        int result = this.cacheOperation.getName().compareTo(other.cacheOperation.getName());
881                        if (result == 0) {
882                                result = this.methodCacheKey.compareTo(other.methodCacheKey);
883                        }
884                        return result;
885                }
886        }
887
888        /**
889         * Internal holder class for recording that a cache method was invoked.
890         */
891        private static class InvocationAwareResult {
892
893                boolean invoked;
894
895        }
896
897}