Merge pull request #16006 from saikatcse03/master
[BAEL-7127] Implement two levels of caching with local and distributed cache
This commit is contained in:
commit
a7fa8842e5
|
@ -51,7 +51,7 @@
|
|||
<version>${caffeine.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>it.ozimov</groupId>
|
||||
<groupId>com.github.codemonstur</groupId>
|
||||
<artifactId>embedded-redis</artifactId>
|
||||
<version>${embedded.redis.version}</version>
|
||||
<exclusions>
|
||||
|
@ -65,7 +65,7 @@
|
|||
</dependencies>
|
||||
|
||||
<properties>
|
||||
<embedded.redis.version>0.7.3</embedded.redis.version>
|
||||
<embedded.redis.version>1.4.0</embedded.redis.version>
|
||||
<caffeine.version>3.1.8</caffeine.version>
|
||||
<start-class>com.baeldung.caching.ttl.CachingTTLApplication</start-class>
|
||||
</properties>
|
||||
|
|
|
@ -0,0 +1,71 @@
|
|||
package com.baeldung.caching.twolevelcache;
|
||||
|
||||
import com.github.benmanes.caffeine.cache.Caffeine;
|
||||
|
||||
import org.springframework.cache.CacheManager;
|
||||
import org.springframework.cache.annotation.AnnotationCacheOperationSource;
|
||||
import org.springframework.cache.annotation.EnableCaching;
|
||||
import org.springframework.cache.caffeine.CaffeineCache;
|
||||
import org.springframework.cache.interceptor.CacheInterceptor;
|
||||
import org.springframework.cache.interceptor.CacheOperationSource;
|
||||
import org.springframework.cache.support.SimpleCacheManager;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Primary;
|
||||
import org.springframework.data.redis.cache.RedisCacheConfiguration;
|
||||
import org.springframework.data.redis.cache.RedisCacheManager;
|
||||
import org.springframework.data.redis.connection.RedisConnectionFactory;
|
||||
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
|
||||
import org.springframework.data.redis.serializer.RedisSerializationContext;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.Arrays;
|
||||
|
||||
@Configuration
|
||||
@EnableCaching
|
||||
public class CacheConfig {
|
||||
|
||||
@Bean
|
||||
@Primary
|
||||
public CacheManager caffeineCacheManager(CaffeineCache caffeineCache) {
|
||||
SimpleCacheManager manager = new SimpleCacheManager();
|
||||
manager.setCaches(Arrays.asList(caffeineCache));
|
||||
return manager;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public CaffeineCache caffeineCacheConfig() {
|
||||
return new CaffeineCache("customerCache", Caffeine.newBuilder()
|
||||
.expireAfterWrite(Duration.ofSeconds(3))
|
||||
.initialCapacity(1)
|
||||
.maximumSize(2000)
|
||||
.build());
|
||||
}
|
||||
|
||||
@Bean
|
||||
public CacheManager redisCacheManager(RedisConnectionFactory connectionFactory, RedisCacheConfiguration redisCacheConfiguration) {
|
||||
return RedisCacheManager.RedisCacheManagerBuilder.fromConnectionFactory(connectionFactory)
|
||||
.withCacheConfiguration("customerCache", redisCacheConfiguration)
|
||||
.build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public RedisCacheConfiguration cacheConfiguration() {
|
||||
return RedisCacheConfiguration.defaultCacheConfig()
|
||||
.entryTtl(Duration.ofMinutes(5))
|
||||
.disableCachingNullValues()
|
||||
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()));
|
||||
}
|
||||
|
||||
@Bean
|
||||
public CacheInterceptor cacheInterceptor(CacheManager caffeineCacheManager, CacheOperationSource cacheOperationSource) {
|
||||
CacheInterceptor interceptor = new CustomerCacheInterceptor(caffeineCacheManager);
|
||||
interceptor.setCacheOperationSources(cacheOperationSource);
|
||||
return interceptor;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public CacheOperationSource cacheOperationSource() {
|
||||
return new AnnotationCacheOperationSource();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
package com.baeldung.caching.twolevelcache;
|
||||
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.Id;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
@Data
|
||||
@Entity
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
@Setter
|
||||
public class Customer implements Serializable {
|
||||
|
||||
@Id
|
||||
private String id;
|
||||
|
||||
private String name;
|
||||
|
||||
private String email;
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
package com.baeldung.caching.twolevelcache;
|
||||
|
||||
import org.springframework.cache.Cache;
|
||||
import org.springframework.cache.CacheManager;
|
||||
import org.springframework.cache.interceptor.CacheInterceptor;
|
||||
import org.springframework.data.redis.cache.RedisCache;
|
||||
|
||||
public class CustomerCacheInterceptor extends CacheInterceptor {
|
||||
|
||||
private final CacheManager caffeineCacheManager;
|
||||
|
||||
public CustomerCacheInterceptor(CacheManager caffeineCacheManager) {
|
||||
this.caffeineCacheManager = caffeineCacheManager;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Cache.ValueWrapper doGet(Cache cache, Object key) {
|
||||
Cache.ValueWrapper existingCacheValue = super.doGet(cache, key);
|
||||
|
||||
if (existingCacheValue != null && cache.getClass() == RedisCache.class) {
|
||||
Cache caffeineCache = caffeineCacheManager.getCache(cache.getName());
|
||||
if (caffeineCache != null) {
|
||||
caffeineCache.putIfAbsent(key, existingCacheValue.get());
|
||||
}
|
||||
}
|
||||
|
||||
return existingCacheValue;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
package com.baeldung.caching.twolevelcache;
|
||||
|
||||
import org.springframework.data.repository.CrudRepository;
|
||||
|
||||
public interface CustomerRepository extends CrudRepository<Customer, String> {
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
package com.baeldung.caching.twolevelcache;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.cache.annotation.Cacheable;
|
||||
import org.springframework.cache.annotation.Caching;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@Service
|
||||
public class CustomerService {
|
||||
|
||||
private final CustomerRepository customerRepository;
|
||||
|
||||
@Autowired
|
||||
public CustomerService(CustomerRepository customerRepository) {
|
||||
this.customerRepository = customerRepository;
|
||||
}
|
||||
|
||||
@Caching(cacheable = {
|
||||
@Cacheable(cacheNames = "customerCache", cacheManager = "caffeineCacheManager"),
|
||||
@Cacheable(cacheNames = "customerCache", cacheManager = "redisCacheManager")
|
||||
})
|
||||
public Customer getCustomer(String id) {
|
||||
return customerRepository.findById(id)
|
||||
.orElseThrow(RuntimeException::new);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
package com.baeldung.caching.twolevelcache;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
|
||||
@SpringBootApplication
|
||||
public class TwoLevelCacheApplication {
|
||||
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(TwoLevelCacheApplication.class, args);
|
||||
}
|
||||
}
|
|
@ -11,3 +11,4 @@ caching.spring.hotelListTTL=43200
|
|||
# Connection details
|
||||
#spring.redis.host=localhost
|
||||
#spring.redis.port=6379
|
||||
spring.main.allow-bean-definition-overriding=true
|
||||
|
|
|
@ -5,8 +5,6 @@ import static org.mockito.BDDMockito.given;
|
|||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
import org.junit.jupiter.api.Disabled;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
|
@ -20,10 +18,12 @@ import org.springframework.cache.CacheManager;
|
|||
import org.springframework.cache.annotation.EnableCaching;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.test.context.junit.jupiter.SpringExtension;
|
||||
import redis.embedded.RedisServer;
|
||||
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import jakarta.annotation.PreDestroy;
|
||||
import redis.embedded.RedisServer;
|
||||
import java.io.IOException;
|
||||
import java.util.Optional;
|
||||
|
||||
@Import({ CacheConfig.class, ItemService.class })
|
||||
@ExtendWith(SpringExtension.class)
|
||||
|
@ -69,17 +69,17 @@ class ItemServiceCachingIntegrationTest {
|
|||
|
||||
private final RedisServer redisServer;
|
||||
|
||||
public EmbeddedRedisConfiguration() {
|
||||
public EmbeddedRedisConfiguration() throws IOException {
|
||||
this.redisServer = new RedisServer();
|
||||
}
|
||||
|
||||
@PostConstruct
|
||||
public void startRedis() {
|
||||
public void startRedis() throws IOException {
|
||||
redisServer.start();
|
||||
}
|
||||
|
||||
@PreDestroy
|
||||
public void stopRedis() {
|
||||
public void stopRedis() throws IOException {
|
||||
this.redisServer.stop();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,124 @@
|
|||
package com.baeldung.caching.twolevelcache;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.BDDMockito.given;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.autoconfigure.ImportAutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.cache.CacheAutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration;
|
||||
import org.springframework.boot.test.context.TestConfiguration;
|
||||
import org.springframework.boot.test.mock.mockito.MockBean;
|
||||
import org.springframework.cache.CacheManager;
|
||||
import org.springframework.cache.annotation.EnableCaching;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.test.context.junit.jupiter.SpringExtension;
|
||||
import redis.embedded.RedisServer;
|
||||
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import jakarta.annotation.PreDestroy;
|
||||
import java.io.IOException;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
@Import({ CacheConfig.class,CustomerService.class })
|
||||
@ExtendWith(SpringExtension.class)
|
||||
@ImportAutoConfiguration(classes = { CacheAutoConfiguration.class, RedisAutoConfiguration.class })
|
||||
@EnableCaching
|
||||
class CustomerServiceCachingIntegrationTest {
|
||||
|
||||
@MockBean
|
||||
private CustomerRepository customerRepository;
|
||||
|
||||
@Autowired
|
||||
private CustomerService customerService;
|
||||
|
||||
@Autowired
|
||||
private CacheManager redisCacheManager;
|
||||
|
||||
@Autowired
|
||||
private CacheManager caffeineCacheManager;
|
||||
|
||||
@Test
|
||||
void givenCustomerIsPresent_whenGetCustomerCalled_thenReturnCustomerAndCacheIt() {
|
||||
String CUSTOMER_ID = "100";
|
||||
Customer customer = new Customer(CUSTOMER_ID, "test", "test@mail.com");
|
||||
|
||||
given(customerRepository.findById(CUSTOMER_ID)).willReturn(Optional.of(customer));
|
||||
|
||||
Customer customerCacheMiss = customerService.getCustomer(CUSTOMER_ID);
|
||||
|
||||
assertThat(customerCacheMiss).isEqualTo(customer);
|
||||
verify(customerRepository, times(1)).findById(CUSTOMER_ID);
|
||||
assertThat(customerFromCaffeineCache(CUSTOMER_ID)).isEqualTo(customer);
|
||||
assertThat(customerFromRedisCache(CUSTOMER_ID)).isEqualTo(customer);
|
||||
}
|
||||
|
||||
@Test
|
||||
void givenCustomerIsPresent_whenGetCustomerCalledTwice_thenReturnCustomerAndCacheIt() {
|
||||
String CUSTOMER_ID = "101";
|
||||
Customer customer = new Customer(CUSTOMER_ID, "test", "test@mail.com");
|
||||
given(customerRepository.findById(CUSTOMER_ID)).willReturn(Optional.of(customer));
|
||||
|
||||
Customer customerCacheMiss = customerService.getCustomer(CUSTOMER_ID);
|
||||
Customer customerCacheHit = customerService.getCustomer(CUSTOMER_ID);
|
||||
|
||||
assertThat(customerCacheMiss).isEqualTo(customer);
|
||||
assertThat(customerCacheHit).isEqualTo(customer);
|
||||
verify(customerRepository, times(1)).findById(CUSTOMER_ID);
|
||||
assertThat(customerFromCaffeineCache(CUSTOMER_ID)).isEqualTo(customer);
|
||||
assertThat(customerFromRedisCache(CUSTOMER_ID)).isEqualTo(customer);
|
||||
}
|
||||
|
||||
@Test
|
||||
void givenCustomerIsPresent_whenGetCustomerCalledTwiceAndFirstCacheExpired_thenReturnCustomerAndCacheIt() throws InterruptedException {
|
||||
String CUSTOMER_ID = "102";
|
||||
Customer customer = new Customer(CUSTOMER_ID, "test", "test@mail.com");
|
||||
given(customerRepository.findById(CUSTOMER_ID)).willReturn(Optional.of(customer));
|
||||
|
||||
Customer customerCacheMiss = customerService.getCustomer(CUSTOMER_ID);
|
||||
TimeUnit.SECONDS.sleep(3);
|
||||
assertThat(customerFromCaffeineCache(CUSTOMER_ID)).isEqualTo(null);
|
||||
Customer customerCacheHit = customerService.getCustomer(CUSTOMER_ID);
|
||||
|
||||
verify(customerRepository, times(1)).findById(CUSTOMER_ID);
|
||||
assertThat(customerCacheMiss).isEqualTo(customer);
|
||||
assertThat(customerCacheHit).isEqualTo(customer);
|
||||
assertThat(customerFromCaffeineCache(CUSTOMER_ID)).isEqualTo(customer);
|
||||
assertThat(customerFromRedisCache(CUSTOMER_ID)).isEqualTo(customer);
|
||||
}
|
||||
|
||||
private Object customerFromRedisCache(String key) {
|
||||
return redisCacheManager.getCache("customerCache").get(key) != null ?
|
||||
redisCacheManager.getCache("customerCache").get(key).get() : null;
|
||||
}
|
||||
|
||||
private Object customerFromCaffeineCache(String key) {
|
||||
return caffeineCacheManager.getCache("customerCache").get(key) != null ?
|
||||
caffeineCacheManager.getCache("customerCache").get(key).get() : null;
|
||||
}
|
||||
|
||||
@TestConfiguration
|
||||
static class EmbeddedRedisConfiguration {
|
||||
|
||||
private final RedisServer redisServer;
|
||||
|
||||
public EmbeddedRedisConfiguration() throws IOException {
|
||||
this.redisServer = new RedisServer();
|
||||
}
|
||||
|
||||
@PostConstruct
|
||||
public void startRedis() throws IOException {
|
||||
redisServer.start();
|
||||
}
|
||||
|
||||
@PreDestroy
|
||||
public void stopRedis() throws IOException {
|
||||
this.redisServer.stop();
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue