Skip to content

Commit

Permalink
GPCACHE-8 implemented reloading for annotated services and controllers
Browse files Browse the repository at this point in the history
  • Loading branch information
Burt Beckwith committed May 25, 2012
1 parent 6e503e4 commit 45abf4b
Show file tree
Hide file tree
Showing 3 changed files with 225 additions and 15 deletions.
17 changes: 9 additions & 8 deletions CacheGrailsPlugin.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import grails.plugin.cache.CacheBeanPostProcessor
import grails.plugin.cache.CacheConfigArtefactHandler
import grails.plugin.cache.ConfigLoader
import grails.plugin.cache.GrailsAnnotationCacheOperationSource
import grails.plugin.cache.GrailsConcurrentMapCacheManager
import grails.plugin.cache.web.ProxyAwareMixedGrailsControllerHelper
import grails.plugin.cache.web.filter.DefaultWebKeyGenerator
Expand Down Expand Up @@ -138,7 +139,7 @@ class CacheGrailsPlugin {
grailsCacheFilter(MemoryPageFragmentCachingFilter) {
cacheManager = ref('grailsCacheManager')
// TODO this name might be brittle - perhaps do by type?
cacheOperationSource = ref('org.springframework.cache.annotation.AnnotationCacheOperationSource#0')
cacheOperationSource = ref(GrailsAnnotationCacheOperationSource.BEAN_NAME)
keyGenerator = ref('webCacheKeyGenerator')
expressionEvaluator = ref('webExpressionEvaluator')
}
Expand All @@ -162,15 +163,14 @@ class CacheGrailsPlugin {
return
}

if (event.application.isControllerClass(event.source)) {
// TODO reload CacheOperation config based on updated annotations
}
if (event.application.isControllerClass(event.source) ||
event.application.isServiceClass(event.source)) {

if (event.application.isServiceClass(event.source)) {
// TODO reload CacheOperation config based on updated annotations
def annotationCacheOperationSource = event.ctx.getBean(GrailsAnnotationCacheOperationSource.BEAN_NAME)
annotationCacheOperationSource.reset()
log.debug 'Reset GrailsAnnotationCacheOperationSource cache'
}

if (event.application.isCacheConfigClass(event.source)) {
else if (event.application.isCacheConfigClass(event.source)) {
reloadCaches event.ctx
}
}
Expand All @@ -181,6 +181,7 @@ class CacheGrailsPlugin {

private void reloadCaches(ctx) {
ctx.grailsCacheConfigLoader.reload ctx
log.debug 'Reloaded grailsCacheConfigLoader'
}

private boolean isEnabled(GrailsApplication application) {
Expand Down
2 changes: 1 addition & 1 deletion src/java/grails/plugin/cache/CacheBeanPostProcessor.java
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) {
log.info("postProcessBeanDefinitionRegistry start");

AbstractBeanDefinition beanDef = (AbstractBeanDefinition)registry.getBeanDefinition(
"org.springframework.cache.annotation.AnnotationCacheOperationSource#0");
GrailsAnnotationCacheOperationSource.BEAN_NAME);

// change the class to the plugin's subclass
beanDef.setBeanClass(GrailsAnnotationCacheOperationSource.class);
Expand Down
221 changes: 215 additions & 6 deletions src/java/grails/plugin/cache/GrailsAnnotationCacheOperationSource.java
Original file line number Diff line number Diff line change
Expand Up @@ -14,27 +14,72 @@
*/
package grails.plugin.cache;

import java.io.Serializable;
import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;

import org.codehaus.groovy.grails.commons.ControllerArtefactHandler;
import org.codehaus.groovy.grails.commons.GrailsApplication;
import org.springframework.cache.annotation.AnnotationCacheOperationSource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cache.annotation.CacheAnnotationParser;
import org.springframework.cache.annotation.SpringCacheAnnotationParser;
import org.springframework.cache.interceptor.CacheOperation;
import org.springframework.cache.interceptor.CacheOperationSource;
import org.springframework.core.BridgeMethodResolver;
import org.springframework.util.ClassUtils;
import org.springframework.util.ObjectUtils;

/**
* getCacheOperations is called when beans are initialized and also from
* PageFragmentCachingFilter during requests; the filter needs annotations on
* controllers but if the standard lookup includes controllers, the return
* values from the controller method calls are cached unnecessarily.
*
* Based on org.springframework.cache.annotation.AnnotationCacheOperationSource.
*
* @author Costin Leau
* @author Burt Beckwith
*/
public class GrailsAnnotationCacheOperationSource extends AnnotationCacheOperationSource {
public class GrailsAnnotationCacheOperationSource implements CacheOperationSource, Serializable {

private static final long serialVersionUID = 1;

public static final String BEAN_NAME = "org.springframework.cache.annotation.AnnotationCacheOperationSource#0";

/**
* Canonical value held in cache to indicate no caching attribute was
* found for this method and we don't need to look again.
*/
protected static final Collection<CacheOperation> NULL_CACHING_ATTRIBUTE = Collections.emptyList();

protected GrailsApplication application;
protected boolean publicMethodsOnly = true;
protected Logger logger = LoggerFactory.getLogger(getClass());

protected final Set<CacheAnnotationParser> annotationParsers =
new LinkedHashSet<CacheAnnotationParser>(1);

/**
* Cache of CacheOperations, keyed by DefaultCacheKey (Method + target Class).
*/
protected final Map<Object, Collection<CacheOperation>> attributeCache =
new ConcurrentHashMap<Object, Collection<CacheOperation>>();

/**
* Constructor.
*/
public GrailsAnnotationCacheOperationSource() {
annotationParsers.add(new SpringCacheAnnotationParser());
}

public Collection<CacheOperation> getCacheOperations(Method method, Class<?> targetClass,
boolean includeControllers) {
Expand All @@ -45,19 +90,143 @@ public Collection<CacheOperation> getCacheOperations(Method method, Class<?> tar

// will typically be called with includeControllers = true (i.e. from the filter)
// so controller methods will be considered
return super.getCacheOperations(method, targetClass);
return doGetCacheOperations(method, targetClass);
}

@Override
public Collection<CacheOperation> getCacheOperations(Method method, Class<?> targetClass) {

// when called directly excluded controllers
// exclude controllers when called directly

if (isControllerClass(targetClass)) {
return null;
}

return super.getCacheOperations(method, targetClass);
return doGetCacheOperations(method, targetClass);
}

/**
* Determine the caching attribute for this method invocation.
* <p>Defaults to the class's caching attribute if no method attribute is found.
* @param method the method for the current invocation (never {@code null})
* @param targetClass the target class for this invocation (may be {@code null})
* @return {@link CacheOperation} for this method, or {@code null} if the method
* is not cacheable
*/
protected Collection<CacheOperation> doGetCacheOperations(Method method, Class<?> targetClass) {
// First, see if we have a cached value.
Object cacheKey = getCacheKey(method, targetClass);
Collection<CacheOperation> cached = attributeCache.get(cacheKey);
if (cached == null) {
// We need to work it out.
Collection<CacheOperation> cacheOps = computeCacheOperations(method, targetClass);
// Put it in the cache.
if (cacheOps == null) {
attributeCache.put(cacheKey, NULL_CACHING_ATTRIBUTE);
}
else {
logger.debug("Adding cacheable method '{}' with attribute: {}", method.getName(), cacheOps);
attributeCache.put(cacheKey, cacheOps);
}
return cacheOps;
}

if (cached == NULL_CACHING_ATTRIBUTE) {
return null;
}

// Value will either be canonical value indicating there is no caching attribute,
// or an actual caching attribute.
return cached;
}

/**
* For dev mode when rediscovering annotations is needed.
*/
public void reset() {
attributeCache.clear();
}

/**
* Determine a cache key for the given method and target class.
* <p>Must not produce same key for overloaded methods.
* Must produce same key for different instances of the same method.
* @param method the method (never {@code null})
* @param targetClass the target class (may be {@code null})
* @return the cache key (never {@code null})
*/
protected Object getCacheKey(Method method, Class<?> targetClass) {
return new DefaultCacheKey(method, targetClass);
}

protected Collection<CacheOperation> computeCacheOperations(Method method, Class<?> targetClass) {
// Don't allow no-public methods as required.
if (publicMethodsOnly && !Modifier.isPublic(method.getModifiers())) {
return null;
}

// The method may be on an interface, but we need attributes from the target class.
// If the target class is null, the method will be unchanged.
Method specificMethod = ClassUtils.getMostSpecificMethod(method, targetClass);
// If we are dealing with method with generic parameters, find the original method.
specificMethod = BridgeMethodResolver.findBridgedMethod(specificMethod);

// First try is the method in the target class.
Collection<CacheOperation> opDef = findCacheOperations(specificMethod);
if (opDef != null) {
return opDef;
}

// Second try is the caching operation on the target class.
opDef = findCacheOperations(specificMethod.getDeclaringClass());
if (opDef != null) {
return opDef;
}

if (specificMethod != method) {
// Fall back is to look at the original method.
opDef = findCacheOperations(method);
if (opDef != null) {
return opDef;
}
// Last fall back is the class of the original method.
return findCacheOperations(method.getDeclaringClass());
}

return null;
}

protected Collection<CacheOperation> findCacheOperations(Class<?> clazz) {
return determineCacheOperations(clazz);
}

protected Collection<CacheOperation> findCacheOperations(Method method) {
return determineCacheOperations(method);
}

/**
* Determine the cache operation(s) for the given method or class.
* <p>This implementation delegates to configured
* {@link CacheAnnotationParser}s for parsing known annotations into
* Spring's metadata attribute class.
* <p>Can be overridden to support custom annotations that carry
* caching metadata.
* @param ae the annotated method or class
* @return the configured caching operations, or {@code null} if none found
*/
protected Collection<CacheOperation> determineCacheOperations(AnnotatedElement ae) {
Collection<CacheOperation> ops = null;

for (CacheAnnotationParser annotationParser : annotationParsers) {
Collection<CacheOperation> annOps = annotationParser.parseCacheAnnotations(ae);
if (annOps != null) {
if (ops == null) {
ops = new ArrayList<CacheOperation>();
}
ops.addAll(annOps);
}
}

return ops;
}

protected boolean isControllerClass(Class<?> targetClass) {
Expand All @@ -71,4 +240,44 @@ protected boolean isControllerClass(Class<?> targetClass) {
public void setGrailsApplication(GrailsApplication grailsApplication) {
application = grailsApplication;
}

/**
* Dependency injection for whether to only consider public methods
* @param allow
*/
public void setAllowPublicMethodsOnly(boolean allow) {
publicMethodsOnly = allow;
}

/**
* Default cache key for the CacheOperation cache.
*/
protected static class DefaultCacheKey {

protected final Method method;
protected final Class<?> targetClass;

public DefaultCacheKey(Method method, Class<?> targetClass) {
this.method = method;
this.targetClass = targetClass;
}

@Override
public boolean equals(Object other) {
if (this == other) {
return true;
}
if (!(other instanceof DefaultCacheKey)) {
return false;
}
DefaultCacheKey otherKey = (DefaultCacheKey) other;
return method.equals(otherKey.method) &&
ObjectUtils.nullSafeEquals(targetClass, otherKey.targetClass);
}

@Override
public int hashCode() {
return method.hashCode() * 29 + (targetClass == null ? 0 : targetClass.hashCode());
}
}
}

0 comments on commit 45abf4b

Please sign in to comment.