diff --git a/spring-boot-modules/spring-caching-2/pom.xml b/spring-boot-modules/spring-caching-2/pom.xml
index ec9215aa32..95fae4a0ef 100644
--- a/spring-boot-modules/spring-caching-2/pom.xml
+++ b/spring-boot-modules/spring-caching-2/pom.xml
@@ -51,7 +51,7 @@
${caffeine.version}
- it.ozimov
+ com.github.codemonstur
embedded-redis
${embedded.redis.version}
@@ -65,7 +65,7 @@
- 0.7.3
+ 1.4.0
3.1.8
com.baeldung.caching.ttl.CachingTTLApplication
diff --git a/spring-boot-modules/spring-caching-2/src/main/java/com/baeldung/caching/twolevelcache/CacheConfig.java b/spring-boot-modules/spring-caching-2/src/main/java/com/baeldung/caching/twolevelcache/CacheConfig.java
new file mode 100644
index 0000000000..576bcd97ab
--- /dev/null
+++ b/spring-boot-modules/spring-caching-2/src/main/java/com/baeldung/caching/twolevelcache/CacheConfig.java
@@ -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();
+ }
+}
\ No newline at end of file
diff --git a/spring-boot-modules/spring-caching-2/src/main/java/com/baeldung/caching/twolevelcache/Customer.java b/spring-boot-modules/spring-caching-2/src/main/java/com/baeldung/caching/twolevelcache/Customer.java
new file mode 100644
index 0000000000..0b985d35f3
--- /dev/null
+++ b/spring-boot-modules/spring-caching-2/src/main/java/com/baeldung/caching/twolevelcache/Customer.java
@@ -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;
+}
\ No newline at end of file
diff --git a/spring-boot-modules/spring-caching-2/src/main/java/com/baeldung/caching/twolevelcache/CustomerCacheInterceptor.java b/spring-boot-modules/spring-caching-2/src/main/java/com/baeldung/caching/twolevelcache/CustomerCacheInterceptor.java
new file mode 100644
index 0000000000..f1a8dca0db
--- /dev/null
+++ b/spring-boot-modules/spring-caching-2/src/main/java/com/baeldung/caching/twolevelcache/CustomerCacheInterceptor.java
@@ -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;
+ }
+}
diff --git a/spring-boot-modules/spring-caching-2/src/main/java/com/baeldung/caching/twolevelcache/CustomerRepository.java b/spring-boot-modules/spring-caching-2/src/main/java/com/baeldung/caching/twolevelcache/CustomerRepository.java
new file mode 100644
index 0000000000..098112f152
--- /dev/null
+++ b/spring-boot-modules/spring-caching-2/src/main/java/com/baeldung/caching/twolevelcache/CustomerRepository.java
@@ -0,0 +1,6 @@
+package com.baeldung.caching.twolevelcache;
+
+import org.springframework.data.repository.CrudRepository;
+
+public interface CustomerRepository extends CrudRepository {
+}
diff --git a/spring-boot-modules/spring-caching-2/src/main/java/com/baeldung/caching/twolevelcache/CustomerService.java b/spring-boot-modules/spring-caching-2/src/main/java/com/baeldung/caching/twolevelcache/CustomerService.java
new file mode 100644
index 0000000000..088b66919c
--- /dev/null
+++ b/spring-boot-modules/spring-caching-2/src/main/java/com/baeldung/caching/twolevelcache/CustomerService.java
@@ -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);
+ }
+}
\ No newline at end of file
diff --git a/spring-boot-modules/spring-caching-2/src/main/java/com/baeldung/caching/twolevelcache/TwoLevelCacheApplication.java b/spring-boot-modules/spring-caching-2/src/main/java/com/baeldung/caching/twolevelcache/TwoLevelCacheApplication.java
new file mode 100644
index 0000000000..9bc2c65e6e
--- /dev/null
+++ b/spring-boot-modules/spring-caching-2/src/main/java/com/baeldung/caching/twolevelcache/TwoLevelCacheApplication.java
@@ -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);
+ }
+}
\ No newline at end of file
diff --git a/spring-boot-modules/spring-caching-2/src/main/resources/application.properties b/spring-boot-modules/spring-caching-2/src/main/resources/application.properties
index 38f3537d01..49bd715e43 100644
--- a/spring-boot-modules/spring-caching-2/src/main/resources/application.properties
+++ b/spring-boot-modules/spring-caching-2/src/main/resources/application.properties
@@ -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
diff --git a/spring-boot-modules/spring-caching-2/src/test/java/com/baeldung/caching/redis/ItemServiceCachingIntegrationTest.java b/spring-boot-modules/spring-caching-2/src/test/java/com/baeldung/caching/redis/ItemServiceCachingIntegrationTest.java
index 8868edb74f..01740ba780 100644
--- a/spring-boot-modules/spring-caching-2/src/test/java/com/baeldung/caching/redis/ItemServiceCachingIntegrationTest.java
+++ b/spring-boot-modules/spring-caching-2/src/test/java/com/baeldung/caching/redis/ItemServiceCachingIntegrationTest.java
@@ -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();
}
}
diff --git a/spring-boot-modules/spring-caching-2/src/test/java/com/baeldung/caching/twolevelcache/CustomerServiceCachingIntegrationTest.java b/spring-boot-modules/spring-caching-2/src/test/java/com/baeldung/caching/twolevelcache/CustomerServiceCachingIntegrationTest.java
new file mode 100644
index 0000000000..3db53198fc
--- /dev/null
+++ b/spring-boot-modules/spring-caching-2/src/test/java/com/baeldung/caching/twolevelcache/CustomerServiceCachingIntegrationTest.java
@@ -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();
+ }
+ }
+}
\ No newline at end of file