diff --git a/.gitignore b/.gitignore index f1bfed1..6f40b19 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,5 @@ plugin.xml .DS_Store .idea/ *.iml +CodeNarcReport.html +.*.swp diff --git a/SpringcacheGrailsPlugin.groovy b/SpringcacheGrailsPlugin.groovy index 375afe2..62bd6d2 100644 --- a/SpringcacheGrailsPlugin.groovy +++ b/SpringcacheGrailsPlugin.groovy @@ -5,7 +5,7 @@ * 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 + * 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, @@ -23,11 +23,14 @@ import org.springframework.web.filter.DelegatingFilterProxy import grails.plugin.springcache.aop.* import grails.plugin.springcache.web.* import org.springframework.cache.ehcache.* +import java.lang.management.ManagementFactory +import net.sf.ehcache.management.ManagementService +import org.springframework.context.ApplicationContext class SpringcacheGrailsPlugin { - def version = "1.4" - def grailsVersion = "1.2.0 > *" + def version = "1.4.1" + def grailsVersion = "2.0.0 > *" def dependsOn = [:] def pluginExcludes = [ "grails-app/views/**", @@ -79,7 +82,7 @@ class SpringcacheGrailsPlugin { def doWithSpring = { if (!isEnabled(application)) { log.warn "Springcache plugin is disabled" - springcacheFilter(NoOpFilter) + springcacheFilter(NoOpFilter) } else { if (application.config.grails.spring.disable.aspectj.autoweaving) { log.warn "Service method caching is not compatible with the config setting 'grails.spring.disable.aspectj.autoweaving = false'" @@ -127,6 +130,7 @@ class SpringcacheGrailsPlugin { def doWithDynamicMethods = {ctx -> PageInfo.metaClass.mixin(HeadersCategory) + attachGetCached(applicationContext) } def doWithApplicationContext = { applicationContext -> @@ -134,16 +138,48 @@ class SpringcacheGrailsPlugin { for (tagLibClass in application.tagLibClasses) { decorator.decorate(tagLibClass, applicationContext."${tagLibClass.fullName}") } + + def jmxConfig = application.config.springcache.jmx + if (jmxConfig) { + def springcacheCacheManager = applicationContext.getBean("springcacheCacheManager") + ManagementService.registerMBeans(springcacheCacheManager, ManagementFactory.platformMBeanServer, + jmxConfig.cacheManager, + jmxConfig.cache, + jmxConfig.cacheConfiguration, + jmxConfig.cacheStatistics + ) + } + } def onChange = { event -> + ApplicationContext ctx = event.ctx + GrailsApplication application = event.application + if (application.isTagLibClass(event.source)) { def tagLibClass = application.getTagLibClass(event.source.name) - def instance = event.ctx."${event.source.name}" + def instance = ctx."${event.source.name}" def decorator = new CachingTagLibDecorator(event.ctx.springcacheService) decorator.decorate(tagLibClass, instance) } + + attachGetCached(ctx) + } + + def attachGetCached(ApplicationContext ctx) { + ctx.beanDefinitionNames.each { beanName -> + def mc + if(ctx.isSingleton(beanName)) { + mc = ctx.getBean(beanName).getMetaClass() + } else { + mc = ctx.getType(beanName).getMetaClass() + } + + mc.getCached = { -> + ctx.getBean(beanName) + } + } } def getWebXmlFilterOrder() { diff --git a/TODO.md b/TODO.md index 60d3776..9e9ebe5 100644 --- a/TODO.md +++ b/TODO.md @@ -7,7 +7,6 @@ ## General * convert annotation classes to Groovy so they can use null defaults -* expose caching statistics (JMX?) * support reloading via onChange and onConfigChange [GRAILSPLUGINS-1825][1825] ## Controller caching @@ -27,4 +26,4 @@ * cacheable version of g:render? -[1825]:http://jira.codehaus.org/browse/GRAILSPLUGINS-1825 \ No newline at end of file +[1825]:http://jira.codehaus.org/browse/GRAILSPLUGINS-1825 diff --git a/grails-app/conf/BuildConfig.groovy b/grails-app/conf/BuildConfig.groovy index 8bdf087..5db5697 100644 --- a/grails-app/conf/BuildConfig.groovy +++ b/grails-app/conf/BuildConfig.groovy @@ -6,6 +6,7 @@ grails.project.test.reports.dir = "target/test-reports" grails.project.dependency.resolution = { inherits "global" + log "warn" repositories { grailsHome() diff --git a/src/docs/guide/1. Introduction.gdoc b/src/docs/guide/1. Introduction.gdoc index 5788fab..6de90a3 100644 --- a/src/docs/guide/1. Introduction.gdoc +++ b/src/docs/guide/1. Introduction.gdoc @@ -8,8 +8,8 @@ The plugin depends on the "EhCache":http://ehcache.org/ and "EhCache-Web":http:/ h3. Contact -The plugin code is hosted on [GitHub|http://github.com/robfletcher/grails-springcache]. Please feel free to fork the plugin and contribute patches. +The plugin code is hosted on [GitHub|https://github.com/gpc/grails-springcache]. Please feel free to fork the plugin and contribute patches. -Please raise defects or enhancements against the Grails Springcache plugin component on the [Codehaus JIRA|http://jira.codehaus.org/browse/GRAILSPLUGINS/component/14010]. +Please raise defects or enhancements against the Grails Springcache plugin component on the [Codehaus JIRA|http://jira.grails.org/browse/GPSPRINGCACHE]. Questions, comments? [rob@energizedwork.com|mailto:rob@energizedwork.com] or better still contact me via the [Grails User mailing list|http://grails.org/Mailing+lists]. diff --git a/src/docs/guide/1.1. Known Issues.gdoc b/src/docs/guide/1.1. Known Issues.gdoc index 08b27fc..7875d16 100644 --- a/src/docs/guide/1.1. Known Issues.gdoc +++ b/src/docs/guide/1.1. Known Issues.gdoc @@ -1,2 +1,2 @@ - * "GRAILSPLUGINS-2553":http://jira.codehaus.org/browse/GRAILSPLUGINS-2553 Cached methods with default arguments are not intercepted unless all the arguments are specified. Therefore the cache is completely bypassed (neither hit or missed). - * "GRAILSPLUGINS-2544":http://jira.codehaus.org/browse/GRAILSPLUGINS-2544 Due to a bug with Grails itself query string parameters are not included in the generated cache key. This can be a serious issue if you are using pagination for example as query string parameters such as @offset@ will be ignored resulting in the same cached content being served for every page. This *only* affects Grails version 1.3.4. + * "GPSPRINGCACHE-11":http://jira.grails.org/browse/GPSPRINGCACHE-11 Cached methods with default arguments are not intercepted unless all the arguments are specified. Therefore the cache is completely bypassed (neither hit or missed). + * "GPSPRINGCACHE-14G":http://jira.grails.org/browse/GPSPRINGCACHE-14 Due to a bug with Grails itself query string parameters are not included in the generated cache key. This can be a serious issue if you are using pagination for example as query string parameters such as @offset@ will be ignored resulting in the same cached content being served for every page. This *only* affects Grails version 1.3.4. diff --git a/src/docs/guide/1.2. Release Notes.gdoc b/src/docs/guide/1.2. Release Notes.gdoc index fb6e250..a83f185 100644 --- a/src/docs/guide/1.2. Release Notes.gdoc +++ b/src/docs/guide/1.2. Release Notes.gdoc @@ -1,3 +1,7 @@ +h4. 1.3.2 + + * exposing ehcache to JMX + h4. 1.3.1 * Fixes compilation problem on recent Groovy versions "GRAILSPLUGINS-2820":http://jira.codehaus.org/browse/GRAILSPLUGINS-2820 diff --git a/src/docs/guide/3.2 Calling Cached Methods Internally.gdoc b/src/docs/guide/3.2 Calling Cached Methods Internally.gdoc index 8e24d72..fbf9ceb 100644 --- a/src/docs/guide/3.2 Calling Cached Methods Internally.gdoc +++ b/src/docs/guide/3.2 Calling Cached Methods Internally.gdoc @@ -28,7 +28,7 @@ class ExampleService { def grailsApplication def nonCachedMethod() { - grailsApplication.mainContext.exampleService.cachedMethod() + cached.cachedMethod() } @Cacheable('cachedMethodCache') @@ -38,4 +38,4 @@ class ExampleService { } {code} -Instead of calling the method on @this@, we obtain the proxy via the application context (i.e. @grailsApplication.mainContext.exampleService@) and call the method on that. This way we go through the caching mechanism. \ No newline at end of file +Instead of calling the method on @this@, we obtain the proxy via the injected @cached@ property, and call the method on that. This way we go through the caching mechanism. diff --git a/src/docs/guide/7. Cache Configuration.gdoc b/src/docs/guide/7. Cache Configuration.gdoc index 5c6a2fa..de8aa8c 100644 --- a/src/docs/guide/7. Cache Configuration.gdoc +++ b/src/docs/guide/7. Cache Configuration.gdoc @@ -53,3 +53,21 @@ springcache { Under the hood this is simply setting up @EhCacheFactoryBean@ instances in the Spring context, so it is up to you whether you prefer to use @resources.groovy@ or @Config.groovy@ there is not much difference. The properties shown are just examples, see the "EhCacheFactoryBean":http://static.springsource.org/spring/docs/3.0.x/javadoc-api/org/springframework/cache/ehcache/EhCacheFactoryBean.html documentation for full details of all the properties you can set. + + +h3. exposing ehcache to JMX + +Ehcache is already prepared to expose itself to JMX. To enable this use the following snippet in @grails-app/conf/Config.groovy@: + +{code} +springcache { + jmx { + cacheManager = true + cache = true + cacheConfiguration = true + cacheStatistics = true + } +} +{code} + +For details, see [Ehcache's JMX manual|http://ehcache.org/documentation/operations/jmx]. \ No newline at end of file diff --git a/src/docs/ref/Tag Lib/getCacheDirectives.gdoc b/src/docs/ref/Tag Lib/getCacheDirectives.gdoc new file mode 100644 index 0000000..ea6a30a --- /dev/null +++ b/src/docs/ref/Tag Lib/getCacheDirectives.gdoc @@ -0,0 +1,20 @@ + +h1. getCacheDirectives + +h2. Purpose + + + +h2. Examples + +{code:java} +foo.getCacheDirectives() +{code} + +h2. Description + + + +Arguments: + +[] diff --git a/src/docs/ref/Tag Lib/getDateHeader.gdoc b/src/docs/ref/Tag Lib/getDateHeader.gdoc new file mode 100644 index 0000000..b08df83 --- /dev/null +++ b/src/docs/ref/Tag Lib/getDateHeader.gdoc @@ -0,0 +1,21 @@ + +h1. getDateHeader + +h2. Purpose + + + +h2. Examples + +{code:java} +foo.getDateHeader(string) +{code} + +h2. Description + + + +Arguments: + +[* @string@ +] diff --git a/src/docs/ref/Tag Lib/getHeader.gdoc b/src/docs/ref/Tag Lib/getHeader.gdoc new file mode 100644 index 0000000..a7804cb --- /dev/null +++ b/src/docs/ref/Tag Lib/getHeader.gdoc @@ -0,0 +1,21 @@ + +h1. getHeader + +h2. Purpose + + + +h2. Examples + +{code:java} +foo.getHeader(string) +{code} + +h2. Description + + + +Arguments: + +[* @string@ +] diff --git a/src/docs/ref/Tag Lib/isMatch.gdoc b/src/docs/ref/Tag Lib/isMatch.gdoc new file mode 100644 index 0000000..94ab1d2 --- /dev/null +++ b/src/docs/ref/Tag Lib/isMatch.gdoc @@ -0,0 +1,21 @@ + +h1. isMatch + +h2. Purpose + + + +h2. Examples + +{code:java} +foo.isMatch(httpServletRequest) +{code} + +h2. Description + + + +Arguments: + +[* @httpServletRequest@ +] diff --git a/src/docs/ref/Tag Lib/isModified.gdoc b/src/docs/ref/Tag Lib/isModified.gdoc new file mode 100644 index 0000000..66dc2a9 --- /dev/null +++ b/src/docs/ref/Tag Lib/isModified.gdoc @@ -0,0 +1,21 @@ + +h1. isModified + +h2. Purpose + + + +h2. Examples + +{code:java} +foo.isModified(httpServletRequest) +{code} + +h2. Description + + + +Arguments: + +[* @httpServletRequest@ +] diff --git a/src/docs/ref/Tag Lib/toString.gdoc b/src/docs/ref/Tag Lib/toString.gdoc new file mode 100644 index 0000000..d73a6e9 --- /dev/null +++ b/src/docs/ref/Tag Lib/toString.gdoc @@ -0,0 +1,20 @@ + +h1. toString + +h2. Purpose + + + +h2. Examples + +{code:java} +foo.toString() +{code} + +h2. Description + + + +Arguments: + +[] diff --git a/src/groovy/grails/plugin/springcache/web/GrailsFragmentCachingFilter.groovy b/src/groovy/grails/plugin/springcache/web/GrailsFragmentCachingFilter.groovy index 6c74f98..95b7fd9 100644 --- a/src/groovy/grails/plugin/springcache/web/GrailsFragmentCachingFilter.groovy +++ b/src/groovy/grails/plugin/springcache/web/GrailsFragmentCachingFilter.groovy @@ -35,7 +35,12 @@ class GrailsFragmentCachingFilter extends PageFragmentCachingFilter { SpringcacheService springcacheService CacheManager cacheManager - private final ThreadLocal contextHolder = new ThreadLocal() + private final ThreadLocal> contextHolder = new ThreadLocal>() { + @Override + protected Stack initialValue() { + new Stack() + } + } static final String X_SPRINGCACHE_CACHED = "X-Springcache-Cached" /** @@ -249,15 +254,20 @@ class GrailsFragmentCachingFilter extends PageFragmentCachingFilter { } private void initContext() { - contextHolder.set(new FilterContext()) + contextStack.push(new FilterContext()) } private FilterContext getContext() { - contextHolder.get() + contextStack.peek() } private void destroyContext() { - contextHolder.remove() + contextStack.pop() + if (contextStack.empty()) contextHolder.remove() + } + + private Stack getContextStack() { + contextHolder.get() } } diff --git a/test/projects/springcache-test/grails-app/conf/BuildConfig.groovy b/test/projects/springcache-test/grails-app/conf/BuildConfig.groovy index a610b1e..8a67c0d 100644 --- a/test/projects/springcache-test/grails-app/conf/BuildConfig.groovy +++ b/test/projects/springcache-test/grails-app/conf/BuildConfig.groovy @@ -4,7 +4,9 @@ grails.project.class.dir = "target/classes" grails.project.test.class.dir = "target/test-classes" grails.project.test.reports.dir = "target/test-reports" grails.project.dependency.resolution = { - inherits "global" + inherits("global") { + excludes "xml-apis" + } log "warn" repositories { grailsPlugins() @@ -39,5 +41,17 @@ grails.project.dependency.resolution = { compile ":resources:1.1.6" runtime ":jquery:1.7.1" } + plugins { + build ":tomcat:$grailsVersion" + compile ":bean-fields:0.5" + compile ":cache-headers:1.1.2" + compile ":rateable:0.6.2" + compile ":shiro:1.1.1" + compile ":yui:2.7.0.1" + runtime ":hibernate:$grailsVersion" + test ":build-test-data:1.1.1" + test ":geb:0.5.1" + test ":spock:0.5-groovy-1.7" + } } grails.plugin.location.springcache = "../../.." diff --git a/test/projects/springcache-test/grails-app/conf/Config.groovy b/test/projects/springcache-test/grails-app/conf/Config.groovy index 5405adc..75ff3bb 100644 --- a/test/projects/springcache-test/grails-app/conf/Config.groovy +++ b/test/projects/springcache-test/grails-app/conf/Config.groovy @@ -100,6 +100,7 @@ springcache { userControllerCache latestControllerCache popularControllerCache + forwardingControllerCache layoutsCache configuredCache { timeToLive = 86400 diff --git a/test/projects/springcache-test/grails-app/controllers/forwarding/ForwardingController.groovy b/test/projects/springcache-test/grails-app/controllers/forwarding/ForwardingController.groovy new file mode 100644 index 0000000..c23186e --- /dev/null +++ b/test/projects/springcache-test/grails-app/controllers/forwarding/ForwardingController.groovy @@ -0,0 +1,22 @@ +package forwarding + +import grails.plugin.springcache.annotations.Cacheable + +class ForwardingController { + + @Cacheable("forwardingControllerCache") + def cachedForwardsToCached = { forward action: "cachedAction" } + + @Cacheable("forwardingControllerCache") + def cachedForwardsToUncached = { forward action: "uncachedAction" } + + def uncachedForwardsToCached = { forward action: "cachedAction" } + + def uncachedForwardsToUncached = { forward action: "uncachedAction" } + + @Cacheable("forwardingControllerCache") + def cachedAction = { render contentType: "text/plain", text: System.currentTimeMillis() } + + def uncachedAction = { render contentType: "text/plain", text: System.currentTimeMillis() } + +} diff --git a/test/projects/springcache-test/test/functional/grails/plugin/springcache/web/ForwardingSpec.groovy b/test/projects/springcache-test/test/functional/grails/plugin/springcache/web/ForwardingSpec.groovy new file mode 100644 index 0000000..39a84cc --- /dev/null +++ b/test/projects/springcache-test/test/functional/grails/plugin/springcache/web/ForwardingSpec.groovy @@ -0,0 +1,67 @@ +package grails.plugin.springcache.web + +import grails.plugin.springcache.SpringcacheService +import groovyx.net.http.RESTClient +import net.sf.ehcache.Ehcache +import org.apache.http.HttpStatus +import org.codehaus.groovy.grails.commons.ApplicationHolder +import spock.lang.* + +@Issue("http://jira.grails.org/browse/GPSPRINGCACHE-5") +@Stepwise +class ForwardingSpec extends Specification { + + @Shared SpringcacheService springcacheService = ApplicationHolder.application.mainContext.springcacheService + @Shared Ehcache forwardingControllerCache = ApplicationHolder.application.mainContext.forwardingControllerCache + + private RESTClient http = new RESTClient("http://localhost:8080/") + + @Unroll("an initial hit on #action primes the cache") + def "an initial hit on an action primes the cache"() { + when: + def response = http.get(path: "/forwarding/$action") + + then: + response.status == HttpStatus.SC_OK + response.data.text ==~ /\d+/ + + and: + cacheMisses == old(cacheMisses) + misses + cacheHits == old(cacheHits) + hits + + where: + action | hits | misses + "uncachedForwardsToCached" | 0 | 1 + "cachedForwardsToUncached" | 0 | 1 + "cachedForwardsToCached" | 1 | 1 + } + + @Unroll("a subsequent hit on #action primes the cache") + def "a subsequent hit on an action hits the cache"() { + when: + def response = http.get(path: "/forwarding/$action") + + then: + response.status == HttpStatus.SC_OK + response.data.text ==~ /\d+/ + + and: + cacheMisses == old(cacheMisses) + misses + cacheHits == old(cacheHits) + hits + + where: + action | hits | misses + "uncachedForwardsToCached" | 1 | 0 + "cachedForwardsToUncached" | 1 | 0 + "cachedForwardsToCached" | 1 | 0 + } + + private long getCacheMisses() { + forwardingControllerCache.statistics.cacheMisses + } + + private long getCacheHits() { + forwardingControllerCache.statistics.cacheHits + } + +}