I'm trying to work out how I can add caching to method calls on a third party Java class. I'm using Spring Boot for my application.
I've come up with this class in my attempts to get caching working.
package test;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cache.interceptor.CacheOperation;
import org.springframework.cache.interceptor.CacheProxyFactoryBean;
import org.springframework.cache.interceptor.CacheableOperation;
import org.springframework.cache.interceptor.NameMatchCacheOperationSource;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.Collection;
import java.util.Date;
import java.util.HashSet;
@SpringBootApplication
@EnableCaching
@Configuration
public class MyApp {
public static void main(String[] args) {
ConfigurableApplicationContext context = SpringApplication.run(MyApp.class, args);
Greeter greeter = context.getBean(Greeter.class);
System.out.println(new Date() + " : " + greeter.getGreeting("Bob"));
System.out.println(new Date() + " : " +greeter.getGreeting("Fred"));
System.out.println(new Date() + " : " +greeter.getGreeting("Bob"));
System.out.println(new Date() + " : " +greeter.getGreeting("Fred"));
System.out.println(new Date() + " : " +greeter.getGreeting("Bob"));
System.out.println(new Date() + " : " +greeter.getGreeting("Fred"));
}
@Bean
public Greeter greeter() {
final NameMatchCacheOperationSource nameMatchCacheOperationSource = new NameMatchCacheOperationSource();
Collection<CacheOperation> cacheOperations = new HashSet<CacheOperation>();
cacheOperations.add(new CacheableOperation.Builder().build());
nameMatchCacheOperationSource.addCacheMethod("*", cacheOperations);
CacheProxyFactoryBean cacheProxyFactoryBean = new CacheProxyFactoryBean();
cacheProxyFactoryBean.setTarget(new MySlowGreeter());
cacheProxyFactoryBean.setProxyInterfaces(new Class[] {Greeter.class});
cacheProxyFactoryBean.setCacheOperationSources(nameMatchCacheOperationSource);
cacheProxyFactoryBean.afterPropertiesSet();
return (Greeter) cacheProxyFactoryBean.getObject();
}
interface Greeter {
String getGreeting(String name);
}
class MySlowGreeter implements Greeter {
public String getGreeting(String name) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "Hello " + name;
}
}
}
The hope is that I'd be able create a bean in my Spring config that wraps calls to Greeter.getGreeting(..)
and returns cached results if they exist. However no caching is taking place.
Any ideas?
Alright, I have more information for you. But first, I want to address some issues with your code above.
1) The first issue involves your use of the o.s.cache.interceptor.CacheProxyFactoryBean
in the "greeter" @Bean
definition of your application @Configuration
class (i.e. "MyApp").
Anytime you use 1 of Spring's FactoryBeans
(e.g. CacheProxyFactoryBean
) or implement your own, what you return from the @Bean
method is the FactoryBean
itself, not the product of the FactoryBean
. So, instead of return factoryBean.getObject()
, you would return the FactoryBean
, like so...
@Bean
GreeterFactoryBean greeter() {
GreeterFactoryBean factoryBean = new GreeterFactoryBean();
factoryBean.set...
return factoryBean;
}
Here, GreeterFactoryBean
implements o.s.beans.factory.FactoryBean
.
As Spring's Reference Documentation points out, the Spring container knows to return the product of the FactoryBean
(e.g. a [Singleton] Greeter
instance) and not the FactoryBean
itself, as the "defined" bean in the Spring container for this @Bean
method. The name of the bean will be the name of the @Bean
method if not explicitly defined with @Bean
(e.g. @Bean("Greeter")
).
If the FactoryBean
also implements Spring's lifecycle callback interfaces (e.g. o.s.beans.factory.InitializingBean
or o.s.beans.factory.DisposableBean
, etc), the Spring container will know to invoke those lifecycle callbacks at the "appropriate" time, during the Spring container's initialization process.
Therefore, it is unnecessary to invoke either CacheProxyFactoryBean.afterPropertiesSet()
or CacheProxyFactoryBean.getObject()
inside the "greeter" @Bean
definition. Doing so actually violates the Spring container's initialization contract, and you could run into premature "initialization" problems, especially if the provided FactoryBean
implements other Spring container interfaces (e.g. BeanClassLoaderAware
, or EnvironmentAware
, and so on and so forth).
Be careful!
2) Second, this is less of an issue/problem with your example than something to just be aware of. You stated in this SO post that you are trying to add "caching" behavior to 3rd party library classes.
The approach you use above is applicable only when you are able to instantiate the 3rd party class (e.g. Greeter
) in your application yourself.
However, if the 3rd party library or framework instantiates the class on your behalf, as a result of configuring the library/framework (e.g. think JDBC Driver and Hibernate), you lose the ability to introduce caching behavior to this class in your application, unless your resort to Load-Time Weaving (LTW). Read the docs for more details.
Alright, onto the solution.
I wrote a test to reproduce this problem and to better understand what is happening inside the Spring Framework. You can find my completed test here.
TestConfigurationOne
is effectively the same approach you employed to
create caching proxies programmatically, with modifications based on what I discussed above, and also, to address what I think is a bug in the core Spring Framework (NOTE: I was using Spring Framework 5.0.1.RELEASE
in my test).
In order to get your configuration approach with CacheProxyFactoryBean
to work, I needed to extend the CacheProxyFactoryBean
class. In addition to extending the CacheProxyFactoryBean
, I also needed to implement the SmartInitializingSingleton
interface and the BeanFactoryAware
interface for reasons what will become apparent in a moment. See 9 for the complete implementation.
Internally, Spring Framework's o.s.cache.interceptor.CacheProxyFactoryBean
is making use of a o.s.cache.interceptor.CacheInterceptor
. It also goes onto "initialize" this CacheInterceptor
instance, here and here. However, this does not complete the initialization since the CacheInterceptor
also, indirectly, implements the SmartInitializingSingleton
interface, by extending CacheAspectSupport
. If the SmartInitializingSingleton
implemented CacheInterceptor.afterSingletonsInstantiated()
method is never called, then the initialized
bit is never tripped, and any cacheable operations will not be cached, resulting in the cacheable operation being invoked every single time (thus, ignoring any introduced caching behavior).
This is the exact reason why I extended the CacheProxyFactoryBean
in my test class, to capture the "mainInterceptor" (i.e. the CacheInterceptor
) and then call the afterSingletonsInstantiated()
method at the appropriate moment during the Spring container's initialization phase, which is why my SmartCacheProxyFactoryBean
extension implements SmartInitializingSingleton
, to delegate to the CacheInterceptor.afterSingletonsInstantiated()
method.
Additionally, the CacheInterceptor
is BeanFactoryAware
and requires the Spring BeanFactory
to carry out its function, hence the reason I inspect this "mainInterceptor" and set the BeanFactory
appropriately, here.
Another approach, which I recommend, is to use TestConfigurationTwo
.
In this configuration, I am configuring the Spring AOP Advice (i.e. the CacheInterceptor
) directly, returning it from the @Bean
definition method, "cacheInterceptor", which allows the Spring container to invoked the lifecycle callbacks, appropriately.
Then, I go onto use this Advice in the cache proxy creation of the 3rd party class (i.e. "Greeter
").
You should be careful to pass in the bean created from the "cacheInterceptor
" bean definition to the "greeter
" bean definition, like so. If you were to invoke the "cacheInterceptor
" bean definition method from within your bean "greeter
" bean definition, like so many users inappropriately do (for example), then you would forgo the Spring container lifecycle callbacks! Don't do this! The reasons for this are explained here.
Also, to read more information on the programmatic creation of proxies, you should read this.
Ok, that about covers it.
The test class I provided (i.e. "ProgrammaticCachingWithSpringIntegrationTests
"). Feel free to play around with it and let me know if you have any follow-up questions.
Hope this helps!
Cheers!