Spring 4 Caching Annotations Tutorial

Wondering how to use Caching in your Spring App? Jump right in. This post goes over the caching in detail, explaining commonly used Spring Caching annotations with examples including @Cacheable, @CachePut, @CacheEvict, @Caching, @CacheConfig & @EnableCaching. Let’s get going.

A full code example is present at the end of the chapter showing these annotations in action.

Post Spring 4 Caching Tutorial-With EHCache contains a detailed example of using Spring Cache with popular EhCache.

Caching Annotations

  • @Cacheable : Triggers cache population
  • @CachePut : Updates the cache, without interfering with the method execution
  • @CacheEvict : Triggers cache eviction[removing items from cache]
  • @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, enables Caching

How to Enable Caching?

@EnableCaching annotation, usually applied on a @Configuration class, 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.

@EnableCaching
@Configuration
@ComponentScan(basePackages = "com.websystique.spring")
public class AppConfig {
	
	@Bean
	public CacheManager cacheManager() {
		//A EhCache based Cache manager
		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.CacheManager is the common Cache abstraction provided by spring to handle all caching related activities. CacheManager controls and manages Caches [ org.springframework.cache.Cache] and can be used to retrieve these for storage. Since it’s an abstraction, we need a concrete implementation for cache store. Several options are available in market: JDK java.util.concurrent.ConcurrentMap based caches, EhCache, Gemfire cache, Caffeine, Guava caches and JSR-107 compliant caches, to name a few. In above example, we are using Ehcache for that purpose. At the same time, we are specifying the setting for Ehcache using EhCacheManagerFactoryBean’s configLocation property.If it is not specified explicitly, it defaults to ehcache.xml.

1. @Cacheable

Used for Cache-population. @Cacheable annotation indicates that the result of invoking a method (or all methods in a class) can be cached. Almost anything [object,array,list,..] can be cached. A cache itself can be imagined as a key-value based store. 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, without executing the method.

@Cacheable annotation supports many optional attributes to control cache population. These attributes can use SpEL to specify the caching criteria.

value : Specifies the name of the cache being used.
key : Used to specify the key for cache storage. You can use SpEL to specify the key.

@Cacheable(value="products", key="#product.name")
public Product findProduct(Product product){//product name will be used as a key
..
return aproduct;
} 

Important: If you missed to provide the ‘key’ attribute, Spring may generate the key based on method argument itself [product as a key]. Hence you must make sure to implement hashCode() and equals() for that modal object. In contrast, you can use KeyGenerator to generate a key for you.

condition : Conditional Caching. Item will be cached only if the condition mentioned in ‘condition’ met. Note that condition applies to method argument and evaluated before method execution.

@Cacheable(value="products", key="#product.name", condition="#product.price<500")
public Product findProduct(Product product){
..
return aproduct;
} 

unless : Conditional Caching, applies to return value of method. Item will be cached, unless the condition mentioned in ‘unless’ met. Note that condition applies to return value of method.#result refers to method return value.

@Cacheable(value="products", key="#product.name", condition="#product.price<500", unless="#result.outofstock")
public Product findProduct(Product product){
..
return aproduct;
} 

Multiple Caches:
@Cacheable can use multiple caches at the same time. In this situation, a requested item will be checked in all the mentioned cached and if it found in any of them, method will not be executed.If it does not exist in any of the cache, method will gets executed and it’s result will be stored in all of those caches.

@Cacheable({"products", "items"})
public Product findProduct(Product product) {...
..
return aproduct;
}

2. @CachePut

Used for Cache-update operation. Method annotated with @CachePut are always gets executed and there result gets stored in the cache, eventually overriding any entry with same key in cache. @CachePut, like @Cacheable, supports several attributes, having similar functionality as described above.

Think about a product-refresh operation, where we want a specific product to be re-calculated [may be due to a new price] and then store that product in cache for any future reference. Note that while @CacheEvict is used to remove an item[or all of them] from cache, @CachePut is to update an item.

	@CachePut(value = "products", key = "#product.name" , unless="#result==null")
	public Product updateProduct(Product product) {
		logger.info("<!----------Entering updateProduct ------------------->");
		for(Product p : products){
			if(p.getName().equalsIgnoreCase(product.getName()))
				p.setPrice(product.getPrice());
				return p;
		}
		return null;
	}

Above method will be executed each time it gets called and result will be stored in cache [unless the #result , product in this case is null].

3. @CacheEvict

Used for Cache-removal /cache-cleanup operation. @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.

	@CacheEvict(value = "products", key = "#product.name")
	public void refreshProduct(Product product) {
		//This method will remove only this specific product from 'products' cache.
	}	

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

4. @Caching

@Caching annotation comes handy when you want to specify multiple annotations of the same type, such as @CacheEvict or @CachePut on same method.

Let’s say you have two caches containing same product using same keys. Now, if you want to evict the specific product from both caches, it’s straight forward.

	@CacheEvict(value = {"products", "items"}, key = "#product.name")
	public void refreshProduct(Product product) {
		//This method will remove only this specific product from 'products' & 'items' cache.
	}	

But what if they are using different keys? You may think something like below would be good enough.

	@CacheEvict(value = "products", key = "#product.name")
	@CacheEvict(value = "items"  ,  key = "#product.id")
	public void refreshProduct(Product product) {
		//This method will remove only this specific product from 'products' & 'items' cache.
	}	

Instead you will get a compiler error, as it is not allowed by the language itself to have two annotations of the same time on same element.

@Caching to the rescue.


	@Caching(evict = {
    		@CacheEvict(value = "products", key="#product.name"),
    		@CacheEvict(value = "items"   , key = "#product.id")
	})
	public void refreshProduct(Product product) {
		//This method will remove only this specific product from 'products' & 'items' cache.
	}	

5. @CacheConfig

@CacheConfig is a class-level annotation which can be used to specify the common caching related settings directly on class level, thus freeing user from duplicating them on each method level. You can of course override the setting specified on class level, on individual method. Common configuration setting that can be specified at class level are cache names, custom KeyGenerator, the custom CacheManager & custom CacheResolve.

@CacheConfig(value="products", keyGenerator="myKeyGenerator")
class MyClass{

	@Cacheable
	public Product findProduct(Product product) {...
		..
		return aproduct;
	}

	@Cacheable(value="items")
	public Product findSoldProduct(Product product) {...
		..
		return aproduct;
	}

}

In above example, findProduct will be using “products” cache, while findSoldProduct has overwritten the cache to be used. Additionally, both of them will use the keyGenerator specified on class level.


Complete Example

Below shown is a trivial service containing the most common use-case for caching.

package com.websystique.spring.service;

import java.util.ArrayList;
import java.util.List;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.CachePut;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;

import com.websystique.spring.model.Product;

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

	private static final Logger logger = LoggerFactory.getLogger(ProductServiceImpl.class);
	
	private static List<Product> products;
	
	static{
		products = getDummyProducts();
	}
	
	@Override
	@Cacheable(value="products", key="#name", condition="#name!='HTC'" , unless="#result==null")
	public Product getByName(String name) {
		logger.info("<!----------Entering getByName()--------------------->");
		for(Product p : products){
			if(p.getName().equalsIgnoreCase(name))
				return p;
		}
		return null;
	}


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

	@Override
	@CachePut(value = "products", key = "#product.name" , unless="#result==null")
	public Product updateProduct(Product product) {
		logger.info("<!----------Entering updateProduct ------------------->");
		for(Product p : products){
			if(p.getName().equalsIgnoreCase(product.getName()))
				p.setPrice(product.getPrice());
				return p;
		}
		return null;
	}
	

	private static List<Product> getDummyProducts(){
		List<Product> products = new ArrayList<Product>();
		products.add(new Product("IPhone",500));
		products.add(new Product("Samsung",600));
		products.add(new Product("HTC",800));
		return products;
	}

}
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.model.Product;
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("HTC ->" + service.getByName("HTC"));
		logger.info("HTC ->" + service.getByName("HTC"));
		logger.info("HTC ->" + service.getByName("HTC"));

		Product product = new Product("IPhone",550);
		service.updateProduct(product);
		
		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();
	}
}

Output:

19:54:18.009 [main] INFO com.websystique.spring.service.ProductServiceImpl - <!----------Entering getByName()--------------------->
19:54:18.012 [main] INFO com.websystique.spring.configuration.SampleApplication - IPhone ->Product [name=IPhone, price=500.0]
19:54:18.013 [main] INFO com.websystique.spring.configuration.SampleApplication - IPhone ->Product [name=IPhone, price=500.0]
19:54:18.013 [main] INFO com.websystique.spring.configuration.SampleApplication - IPhone ->Product [name=IPhone, price=500.0]

//HTC is not cachable, thanks to condition="#name!='HTC'"
19:54:18.013 [main] INFO com.websystique.spring.service.ProductServiceImpl - <!----------Entering getByName()--------------------->
19:54:18.013 [main] INFO com.websystique.spring.configuration.SampleApplication - HTC ->Product [name=HTC, price=800.0]
19:54:18.014 [main] INFO com.websystique.spring.service.ProductServiceImpl - <!----------Entering getByName()--------------------->
19:54:18.014 [main] INFO com.websystique.spring.configuration.SampleApplication - HTC ->Product [name=HTC, price=800.0]
19:54:18.014 [main] INFO com.websystique.spring.service.ProductServiceImpl - <!----------Entering getByName()--------------------->
19:54:18.014 [main] INFO com.websystique.spring.configuration.SampleApplication - HTC ->Product [name=HTC, price=800.0]

//Only Cache item gets update, method is still not executed, cool...
19:54:18.017 [main] INFO com.websystique.spring.service.ProductServiceImpl - <!----------Entering updateProduct ------------------->
19:54:18.026 [main] INFO com.websystique.spring.configuration.SampleApplication - IPhone ->Product [name=IPhone, price=550.0]
19:54:18.026 [main] INFO com.websystique.spring.configuration.SampleApplication - IPhone ->Product [name=IPhone, price=550.0]
19:54:18.026 [main] INFO com.websystique.spring.configuration.SampleApplication - IPhone ->Product [name=IPhone, price=550.0]

//Removing all items from Cache, will trigger an execution on next lookup.
19:54:18.026 [main] INFO com.websystique.spring.configuration.SampleApplication - Refreshing all products

19:54:18.037 [main] INFO com.websystique.spring.service.ProductServiceImpl - <!----------Entering getByName()--------------------->
19:54:18.038 [main] INFO com.websystique.spring.configuration.SampleApplication - IPhone [after refresh]->Product [name=IPhone, price=550.0]
19:54:18.038 [main] INFO com.websystique.spring.configuration.SampleApplication - IPhone [after refresh]->Product [name=IPhone, price=550.0]
19:54:18.038 [main] INFO com.websystique.spring.configuration.SampleApplication - IPhone [after refresh]->Product [name=IPhone, price=550.0]
package com.websystique.spring.service;

import com.websystique.spring.model.Product;

public interface ProductService {

	Product getByName(String name);
	void refreshAllProducts();
	Product updateProduct(Product product);
	
}

package com.websystique.spring.model;

import java.io.Serializable;

public class Product implements Serializable{

	private String name;
	private double price;

	public Product(String name, double price){
		this.name = name;
		this.price = price;
	}
	
//getter/setter
}

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>Spring4CachingAnnotationsExample</artifactId>
	<version>1.0.0</version>
	<packaging>jar</packaging>

	<name>Spring4CachingAnnotationsExample</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>
//ehcache.xml
<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.

Project Structure

SpringCachingExample_2
That’s it. As we saw, Spring Cache is simple to use, provides integration point for all the popular cache providers out there.

Download Source Code


References