Spring 4 Cache Tutorial with EhCache

Caching is crucial for today’s high performance hungry applications. Adding Caching in your Spring based application is rather trivial. There are many caching solutions available in market today namely EhCache, Guava Cache, Caffeine Cache, OScache, JBoss Cache,etc. Spring provides seamless integration with these caching solution using minimal configuration.

Post Spring 4 Caching Annotations Tutorial contains detailed examples with explanations of all spring Caching Annotataions.

Cache Abstraction

Spring Cache Abstraction is a set of interfaces and annotations, used for applying caching to Java methods. The idea is to cache the returned value of a method for a given input[method argument e.g.], and for any further request with same input, return the result from cache without even executing the method, thus reducing the number of executions. Spring’s org.springframework.cache.Cache and org.springframework.cache.CacheManager are main interfaces used for cache management. Do note that Spring Caching is still an abstraction [not a cache implementation] and requires an actual implementation in order to store the cache data.

Caching Providers/Implementations

Spring provides few out of the box abstraction implementations: JDK java.util.concurrent.ConcurrentMap based caches, EhCache, Caffeine, Gemfire cache, Guava caches and JSR-107 compliant caches. Other cache providers can be integrated with minimal configuration. For this post, we will use EhCache as a provider.

Caching Annotations

  • @Cacheable : triggers cache population
  • @CacheEvict : triggers cache eviction
  • @CachePut : updates the cache without interfering with the method execution
  • @Caching : regroups multiple cache operations to be applied on a method
  • @CacheConfig : shares some common cache-related settings at class-level
  • @EnableCaching : Configuration level annotation which enables Caching

In this post, we will learn the basics of caching and commonly used annotations including @EnableCaching, @Cacheable & @CacheEvict using EhCache as caching provider.


Without Caching

In order to understand the need of caching and appreciate caching advantages, lets first see an example where no caching is used.

Below example demonstrates a trivial service , returning specific products. The method which actually lookup products is deliberately made slow to understand the concept.

package com.websystique.spring.model;

public class Product {

	private String name;
	private double price;

	public Product(String name, double price){
		this.name = name;
		this.price = price;
	}
	
	public String getName() {
		return name;
	}
	public void setName(String name) {
		this.name = name;
	}
	public double getPrice() {
		return price;
	}
	public void setPrice(double price) {
		this.price = price;
	}

	@Override
	public String toString() {
		return "Product [name=" + name + ", price=" + price + "]";
	}
}
package com.websystique.spring.service;

import com.websystique.spring.model.Product;

public interface ProductService {

	Product getByName(String name);
}
package com.websystique.spring.service;

import org.springframework.stereotype.Service;

import com.websystique.spring.model.Product;

@Service("productService")
public class ProductServiceImpl implements ProductService{

	@Override
	public Product getByName(String name) {
		slowLookupOperation();
		return new Product(name,100);
	}

	public void slowLookupOperation(){
		 try {
	            long time = 5000L;
	            Thread.sleep(time);
	        } catch (InterruptedException e) {
	            throw new IllegalStateException(e);
	       }
	}
}

And the main to run the application:

package com.websystique.spring.configuration;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.support.AbstractApplicationContext;

import com.websystique.spring.service.ProductService;

public class SampleApplication {

	private static final Logger logger = LoggerFactory.getLogger(SampleApplication.class);
	
	public static void main(String[] args){
		AbstractApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);

		ProductService service = (ProductService) context.getBean("productService");
		logger.info("IPhone ->" + service.getByName("IPhone"));
		logger.info("IPhone ->" + service.getByName("IPhone"));
		logger.info("IPhone ->" + service.getByName("IPhone"));
		((AbstractApplicationContext) context).close();
	}
}

If you run above example, following will be the output. Note that each lookup takes 5 seconds, even if the product we are looking for is exactly same.

18:10:05.653 [main] INFO com.websystique.spring.configuration.SampleApplication - IPhone ->Product [name=IPhone, price=100.0]
18:10:10.654 [main] INFO com.websystique.spring.configuration.SampleApplication - IPhone ->Product [name=IPhone, price=100.0]
18:10:15.655 [main] INFO com.websystique.spring.configuration.SampleApplication - IPhone ->Product [name=IPhone, price=100.0]

Now let’s enable caching and see the difference.


With Caching, using EHCache

Spring provides Caching abstractions and annotations to seamlessly adding caching support in any spring application. Main caching annotations being used are @EnableCaching, @Cacheable & @CacheEvict.

1. @Cacheable [ Used for populating and accessing Cache]

@Cacheable annotation indicates that the result of invoking a method (or all methods in a class) can be cached. A cache itself can be imagined as a key-value based Map. First time a method annotated with @Cacheable gets called, it gets executed and it’s return value is stored in Cache using a key[method parameter for instance, ]. Next time, if the method gets called using same key[same parameter for instance], the result is returned directly from Cache, method invocation does not take place.

By default it uses the method parameters [product name in this case] to compute the key , but a SpEL expression can be provided via the key() attribute, or a custom KeyGenerator implementation can replace the default one. Check out the Official reference for details of all possible attributes.

package com.websystique.spring.service;

import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;

import com.websystique.spring.model.Product;

@Service("productService")
public class ProductServiceImpl implements ProductService{

	@Override
	@Cacheable("products")
	public Product getByName(String name) {
		slowLookupOperation();
		return new Product(name,100);
	}

	@CacheEvict(value = "products", allEntries = true)
	public void refreshAllProducts() {
	   //This method will remove all 'products' from cache, say as a result of flush API call.
	}	

	public void slowLookupOperation(){
		 try {
	            long time = 5000L;
	            Thread.sleep(time);
	        } catch (InterruptedException e) {
	            throw new IllegalStateException(e);
	       }
	}
}

2. @CacheEvict [ Used for removing items from Cache]

@CacheEvict annotation indicates that a method (or all methods on a class) triggers a cache evict operation, removing specific[or all] items from cache. Various attributes provides complete control to enforce the required behavior for cache-eviction.

3. @EnableCaching [ Used for Enabling Caching support in Spring Applicaion]

@EnableCaching annotation triggers a post processor that inspects every Spring bean for the presence of caching annotations[@Cacheable, @CacheEvict, @CachePut] on public methods. If such an annotation is found, a proxy is automatically created to intercept the method call and handle the caching behavior accordingly.

package com.websystique.spring.configuration;

import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cache.ehcache.EhCacheCacheManager;
import org.springframework.cache.ehcache.EhCacheManagerFactoryBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;

@EnableCaching
@Configuration
@ComponentScan(basePackages = "com.websystique.spring")
public class AppConfig {

	/*	
	//Suitable for basic use cases, no persistence capabilities or eviction contracts.
	@Bean
	public CacheManager cacheManager() {
        	// configure and return an implementation of Spring's CacheManager SPI
	        SimpleCacheManager cacheManager = new SimpleCacheManager();
        	cacheManager.setCaches(Arrays.asList(new ConcurrentMapCache("products")));
	        return cacheManager;
    	}
   */
	
	//EhCache based CacheManager, most commonly used in Enterprise applications.
	@Bean
	public CacheManager cacheManager() {
		return new EhCacheCacheManager(ehCacheCacheManager().getObject());
	}

	@Bean
	public EhCacheManagerFactoryBean ehCacheCacheManager() {
		EhCacheManagerFactoryBean factory = new EhCacheManagerFactoryBean();
		factory.setConfigLocation(new ClassPathResource("ehcache.xml"));
		factory.setShared(true);
		return factory;
	}
	
}

org.springframework.cache.Cache & org.springframework.cache.CacheManager are the main abstractions provided by Spring. Please do note that these are the abstraction providing the caching logic, but does not provide the actual storage to store the cache data. Fortunately, there are few implementations of that abstraction available out of the box: JDK java.util.concurrent.ConcurrentMap based caches, EhCache, Gemfire cache, Caffeine, Guava caches and JSR-107 compliant caches.

The first CacheManager[commented] is based on JDK ConcurrentMap which is enough for simple use cases but does not support the persistence or eviction policy. For an enterprise solution, Ehcache is the preferred choice, providing advanced features. We will be focusing on EhCache in this post. We will need following dependency to be added in our pom.xml [available at the end of post] for EhCache support.

		<dependency>
		    <groupId>net.sf.ehcache</groupId>
		    <artifactId>ehcache</artifactId>
		    <version>2.10.2.2.21</version>
		</dependency>

Above EhCacheManagerFactoryBean accepts an XML file[ehcache.xml] providing the caching configuration for individual cache items.

<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:noNamespaceSchemaLocation="ehcache.xsd" 
	updateCheck="true"
	monitoring="autodetect" 
	dynamicConfig="true">

	<diskStore path="java.io.tmpdir" />
	
	<cache name="products" 
		maxEntriesLocalHeap="100"
		maxEntriesLocalDisk="1000" 
		eternal="false" 
		timeToIdleSeconds="300" 
		timeToLiveSeconds="600"
		memoryStoreEvictionPolicy="LFU" 
		transactionalMode="off">
		<persistence strategy="localTempSwap" />
	</cache>

</ehcache>

Here we are setting up a cache with name ‘products’. Maximum 100 products will be kept in in-memory [on-heap] store, while maximum 1000 products will be maintained in the DiskStore, on the path specified ‘java.io.tmpdir’ which refers to default temp file path. A product will be expired if it is idle for more than 5 minutes and lives for more than 10 minutes. A detailed description of individual properties can be found at Ehcache Official Reference.

Let’s adapt the main to see the things in action.

package com.websystique.spring.configuration;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.support.AbstractApplicationContext;

import com.websystique.spring.service.ProductService;

public class SampleApplication {

	private static final Logger logger = LoggerFactory.getLogger(SampleApplication.class);
	
	public static void main(String[] args){
		AbstractApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);

		ProductService service = (ProductService) context.getBean("productService");
		
		logger.info("IPhone ->" + service.getByName("IPhone"));
		logger.info("IPhone ->" + service.getByName("IPhone"));
		logger.info("IPhone ->" + service.getByName("IPhone"));
		logger.info("Refreshing all products");
		service.refreshAllProducts();
		logger.info("IPhone [after refresh]->" + service.getByName("IPhone"));
		logger.info("IPhone [after refresh]->" + service.getByName("IPhone"));
		logger.info("IPhone [after refresh]->" + service.getByName("IPhone"));
		
		((AbstractApplicationContext) context).close();
	}
}

The output speaks for itself. For the 1st request, method was executed, while for 2nd, 3rd, we got them from cache. Then we fired a remove-all-products-from-cache. 5th line shows that method was re-executed and took 5 seconds, while 6th,7th were again from cache.

19:18:48.439 [main] INFO com.websystique.spring.configuration.SampleApplication - IPhone ->Product [name=IPhone, price=100.0]
19:18:48.439 [main] INFO com.websystique.spring.configuration.SampleApplication - IPhone ->Product [name=IPhone, price=100.0]
19:18:48.439 [main] INFO com.websystique.spring.configuration.SampleApplication - IPhone ->Product [name=IPhone, price=100.0]
19:18:48.439 [main] INFO com.websystique.spring.configuration.SampleApplication - Refreshing all products
19:18:53.457 [main] INFO com.websystique.spring.configuration.SampleApplication - IPhone [after refresh]->Product [name=IPhone, price=100.0]
19:18:53.457 [main] INFO com.websystique.spring.configuration.SampleApplication - IPhone [after refresh]->Product [name=IPhone, price=100.0]
19:18:53.457 [main] INFO com.websystique.spring.configuration.SampleApplication - IPhone [after refresh]->Product [name=IPhone, price=100.0]

In the next post, we will develop an interesting use case involving @CahcePut along with other goodies we have seen here.Stay tune.

pom.xml

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>

	<groupId>com.websystique.spring</groupId>
	<artifactId>Spring4CachingExample</artifactId>
	<version>1.0.0</version>
	<packaging>jar</packaging>

	<name>Spring4CachingExample</name>

	<properties>
		<springframework.version>4.3.0.RELEASE</springframework.version>
	</properties>

	<dependencies>
		<dependency>
			<groupId>org.springframework</groupId>
			<artifactId>spring-core</artifactId>
			<version>${springframework.version}</version>
		</dependency>
		<dependency>
			<groupId>org.springframework</groupId>
			<artifactId>spring-context</artifactId>
			<version>${springframework.version}</version>
		</dependency>
		<dependency>
			<groupId>org.springframework</groupId>
			<artifactId>spring-context-support</artifactId>
			<version>${springframework.version}</version>
		</dependency>
		<!-- EHCache -->
		<dependency>
		    <groupId>net.sf.ehcache</groupId>
		    <artifactId>ehcache</artifactId>
		    <version>2.10.2.2.21</version>
		</dependency>
		<!-- SLF4J/Logback -->
        <dependency>
            <groupId>ch.qos.logback</groupId>
            <artifactId>logback-classic</artifactId>
            <version>1.1.7</version>
        </dependency>		
	</dependencies>

	<build>
		<plugins>
			<plugin>
				<groupId>org.apache.maven.plugins</groupId>
				<artifactId>maven-compiler-plugin</artifactId>
				<version>3.2</version>
				<configuration>
					<source>1.7</source>
					<target>1.7</target>
				</configuration>
			</plugin>
		</plugins>
	</build>
</project>

Project Structure

SpringCachingExample_1

That’s it. Next post discusses all Spring Cache annotations in detail with help of examples.

Download Source Code


References