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:
@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;
}
}
The above controller depends on MessageStorage, simple DAO, that is defined as follows:
public interface MessageStorage {
Message findMessage(long id);
Collection<Message> findAllMessages();
void addMessage(Message message);
}
The only implementation of MessageStorage is MemoryMessageStorage:
@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) {
// ...
}
}
Whit the minimum dependencies presented below we are able to run the application (See Download section to get complete application).
<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>
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:
<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>
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:
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>1.6.1</version>
<type>jar</type>
<scope>compile</scope>
</dependency>
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)
@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) {
// ...
}
}
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 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>
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 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>
Configure cache manager so that it uses Ehcache configuration. To do so, add configLocation property to cacheManager bean in Spring context file:
<bean id="cacheManager" class="org.springframework.cache.ehcache.EhCacheManagerFactoryBean">
<property name="configLocation" value="/WEB-INF/ehcache.xml"/>
</bean>
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
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):
@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);
}
To make the testing easier we use two more dependencies: Spring Test and Mockito:
<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>
The test class will run with SpringJUnit4ClassRunner:
@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));
}
}
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 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>
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:
Pingback: Blog bookmarks 08/10/2010 « My Diigo bookmarks
Pingback: Cachés y Spring « Java Mania
Pingback: EhCache Upgrade: A Farewell to Spring Modules « A Technical Discourse
Pingback: Cache Spring JDBC template using ehcache annotations « Thomsun's Blog
Pingback: EhCache Upgrade: A Farewell to Spring Modules « A Technical Discourse
Pingback: Skype and Agile software development were really hot on our blog this year