Monday, June 20, 2011

Grails - Spring Security with Spring Cache: Caching content per user [Updated]

In my previous post I explained how to customize the Spring Cache plug in so we could cache content considering what user is logged in or not. However, that code was written using the version 1.2.1 of the plug in and if you want to upgrade to the latest version, which currently is 1.3.1, then a few changes need to happen in the code for this to work. Because of this I am going to explain what these changes are so this customization continues working for you.

In the original example I mentioned that we needed to extend the class MimeTypeAwareKeyGenerator, we used this class specifically because we need to handle different content types. However, in the new version of the plug in this class no longer exists, what we have now is a class called WebContentKeyGenerator that extends DefaultKeyGenerator.

The WebContentKeyGenerator is a multipurpose implementation of the KeyGenerator interface. The different purposes of the key generator are managed through several boolean properties, all false by default, these properties are: [1]
  • ajax: If set to true then keys will differ depending on the presence or absence of the X-Requested-With request header so AJAX requests will be cached separately from regular requests. This is useful when you have an action that renders different content when it is requested via AJAX.
  • contentType: If true keys will differ depending on the requested content format as determined by the format meta-property on HttpServletRequest. This is useful when you use content negotiation in a request so that responses with different formats are cached separately.
  • requestMethod: If true keys will differ depending on the request HTTP method. This is useful for some RESTful controllers (although if different request methods are mapped to different actions you do not need to use this mechanism). GET and HEAD requests are considered the same for the purposes of key generation.

So our new key generator class should look like this:
public class MimeTypeAndAuthenticationAwareKeyGenerator extends WebContentKeyGenerator {

  @Override
  protected void generateKeyInternal(CacheKeyBuilder builder, ContentCacheParameters context) {
    super.generateKeyInternal(builder, context)
    def springSecurityService = ApplicationHolder.application.mainContext.getBean('springSecurityService')
    if (springSecurityService?.isLoggedIn()) {
      builder << "authUserId=${springSecurityService.principal.getId()}".toString()
    }
  }

}
As you can see the code is still very similar, but besides extending a different class, the signature of the generateKeyInternal method has also changed, instead of receiving a FilterContext object as a second parameter, you need to receive a ContentCacheParameters object.

To configure the plug in to use our class we used the Config.groovy configuration to tell the springcacheFilter what key generator to use. However, this needs to be done differently now, you have two options:

1. Setting the key generator by action: You can specify your key generator for a specific action by adding the keyGenerator element to the @Cacheable annotation specifying the name of a Spring bean that implements the KeyGenerator interface, something like:

@Cacheable(cache = "albumControllerCache", keyGenerator = "mySpecialKeyGenerator")
def doSomething = {
    // …
}

2. Overriding the default key generator: You can also override the default key generator instance in the resources.groovy file, which is named springcacheDefaultKeyGenerator. You can do something like:
springcacheDefaultKeyGenerator(MimeTypeAndAuthenticationAwareKeyGenerator) {
     ajax = true
     contentType = true
    }

As you can see we now have different options that allow us to have more control on how caching is managed for different actions giving us more flexibility.

[1] Taken from Spring Cache Plug in documentation.

20 comments:

  1. Hello,

    I'm trying to configure my own key generator but it is not working properly.

    I have created the file /grails-app/conf/com/example/web/key/MyKeyGenerator.groovy with the following content:

    package com.example.web.key

    import grails.plugin.springcache.key.CacheKeyBuilder
    import grails.plugin.springcache.web.ContentCacheParameters
    import grails.plugin.springcache.web.key.WebContentKeyGenerator
    import org.springframework.web.servlet.i18n.SessionLocaleResolver

    class MyKeyGenerator extends WebContentKeyGenerator {

    @Override protected void generateKeyInternal(CacheKeyBuilder builder, ContentCacheParameters context) {
    super.generateKeyInternal(builder, context)
    builder << "Current language: ${context.request.session[SessionLocaleResolver.LOCALE_SESSION_ATTRIBUTE_NAME]}"
    }
    }


    And in the file resources.groovy I have the following lines:

    // Place your Spring DSL code here
    beans = {

    myKeyGenerator(com.example.web.key.MyKeyGenerator) {

    }
    }

    But when I try to use it in a controller I always get the following error:


    2011-08-29 23:10:40,161 [http-8080-6] ERROR [/Demo].[grails] - Servlet.service() for servlet grails threw exception
    java.lang.NullPointerException: Cannot invoke method generateKey() on null object
    at org.codehaus.groovy.runtime.NullObject.invokeMethod(NullObject.java:77)
    at org.codehaus.groovy.runtime.callsite.PogoMetaClassSite.call(PogoMetaClassSite.java:45)
    at org.codehaus.groovy.runtime.callsite.CallSiteArray.defaultCall(CallSiteArray.java:40)
    at org.codehaus.groovy.runtime.callsite.NullCallSite.call(NullCallSite.java:32)


    Please, could you tell me what I am not configuring right?

    Thank you very much and regars,
    Iván.

    ReplyDelete
  2. Ivan,

    I think the problem is with how you are setting the generator in the resources.groovy, you are doing:

    beans = {
    myKeyGenerator(com.example.web.key.MyKeyGenerator) {
    }
    }

    but it should be:

    beans = {
    springcacheDefaultKeyGenerator(com.example.web.key.MyKeyGenerator) {
    }
    }

    Try changing that and let me know how it goes.

    Maricel.

    ReplyDelete
  3. Thank you for your quick reply. The problem was in the @cacheable annotation. I was misspelling the name of the keyGenerator. Instead of writing "myKeyGenerator" I was writing "MyKeyGenerator" with capital 'M'. This works defining my bean as myKeyGenerator(com.example...) in resources.groovy

    @Cacheable(cache = "translationControllerCache", keyGenerator = "myKeyGenerator")

    Your reply is working but now the @cacheable annotation must be:
    @Cacheable(cache = "translationControllerCache")

    Without defining the key generator.

    Thanks a lot!!.

    Regards, Iván.

    ReplyDelete
  4. I am glad it worked out for you Ivan and yes both options work :-)

    ReplyDelete
  5. I would really like your post ,it would really explain each and every point clearly well thanks for sharing.
    Chevy HHR Turbo

    ReplyDelete
  6. Hi,

    I have created a sample grails project (v1.3.5) to test caching contents per user. But, it is not working as I expected. You can find more detail on the problem in below link:

    https://docs.google.com/leaf?id=0B3j9inzR_LLhNDY3N2M5OTItM2Y3Mi00YmZlLTlmNTctZmFlZGRiODI0M2Ni&hl=en_US

    I have uploaded the source code for this project at below location. It will be grateful if someone looks into it & let me know where I'm wrong.

    https://docs.google.com/leaf?id=0B3j9inzR_LLhYjhiMmY3MDUtNTVlYi00ZTE2LWIyYTctZDAxMjQ4YTg2NTVi&hl=en_US

    Thanks,

    Kishore

    ReplyDelete
  7. I created MimeTypeAndAuthenticationAwareKeyGenerator.groovy in /grails-app/conf folder.

    import grails.plugin.springcache.web.key.WebContentKeyGenerator
    import grails.plugin.springcache.key.CacheKeyBuilder
    import grails.plugin.springcache.web.ContentCacheParameters
    import org.codehaus.groovy.grails.commons.ApplicationHolder

    public class MimeTypeAndAuthenticationAwareKeyGenerator extends WebContentKeyGenerator {

    @Override
    protected void generateKeyInternal(CacheKeyBuilder builder, ContentCacheParameters context) {
    super.generateKeyInternal(builder, context)
    def springSecurityService = ApplicationHolder.application.mainContext.getBean('springSecurityService')
    if (springSecurityService?.isLoggedIn()) {
    builder << "authUserId=${springSecurityService.principal.getId()}".toString()
    }
    }

    }

    And added below lines in resources.groovy

    beans = {
    springcacheDefaultKeyGenerator(MimeTypeAndAuthenticationAwareKeyGenerator) {
    ajax = true
    contentType = true
    }
    }

    Caching is working. But not per user!!!!!!!!!!
    [I've shared proj source & more description in one of my thread at http://maricel-tech.blogspot.com/2011/02/grails-spring-security-with-spring.html#comment-form]

    Any idea?

    ReplyDelete
  8. I'm confused where "mySpecialKeyGenerator" is defined?

    @Cacheable(cache = "albumControllerCache", keyGenerator = "mySpecialKeyGenerator")

    ReplyDelete
  9. Kishora,

    You have two options on how to use your custom key generator:

    1. You can use it per action

    @Cacheable(cache = "albumControllerCache", keyGenerator = "mySpecialKeyGenerator")

    In this case you are saying that you want a given action to use a generator that you are defining that is not the default. You would need to create the class for it and define the bean in the spring resources so it can be accessed.

    2. Option two is when you want your generator to override the plug in default. This what you are doing when you do:

    beans = {
    springcacheDefaultKeyGenerator(MimeTypeAndAuthenticationAwareKeyGenerator) {
    ajax = true
    contentType = true
    }
    }

    Hope this helps!

    ReplyDelete
  10. Hi Maricel,

    I have applied second method (springcacheDefaultKeyGenerator) in a sample grails project to test caching contents per user. But, it is not working as I expected. I'm stuck here. It will be great if it is possible for you take a look at the source code? I might have made a silly mistake.

    Source code is shared at:

    https://docs.google.com/leaf?id=0B3j9inzR_LLhYjhiMmY3MDUtNTVlYi00ZTE2LWIyYTctZDAxMjQ4YTg2NTVi&hl=en_US

    Thanks,

    Kishore

    ReplyDelete
  11. I can't see your code in that link, it just shows me a file to download and when I do it is unreadable. Could you post it again please?

    ReplyDelete
  12. I have uploaded again at:

    https://docs.google.com/viewer?a=v&pid=explorer&chrome=true&srcid=0B3j9inzR_LLhODg0MjBiNzItYTZkYi00OWVmLTk1MmEtYmUwY2M1YjJjMjky&hl=en_US

    Thanks,

    Kishore

    ReplyDelete
  13. Hi Maricel,

    I guess you didn't get my last uploaded file too. If you can share your email id on kishor.ys@gmail.com, then I can send it as an attachment.

    Regards,

    Kishore

    ReplyDelete
  14. I reviewed the code and everything looks good with the key generator. Running the application and debugging the code I see that for some reason is not working, so what I did was I added the @Cacheable tag to the action in the controller instead of the service and that seems to work.

    You can put a breakpoint in the class MimeTypeAndAuthenticationAwareKeyGenerator and follow the execution that will take you to this class GrailsFragmentCachingFilter, in there you can verify that the keys are generated with a different value depending on the user logged in and if the page is being loaded from the cache or not.

    The generator should work with the Service method as well, not sure why it is not working, I am going to look into a bit more and let you know.

    ReplyDelete
  15. I just remembered that the Service methods do not use the key generator, the key generator is used for content caching. The key for a service method is generated based on the object, the method signature and the method parameters, since in this case the method doesn't have parameters then the key is always the same.

    Read this for more information about this
    http://gpc.github.com/grails-springcache/docs/guide/3.%20Caching%20Service%20Methods.html

    I verified it by adding a parameter to the service method, so the user name is passed from the controller to the service and it worked.

    Makes sense?

    ReplyDelete
  16. Hi Miracel,

    You made my day.
    It works perfectly fine!!!!!!!!!!

    Thanks a lot for your time.

    Kishore

    ReplyDelete
  17. class DashboardController:
    ----------------------------------
    @Cacheable(cache = "myBooksCache")
    def getMyBooks = {
    // list books

    }

    class BookController:
    ----------------------------------
    @CacheFlush("myBooksCache")
    def save = {
    // save book
    }

    When all users opens dashboard, a cache ("myBooksCache") will be created for each user.
    When a book is added by user1 (/book/save called), "myBooksCache" cache will be flushed for all the users (user2, user3..)! Expected to clear only user1's cache.

    Caching content per user works fine.
    Any idea how to handle flushing cache per user?

    Source:
    https://docs.google.com/viewer?a=v&pid=explorer&chrome=true&srcid=0B3j9inzR_LLhMDc4MmVkMDItZjgyZS00NTkxLTg5YTktYTY2NGVjN2ZhYWE4&hl=en_US

    ReplyDelete
  18. Sorry, I haven't reviewed how cache flushing per user works, but you might want to check the plug in code to see if it takes into consideration the content key when doing the flushing, I would supposed it does.

    ReplyDelete
  19. Hi, very interesting post about Spring Cache and Grails, but is anyone having problems with the content cache?

    Imagine we have the next scenario. We have just one domain class and we implement static scaffolding and we want to cache the list method. This cache will be flush everytime we save, update or delete an item. But, if you list and then you try to create a new item without saving it and go back to the list, the application doesn't show the content correctly because the content generated in the main layout has not been cached.

    It means that using springcache, the content in the layouts won't be saved? The documentation doesn't say anything about it but I feel that's the problem.

    ReplyDelete
  20. hello, your all post is very nice and understandable and working.i am happy to get your blog and solve my very soon.Grails Technology , Spring3 Technology

    ReplyDelete