Categories: spring

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


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

View Comments

  • i can't find anywhere some other examples where the unless operation is used different of s comparisom with null... i need to get my http response code and check is its != 200 how can i do it?

  • great article! i wanted to know, if there is a way to cache on start up? lets take the same example as the one here.. where we call the getByName(name) method on start up in some method and store it in cache... i tried using a @PostConstruct on that method but it seems we cannot call a method that uses @Cacheable from the @PostConstruct method... any suggestions?

    • Hi Anit, I see your point. I did not try this specific use case you are referring [always cached on first use], and documentation do mention about proxy being uninitialized during @PostConstruct, therefore not allowing @Cacheable. You may try delegating Caching to another bean and using that bean caching initialization into your bean @PostConstruct.

  • Sir, You're best and tutorials too. May I request you to please create tutorials for Spring Boot too ?

Share
Published by

Recent Posts

Spring Boot + AngularJS + Spring Data + JPA CRUD App Example

In this post we will be developing a full-blown CRUD application using Spring Boot, AngularJS, Spring Data, JPA/Hibernate and MySQL,…

7 years ago

Spring Boot Rest API Example

Spring Boot complements Spring REST support by providing default dependencies/converters out of the box. Writing RESTful services in Spring Boot…

7 years ago

Spring Boot WAR deployment example

Being able to start the application as standalone jar is great, but sometimes it might not be possible to run…

7 years ago

Spring Boot Introduction + hello world example

Spring framework has taken the software development industry by storm. Dependency Injection, rock solid MVC framework, Transaction management, messaging support,…

7 years ago

Secure Spring REST API using OAuth2

Let's secure our Spring REST API using OAuth2 this time, a simple guide showing what is required to secure a…

8 years ago

AngularJS+Spring Security using Basic Authentication

This post shows how an AngularJS application can consume a REST API which is secured with Basic authentication using Spring…

8 years ago