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:
Client
, which defines queryWithCache
. queryWithCache
is called when a customer logs in.ClientCacheManager
, which defines updateCache
. updateCache
is not called by user actions, but by a @PostConstruct
method and by the scheduler.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?
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.