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 }