Hot questions for Spring Cache

Top 10 Java Open Source / Spring / Spring Cache

Question:

I have developed a Spring Data repository, MemberRepository interface, that extends org.springframework.data.jpa.repository.JpaRepository. MemberRepository has a method:

@Cacheable(CacheConfiguration.DATABASE_CACHE_NAME)
Member findByEmail(String email);

The result is cached by Spring cache abstraction (backed by a ConcurrentMapCache).

The issue I have is that I want to write an integration test (against hsqldb) that asserts that the result is retrieved from db the first time and from cache the second time.

I initially thought of mocking the jpa infrastructure (entity manager, etc.) and somehow assert that the entity manager is not called the second time but it seems too hard/cumbersome (see https://stackoverflow.com/a/23442457/536299).

Can someone then please provide advice as to how to test the caching behavior of a Spring Data Repository method annotated with @Cacheable?


Answer:

If you want to test a technical aspect like caching, don't use a database at all. It's important to understand what you'd like to test here. You want to make sure the method invocation is avoided for the invocation with the very same arguments. The repository fronting a database is a completely orthogonal aspect to this topic.

Here's what I'd recommend:

  1. Set up an integration test that configures declarative caching (or imports the necessary bit's and pieces from your production configuration.
  2. Configure a mock instance of your repository.
  3. Write a test case to set up the expected behavior of the mock, invoke the methods and verify the output accordingly.
Sample
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration
public class CachingIntegrationTest {

  // Your repository interface
  interface MyRepo extends Repository<Object, Long> {

    @Cacheable("sample")
    Object findByEmail(String email);
  }

  @Configuration
  @EnableCaching
  static class Config {

    // Simulating your caching configuration
    @Bean
    CacheManager cacheManager() {
      return new ConcurrentMapCacheManager("sample");
    }

    // A repository mock instead of the real proxy
    @Bean
    MyRepo myRepo() {
      return Mockito.mock(MyRepo.class);
    }
  }

  @Autowired CacheManager manager;
  @Autowired MyRepo repo;

  @Test
  public void methodInvocationShouldBeCached() {

    Object first = new Object();
    Object second = new Object();

    // Set up the mock to return *different* objects for the first and second call
    Mockito.when(repo.findByEmail(Mockito.any(String.class))).thenReturn(first, second);

    // First invocation returns object returned by the method
    Object result = repo.findByEmail("foo");
    assertThat(result, is(first));

    // Second invocation should return cached value, *not* second (as set up above)
    result = repo.findByEmail("foo");
    assertThat(result, is(first));

    // Verify repository method was invoked once
    Mockito.verify(repo, Mockito.times(1)).findByEmail("foo");
    assertThat(manager.getCache("sample").get("foo"), is(notNullValue()));

    // Third invocation with different key is triggers the second invocation of the repo method
    result = repo.findByEmail("bar");
    assertThat(result, is(second));
  }
}

As you can see, we do a bit of over-testing here:

  1. The most relevant check, I think is that the second call returns the first object. That's what the caching is all about. The first two calls with the same key return the same object, whereas the third call with a different key results in the second actual invocation on the repository.
  2. We strengthen the test case by checking that the cache actually has a value for the first key. One could even extend that to check for the actual value. On the other hand, I also think it's fine to avoid doing that as you tend to test more of the internals of the mechanism rather than the application level behavior.
Key take-aways
  1. You don't need any infrastructure to be in place to test container behavior.
  2. Setting a test case up is easy and straight forward.
  3. Well-designed components let you write simple test cases and require less integration leg work for testing.

Question:

I'm using Spring Cache abstraction and I have multiple caches defined. Sometimes, when data changes, I want to evict more than one caches. Is there away to evict multiple cache using Spring's @CacheEvict annotation?


Answer:

You can do this:

@Caching(evict = {
    @CacheEvict("primary"),
    @CacheEvict(value = "secondary", key = "#p0")
})

Check out the Reference for details

Question:

Currently I am working with a Spring Cache and the @Cacheable/@CacheEvict annotations.

I would like to get some sort of a console log statement like "INFO: i got those values from the cache, NOT from the host. awesome"

Is there a clean and easy way to do this? We are using slf4j apparently btw, if that is of any interest.


Answer:

Spring itself logs some of its Caching Abstractions behaviors under the org.springframework.cache logger in trace level. So, if you append logs under the org.springframework.cache logger to an appropriate appender, you would have some useful information on, say, the console. If you're using Logback, you could use something like the following in your logback.xml:

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>%msg%n</pattern>
        </encoder>
    </appender>

    <logger name="org.springframework.cache" level="trace">
        <appender-ref ref="STDOUT" />
    </logger>
</configuration>

With this configuration, you should see something like following on your console:

Cache entry for key 'Page request [number: 0, size 20, sort: null]' found in cache 'persons'

Question:

I'm looking for 2 things:

  1. How to disable all caching during development with Spring boot "dev" profile. There doesn't seam to be a general setting to turn it all off in application.properties. What's the easiest way?

  2. How to disable caching for a specific method? I tried to use SpEl like this:

    @Cacheable(value = "complex-calc", condition = "#{${spring.profiles.active} != 'dev'}") public String someBigCalculation(String input){ ... }

But I can get it to work. There are a couple of questions on SO related to this, but they refer to XML config or other things, but I'm using Spring Boot 1.3.3 and this uses auto-configuration.

I don't want to over-complicate things.


Answer:

The type of cache is by default automatically detected and configured. However you can specify which cache type to use by adding spring.cache.type to your configuration. To disable it set the value to NONE.

As you want to do it for a specific profile add it to that profiles application.properties in this case modify the application-dev.properties and add

spring.cache.type=NONE

This will disable caching.

Question:

I am trying to load some context from an RSS feed and pass it as a cache to the client using ehcache library in spring. here is my code :

    import org.springframework.cache.annotation.Cacheable;
@Service
public class GlossaryReaderService {

    @Cacheable(value = "glossaryList")
    public List<Glossary> readGlossary(String url) {

        XmlReader reader = null;
        List<Glossary> extractedGlossay = new ArrayList<Glossary>();
        SyndEntry entry;
        SyndContent desc;
        Glossary glossaryList = null;
        try {
            String decodedURL = URLDecoder.decode(url, "UTF-8");
            reader = new XmlReader(new URL(decodedURL));
            SyndFeed feed = new SyndFeedInput().build(reader);

            for (Iterator i = feed.getEntries().iterator(); i.hasNext();) {
                entry = (SyndEntry) i.next();
                desc = entry.getDescription();
                if (desc != null) { ...
                        extractedGlossay.add(glossaryList);
                    }
                }
            }

        } catch (IOException | IllegalArgumentException | FeedException e) {
            log.error("Error reading RSS feed", e);
        } finally {
            IOUtils.closeQuietly(reader);
        }

        return extractedGlossay;
    }

also under at /webmi/src/main/resources/ehcache.xml I have the echahe.xml :

<?xml version="1.0" encoding="UTF-8"?>
   <ehcache>
  <defaultCache maxElementsInMemory="500" eternal="true"
    overflowToDisk="false" memoryStoreEvictionPolicy="LFU" />
  <cache name="info" maxElementsInMemory="500" eternal="true"
    overflowToDisk="false" memoryStoreEvictionPolicy="LFU" />
   </ehcache>

but the problem is that when I want to add the beans in the application-content it gives error. here is the partin application-content.xml :

<!--  CACHE CONFIGURATION  -->
<cache:annotation-driven />
<bean id="cacheManager"
class="org.springframework.cache.ehcache.EhCacheCacheManager"
p:cache-manager-ref="ehcache" />
    <bean id="ehcache class="org.springframework.cache.ehcache.EhCacheManagerFactoryBean" p:config location="classpath:ehcache.xml" />

but it give me the following error:

 2014-02-26 16:56:47 [ERROR] ContextLoader  - Context initialization failed
org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'org.springframework.cache.interceptor.CacheInterceptor#0': Cannot resolve reference to bean 'cacheManager' while setting bean property 'cacheManager'; nested exception is org.springframework.beans.factory.CannotLoadBeanClassException: Cannot find class [org.springframework.cache.ehcache.EhCacheCacheManager] for bean with name 'cacheManager' defined in class path resource [application-context.xml]; nested exception is java.lang.ClassNotFoundException: org.springframework.cache.ehcache.EhCacheCacheManager
    at org.springframework.beans.factory.support.BeanDefinitionValueResolver.resolveReference(BeanDefinitionValueResolver.java:329)
    at org.springframework.beans.factory.support.BeanDefinitionValueResolver.resolveValueIfNecessary(BeanDefinitionValueResolver.java:107)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.applyPropertyValues(AbstractAutowireCapableBeanFactory.java:1391)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.populateBean(AbstractAutowireCapableBeanFactory.java:1132)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:522)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:461)
    at org.springframework.beans.factory.support.AbstractBeanFactory$1.getObject(AbstractBeanFactory.java:295)
    at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:223)
    at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:292)
    at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:194)
    at org.springframework.beans.factory.support.DefaultListableBeanFactory.preInstantiateSingletons(DefaultListableBeanFactory.java:626)
    at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:932)
    at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:479)
    at org.springframework.web.context.ContextLoader.configureAndRefreshWebApplicationContext(ContextLoader.java:389)
    at org.springframework.web.context.ContextLoader.initWebApplicationContext(ContextLoader.java:294)
    at org.springframework.web.context.ContextLoaderListener.contextInitialized(ContextLoaderListener.java:112)
    at org.apache.catalina.core.StandardContext.listenerStart(StandardContext.java:4939)
    at org.apache.catalina.core.StandardContext.startInternal(StandardContext.java:5434)
    at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:150)
    at org.apache.catalina.core.ContainerBase$StartChild.call(ContainerBase.java:1559)
    at org.apache.catalina.core.ContainerBase$StartChild.call(ContainerBase.java:1549)
    at java.util.concurrent.FutureTask$Sync.innerRun(FutureTask.java:334)
    at java.util.concurrent.FutureTask.run(FutureTask.java:166)
    at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1145)
    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:615)
    at java.lang.Thread.run(Thread.java:724)
Caused by: org.springframework.beans.factory.CannotLoadBeanClassException: Cannot find class [org.springframework.cache.ehcache.EhCacheCacheManager] for bean with name 'cacheManager' defined in class path resource [application-context.xml]; nested exception is java.lang.ClassNotFoundException: org.springframework.cache.ehcache.EhCacheCacheManager
    at org.springframework.beans.factory.support.AbstractBeanFactory.resolveBeanClass(AbstractBeanFactory.java:1266)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:438)
    at org.springframework.beans.factory.support.AbstractBeanFactory$1.getObject(AbstractBeanFactory.java:295)
    at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:223)
    at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:292)
    at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:194)
    at org.springframework.beans.factory.support.BeanDefinitionValueResolver.resolveReference(BeanDefinitionValueResolver.java:323)
    ... 25 more
Caused by: java.lang.ClassNotFoundException: org.springframework.cache.ehcache.EhCacheCacheManager
    at org.apache.catalina.loader.WebappClassLoader.loadClass(WebappClassLoader.java:1714)
    at org.apache.catalina.loader.WebappClassLoader.loadClass(WebappClassLoader.java:1559)
    at org.springframework.util.ClassUtils.forName(ClassUtils.java:260)
    at org.springframework.beans.factory.support.AbstractBeanDefinition.resolveBeanClass(AbstractBeanDefinition.java:416)
    at org.springframework.beans.factory.support.AbstractBeanFactory.doResolveBeanClass(AbstractBeanFactory.java:1287)
    at org.springframework.beans.factory.support.AbstractBeanFactory.resolveBeanClass(AbstractBeanFactory.java:1258)
    ... 31 more
Feb 26, 2014 4:56:47 PM org.apache.catalina.core.StandardContext listenerStart
SEVERE: Exception sending context initialized event to listener instance of class org.springframework.web.context.ContextLoaderListener
org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'org.springframework.cache.interceptor.CacheInterceptor#0': Cannot resolve reference to bean 'cacheManager' while setting bean property 'cacheManager'; nested exception is org.springframework.beans.factory.CannotLoadBeanClassException: Cannot find class [org.springframework.cache.ehcache.EhCacheCacheManager] for bean with name 'cacheManager' defined in class path resource [application-context.xml]; nested exception is java.lang.ClassNotFoundException: org.springframework.cache.ehcache.EhCacheCacheManager
    at org.springframework.beans.factory.support.BeanDefinitionValueResolver.resolveReference(BeanDefinitionValueResolver.java:329)
    at org.springframework.beans.factory.support.BeanDefinitionValueResolver.resolveValueIfNecessary(BeanDefinitionValueResolver.java:107)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.applyPropertyValues(AbstractAutowireCapableBeanFactory.java:1391)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.populateBean(AbstractAutowireCapableBeanFactory.java:1132)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:522)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:461)
    at org.springframework.beans.factory.support.AbstractBeanFactory$1.getObject(AbstractBeanFactory.java:295)
    at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:223)
    at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:292)
    at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:194)
    at org.springframework.beans.factory.support.DefaultListableBeanFactory.preInstantiateSingletons(DefaultListableBeanFactory.java:626)
    at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:932)
    at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:479)
    at org.springframework.web.context.ContextLoader.configureAndRefreshWebApplicationContext(ContextLoader.java:389)
    at org.springframework.web.context.ContextLoader.initWebApplicationContext(ContextLoader.java:294)
    at org.springframework.web.context.ContextLoaderListener.contextInitialized(ContextLoaderListener.java:112)
    at org.apache.catalina.core.StandardContext.listenerStart(StandardContext.java:4939)
    at org.apache.catalina.core.StandardContext.startInternal(StandardContext.java:5434)
    at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:150)
    at org.apache.catalina.core.ContainerBase$StartChild.call(ContainerBase.java:1559)
    at org.apache.catalina.core.ContainerBase$StartChild.call(ContainerBase.java:1549)
    at java.util.concurrent.FutureTask$Sync.innerRun(FutureTask.java:334)
    at java.util.concurrent.FutureTask.run(FutureTask.java:166)
    at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1145)
    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:615)
    at java.lang.Thread.run(Thread.java:724)

I have been working on this the whole day, but I dont dont know what is the problem. please if anybody know the answer, help !!!

Edit: I have added the spring-contect-support, here is the dependencies in the Pom :

    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-context-support</artifactId>
        <version>4.0.1.RELEASE</version>
    </dependency>
    <!-- Ehcache -->
    <dependency>
        <groupId>net.sf.ehcache</groupId>
        <artifactId>ehcache</artifactId>
        <version>2.8.1</version>
    </dependency>

but the error has been changed to the following :

 SEVERE: Servlet.service() for servlet [default-dispatcher] in context with path [/webmi] threw exception [Request processing failed; nested exception is java.lang.IllegalArgumentException: Cannot find cache named [glossaryList] for CacheableOperation[public java.util.List com.service.GlossaryReaderService.readGlossary(java.lang.String)] caches=[glossaryList] | condition='' | key=''] with root cause
java.lang.IllegalArgumentException: Cannot find cache named [glossaryList] for CacheableOperation[public java.util.List com.service.GlossaryReaderService.readGlossary(java.lang.String)] caches=[glossaryList] | condition='' | key=''
    at org.springframework.cache.interceptor.CacheAspectSupport.getCaches(CacheAspectSupport.java:163)
    at org.springframework.cache.interceptor.CacheAspectSupport$CacheOperationContext.<init>(CacheAspectSupport.java:443)
    at org.springframework.cache.interceptor.CacheAspectSupport.getOperationContext(CacheAspectSupport.java:173)
    at org.springframework.cache.interceptor.CacheAspectSupport.createOperationContext(CacheAspectSupport.java:404)
    at org.springframework.cache.interceptor.CacheAspectSupport.execute(CacheAspectSupport.java:192)
    at org.springframework.cache.interceptor.CacheInterceptor.invoke(CacheInterceptor.java:66)
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:172)
    at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:631)
    at com.lloydsbanking.webmi.service.GlossaryReaderService$$EnhancerByCGLIB$$26769e11.readGlossary(<generated>)
    at com.webmi.web.RssController.getGlossory(RssController.java:50)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)

Answer:

This class is located in the spring-context-support module that is optional. Please make sure that you have added it as a dependency of your project.

<dependency>
   <groupId>org.springframework</groupId>
   <artifactId>spring-context-support</artifactId>
   <version>X.Y.Z.RELEASE</version>
</dependency>

Question:

I'm using spring-cache to improve database queries, which works fine as follows:

@Bean
public CacheManager cacheManager() {
    return new ConcurrentMapCacheManager("books");
}

@Cacheable("books")
public Book getByIsbn(String isbn) {
    return dao.findByIsbn(isbn);
}

But now I want to prepopulate the full book-cache on startup. Which means I want to call dao.findAll() and put all values into the cache. This routine shall than only be scheduled periodically.

But how can I explicit populate a cache when using @Cacheable?


Answer:

Just use the cache as before, add a scheduler to update cache, code snippet is below.

@Service
public class CacheScheduler {
    @Autowired
    BookDao bookDao;
    @Autowired
    CacheManager cacheManager;

    @PostConstruct
    public void init() {
        update();
        scheduleUpdateAsync();
    }

    public void update() {
        for (Book book : bookDao.findAll()) {
            cacheManager.getCache("books").put(book.getIsbn(), book);
        }
    }
}

Make sure your KeyGenerator will return the object for one parameter (as default). Or else, expose the putToCache method in BookService to avoid using cacheManager directly.

@CachePut(value = "books", key = "#book.isbn")
public Book putToCache(Book book) {
    return book;
}

Question:

I'm learning Spring WebFlux and during writing a sample application I found a concern related to Reactive types (Mono/Flux) combined with Spring Cache.

Consider the following code-snippet (in Kotlin):

@Repository
interface TaskRepository : ReactiveMongoRepository<Task, String>

@Service
class TaskService(val taskRepository: TaskRepository) {

    @Cacheable("tasks")
    fun get(id: String): Mono<Task> = taskRepository.findById(id)
}

Is this valid and safe way of caching method calls returning Mono or Flux? Maybe there are some other principles to do this?

The following code is working with SimpleCacheResolver but by default fails with Redis because of the fact that Mono is not Serializable. In order to make them work e.g Kryo serializer needs to be used.


Answer:

Hack way

For now, there is no fluent integration of @Cacheable with Reactor 3. However, you may bypass that thing by adding .cache() operator to returned Mono

@Repository
interface TaskRepository : ReactiveMongoRepository<Task, String>

@Service
class TaskService(val taskRepository: TaskRepository) {

    @Cacheable("tasks")
    fun get(id: String): Mono<Task> = taskRepository.findById(id).cache()
}

That hack cache and share returned from taskRepository data. In turn, spring cacheable will cache a reference of returned Mono and then, will return that reference. In other words, it is a cache of mono which holds the cache :).

Reactor Addons Way

There is an addition to Reactor 3 which allows fluent integration with modern in-memory caches like caffeine, jcache, etc. Using that technique you will be capable to cache your data easily:

@Repository
interface TaskRepository : ReactiveMongoRepository<Task, String>

@Service
class TaskService(val taskRepository: TaskRepository) {

    @Autowire
    CacheManager manager;


    fun get(id: String): Mono<Task> = CacheMono.lookup(reader(), id)
                                               .onCacheMissResume(() -> taskRepository.findById(id))
                                               .andWriteWith(writer());

    fun reader(): CacheMono.MonoCacheReader<String, Task> = key -> Mono.<Signal<Task>>justOrEmpty((Signal) manager.getCache("tasks").get(key).get())
    fun writer(): CacheMono.MonoCacheWriter<String, Task> = (key, value) -> Mono.fromRunnable(() -> manager.getCache("tasks").put(key, value));
} 

Note: Reactor addons caching own abstraction which is Signal<T>, so, do not worry about that and following that convention