1 /*
2  * Copyright 2002-2011 the original author or authors.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package org.springframework.cache.interceptor;
18 
19 import java.lang.reflect.Method;
20 import java.util.ArrayList;
21 import java.util.Collection;
22 import java.util.LinkedHashMap;
23 import java.util.Map;
24 import java.util.Set;
25 
26 import org.apache.commons.logging.Log;
27 import org.apache.commons.logging.LogFactory;
28 import org.springframework.aop.framework.AopProxyUtils;
29 import org.springframework.beans.factory.InitializingBean;
30 import org.springframework.cache.Cache;
31 import org.springframework.cache.CacheManager;
32 import org.springframework.expression.EvaluationContext;
33 import org.springframework.util.Assert;
34 import org.springframework.util.ClassUtils;
35 import org.springframework.util.CollectionUtils;
36 import org.springframework.util.StringUtils;
37 
38 /**
39  * Base class for caching aspects, such as the {@link CacheInterceptor}
40  * or an AspectJ aspect.
41  *
42  * <p>This enables the underlying Spring caching infrastructure to be
43  * used easily to implement an aspect for any aspect system.
44  *
45  * <p>Subclasses are responsible for calling methods in this class in
46  * the correct order.
47  *
48  * <p>Uses the <b>Strategy</b> design pattern. A {@link CacheManager}
49  * implementation will perform the actual cache management, and a
50  * {@link CacheOperationSource} is used for determining caching
51  * operations.
52  *
53  * <p>A cache aspect is serializable if its {@code CacheManager} and
54  * {@code CacheOperationSource} are serializable.
55  *
56  * @author Costin Leau
57  * @author Juergen Hoeller
58  * @author Chris Beams
59  * @since 3.1
60  */
61 public abstract class CacheAspectSupport implements InitializingBean {
62 
63 	public interface Invoker {
invoke()64 		Object invoke();
65 	}
66 
67 	protected final Log logger = LogFactory.getLog(getClass());
68 
69 	private CacheManager cacheManager;
70 
71 	private CacheOperationSource cacheOperationSource;
72 
73 	private final ExpressionEvaluator evaluator = new ExpressionEvaluator();
74 
75 	private KeyGenerator keyGenerator = new DefaultKeyGenerator();
76 
77 	private boolean initialized = false;
78 
79 	private static final String CACHEABLE = "cacheable", UPDATE = "cacheupdate", EVICT = "cacheevict";
80 
81 	/**
82 	 * Set the CacheManager that this cache aspect should delegate to.
83 	 */
setCacheManager(CacheManager cacheManager)84 	public void setCacheManager(CacheManager cacheManager) {
85 		this.cacheManager = cacheManager;
86 	}
87 
88 	/**
89 	 * Return the CacheManager that this cache aspect delegates to.
90 	 */
getCacheManager()91 	public CacheManager getCacheManager() {
92 		return this.cacheManager;
93 	}
94 
95 	/**
96 	 * Set one or more cache operation sources which are used to find the cache
97 	 * attributes. If more than one source is provided, they will be aggregated using a
98 	 * {@link CompositeCacheOperationSource}.
99 	 * @param cacheOperationSources must not be {@code null}
100 	 */
setCacheOperationSources(CacheOperationSource... cacheOperationSources)101 	public void setCacheOperationSources(CacheOperationSource... cacheOperationSources) {
102 		Assert.notEmpty(cacheOperationSources);
103 		this.cacheOperationSource =
104 				(cacheOperationSources.length > 1 ?
105 						new CompositeCacheOperationSource(cacheOperationSources) :
106 						cacheOperationSources[0]);
107 	}
108 
109 	/**
110 	 * Return the CacheOperationSource for this cache aspect.
111 	 */
getCacheOperationSource()112 	public CacheOperationSource getCacheOperationSource() {
113 		return this.cacheOperationSource;
114 	}
115 
116 	/**
117 	 * Set the KeyGenerator for this cache aspect.
118 	 * Default is {@link DefaultKeyGenerator}.
119 	 */
setKeyGenerator(KeyGenerator keyGenerator)120 	public void setKeyGenerator(KeyGenerator keyGenerator) {
121 		this.keyGenerator = keyGenerator;
122 	}
123 
124 	/**
125 	 * Return the KeyGenerator for this cache aspect,
126 	 */
getKeyGenerator()127 	public KeyGenerator getKeyGenerator() {
128 		return this.keyGenerator;
129 	}
130 
afterPropertiesSet()131 	public void afterPropertiesSet() {
132 		if (this.cacheManager == null) {
133 			throw new IllegalStateException("'cacheManager' is required");
134 		}
135 		if (this.cacheOperationSource == null) {
136 			throw new IllegalStateException("The 'cacheOperationSources' property is required: "
137 					+ "If there are no cacheable methods, then don't use a cache aspect.");
138 		}
139 
140 		this.initialized = true;
141 	}
142 
143 	/**
144 	 * Convenience method to return a String representation of this Method
145 	 * for use in logging. Can be overridden in subclasses to provide a
146 	 * different identifier for the given method.
147 	 * @param method the method we're interested in
148 	 * @param targetClass class the method is on
149 	 * @return log message identifying this method
150 	 * @see org.springframework.util.ClassUtils#getQualifiedMethodName
151 	 */
methodIdentification(Method method, Class<?> targetClass)152 	protected String methodIdentification(Method method, Class<?> targetClass) {
153 		Method specificMethod = ClassUtils.getMostSpecificMethod(method, targetClass);
154 		return ClassUtils.getQualifiedMethodName(specificMethod);
155 	}
156 
getCaches(CacheOperation operation)157 	protected Collection<Cache> getCaches(CacheOperation operation) {
158 		Set<String> cacheNames = operation.getCacheNames();
159 		Collection<Cache> caches = new ArrayList<Cache>(cacheNames.size());
160 		for (String cacheName : cacheNames) {
161 			Cache cache = this.cacheManager.getCache(cacheName);
162 			if (cache == null) {
163 				throw new IllegalArgumentException("Cannot find cache named [" + cacheName + "] for " + operation);
164 			}
165 			caches.add(cache);
166 		}
167 		return caches;
168 	}
169 
getOperationContext(CacheOperation operation, Method method, Object[] args, Object target, Class<?> targetClass)170 	protected CacheOperationContext getOperationContext(CacheOperation operation, Method method, Object[] args,
171 			Object target, Class<?> targetClass) {
172 
173 		return new CacheOperationContext(operation, method, args, target, targetClass);
174 	}
175 
execute(Invoker invoker, Object target, Method method, Object[] args)176 	protected Object execute(Invoker invoker, Object target, Method method, Object[] args) {
177 		// check whether aspect is enabled
178 		// to cope with cases where the AJ is pulled in automatically
179 		if (!this.initialized) {
180 			return invoker.invoke();
181 		}
182 
183 		// get backing class
184 		Class<?> targetClass = AopProxyUtils.ultimateTargetClass(target);
185 		if (targetClass == null && target != null) {
186 			targetClass = target.getClass();
187 		}
188 		final Collection<CacheOperation> cacheOp = getCacheOperationSource().getCacheOperations(method, targetClass);
189 
190 		// analyze caching information
191 		if (!CollectionUtils.isEmpty(cacheOp)) {
192 			Map<String, Collection<CacheOperationContext>> ops = createOperationContext(cacheOp, method, args, target, targetClass);
193 
194 			// start with evictions
195 			inspectBeforeCacheEvicts(ops.get(EVICT));
196 
197 			// follow up with cacheable
198 			CacheStatus status = inspectCacheables(ops.get(CACHEABLE));
199 
200 			Object retVal = null;
201 			Map<CacheOperationContext, Object> updates = inspectCacheUpdates(ops.get(UPDATE));
202 
203 			if (status != null) {
204 				if (status.updateRequired) {
205 					updates.putAll(status.cUpdates);
206 				}
207 				// return cached object
208 				else {
209 					return status.retVal;
210 				}
211 			}
212 
213 			retVal = invoker.invoke();
214 
215 			inspectAfterCacheEvicts(ops.get(EVICT));
216 
217 			if (!updates.isEmpty()) {
218 				update(updates, retVal);
219 			}
220 
221 			return retVal;
222 		}
223 
224 		return invoker.invoke();
225 	}
226 
inspectBeforeCacheEvicts(Collection<CacheOperationContext> evictions)227 	private void inspectBeforeCacheEvicts(Collection<CacheOperationContext> evictions) {
228 		inspectCacheEvicts(evictions, true);
229 	}
230 
inspectAfterCacheEvicts(Collection<CacheOperationContext> evictions)231 	private void inspectAfterCacheEvicts(Collection<CacheOperationContext> evictions) {
232 		inspectCacheEvicts(evictions, false);
233 	}
234 
inspectCacheEvicts(Collection<CacheOperationContext> evictions, boolean beforeInvocation)235 	private void inspectCacheEvicts(Collection<CacheOperationContext> evictions, boolean beforeInvocation) {
236 
237 		if (!evictions.isEmpty()) {
238 
239 			boolean log = logger.isTraceEnabled();
240 
241 			for (CacheOperationContext context : evictions) {
242 				CacheEvictOperation evictOp = (CacheEvictOperation) context.operation;
243 
244 				if (beforeInvocation == evictOp.isBeforeInvocation()) {
245 					if (context.isConditionPassing()) {
246 						// for each cache
247 						// lazy key initialization
248 						Object key = null;
249 
250 						for (Cache cache : context.getCaches()) {
251 							// cache-wide flush
252 							if (evictOp.isCacheWide()) {
253 								cache.clear();
254 								if (log) {
255 									logger.trace("Invalidating entire cache for operation " + evictOp + " on method " + context.method);
256 								}
257 							} else {
258 								// check key
259 								if (key == null) {
260 									key = context.generateKey();
261 								}
262 								if (log) {
263 									logger.trace("Invalidating cache key " + key + " for operation " + evictOp + " on method " + context.method);
264 								}
265 								cache.evict(key);
266 							}
267 						}
268 					} else {
269 						if (log) {
270 							logger.trace("Cache condition failed on method " + context.method + " for operation " + context.operation);
271 						}
272 					}
273 				}
274 			}
275 		}
276 	}
277 
inspectCacheables(Collection<CacheOperationContext> cacheables)278 	private CacheStatus inspectCacheables(Collection<CacheOperationContext> cacheables) {
279 		Map<CacheOperationContext, Object> cUpdates = new LinkedHashMap<CacheOperationContext, Object>(cacheables.size());
280 
281 		boolean updateRequire = false;
282 		Object retVal = null;
283 
284 		if (!cacheables.isEmpty()) {
285 			boolean log = logger.isTraceEnabled();
286 			boolean atLeastOnePassed = false;
287 
288 			for (CacheOperationContext context : cacheables) {
289 				if (context.isConditionPassing()) {
290 					atLeastOnePassed = true;
291 					Object key = context.generateKey();
292 
293 					if (log) {
294 						logger.trace("Computed cache key " + key + " for operation " + context.operation);
295 					}
296 					if (key == null) {
297 						throw new IllegalArgumentException(
298 								"Null key returned for cache operation (maybe you are using named params on classes without debug info?) "
299 										+ context.operation);
300 					}
301 
302 					// add op/key (in case an update is discovered later on)
303 					cUpdates.put(context, key);
304 
305 					boolean localCacheHit = false;
306 
307 					// check whether the cache needs to be inspected or not (the method will be invoked anyway)
308 					if (!updateRequire) {
309 						for (Cache cache : context.getCaches()) {
310 							Cache.ValueWrapper wrapper = cache.get(key);
311 							if (wrapper != null) {
312 								retVal = wrapper.get();
313 								localCacheHit = true;
314 								break;
315 							}
316 						}
317 					}
318 
319 					if (!localCacheHit) {
320 						updateRequire = true;
321 					}
322 				}
323 				else {
324 					if (log) {
325 						logger.trace("Cache condition failed on method " + context.method + " for operation " + context.operation);
326 					}
327 				}
328 			}
329 
330 			// return a status only if at least on cacheable matched
331 			if (atLeastOnePassed) {
332 				return new CacheStatus(cUpdates, updateRequire, retVal);
333 			}
334 		}
335 
336 		return null;
337 	}
338 
339 	private static class CacheStatus {
340 		// caches/key
341 		final Map<CacheOperationContext, Object> cUpdates;
342 		final boolean updateRequired;
343 		final Object retVal;
344 
CacheStatus(Map<CacheOperationContext, Object> cUpdates, boolean updateRequired, Object retVal)345 		CacheStatus(Map<CacheOperationContext, Object> cUpdates, boolean updateRequired, Object retVal) {
346 			this.cUpdates = cUpdates;
347 			this.updateRequired = updateRequired;
348 			this.retVal = retVal;
349 		}
350 	}
351 
inspectCacheUpdates(Collection<CacheOperationContext> updates)352 	private Map<CacheOperationContext, Object> inspectCacheUpdates(Collection<CacheOperationContext> updates) {
353 
354 		Map<CacheOperationContext, Object> cUpdates = new LinkedHashMap<CacheOperationContext, Object>(updates.size());
355 
356 		if (!updates.isEmpty()) {
357 			boolean log = logger.isTraceEnabled();
358 
359 			for (CacheOperationContext context : updates) {
360 				if (context.isConditionPassing()) {
361 
362 					Object key = context.generateKey();
363 
364 					if (log) {
365 						logger.trace("Computed cache key " + key + " for operation " + context.operation);
366 					}
367 					if (key == null) {
368 						throw new IllegalArgumentException(
369 								"Null key returned for cache operation (maybe you are using named params on classes without debug info?) "
370 										+ context.operation);
371 					}
372 
373 					// add op/key (in case an update is discovered later on)
374 					cUpdates.put(context, key);
375 				}
376 				else {
377 					if (log) {
378 						logger.trace("Cache condition failed on method " + context.method + " for operation " + context.operation);
379 					}
380 				}
381 			}
382 		}
383 
384 		return cUpdates;
385 	}
386 
update(Map<CacheOperationContext, Object> updates, Object retVal)387 	private void update(Map<CacheOperationContext, Object> updates, Object retVal) {
388 		for (Map.Entry<CacheOperationContext, Object> entry : updates.entrySet()) {
389 			for (Cache cache : entry.getKey().getCaches()) {
390 				cache.put(entry.getValue(), retVal);
391 			}
392 		}
393 	}
394 
createOperationContext(Collection<CacheOperation> cacheOp, Method method, Object[] args, Object target, Class<?> targetClass)395 	private Map<String, Collection<CacheOperationContext>> createOperationContext(Collection<CacheOperation> cacheOp,
396 			Method method, Object[] args, Object target, Class<?> targetClass) {
397 		Map<String, Collection<CacheOperationContext>> map = new LinkedHashMap<String, Collection<CacheOperationContext>>(3);
398 
399 		Collection<CacheOperationContext> cacheables = new ArrayList<CacheOperationContext>();
400 		Collection<CacheOperationContext> evicts = new ArrayList<CacheOperationContext>();
401 		Collection<CacheOperationContext> updates = new ArrayList<CacheOperationContext>();
402 
403 		for (CacheOperation cacheOperation : cacheOp) {
404 			CacheOperationContext opContext = getOperationContext(cacheOperation, method, args, target, targetClass);
405 
406 			if (cacheOperation instanceof CacheableOperation) {
407 				cacheables.add(opContext);
408 			}
409 
410 			if (cacheOperation instanceof CacheEvictOperation) {
411 				evicts.add(opContext);
412 			}
413 
414 			if (cacheOperation instanceof CachePutOperation) {
415 				updates.add(opContext);
416 			}
417 		}
418 
419 		map.put(CACHEABLE, cacheables);
420 		map.put(EVICT, evicts);
421 		map.put(UPDATE, updates);
422 
423 		return map;
424 	}
425 
426 	protected class CacheOperationContext {
427 
428 		private final CacheOperation operation;
429 
430 		private final Collection<Cache> caches;
431 
432 		private final Object target;
433 
434 		private final Method method;
435 
436 		private final Object[] args;
437 
438 		// context passed around to avoid multiple creations
439 		private final EvaluationContext evalContext;
440 
CacheOperationContext(CacheOperation operation, Method method, Object[] args, Object target, Class<?> targetClass)441 		public CacheOperationContext(CacheOperation operation, Method method, Object[] args, Object target, Class<?> targetClass) {
442 			this.operation = operation;
443 			this.caches = CacheAspectSupport.this.getCaches(operation);
444 			this.target = target;
445 			this.method = method;
446 			this.args = args;
447 
448 			this.evalContext = evaluator.createEvaluationContext(caches, method, args, target, targetClass);
449 		}
450 
isConditionPassing()451 		protected boolean isConditionPassing() {
452 			if (StringUtils.hasText(this.operation.getCondition())) {
453 				return evaluator.condition(this.operation.getCondition(), this.method, this.evalContext);
454 			}
455 			return true;
456 		}
457 
458 		/**
459 		 * Computes the key for the given caching operation.
460 		 * @return generated key (null if none can be generated)
461 		 */
generateKey()462 		protected Object generateKey() {
463 			if (StringUtils.hasText(this.operation.getKey())) {
464 				return evaluator.key(this.operation.getKey(), this.method, this.evalContext);
465 			}
466 			return keyGenerator.generate(this.target, this.method, this.args);
467 		}
468 
getCaches()469 		protected Collection<Cache> getCaches() {
470 			return this.caches;
471 		}
472 	}
473 }