Java – caching nested cacheable operations through spring cache

I was given the task of using spring cache for one of our services to reduce the number of database lookups When testing the implementation, I noticed that some cacheable operations were called multiple times through log statements The survey shows that if the caching operation is invoked in the caching method, the nested operation is not at all. Therefore, nesting operations later will lead to further lookup.

A simple unit test describing the problem is listed below:

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = {SpringCacheTest.Config.class} )
@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_CLASS)
public class SpringCacheTest {

  private final static String CACHE_NAME = "testCache";
  private final static Logger LOG = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
  private final static AtomicInteger methodInvocations = new AtomicInteger(0);

  public interface ICacheableService {

    String methodA(int length);
    String methodB(String name);
  }

  @Resource
  private ICacheableService cache;

  @Test
  public void testNestedCaching() {

    String name = "test";
    cache.methodB(name);
    assertThat(methodInvocations.get(),is(equalTo(2)));

    cache.methodA(name.length());
    // should only be 2 as methodA for this length was already invoked before
    assertThat(methodInvocations.get(),is(equalTo(3)));
  }

  @Configuration
  public static class Config {

    @Bean
    public CacheManager getCacheManager() {
      SimpleCacheManager cacheManager = new SimpleCacheManager();
      cacheManager.setCaches(Arrays.asList(new ConcurrentMapCache(CACHE_NAME)));
      return cacheManager;
    }

    @Bean
    public ICacheableService getMockedEntityService() {
      return new ICacheableService() {
        private final Random random = new Random();

        @Cacheable(value = CACHE_NAME,key = "#root.methodName.concat('_').concat(#p0)")
        public String methodA(int length) {
          methodInvocations.incrementAndGet();
          LOG.debug("Invoking methodA");
          char[] chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789".tocharArray();
          StringBuilder sb = new StringBuilder();
          for (int i=0; i<length; i++) {
            sb.append(chars[random.nextInt(chars.length)]);
          }
          String result = sb.toString();
          LOG.debug("Returning {} for length: {}",result,length);
          return result;
        }

        @Cacheable(value = CACHE_NAME,key = "#root.methodName.concat('_').concat(#p0)")
        public String methodB(String name) {
          methodInvocations.incrementAndGet();
          LOG.debug("Invoking methodB");

          String rand = methodA(name.length());
          String result = name+"_"+rand;
          LOG.debug("Returning {} for name: {}",name);
          return result;
        }
      };
    }
  }
}

The actual work of both methods is not important to the test case itself, because only the cache should be tested

In some way, I understand why the results of nested operations are not cached, but I want to know whether there are available configurations. I haven't thought of enabling caching for the return values of nested cacheable operations

I know that refactoring and providing the return value of nested operations as parameters of external operations will work, but this may involve changing the configuration of some operations (and unit testing them) or other solutions (if in our specific case, this will be preferable)

Solution

The problem is that you access methoda directly from methodb, so this prevents Java proxies from handling caching mechanisms In addition, you did not add the @ enablecaching annotation, so there was actually no caching at all in your test

The following tests show that if you browse the proxy created by spring correctly, the nested cache mode will work as expected:

import static org.junit.Assert.*;

import java.util.Arrays;
import java.util.Random;
import java.util.concurrent.atomic.AtomicInteger;

import javax.annotation.Resource;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cache.concurrent.ConcurrentMapCache;
import org.springframework.cache.support.SimpleCacheManager;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = { SpringCacheTest.Config.class })
@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_CLASS)
public class SpringCacheTest {

    private final static String CACHE_NAME = "testCache";
    private final static AtomicInteger methodInvocations = new AtomicInteger(0);

    public interface ICacheableService {

        String methodA(int length);

        String methodB(String name);
    }

    @Resource
    private ICacheableService cache;

    @Test
    public void testNestedCaching() {

        String name = "test";
        cache.methodB(name);
        assertEquals(methodInvocations.get(),2);

        cache.methodA(name.length());
        // should only be 2 as methodA for this length was already invoked before
        assertEquals(methodInvocations.get(),2);
    }

    @Configuration
    @EnableCaching
    public static class Config {

        @Bean
        public CacheManager getCacheManager() {
            SimpleCacheManager cacheManager = new SimpleCacheManager();
            cacheManager.setCaches(Arrays.asList(new ConcurrentMapCache(CACHE_NAME)));
            return cacheManager;
        }

        @Bean
        public ICacheableService getMockedEntityService() {
            return new ICacheableService() {
                private final Random random = new Random();

                @Autowired
                ApplicationContext context;

                @Override
                @Cacheable(value = CACHE_NAME,key = "#root.methodName.concat('_').concat(#p0)")
                public String methodA(int length) {
                    methodInvocations.incrementAndGet();
                    System.out.println("Invoking methodA");
                    char[] chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789".tocharArray();
                    StringBuilder sb = new StringBuilder();
                    for (int i = 0; i < length; i++) {
                        sb.append(chars[random.nextInt(chars.length)]);
                    }
                    String result = sb.toString();
                    System.out.println("Returning " + result + " for length: " + length);
                    return result;
                }

                @Override
                @Cacheable(value = CACHE_NAME,key = "#root.methodName.concat('_').concat(#p0)")
                public String methodB(String name) {
                    methodInvocations.incrementAndGet();
                    System.out.println("Invoking methodB");
                    ICacheableService cache = context.getBean(ICacheableService.class);
                    String rand = cache.methodA(name.length());
                    String result = name + "_" + rand;
                    System.out.println("Returning " + result + " for name: " + name);
                    return result;
                }
            };
        }
    }
}
The content of this article comes from the network collection of netizens. It is used as a learning reference. The copyright belongs to the original author.
THE END
分享
二维码
< <上一篇
下一篇>>