Performance is a big problem in software development. And Caching is one solution to speed up system. In the tutorial, JavaSampleApproach will show you how to use SpringBoot Caffeine cache with PostGreSQL backend.
Related posts:
– How to work with Spring Cache | Spring Boot
– Couchbase – How to create Spring Cache Couchbase application with SpringBoot
– SpringBoot Hazelcast cache with PostgreSQL backend
Contents
I. Technologies
– Java 1.8
– Maven 3.3.9
– Spring Tool Suite – Version 3.8.1.RELEASE
– Spring Boot: 1.5.6RELEASE
– Couchbase 4.6.2
II. SpringBoot Caffeine cache
What is Caffeine cache?
In the org.springframework.boot.autoconfigure.cache.CacheProperties
, we can see:
[..] @ConfigurationProperties(prefix = "spring.cache") public class CacheProperties { [..] /** * Guava specific cache properties. */ public static class Guava { @Deprecated @DeprecatedConfigurationProperty(reason = "Caffeine will supersede the Guava support in Spring Boot 2.0", replacement = "spring.cache.caffeine.spec") public String getSpec() { return this.spec; } @Deprecated public void setSpec(String spec) { this.spec = spec; } [..]
-> Caffeine is a Java 8 rewrite of Guava’s cache that supersede the Guava support.
In the tutorial, we use SpringBoot and Caffeine cache with PostGreSQL backend as below architecture:
For work with SpringCache Caffeine, we need dependencies:
org.springframework.boot spring-boot-starter-cache com.github.ben-manes.caffeine caffeine
If Caffeine is present, spring-boot-starter-cache
‘Starter’ will auto-configure a CaffeineCacheManager. Caches can be created on startup using the spring.cache.cache-names. We can cutomize a cache via properties in order:
1. A cache spec defined by spring.cache.caffeine.spec
2. A com.github.benmanes.caffeine.cache.CaffeineSpec
bean is defined
3. A com.github.benmanes.caffeine.cache.Caffeine
bean is defined
More details, we need check out the sourcecode of com.github.benmanes.caffeine.cache.CaffeineSpec
or
com.github.benmanes.caffeine.cache.Caffeine
:
... public final class CaffeineSpec { ... long maximumSize = UNSET_INT; long maximumWeight = UNSET_INT; ... long expireAfterAccessDuration = UNSET_INT; long expireAfterWriteDuration = UNSET_INT; long refreshAfterWriteDuration = UNSET_INT; ...
Explanation:
– maximumSize
: Specifies the maximum number of entries the cache may contain
– maximumWeight
: Specifies the maximum weight of entries the cache may contain
– expireAfterAccessDuration
: Specifies that each entry should be automatically removed from the cache once a fixed duration has elapsed after the entry’s creation, the most recent replacement of its value, or its last read.
– expireAfterWriteDuration
: Specifies that each entry should be automatically removed from the cache once a fixed duration has elapsed after the entry’s creation, or the most recent replacement of its value.
– refreshAfterWriteDuration
: Specifies that active entries are eligible for automatic refresh once a fixed duration has elapsed after the entry’s creation, or the most recent replacement of its value.
Example:
spring.cache.cache-names:customer spring.cache.caffeine.spec: maximumSize=500, expireAfterAccess=86400s
III. Practice
In the tutorial, we create a SpringBoot project as below:
Step to do:
– Create SpringBoot project
– Create data model
– Create Repository
– Implement Service on top of Cache
– Implement RestAPIs
– Implement CommandLineRunner to init data
– Configuration
– Run and check results
1. Create SpringBoot project
Using SpringToolSuite to create a SpringBoot project, then add dependencies:
[...]org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-starter-cache com.github.ben-manes.caffeine caffeine org.springframework.boot spring-boot-starter-data-jpa [...] org.postgresql postgresql runtime
2. Create data model
– Create a Customer model:
package com.javasampleapproach.caffeinecache.model; import javax.persistence.Column; import javax.persistence.Entity; import javax.persistence.Id; import javax.persistence.Table; @Entity @Table(name = "customer") public class Customer { @Id private String id; @Column(name = "firstname") private String firstName; @Column(name = "lastname") private String lastName; public Customer(){ } public Customer(String id, String firstName, String lastName){ this.id = id; this.firstName = firstName; this.lastName = lastName; } public String getId() { return this.id; } public void setId(String id) { this.id = id; } public String getFirstName() { return this.firstName; } public void setFirstName(String firstName) { this.firstName = firstName; } public String getLastName() { return this.lastName; } public void setLastName(String lastName) { this.lastName = lastName; } @Override public String toString() { return String.format("Customer[ id=%s, firstName=%s, lastName=%s]", this.id, this.firstName, this.lastName); } }
3. Create Repository
– Using CrudRepository
, create a interface CustomerRepository
:
package com.javasampleapproach.caffeinecache.repo; import org.springframework.data.repository.CrudRepository; import com.javasampleapproach.caffeinecache.model.Customer; public interface CustomerRepository extends CrudRepository{ }
4. Implement Service on top of Cache
Implement a cache component CustomerCache
as below:
package com.javasampleapproach.caffeinecache.service.cache; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.cache.annotation.CacheConfig; import org.springframework.cache.annotation.CacheEvict; import org.springframework.cache.annotation.CachePut; import org.springframework.cache.annotation.Cacheable; import org.springframework.stereotype.Component; import com.javasampleapproach.caffeinecache.model.Customer; import com.javasampleapproach.caffeinecache.repo.CustomerRepository; @Component @CacheConfig(cacheNames = {"customer"}) public class CustomerCache { @Autowired CustomerRepository customerRepository; @Cacheable(key="#id") public Customer getOnCache(String id){ System.out.println("############# Backend processing..."); // simulation the time for processing try { Thread.sleep(5000); } catch (InterruptedException e) { } return customerRepository.findOne(id); } @CachePut(key="#id") public Customer putOnCache(String firstName, String id){ // find a customer in repository Customer cust = customerRepository.findOne(id); // modify above customer by first-name cust.setFirstName(firstName); // save to database return customerRepository.save(cust); } @CacheEvict(key = "#id") public void evict(String id){ } }
Then implement CustomerServices
service on top of CustomerCache
:
package com.javasampleapproach.caffeinecache.service; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import com.javasampleapproach.caffeinecache.model.Customer; import com.javasampleapproach.caffeinecache.service.cache.CustomerCache; @Service public class CustomerServices { @Autowired CustomerCache customerCache; public Customer putCustomer(String firstName, String id){ return customerCache.putOnCache(firstName, id); } public Customer get(String id){ return customerCache.getOnCache(id); } public void evict(String id){ customerCache.evict(id); } }
5. Implement RestAPIs
Create a WebController with 3 APIs {/api/cachable
, /api/cacheput
, /api/cacheevict
}:
package com.javasampleapproach.caffeinecache.controller; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import com.javasampleapproach.caffeinecache.model.Customer; import com.javasampleapproach.caffeinecache.service.CustomerServices; @RestController @RequestMapping("/api") public class WebController { @Autowired CustomerServices service; @RequestMapping("/cachable") public Customer get(@RequestParam("id")String id){ return service.get(id); } @RequestMapping("/cacheput") public String put(@RequestParam("firstname") String firstName, @RequestParam("id")String id){ service.putCustomer(firstName, id); return "Done"; } @RequestMapping("/cacheevict") public String evict(@RequestParam("id")String id){ service.evict(id); return "Done"; } }
6. Implement CommandLineRunner to init data
In main class, use CustomerRepository
and CommandLineRunner
to init data:
package com.javasampleapproach.caffeinecache; import java.util.Arrays; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.CommandLineRunner; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cache.annotation.EnableCaching; import com.javasampleapproach.caffeinecache.model.Customer; import com.javasampleapproach.caffeinecache.repo.CustomerRepository; @EnableCaching @SpringBootApplication public class SpringBootCaffeineCacheApplication implements CommandLineRunner{ @Autowired CustomerRepository customerRepo; public static void main(String[] args) { SpringApplication.run(SpringBootCaffeineCacheApplication.class, args); } @Override public void run(String... arg0) throws Exception { // initial data to PostGreSQL database customerRepo.save(Arrays.asList(new Customer("1", "Jack", "Smith"), new Customer("2", "Adam", "Johnson"))); } }
Note: @EnableCaching
is used to enable caching.
7. Configuration
Open application.properties file, configure spring.cache.*
and spring.datasource.*
spring.cache.cache-names:customer spring.cache.caffeine.spec: maximumSize=500, expireAfterAccess=86400s spring.datasource.url=jdbc:postgresql://localhost/testdb spring.datasource.username=postgres spring.datasource.password=123 spring.jpa.generate-ddl=true
8. Run and check results
Build and Run the SpringBoot project as commandlines: {mvn clean install
, mvn spring-boot:run
}.
-> See PostgreSQL database:
– Make request 1: http://localhost:8080/api/cachable?id=1
-> Results:
Service process slowly and,
Server has displayed a text on console:
############# Backend processing...
– Make request 2: http://localhost:8080/api/cachable?id=1
Now the response is faster because Customer
with id = 1 has been cached before, the application just get data from cache storage.
– Make request 3: http://localhost:8080/api/cacheput?id=1&firstname=Peter
Message is returned on Browser:
Done
Now customer with id=1 is modified: firstname=’Peter’, NOT ‘Jack’.
– Make request 4: http://localhost:8080/api/cachable?id=1
Response is faster BUT the result is difference from first request:
– Make request 5 – Make a cache-evict request: http://localhost:8080/api/cacheevict?id=1
Browser displays:
Done
Now customer with id=1 was evicted from cache storage.
– Make request 6: http://localhost:8080/api/cachable?id=1
Now the behavior of the browser is the same as the first request because customer with id=1 was evicted to cache, and the method under @Cachable
is executed.
Service process slowly and result:
############# Backend processing...