javaspringcaching

Cannot load data with @CachePut at startup


I have a Spring (5.3.35) program on Tomcat which is automatically updating a cached result (no parameters):

@Component
class ClientImpl
    implements Client, ClientCacheManager {

    @Override
    @Cacheable(Configurator.CACHE_NAME)
    public String queryWithCache() {
        return this.query();
    }

    @Override
    @CachePut(Configurator.CACHE_NAME)
    @Scheduled(cron = "#{configuration.cacheUpdateCron}")
    public String updateCache() {
        log.debug("Updating query cache");
        return this.query();
    }

    @Override
    public String query() {
        [...] // do stuff, return String
    }
}

There are the two interfaces:

Since I want that, at startup, before any customer logs in, the cache is filled, I did define a CacheInitializer component:

@Component
public class CacheInitializer{

    private static final Logger log = LoggerFactory.getLogger(InicializadorCacheJson.class);

    @Autowired
    private ClientCacheManager clientCacheManager;

    @PostConstruct
    void onStartup() {
        log.debug("Startup data loading");
        this.clientCacheManager.updateCache();
        log.debug("Startup data loaded");
    }
}

When the server is started, the messages 'startup data loading/loaded' show up and there is no indication of error. But, the first time a user is logged in, queryWithCache is invoked and, instead of returning the cached data, the query method is invoked. After this, subsequent invocations of queryWithCache do make use of the cached result.

What could cause this issue?


Solution

  • Don't use @PostConstruct for tasks like this. Those @PostConstruct methods are called after object construction and dependency injection. There is no garantee that proxies for applying AOP have been created at that point. Leading you to invoke the method on an early instance without caching behavior.

    What you should do instead is defer this invocation after you are sure that the application context has been fully loaded. Now in a Spring Boot application that would be fairly easy by providing an ApplicationRunner which will be invoked after context initialization. As you are in a plain Spring application you can use an ApplicationListener and react to ContextRefreshedEvent which will be fired when the ApplicationContext is ready for use.

    @Component
    public class InitializationListener implements ApplicationListener<ContextRefreshedEvent> {
    
      private final Logger log = LoggerFactory.getLogger(getClass());
    
      public void onApplicationEvent(ContextRefreshedEvent evt) {
        var ccm = evt.getApplicationContext().getBean(ClientCacheManager.class);
        log.debug("Startup data loading");
        ccm.updateCache();
        log.debug("Startup data loaded");
      }
    }
    

    The ContextRefreshedEvent contains a link to the ApplicationContext. You can use this ApplicationContext to obtain the bean you need for calling methods on it.

    If you prefer to use injection make sure to mark it with @Lazy just to be sure not to trigger early initialization of beans.

    @Component
    public class InitializationListener implements ApplicationListener<ContextRefreshedEvent> {
    
      private final Logger log = LoggerFactory.getLogger(getClass());
      private final ClientCacheManager ccm;
    
      public InitializationListener(@Lazy ClientCacheManager ccm) {
        this.ccm=ccm;
      }
      public void onApplicationEvent(ContextRefreshedEvent evt) {
        log.debug("Startup data loading");
        this.ccm.updateCache();
        log.debug("Startup data loaded");
      }
    }
    

    Finally if you don't want to extend ApplicationListener you can also use the @EventListener annotation on a method.

    @EventListener(ContextRefreshedEvent.class)
    public void onContextRefreshed() {
      // your logic here.
    }
    

    Either will work, for an ApplicationContext the interface approach is a bit easier, while for you as a programmer the annotation approach might be easier. Either will work.