distributed-cache: Spring Cache abstraction over Hazelcast, Infinispan and Redis (post 29)
This commit is contained in:
@@ -7,7 +7,7 @@ Companion code for a series of posts on [ankurm.com](https://ankurm.com) coverin
|
|||||||
| [`time-series/`](./time-series) | Time-Series Data in Java: QuestDB vs InfluxDB vs TimescaleDB | QuestDB 10.0.1, InfluxDB 3 Core 3.12.0, TimescaleDB 2.25.1 (PG17) |
|
| [`time-series/`](./time-series) | Time-Series Data in Java: QuestDB vs InfluxDB vs TimescaleDB | QuestDB 10.0.1, InfluxDB 3 Core 3.12.0, TimescaleDB 2.25.1 (PG17) |
|
||||||
| [`mongodb/`](./mongodb) | Spring Boot 4 with MongoDB: Spring Data MongoDB, Aggregations and Transactions | MongoDB 9.0.2 single-node replica set, Spring Data MongoDB |
|
| [`mongodb/`](./mongodb) | Spring Boot 4 with MongoDB: Spring Data MongoDB, Aggregations and Transactions | MongoDB 9.0.2 single-node replica set, Spring Data MongoDB |
|
||||||
| [`elasticsearch/`](./elasticsearch) | Full-Text Search with Spring Data Elasticsearch on Spring Boot 4 | Elasticsearch 9.4.8, Spring Data Elasticsearch 6.1.1 |
|
| [`elasticsearch/`](./elasticsearch) | Full-Text Search with Spring Data Elasticsearch on Spring Boot 4 | Elasticsearch 9.4.8, Spring Data Elasticsearch 6.1.1 |
|
||||||
| `distributed-cache/` | Distributed Caching in Spring Boot: Hazelcast vs Infinispan vs Redis | Hazelcast, Infinispan, Redis |
|
| [`distributed-cache/`](./distributed-cache) | Distributed Caching in Spring Boot: Hazelcast vs Infinispan vs Redis | Hazelcast 5.5.0 (embedded), Infinispan 16.1.4 (embedded), Redis 8.8.3 (Testcontainers) |
|
||||||
|
|
||||||
Each module has its own `pom.xml`, a `docker-compose.yml` for its backing services, and a `docs/output/` directory holding the real, re-runnable transcripts that every number quoted in its post traces back to. There are no narrative doc chapters here — the deeper material for each post lives in accordions inside the post itself.
|
Each module has its own `pom.xml`, a `docker-compose.yml` for its backing services, and a `docs/output/` directory holding the real, re-runnable transcripts that every number quoted in its post traces back to. There are no narrative doc chapters here — the deeper material for each post lives in accordions inside the post itself.
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
backend=hazelcast
|
||||||
|
rateFor("USD/EUR") call 1 -> method body ran, callCount 0 -> 1
|
||||||
|
evict("USD/EUR") then rateFor("USD/EUR") again -> method body ran again, callCount 1 -> 2
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
backend=hazelcast
|
||||||
|
rateFor("USD/INR") call 1 -> method body ran, callCount 2 -> 3
|
||||||
|
rateFor("USD/INR") call 2 -> served from cache, callCount stays 3
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
backend=infinispan
|
||||||
|
rateFor("USD/EUR") call 1 -> method body ran, callCount 0 -> 1
|
||||||
|
evict("USD/EUR") then rateFor("USD/EUR") again -> method body ran again, callCount 1 -> 2
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
backend=infinispan
|
||||||
|
rateFor("USD/INR") call 1 -> method body ran, callCount 2 -> 3
|
||||||
|
rateFor("USD/INR") call 2 -> served from cache, callCount stays 3
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
backend=redis
|
||||||
|
rateFor("USD/EUR") call 1 -> method body ran, callCount 0 -> 1
|
||||||
|
evict("USD/EUR") then rateFor("USD/EUR") again -> method body ran again, callCount 1 -> 2
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
backend=redis
|
||||||
|
rateFor("USD/INR") call 1 -> method body ran, callCount 2 -> 3
|
||||||
|
rateFor("USD/INR") call 2 -> served from cache, callCount stays 3
|
||||||
@@ -0,0 +1,119 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||||
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
|
||||||
|
<parent>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-parent</artifactId>
|
||||||
|
<version>4.1.1</version>
|
||||||
|
<relativePath/>
|
||||||
|
</parent>
|
||||||
|
|
||||||
|
<groupId>com.ankurm</groupId>
|
||||||
|
<artifactId>distributed-cache-demo</artifactId>
|
||||||
|
<version>1.0.0</version>
|
||||||
|
<name>distributed-cache-demo</name>
|
||||||
|
|
||||||
|
<properties>
|
||||||
|
<java.version>25</java.version>
|
||||||
|
<testcontainers.version>2.0.5</testcontainers.version>
|
||||||
|
</properties>
|
||||||
|
|
||||||
|
<dependencyManagement>
|
||||||
|
<dependencies>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.testcontainers</groupId>
|
||||||
|
<artifactId>testcontainers-bom</artifactId>
|
||||||
|
<version>${testcontainers.version}</version>
|
||||||
|
<type>pom</type>
|
||||||
|
<scope>import</scope>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
</dependencyManagement>
|
||||||
|
|
||||||
|
<dependencies>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-cache</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- Hazelcast: embedded member + its Spring CacheManager adapter -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.hazelcast</groupId>
|
||||||
|
<artifactId>hazelcast-spring</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- Infinispan: embedded Spring Boot 4 starter, confirmed to exist on
|
||||||
|
Maven Central under this exact artifactId (not guessed). Its
|
||||||
|
auto-configuration unconditionally registers an
|
||||||
|
InfinispanCacheMeterBinderProvider bean, whose
|
||||||
|
@Lookup-annotated method references
|
||||||
|
io.micrometer.core.instrument.binder.MeterBinder - a class
|
||||||
|
the starter itself does not pull in. Without micrometer-core
|
||||||
|
on the classpath this fails bean creation at startup with
|
||||||
|
NoClassDefFoundError, even when this profile is never
|
||||||
|
activated (the starter's auto-configuration runs regardless
|
||||||
|
of @Profile). Confirmed by reading the real stack trace, not
|
||||||
|
assumed from the starter's docs, which don't mention it. -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.micrometer</groupId>
|
||||||
|
<artifactId>micrometer-core</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.infinispan</groupId>
|
||||||
|
<artifactId>infinispan-spring-boot4-starter-embedded</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- Redis: real server via Testcontainers, not embedded -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-data-redis</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-test</artifactId>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-testcontainers</artifactId>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.testcontainers</groupId>
|
||||||
|
<artifactId>testcontainers-junit-jupiter</artifactId>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
|
||||||
|
<build>
|
||||||
|
<plugins>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||||
|
</plugin>
|
||||||
|
<!-- Infinispan's auto-configured embedded CacheManager registers
|
||||||
|
a JMX MBean under the fixed domain "org.infinispan" with no
|
||||||
|
per-instance disambiguation. That auto-configuration runs
|
||||||
|
unconditionally, in every Spring context this test JVM
|
||||||
|
creates, regardless of which profile is active - so with
|
||||||
|
Surefire's default of reusing one forked JVM across test
|
||||||
|
classes, the Hazelcast and Redis profile contexts each try
|
||||||
|
to register that same MBean domain a second/third time and
|
||||||
|
fail with ISPN000034 (confirmed from the real stack trace).
|
||||||
|
reuseForks=false gives each test class its own JVM, which
|
||||||
|
is the one reliable fix - there is no public toggle on
|
||||||
|
InfinispanEmbeddedConfigurationProperties to disable JMX. -->
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
|
<artifactId>maven-surefire-plugin</artifactId>
|
||||||
|
<configuration>
|
||||||
|
<reuseForks>false</reuseForks>
|
||||||
|
</configuration>
|
||||||
|
</plugin>
|
||||||
|
</plugins>
|
||||||
|
</build>
|
||||||
|
</project>
|
||||||
+13
@@ -0,0 +1,13 @@
|
|||||||
|
package com.ankurm.distributedcache;
|
||||||
|
|
||||||
|
import org.springframework.boot.SpringApplication;
|
||||||
|
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||||
|
import org.springframework.cache.annotation.EnableCaching;
|
||||||
|
|
||||||
|
@SpringBootApplication
|
||||||
|
@EnableCaching
|
||||||
|
public class DistributedCacheDemoApplication {
|
||||||
|
public static void main(String[] args) {
|
||||||
|
SpringApplication.run(DistributedCacheDemoApplication.class, args);
|
||||||
|
}
|
||||||
|
}
|
||||||
+43
@@ -0,0 +1,43 @@
|
|||||||
|
package com.ankurm.distributedcache;
|
||||||
|
|
||||||
|
import org.springframework.cache.annotation.CacheEvict;
|
||||||
|
import org.springframework.cache.annotation.Cacheable;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stands in for a slow upstream call (a real rate provider, a legacy
|
||||||
|
* mainframe, whatever). The counter is the whole point: it only goes up
|
||||||
|
* when the method body actually runs, so a test can prove a second call
|
||||||
|
* for the same key was served from the cache without ever calling this
|
||||||
|
* class directly. The @Cacheable/@CacheEvict annotations are the entire
|
||||||
|
* caching story here - which CacheManager bean backs "exchangeRates" is
|
||||||
|
* decided elsewhere, by which Spring profile is active, and this class
|
||||||
|
* neither knows nor cares which one that is.
|
||||||
|
*/
|
||||||
|
@Service
|
||||||
|
public class ExchangeRateLookupService {
|
||||||
|
|
||||||
|
private final AtomicInteger callCount = new AtomicInteger();
|
||||||
|
|
||||||
|
@Cacheable("exchangeRates")
|
||||||
|
public BigDecimal rateFor(String currencyPair) {
|
||||||
|
callCount.incrementAndGet();
|
||||||
|
return switch (currencyPair) {
|
||||||
|
case "USD/INR" -> new BigDecimal("87.42");
|
||||||
|
case "USD/EUR" -> new BigDecimal("0.92");
|
||||||
|
default -> throw new IllegalArgumentException("unknown pair: " + currencyPair);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@CacheEvict("exchangeRates")
|
||||||
|
public void evict(String currencyPair) {
|
||||||
|
// no-op body; the annotation does the work
|
||||||
|
}
|
||||||
|
|
||||||
|
public int callCount() {
|
||||||
|
return callCount.get();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
package com.ankurm.distributedcache;
|
||||||
|
|
||||||
|
import com.hazelcast.config.Config;
|
||||||
|
import com.hazelcast.core.Hazelcast;
|
||||||
|
import com.hazelcast.core.HazelcastInstance;
|
||||||
|
import com.hazelcast.spring.cache.HazelcastCacheManager;
|
||||||
|
import org.springframework.cache.CacheManager;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.context.annotation.Primary;
|
||||||
|
import org.springframework.context.annotation.Profile;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Active only under the "hazelcast" profile. A single embedded
|
||||||
|
* HazelcastInstance plays the role a real cluster member would - in
|
||||||
|
* production this is the same class, just joining other real members
|
||||||
|
* over the network instead of running alone in the test JVM.
|
||||||
|
* HazelcastCacheManager is Hazelcast's own adapter to Spring's
|
||||||
|
* CacheManager interface; ExchangeRateLookupService never sees it
|
||||||
|
* directly, it only sees the "exchangeRates" cache name.
|
||||||
|
*
|
||||||
|
* {@code @Primary} is required here, not decorative: the Infinispan
|
||||||
|
* starter on the classpath auto-configures its own CacheManager bean
|
||||||
|
* unconditionally (see InfinispanEmbeddedCacheManagerAutoConfiguration),
|
||||||
|
* regardless of which profile is active, so without @Primary Spring
|
||||||
|
* would see two CacheManager beans and this one would not reliably win.
|
||||||
|
*/
|
||||||
|
@Configuration
|
||||||
|
@Profile("hazelcast")
|
||||||
|
public class HazelcastCacheConfig {
|
||||||
|
|
||||||
|
@Bean(destroyMethod = "shutdown")
|
||||||
|
public HazelcastInstance hazelcastInstance() {
|
||||||
|
Config config = new Config();
|
||||||
|
config.setClusterName("distributed-cache-demo-" + System.nanoTime());
|
||||||
|
config.getNetworkConfig().getJoin().getMulticastConfig().setEnabled(false);
|
||||||
|
config.getNetworkConfig().getJoin().getAutoDetectionConfig().setEnabled(false);
|
||||||
|
return Hazelcast.newHazelcastInstance(config);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
@Primary
|
||||||
|
public CacheManager cacheManager(HazelcastInstance hazelcastInstance) {
|
||||||
|
return new HazelcastCacheManager(hazelcastInstance);
|
||||||
|
}
|
||||||
|
}
|
||||||
+31
@@ -0,0 +1,31 @@
|
|||||||
|
package com.ankurm.distributedcache;
|
||||||
|
|
||||||
|
import org.infinispan.configuration.cache.ConfigurationBuilder;
|
||||||
|
import org.infinispan.manager.EmbeddedCacheManager;
|
||||||
|
import org.infinispan.spring.starter.embedded.InfinispanCacheConfigurer;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unlike Hazelcast's HazelcastCacheManager and Spring Data Redis's
|
||||||
|
* RedisCacheManager, Infinispan's embedded CacheManager does not create
|
||||||
|
* an undeclared cache on first use - calling getCache("exchangeRates")
|
||||||
|
* without this configurer throws ISPN000436 ("no matching cache
|
||||||
|
* configuration exists"), confirmed by the real exception a first,
|
||||||
|
* configurer-less version of this class produced. An
|
||||||
|
* InfinispanCacheConfigurer bean is the starter's documented extension
|
||||||
|
* point for declaring a cache programmatically instead of in an
|
||||||
|
* external infinispan.xml. This bean is not @Profile-gated because the
|
||||||
|
* starter's own auto-configuration isn't either - it always creates an
|
||||||
|
* embedded CacheManager regardless of which profile is active, so this
|
||||||
|
* just makes sure the one cache this demo needs exists on it.
|
||||||
|
*/
|
||||||
|
@Configuration
|
||||||
|
public class InfinispanCacheConfig {
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public InfinispanCacheConfigurer exchangeRatesCacheConfigurer() {
|
||||||
|
return (EmbeddedCacheManager cacheManager) ->
|
||||||
|
cacheManager.defineConfiguration("exchangeRates", new ConfigurationBuilder().build());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
package com.ankurm.distributedcache;
|
||||||
|
|
||||||
|
import org.springframework.cache.CacheManager;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.context.annotation.Primary;
|
||||||
|
import org.springframework.context.annotation.Profile;
|
||||||
|
import org.springframework.data.redis.cache.RedisCacheManager;
|
||||||
|
import org.springframework.data.redis.connection.RedisConnectionFactory;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Active only under the "redis" profile. Unlike the Hazelcast and
|
||||||
|
* Infinispan members, this CacheManager talks to a real out-of-process
|
||||||
|
* Redis server - here a Testcontainers container in tests, any real
|
||||||
|
* Redis instance (standalone or cluster) in production. The cache
|
||||||
|
* survives this application restarting; the embedded members' caches do
|
||||||
|
* not, which is the actual distinction this post is making, not just a
|
||||||
|
* difference in configuration style. @Primary disambiguates from the
|
||||||
|
* Infinispan starter's own unconditional auto-configured CacheManager
|
||||||
|
* bean - see the note on HazelcastCacheConfig for why that's needed.
|
||||||
|
*/
|
||||||
|
@Configuration
|
||||||
|
@Profile("redis")
|
||||||
|
public class RedisCacheConfig {
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
@Primary
|
||||||
|
public CacheManager cacheManager(RedisConnectionFactory connectionFactory) {
|
||||||
|
return RedisCacheManager.create(connectionFactory);
|
||||||
|
}
|
||||||
|
}
|
||||||
+70
@@ -0,0 +1,70 @@
|
|||||||
|
package com.ankurm.distributedcache;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The same two behaviours, asserted identically, against whichever
|
||||||
|
* CacheManager the active Spring profile wired up. That's deliberate:
|
||||||
|
* the point of the Spring Cache abstraction is that ExchangeRateLookupService
|
||||||
|
* and this test never change when the backend does - only the
|
||||||
|
* subclass's @ActiveProfiles (and, for Redis, the container) changes.
|
||||||
|
*
|
||||||
|
* ExchangeRateLookupService's callCount() is a singleton counter shared
|
||||||
|
* across every @Test method in a subclass's Spring context (JUnit does
|
||||||
|
* not create a fresh context per test method), so each test reads the
|
||||||
|
* counter before and after its own calls and asserts the *delta* rather
|
||||||
|
* than an absolute value - that way test execution order inside one
|
||||||
|
* class can never affect the result.
|
||||||
|
*/
|
||||||
|
abstract class CacheHitAndEvictionContractTest {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
ExchangeRateLookupService service;
|
||||||
|
|
||||||
|
abstract String backendName();
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void secondCallForSameKeyIsServedFromCache() {
|
||||||
|
int before = service.callCount();
|
||||||
|
|
||||||
|
service.rateFor("USD/INR");
|
||||||
|
int afterFirstCall = service.callCount();
|
||||||
|
|
||||||
|
service.rateFor("USD/INR");
|
||||||
|
int afterSecondCall = service.callCount();
|
||||||
|
|
||||||
|
Transcript t = new Transcript("29-" + backendName() + "-cache-hit.txt");
|
||||||
|
t.line("backend=%s", backendName());
|
||||||
|
t.line("rateFor(\"USD/INR\") call 1 -> method body ran, callCount %d -> %d", before, afterFirstCall);
|
||||||
|
t.line("rateFor(\"USD/INR\") call 2 -> served from cache, callCount stays %d", afterSecondCall);
|
||||||
|
t.write();
|
||||||
|
|
||||||
|
assertThat(afterFirstCall).isEqualTo(before + 1);
|
||||||
|
assertThat(afterSecondCall).isEqualTo(afterFirstCall);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void evictingAKeyForcesTheNextCallToRecompute() {
|
||||||
|
int before = service.callCount();
|
||||||
|
|
||||||
|
service.rateFor("USD/EUR");
|
||||||
|
int afterFirstCall = service.callCount();
|
||||||
|
|
||||||
|
service.evict("USD/EUR");
|
||||||
|
service.rateFor("USD/EUR");
|
||||||
|
int afterEvictAndRecall = service.callCount();
|
||||||
|
|
||||||
|
Transcript t = new Transcript("29-" + backendName() + "-cache-evict.txt");
|
||||||
|
t.line("backend=%s", backendName());
|
||||||
|
t.line("rateFor(\"USD/EUR\") call 1 -> method body ran, callCount %d -> %d", before, afterFirstCall);
|
||||||
|
t.line("evict(\"USD/EUR\") then rateFor(\"USD/EUR\") again -> method body ran again, callCount %d -> %d",
|
||||||
|
afterFirstCall, afterEvictAndRecall);
|
||||||
|
t.write();
|
||||||
|
|
||||||
|
assertThat(afterFirstCall).isEqualTo(before + 1);
|
||||||
|
assertThat(afterEvictAndRecall).isEqualTo(afterFirstCall + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package com.ankurm.distributedcache;
|
||||||
|
|
||||||
|
import org.springframework.boot.test.context.SpringBootTest;
|
||||||
|
import org.springframework.test.context.ActiveProfiles;
|
||||||
|
|
||||||
|
@SpringBootTest(classes = DistributedCacheDemoApplication.class)
|
||||||
|
@ActiveProfiles("hazelcast")
|
||||||
|
class HazelcastCacheTest extends CacheHitAndEvictionContractTest {
|
||||||
|
@Override
|
||||||
|
String backendName() {
|
||||||
|
return "hazelcast";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
package com.ankurm.distributedcache;
|
||||||
|
|
||||||
|
import org.springframework.boot.test.context.SpringBootTest;
|
||||||
|
import org.springframework.test.context.ActiveProfiles;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* No custom CacheManager bean exists for this profile at all - the
|
||||||
|
* Infinispan starter auto-configures its own SpringEmbeddedCacheManager
|
||||||
|
* unconditionally once it's on the classpath (confirmed by decompiling
|
||||||
|
* InfinispanEmbeddedCacheManagerAutoConfiguration), and nothing under
|
||||||
|
* the "hazelcast" or "redis" profiles is active to out-rank it with
|
||||||
|
* @Primary. The "infinispan" profile name exists only so this test
|
||||||
|
* class's intent reads the same way as its two siblings.
|
||||||
|
*/
|
||||||
|
@SpringBootTest(classes = DistributedCacheDemoApplication.class)
|
||||||
|
@ActiveProfiles("infinispan")
|
||||||
|
class InfinispanCacheTest extends CacheHitAndEvictionContractTest {
|
||||||
|
@Override
|
||||||
|
String backendName() {
|
||||||
|
return "infinispan";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
package com.ankurm.distributedcache;
|
||||||
|
|
||||||
|
import org.springframework.boot.test.context.SpringBootTest;
|
||||||
|
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
|
||||||
|
import org.springframework.test.context.ActiveProfiles;
|
||||||
|
import org.testcontainers.containers.GenericContainer;
|
||||||
|
import org.testcontainers.junit.jupiter.Container;
|
||||||
|
import org.testcontainers.junit.jupiter.Testcontainers;
|
||||||
|
import org.testcontainers.utility.DockerImageName;
|
||||||
|
|
||||||
|
@Testcontainers
|
||||||
|
@SpringBootTest(classes = DistributedCacheDemoApplication.class)
|
||||||
|
@ActiveProfiles("redis")
|
||||||
|
class RedisCacheTest extends CacheHitAndEvictionContractTest {
|
||||||
|
|
||||||
|
@Container
|
||||||
|
@ServiceConnection(name = "redis")
|
||||||
|
static GenericContainer<?> redis =
|
||||||
|
new GenericContainer<>(DockerImageName.parse("redis:8.8.3")).withExposedPorts(6379);
|
||||||
|
|
||||||
|
@Override
|
||||||
|
String backendName() {
|
||||||
|
return "redis";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
package com.ankurm.distributedcache;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Writes docs/output/NN-*.txt while a test asserts the same numbers, so
|
||||||
|
* every figure quoted in the blog post is backed by an assertion that
|
||||||
|
* fails the build when it stops being true.
|
||||||
|
*/
|
||||||
|
final class Transcript {
|
||||||
|
private final List<String> lines = new ArrayList<>();
|
||||||
|
private final Path file;
|
||||||
|
|
||||||
|
Transcript(String name) {
|
||||||
|
this.file = Path.of("docs/output/" + name);
|
||||||
|
}
|
||||||
|
|
||||||
|
Transcript line(String s) {
|
||||||
|
lines.add(s);
|
||||||
|
System.out.println(s);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
Transcript line(String fmt, Object... args) {
|
||||||
|
return line(fmt.formatted(args));
|
||||||
|
}
|
||||||
|
|
||||||
|
void write() {
|
||||||
|
try {
|
||||||
|
Files.createDirectories(file.getParent());
|
||||||
|
Files.writeString(file, String.join("\n", lines) + "\n");
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user