Instead of writing it on our own and reinventing the wheel, we can use Spring Cache.
Its biggest advantage is unobtrusiveness - besides few annotations we keep our code intact.
Let's see how to do it.
At first let's have a look at some fake service that is the reason for using cache (in real life it may be some database call, web service etc.)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public class SlowService { | |
private Logger logger = LoggerFactory.getLogger(SlowService.class.getName()); | |
public boolean isVipClient(String clientName) { | |
logger.debug("Checking " + clientName); | |
boolean result = false; | |
if (clientName.hashCode() % 2 == 0) { | |
result = true; | |
} | |
return result; | |
} | |
} |
We will cache the invocations of isVipClient(...)
Let's start with adding dependencies to our project:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<dependency> | |
<groupId>ch.qos.logback</groupId> | |
<artifactId>logback-classic</artifactId> | |
<version>1.0.13</version> | |
</dependency> | |
<dependency> | |
<groupId>org.springframework</groupId> | |
<artifactId>spring-context</artifactId> | |
<version>3.2.3.RELEASE</version> | |
</dependency> | |
<dependency> | |
<groupId>org.springframework</groupId> | |
<artifactId>spring-test</artifactId> | |
<version>3.2.3.RELEASE</version> | |
<scope>test</scope> | |
</dependency> | |
<dependency> | |
<groupId>junit</groupId> | |
<artifactId>junit</artifactId> | |
<version>4.11</version> | |
<scope>test</scope> | |
</dependency> | |
<dependency> | |
<groupId>org.kubek2k</groupId> | |
<artifactId>springockito</artifactId> | |
<version>1.0.5</version> | |
<scope>test</scope> | |
</dependency> |
We'd like to use xml-free spring config, so we will define our beans in java:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@Configuration | |
@EnableCaching | |
@ComponentScan("pl.mjedynak") | |
public class AppConfig { | |
public static final String VIP_CLIENTS_CACHE = "vipClients"; | |
@Bean | |
public SlowService slowService() { | |
return new SlowService(); | |
} | |
@Bean | |
public CacheManager cacheManager() { | |
SimpleCacheManager cacheManager = new SimpleCacheManager(); | |
cacheManager.setCaches(Arrays.asList(new ConcurrentMapCache(VIP_CLIENTS_CACHE))); | |
return cacheManager; | |
} | |
} |
We have our SlowService defined as a bean. We also have the cacheManager along with a cache name and its implementation - in our case it's based on ConcurrentHashMap. We can't forget about enabling caching via annotation.
The only thing to do is to add annotation @Cacheable with a cache name to our method:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@Cacheable(AppConfig.VIP_CLIENTS_CACHE) | |
public boolean isVipClient(String clientName) { ... |
How to test our cache?
One way to do it is to check if the double invocation of our method with the same parameter will actually execute the method only once.
We will use springockito to inject a spy into our class and verify its behaviour.
We need to start with a spring test context:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?xml version="1.0" encoding="UTF-8"?> | |
<beans xmlns="http://www.springframework.org/schema/beans" | |
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" | |
xmlns:context="http://www.springframework.org/schema/context" | |
xmlns:mockito="http://www.mockito.org/spring/mockito" | |
xsi:schemaLocation="http://www.springframework.org/schema/beans | |
http://www.springframework.org/schema/beans/spring-beans-3.1.xsd | |
http://www.springframework.org/schema/context | |
http://www.springframework.org/schema/context/spring-context.xsd | |
http://www.mockito.org/spring/mockito classpath:spring/mockito.xsd"> | |
<context:component-scan base-package="pl.mjedynak"/> | |
<mockito:spy beanName="slowService"/> | |
</beans> |
Basically the context will read the configuration from java class and will replace the slowService bean with a spy. It would be nicer to do it with an annotation but at the time of this writing the @WrapWithSpy doesn't work (https://bitbucket.org/kubek2k/springockito/issue/33).
Here's our test:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@RunWith(SpringJUnit4ClassRunner.class) | |
@ContextConfiguration | |
public class SlowServiceIntegrationTest { | |
@Autowired | |
private SlowService slowService; | |
@Test | |
public void shouldUseCacheWhenCallingCachedMethodWithTheSameParameter() { | |
// given | |
String clientName = "WakaWaka"; | |
// when | |
slowService.isVipClient(clientName); | |
slowService.isVipClient(clientName); | |
// then | |
verify(slowService).isVipClient(clientName); | |
} | |
} |
This was the very basic example, spring cache offers more advanced features like cache eviction and update.
The whole project can be found at github.