How to profit from the new Ehcache Annotations for Spring? As a Java developer you would like to know, don’t you? ehcache-spring-annotations – licensed under the Apache License, Version 2.0 – is a project that simplifies caching in Spring based application using popular Ehcache library. Version 1.1.2 of ehcache-spring-annotations has been released only recently. In this article I will present how to create a web project that benefits from ehcache-spring-annotations
Create a new web project
For the showcase, a Spring MVC based web project will be used. If you use Eclipse make sure you have m2eclipse installed, then create a Maven project using JEE 5 web application archetype (group id: org.codehaus.mojo.archetypes, artifact id: webapp-jee5): The above archetype works perfectly with Eclipse 3.6.
Project structure
I created a simple web project that consists of one controller: MessageController:
[java] @Controller public class MessageController { @Autowired(required = true) private MessageStorage messageStorage; public MessageController(MessageStorage messageStorage) { super(); this.messageStorage = messageStorage; } public MessageController() { } @RequestMapping(method = RequestMethod.GET, value = "/message/add") public ModelAndView messageForm() { return new ModelAndView("message-form", "command", new Message()); } @RequestMapping(method = RequestMethod.POST, value = "/message/add") public ModelAndView addMessage(@ModelAttribute Message message) { messageStorage.addMessage(message); return getMessageById(message.getId()); } @RequestMapping(method = RequestMethod.GET, value = "/message/{id}") public ModelAndView getMessageById(@PathVariable("id") long id) { Message message = messageStorage.findMessage(id); ModelAndView mav = new ModelAndView("message-details"); mav.addObject("message", message); return mav; } @RequestMapping(method = RequestMethod.GET, value = "/message") public ModelAndView getAllMessages() { Collection<Message> messages = messageStorage.findAllMessages(); ModelAndView mav = new ModelAndView("messages"); mav.addObject("messages", new CollectionOfElements(messages)); return mav; } } [/java]
The above controller depends on MessageStorage, simple DAO, that is defined as follows:
[java] public interface MessageStorage { Message findMessage(long id); Collection<Message> findAllMessages(); void addMessage(Message message); } [/java]
The only implementation of MessageStorage is MemoryMessageStorage:
[java] @Component public class MemoryMessageStorage implements MessageStorage { private Map<Long, Message> messages; private AtomicLong newID; public MemoryMessageStorage() { // ... // initialize some messages addMessage(new Message("user:1", "content-1")); addMessage(new Message("user:2", "content-2")); addMessage(new Message("user:3", "content-3")); addMessage(new Message("user:4", "content-4")); addMessage(new Message("user:5", "content-5")); } @Override public Message findMessage(long id) { // ... } @Override public Collection<Message> findAllMessages() { // ... } @Override public void addMessage(Message message) { // ... } } [/java]
Whit the minimum dependencies presented below we are able to run the application (See Download section to get complete application).
[xml] <dependencies> <dependency> <groupId>javax.servlet</groupId> <artifactId>servlet-api</artifactId> <version>2.5</version> <scope>provided</scope> </dependency> <dependency> <groupId>javax.servlet.jsp</groupId> <artifactId>jsp-api</artifactId> <version>2.1</version> <scope>provided</scope> </dependency> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.8.1</version> <scope>test</scope> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-webmvc</artifactId> <version>3.0.3.RELEASE</version> <type>jar</type> <scope>compile</scope> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-oxm</artifactId> <version>3.0.3.RELEASE</version> <type>jar</type> <scope>compile</scope> </dependency> <dependency> <groupId>javax.servlet</groupId> <artifactId>jstl</artifactId> <version>1.2</version> <type>jar</type> <scope>compile</scope> </dependency> </dependencies> [/xml]
Introducing Ehcache Annotations for Spring to the web project
Now it’s time to add caching capabilities to the application. We provide caching to MemoryMessageStorage. First of all, add the required dependencies to the POM: Ehcache Annotations for Spring dependency:
[xml] <dependency> <groupId>com.googlecode.ehcache-spring-annotations</groupId> <artifactId>ehcache-spring-annotations</artifactId> <version>1.1.2</version> <type>jar</type> <scope>compile</scope> </dependency> [/xml]
As of time of writing there was version 2.2.0 of Ehcache available. But we use version 2.1.0 that Ehcache Annotations for Spring 1.1.2 depends on. I also added SLF API implementation:
[xml] <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-log4j12</artifactId> <version>1.6.1</version> <type>jar</type> <scope>compile</scope> </dependency> [/xml]
With the above dependencies we are able to use Ehcache Annotations for Spring. We add the annotations to MemoryMessageStorage as mentioned before. There are a few simple rules:
- use cache with name “messageCache” for caching messages for invocations of findMessage(long)
- use cache with name “messagesCache” for caching messages for invocations of findAllMessages()
- remove all elements from “messagesCache” after the invocation of addMessage(Message)
To achieve the above we use @Cachable and @TriggersRemove annotations aspresented below:
[java] @Component public class MemoryMessageStorage implements MessageStorage { private Map<Long, Message> messages; private AtomicLong newID; public MemoryMessageStorage() { // ... } @Override @Cacheable(cacheName = "messageCache") public Message findMessage(long id) { // ... } @Override @Cacheable(cacheName = "messagesCache") public Collection<Message> findAllMessages() { // ... } @Override @TriggersRemove(cacheName = "messagesCache", when = When.AFTER_METHOD_INVOCATION, removeAll = true) public void addMessage(Message message) { // ... } } [/java]
Spring and Ehcache configuration
Having annotations in place, we need to configure the application to use them. This is done via Spring configuration file:
[xml] <?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:oxm="http://www.springframework.org/schema/oxm" xmlns:mvc="http://www.springframework.org/schema/mvc" xmlns:ehcache="http://ehcache-spring-annotations.googlecode.com/svn/schema/ehcache-spring" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.0.xsd http://www.springframework.org/schema/oxm http://www.springframework.org/schema/oxm/spring-oxm-3.0.xsd http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc-3.0.xsd http://ehcache-spring-annotations.googlecode.com/svn/schema/ehcache-spring http://ehcache-spring-annotations.googlecode.com/svn/schema/ehcache-spring/ehcache-spring-1.1.xsd"> <ehcache:annotation-driven /> <ehcache:config cache-manager="cacheManager"> <ehcache:evict-expired-elements interval="60" /> </ehcache:config> <bean id="cacheManager" class="org.springframework.cache.ehcache.EhCacheManagerFactoryBean"/> <!-- rest of the file omitted --> </beans> [/xml]
The last thing to do is to add the Ehcache configuration. Create file named ehcache.xml and place it in /WEB-INF folder of web application:
[xml] <?xml version="1.0" encoding="UTF-8"?> <ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="http://ehcache.org/ehcache.xsd"> <defaultCache eternal="true" maxElementsInMemory="100" overflowToDisk="false" /> <cache name="messageCache" maxElementsInMemory="10" eternal="true" overflowToDisk="false" /> <cache name="messagesCache" maxElementsInMemory="10" eternal="true" overflowToDisk="false" /> </ehcache> [/xml]
Configure cache manager so that it uses Ehcache configuration. To do so, add configLocation property to cacheManager bean in Spring context file:
[xml] <bean id="cacheManager" class="org.springframework.cache.ehcache.EhCacheManagerFactoryBean"> <property name="configLocation" value="/WEB-INF/ehcache.xml"/> </bean> [/xml]
The configuration is done. Run the application on Tomcat 6 server (Run As – Run on server) and observe log output – if DEBUG is enabled you will find similar entries: DEBUG [net.sf.ehcache.Cache]: Initialised cache: messagesCache DEBUG [net.sf.ehcache.Cache]: Initialised cache: messageCache
Do some interactions with the application to observe the caching behavior (make sure DEBUG log level is enabled). The actions are (to see XML output replace “html” with “xml”):
- get the list of all messages – http://localhost:8080/esa/message.html
- get a message by id – http://localhost:8080/esa/message/{id}.html
- add a new message (from) – http://localhost:8080/esa/message/add.html
If you execute getMessages() of MessageController for the first time you should see: DEBUG [com.googlecode.ehcache.annotations.interceptor.EhCacheInterceptor]: Generated key '167334053963' for invocation: ReflectiveMethodInvocation: public abstract java.util.Collection com.goyello.esa.storage.MessageStorage.findAllMessages(); target is of class [com.goyello.esa.storage.MemoryMessageStorage] DEBUG [com.goyello.esa.storage.MemoryMessageStorage]: == got all messages, size=5
While invoking the same method for the second time, the second line from the above log output is gone, because the collection of all messages was retrieved from the cache. To remove cache from the messages in a simple way call addMessage() of MessageController. Then repeat previous steps to make sure the cache was cleared properly.
Unit testing
To be sure that the caching works without reading the log files we create unit tests for caching. For this tutorial we modify the MessageStorage interface and we add void setDelegate(MessageStorage storageDelegate) method to it. The given delegate will be used to check if the caching actually works. The implementation class changes are (for all other methods similarly):
[java] @Override @Cacheable(cacheName = "messageCache") public Message findMessage(long id) { LOG.debug("== find message by id={}", id); if(storageDelegate != null) storageDelegate.findMessage(id); return messages.get(id); } [/java]
To make the testing easier we use two more dependencies: Spring Test and Mockito:
[xml] <dependency> <groupId>org.mockito</groupId> <artifactId>mockito-all</artifactId> <version>1.8.5</version> <type>jar</type> <scope>compile</scope> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-test</artifactId> <version>3.0.3.RELEASE</version> <type>jar</type> <scope>compile</scope> </dependency> [/xml]
The test class will run with SpringJUnit4ClassRunner:
[java] @RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(locations = { "/spring-context-test.xml" }) public class CachingTest { @Autowired ApplicationContext context; @Autowired CacheManager cacheManager; MessageStorage storage; MessageStorage storageDelegate; MessageController controller; @Before public void before() throws Exception { storageDelegate = Mockito.mock(MessageStorage.class); storage = (MessageStorage) context.getBean("messageStorage"); storage.setDelegate(storageDelegate); controller = new MessageController(storage); cacheManager.clearAll(); } @Test public void testCaching_MessagesCache() { controller.getAllMessages(); controller.getAllMessages(); verify(storageDelegate, times(1)).findAllMessages(); } @Test public void testCaching_MessagesCacheRemove() { controller.getAllMessages(); controller.getAllMessages(); controller.addMessage(new Message()); controller.getAllMessages(); verify(storageDelegate, times(2)).findAllMessages(); verify(storageDelegate, times(1)).addMessage(any(Message.class)); } @Test public void testCaching_MessageCache() { controller.getMessageById(1); controller.getMessageById(1); controller.addMessage(new Message()); controller.getMessageById(1); verify(storageDelegate, times(1)).findMessage(1); verify(storageDelegate, times(1)).addMessage(any(Message.class)); } } [/java]
The delegate is a mock object that is used for checking the real number of invocations on a MessageStorage. The Spring context for test looks like this:
[xml] <?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:ehcache="http://ehcache-spring-annotations.googlecode.com/svn/schema/ehcache-spring" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.0.xsd http://ehcache-spring-annotations.googlecode.com/svn/schema/ehcache-spring http://ehcache-spring-annotations.googlecode.com/svn/schema/ehcache-spring/ehcache-spring-1.1.xsd"> <ehcache:annotation-driven /> <ehcache:config cache-manager="cacheManager"> <ehcache:evict-expired-elements interval="60" /> </ehcache:config> <bean id="cacheManager" class="org.springframework.cache.ehcache.EhCacheManagerFactoryBean"> <property name="configLocation" value="classpath:ehcache-test.xml" /> </bean> <bean id="messageStorage" class="com.goyello.esa.storage.MemoryMessageStorage" /> </beans> [/xml]
Now we are ready to run the created test to verify the actual invocations on MemoryMessageStorage. Results we wish to see:
The summary
Using Ehcache Spring Annotations is straightforward. With a couple of simple steps you may introduce caching in your application. I strongly suggest that before using the library in live project you visit the project website and read the documentation to be aware of other functionalities not covered in this article.
Update: Spring 3.1 release, among many enhancements, brings native support for method caching with so-called cache abstraction. To read more about this feature check Quick start with method caching using Spring 3.1 and Ehcache follow-up post.
References
Ehcache Spring Annotations project page – http://code.google.com/p/ehcache-spring-annotations/ Ehcache project page – http://ehcache.org/ Spring 3.0 reference – http://static.springsource.org/spring/docs/3.0.x/spring-framework-reference/html/ Mockito – http://mockito.org/ Maven2 – http://maven.apache.org/
Download and run the project
The complete project can be downloaded here: demo-project After you download the file, unpack it, navigate to the project folder and run following command (maven2 needs to be installed): mvn clean tomcat:run
Then navigate to you browser to see the working application:
Really nice article …. easy to read and understand.
Thanks a lot!
Thx a lot!
Great article. I’m one of the authors of the ehcache-spring-annotations library and appreciate the great writeup.
Thanks!
-Eric
Thank you!
I followed all the steps mentioned above but it doesn’t seem to work. I do not see any ‘Initialised cache: ‘ message. My log4j level is set to INFO.
Don’t I need to set ‘hibernate.cache.use_second_level_cache’ to true in application-context.xml and set values for ‘hibernate.cache.provider_class’?
No, you don’t need hibernate cache provider, because this is not hibernate cache. Checkout demo project attached to the post, you will find all the configuration there.
I’ve used this project in tomcat without any error but when moving it to
virgo web server got some error that is web application failed to
start, how to resolve this plz help me
I build projects usually on Tomcat server. Unfortunately, I don’t know Virgo and I never used it. So I think I cannot help you here. I am really sorry.
Great Article. Explained in very clear fashion. It worked the first time like Magic. Thank you very much.
Great article.That was what I was searching for…
Could you provide the complete project
When selfPopulating=”true” for @Cacheable and ehcache:annotation-driven settings have self-populating-cache-scope=”method” It creates different instances for each annotated method. How do I use @TriggersRemove to remove the item from its respective cache instance
I have downloaded above project and executed as it is ,
It seems need to modify the config,
First calll to – http://localhost:8080/esa/message.html,
it shows below output
DEBUG [com.googlecode.ehcache.annotations.interceptor.EhCacheInterceptor]: Generated key ‘183410223502399’ for invocation: ReflectiveMethodInvocation:
public abstract com.goyello.esa.model.Message com.goyello.esa.storage.MessageStorage.findMessage(long); target is of class [com.goyello.esa.storage.M
emoryMessageStorage]
Then click id – 1 to view message – It shows below message
DEBUG [com.goyello.esa.storage.MemoryMessageStorage]: == find message by id=1
DEBUG [com.googlecode.ehcache.annotations.interceptor.EhCacheInterceptor]: Generated key ‘167334053963’ for invocation: ReflectiveMethodInvocation: pu
blic abstract java.util.Collection com.goyello.esa.storage.MessageStorage.findAllMessages(); target is of class [com.goyello.esa.storage.MemoryMessage
Storage]
Click viewALL to see list of message – below o/p is shown
DEBUG [com.googlecode.ehcache.annotations.interceptor.EhCacheInterceptor]: Generated key ‘183410223502399’ for invocation: ReflectiveMethodInvocation:
public abstract com.goyello.esa.model.Message com.goyello.esa.storage.MessageStorage.findMessage(long); target is of class [com.goyello.esa.storage.M
emoryMessageStorage]
BUT again calling to id – 1 why below o/p is shown, data should retrieve from cache ??
so it means it is not caching data..
DEBUG [com.googlecode.ehcache.annotations.interceptor.EhCacheInterceptor]: Generated key ‘167334053963’ for invocation: ReflectiveMethodInvocation: pu
blic abstract java.util.Collection com.goyello.esa.storage.MessageStorage.findAllMessages(); target is of class [com.goyello.esa.storage.MemoryMessage
Storage]
Very nice article/tutorial – u click once and it’s running … unlike so many others…n/c
If you want to use gradle instead of maven, just put the following into a build.gradle file and import:
—————————————————————–
apply plugin: ‘java’
apply plugin: ‘eclipse’
repositories {
mavenCentral()
}
dependencies {
compile ‘org.springframework:spring-webmvc:3.2.3.RELEASE’
compile ‘org.springframework:spring-web:3.2.3.RELEASE’
compile ‘org.springframework:spring-context:3.2.3.RELEASE’
compile ‘org.springframework:spring-context-support:3.2.3.RELEASE’
compile ‘com.googlecode.ehcache-spring-annotations:ehcache-spring-annotations:1.2.0’
compile ‘org.slf4j:slf4j-log4j12:1.6.1’
testCompile ‘org.mockito:mockito-all:1.9.5’
testCompile ‘junit:junit:4.11’
testCompile ‘org.springframework:spring-test:3.2.3.RELEASE’
}
Great article!