1

I'm trying to use Spring Caching annotations @Cacheable and @CacheEvict together with the GuavaCacheManager.

I've created a test case with these two tests:

  1. cachesById - verifies that two invocations to a method annotatted with @Cacheable returns the same object
  2. evict - verifies that two different instances are returned if a method annotated with @CacheEvict is called in-between those two invocations

Both work fine when i don't specify a key for @CacheEvict, however when I do i get the following exception:

java.lang.NullPointerException
    at com.google.common.base.Preconditions.checkNotNull(Preconditions.java:210)
    at com.google.common.cache.LocalCache$LocalManualCache.invalidate(LocalCache.java:4764)
    at org.springframework.cache.guava.GuavaCache.evict(GuavaCache.java:135)
    at org.springframework.cache.interceptor.AbstractCacheInvoker.doEvict(AbstractCacheInvoker.java:95)
    at org.springframework.cache.interceptor.CacheAspectSupport.performCacheEvict(CacheAspectSupport.java:409)
    at org.springframework.cache.interceptor.CacheAspectSupport.processCacheEvicts(CacheAspectSupport.java:392)
    at org.springframework.cache.interceptor.CacheAspectSupport.execute(CacheAspectSupport.java:362)
    at org.springframework.cache.interceptor.CacheAspectSupport.execute(CacheAspectSupport.java:299)
    at org.springframework.cache.interceptor.CacheInterceptor.invoke(CacheInterceptor.java:61)
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179)
    at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:653)
    at com.myorg.caching.CacheTest$Repo$$EnhancerBySpringCGLIB$$eed50f3e.update(<generated>)
    at com.myorg.caching.CacheTest.evict(CacheTest.java:50)

This can be reproduced by executing the below test.

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(
        classes = { Repo.class, CacheTest.SpringConfig.class },
        loader = AnnotationConfigContextLoader.class)
public class CacheTest {

    private static final String CACHE_NAME = "cacheName";

    @Inject
    private Repo repo;

    @Test
    public void cachesById() {
        Entity aResult1 = repo.getEntity(1);
        Entity aResult2 = repo.getEntity(1);
        assertEquals(aResult1.getId(), aResult2.getId());
        assertSame(aResult1, aResult2);
    }

    @Test
    public void evict() {
        Entity aResult1 = repo.getEntity(1);
        repo.update(aResult1);
        Entity aResult2 = repo.getEntity(1);
        assertEquals(aResult1.getId(), aResult2.getId());
        assertNotSame(aResult1, aResult2);
    }

    /** Mock repository/entity classes below. */

    @Component
    public static class Repo {

        @Cacheable(value = CACHE_NAME, key = "#id")
        public Entity getEntity(int id) {
            return new Entity(id);
        }

        @CacheEvict(value = CACHE_NAME, key = "#id")
        public void update(Entity e) {    
        }
    }


    public static class Entity {
        private int id;

        public Entity(int id) {
            super();
            this.id = id;
        }

        public int getId() {
            return id;
        }

        public void setId(int id) {
            this.id = id;
        }
    }

    /** Guava Cachemanager Spring configuration */

    @Configuration
    @EnableCaching
    public static class SpringConfig {
        @Bean
        public CacheManager cacheManager() {
            GuavaCacheManager manager = new GuavaCacheManager(CACHE_NAME);
            manager.setCacheBuilder(CacheBuilder.newBuilder().expireAfterWrite(
                    1, TimeUnit.MINUTES).recordStats());
            return manager;
        }
    }
}

However the test passes if I change

@CacheEvict(value = CACHE_NAME, key = "#id")
public void update(Entity e) {

into:

@CacheEvict(value = CACHE_NAME)
public void update(Entity e) {

..but then I'm missing the point where I need to specify the cache key for Entity. Does anyone know what I'm missing?

Thanks!

Geir
  • 514
  • 4
  • 18
  • There is no method parameter named `id` so what does it refer to, nothing AKA `null` in java. You have a parameter named `e` of type `Entity` which probably has an `id` property. Change `#id` into `#e.id`. And I suggest a read on the spring caching documentation. – M. Deinum Oct 12 '15 at 14:28

1 Answers1

16

You have to fix you component class from

@Component
public static class Repo {

    @Cacheable(value = CACHE_NAME, key = "#id")
    public Entity getEntity(int id) {
        return new Entity(id);
    }

    @CacheEvict(value = CACHE_NAME, key = "#id")
    public void update(Entity e) {    
    }
}

to

@Component
public static class Repo {

    @Cacheable(value = CACHE_NAME, key = "#id")
    public Entity getEntity(int id) {
        return new Entity(id);
    }

    @CacheEvict(value = CACHE_NAME, key = "#e?.id")
    public void update(Entity e) {    
    }
}

Why? In getEntity method you're caching an Entity object using int id, you have to pass the same int id into the @CacheEvict annotated method. You don't have to change method's signature - by using SPEL you can "get into" entity and use its id field.

Hope I helped.

Trynkiewicz Mariusz
  • 2,722
  • 3
  • 21
  • 27
  • 1
    Why there is a `?` in `key = "#e?.id"` – Thanga Mar 03 '16 at 11:21
  • 9
    @Thanga there is chance that **Entity e** (`#e`) may be null and, therefore, accessing `id` property would cause `NullPointerException`. `?` sign tells that this property might be null and only if it's not null program should access `id`. See: [link](http://docs.spring.io/spring/docs/current/spring-framework-reference/html/expressions.html#expressions-operator-safe-navigation) – Trynkiewicz Mariusz Mar 04 '16 at 10:57
  • Thank you for the explanation @Trynkiewicz Mariusz .. Upvoted – Thanga Mar 04 '16 at 11:01
  • This didn't work for me. It just changed the exception to be an NPE – Arash May 30 '18 at 15:06
  • use cacheEvict from another class.if you call this on same class this will not work – Mugeesh Husain Feb 02 '23 at 03:01