Skip to content

Commit 5c246fa

Browse files
feat: add spring java 17 module (#135)
Co-authored-by: Philipp Heuer <[email protected]>
1 parent 95655be commit 5c246fa

File tree

8 files changed

+363
-2
lines changed

8 files changed

+363
-2
lines changed

.github/workflows/gradle.yml

+2-2
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,10 @@ jobs:
1414
runs-on: ubuntu-latest
1515
steps:
1616
- uses: actions/checkout@v3
17-
- name: Set up JDK 11
17+
- name: Set up JDK 17
1818
uses: actions/setup-java@v3
1919
with:
20-
java-version: '11'
20+
java-version: '17'
2121
distribution: 'temurin'
2222
- name: Build with Gradle
2323
uses: gradle/gradle-build-action@67421db6bd0bf253fb4bd25b31ebb98943c375e1

renovate.json

+10
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,16 @@
4343
"matchPackageNames": ["com.github.ben-manes.caffeine:caffeine"],
4444
"matchPaths": ["provider-caffeine/build.gradle.kts"],
4545
"allowedVersions": "< 3.0.0"
46+
},
47+
{
48+
"matchPackageNames": ["org.springframework.boot:spring-boot-starter-test"],
49+
"matchPaths": ["spring/build.gradle.kts"],
50+
"allowedVersions": "< 3.0.0"
51+
},
52+
{
53+
"matchPackageNames": ["org.springframework:spring-context"],
54+
"matchPaths": ["spring/build.gradle.kts"],
55+
"allowedVersions": "< 6.0.0"
4656
}
4757
]
4858
}

settings.gradle.kts

+2
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ include(
66
":core",
77
":kotlin",
88
":spring",
9+
":spring-java17",
910
":provider-androidx",
1011
":provider-cache2k",
1112
":provider-caffeine",
@@ -22,6 +23,7 @@ project(":api").name = "cache-api"
2223
project(":core").name = "cache-core"
2324
project(":kotlin").name = "cache-kotlin"
2425
project(":spring").name = "cache-spring"
26+
project(":spring-java17").name = "cache-spring-java17"
2527
project(":provider-androidx").name = "cache-provider-androidx"
2628
project(":provider-cache2k").name = "cache-provider-cache2k"
2729
project(":provider-caffeine").name = "cache-provider-caffeine"

spring-java17/build.gradle.kts

+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
dependencies {
2+
api(project(":cache-core"))
3+
implementation("org.springframework:spring-context:6.0.9")
4+
testImplementation("org.springframework.boot:spring-boot-starter-test:3.1.0")
5+
testImplementation("org.awaitility:awaitility:4.2.0")
6+
testImplementation(testFixtures(project(":cache-core")))
7+
testImplementation(project(":cache-provider-caffeine"))
8+
}
9+
10+
java {
11+
sourceCompatibility = JavaVersion.VERSION_17
12+
targetCompatibility = JavaVersion.VERSION_17
13+
}
14+
15+
publishing.publications.withType<MavenPublication> {
16+
pom {
17+
name.set("Xanthic - Spring for JDK 17")
18+
description.set("Xanthic Cache Spring on JDK 17+")
19+
}
20+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
package io.github.xanthic.cache.springjdk17;
2+
3+
import io.github.xanthic.cache.api.Cache;
4+
import org.jetbrains.annotations.NotNull;
5+
import org.springframework.cache.support.AbstractValueAdaptingCache;
6+
7+
import java.util.concurrent.Callable;
8+
9+
public class XanthicSpringCache extends AbstractValueAdaptingCache {
10+
private final String name;
11+
private final Cache<Object, Object> cache;
12+
13+
public XanthicSpringCache(String name, Cache<Object, Object> cache) {
14+
super(true);
15+
this.name = name;
16+
this.cache = cache;
17+
}
18+
19+
@Override
20+
public @NotNull String getName() {
21+
return name;
22+
}
23+
24+
@Override
25+
public @NotNull Object getNativeCache() {
26+
return cache;
27+
}
28+
29+
@SuppressWarnings("unchecked")
30+
@Override
31+
public <T> T get(@NotNull Object key, @NotNull Callable<T> valueLoader) {
32+
return (T) cache.computeIfAbsent(key, k -> getSynchronized(key, valueLoader));
33+
}
34+
35+
@SuppressWarnings("unchecked")
36+
private synchronized <T> T getSynchronized(Object key, Callable<T> valueLoader) {
37+
ValueWrapper result = get(key);
38+
39+
if (result != null) {
40+
return (T) result.get();
41+
}
42+
43+
T value;
44+
try {
45+
value = valueLoader.call();
46+
} catch (Exception e) {
47+
throw new ValueRetrievalException(key, valueLoader, e);
48+
}
49+
return value;
50+
}
51+
52+
@Override
53+
public void put(@NotNull Object key, Object value) {
54+
cache.put(key, toStoreValue(value));
55+
}
56+
57+
@Override
58+
public void evict(@NotNull Object key) {
59+
cache.remove(key);
60+
}
61+
62+
@Override
63+
public void clear() {
64+
cache.clear();
65+
}
66+
67+
@Override
68+
protected Object lookup(@NotNull Object key) {
69+
return cache.get(key);
70+
}
71+
}
72+
73+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
package io.github.xanthic.cache.springjdk17;
2+
3+
import io.github.xanthic.cache.core.CacheApi;
4+
import io.github.xanthic.cache.core.CacheApiSpec;
5+
import lombok.Getter;
6+
import org.jetbrains.annotations.NotNull;
7+
import org.springframework.cache.Cache;
8+
import org.springframework.cache.CacheManager;
9+
import org.springframework.lang.Nullable;
10+
11+
import java.util.Collection;
12+
import java.util.Map;
13+
import java.util.Set;
14+
import java.util.concurrent.ConcurrentHashMap;
15+
import java.util.function.Consumer;
16+
17+
/**
18+
* CacheManager implementation that lazily builds XanthicCache instances for each getCache(java.lang.String) request.
19+
* Also supports a 'static' mode where the set of cache names is pre-defined through cacheNames in the constructor, with no dynamic creation of further cache regions at runtime.
20+
* The configuration of the underlying cache can be fine-tuned through the CacheApiSpec, passed into this CacheManager in the constructor.
21+
*/
22+
public class XanthicSpringCacheManager implements CacheManager {
23+
24+
private final Map<String, Cache> cacheMap = new ConcurrentHashMap<>();
25+
@Getter
26+
private final Set<String> customCacheNames = ConcurrentHashMap.newKeySet();
27+
private final Consumer<CacheApiSpec<Object, Object>> spec;
28+
private final boolean dynamic;
29+
30+
/**
31+
* XanthicSpringCacheManager will manage all xanthic cache instances for spring.
32+
*
33+
* @param spec the default CacheApiSpec used to create a new cache instances
34+
*/
35+
public XanthicSpringCacheManager(Consumer<CacheApiSpec<Object, Object>> spec) {
36+
this.spec = spec;
37+
this.dynamic = true;
38+
}
39+
40+
/**
41+
* XanthicSpringCacheManager will manage all xanthic cache instances for spring.
42+
*
43+
* @param spec the default CacheApiSpec used to create a new cache instances
44+
* @param cacheNames If not null, the number of caches and their names will be fixed, with no creation of further cache keys at runtime.
45+
*/
46+
public XanthicSpringCacheManager(Consumer<CacheApiSpec<Object, Object>> spec, @Nullable Collection<String> cacheNames) {
47+
this.spec = spec;
48+
49+
if (cacheNames != null) {
50+
this.dynamic = false;
51+
for (String name : cacheNames) {
52+
this.cacheMap.put(name, createCache(name, this.spec));
53+
}
54+
} else {
55+
this.dynamic = true;
56+
}
57+
}
58+
59+
@Override
60+
@Nullable
61+
public Cache getCache(@NotNull String name) {
62+
// Optimistic lock-free lookup to avoid contention: https://github.com/spring-projects/spring-framework/issues/30066
63+
Cache optimistic = cacheMap.get(name);
64+
if (optimistic != null || !dynamic)
65+
return optimistic;
66+
67+
return this.cacheMap.computeIfAbsent(name, cacheName -> createCache(cacheName, this.spec));
68+
}
69+
70+
@Override
71+
public @NotNull Collection<String> getCacheNames() {
72+
return this.cacheMap.keySet();
73+
}
74+
75+
/**
76+
* Register a custom xanthic cache by customizing the CacheApiSpec.
77+
*
78+
* @param name the name of the cache
79+
* @param spec configuration for the specified cache
80+
*/
81+
public void registerCache(String name, Consumer<CacheApiSpec<Object, Object>> spec) {
82+
if (!this.dynamic) throw new IllegalStateException("CacheManager has a fixed set of cache keys and does not allow creation of new caches.");
83+
84+
this.cacheMap.put(name, createCache(name, spec));
85+
this.customCacheNames.add(name);
86+
}
87+
88+
private Cache createCache(String name, Consumer<CacheApiSpec<Object, Object>> spec) {
89+
return new XanthicSpringCache(name, CacheApi.create(spec));
90+
}
91+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
package io.github.xanthic.cache.springjdk17;
2+
3+
import io.github.xanthic.cache.springjdk17.config.CacheConfiguration;
4+
import org.junit.jupiter.api.Assertions;
5+
import org.junit.jupiter.api.DisplayName;
6+
import org.junit.jupiter.api.Test;
7+
import org.junit.jupiter.api.extension.ExtendWith;
8+
import org.springframework.beans.factory.annotation.Autowired;
9+
import org.springframework.cache.Cache;
10+
import org.springframework.cache.CacheManager;
11+
import org.springframework.test.context.ContextConfiguration;
12+
import org.springframework.test.context.junit.jupiter.SpringExtension;
13+
14+
import java.util.Objects;
15+
import java.util.concurrent.Callable;
16+
import java.util.concurrent.ExecutorService;
17+
import java.util.concurrent.Executors;
18+
import java.util.concurrent.TimeUnit;
19+
import java.util.concurrent.atomic.AtomicInteger;
20+
21+
import static org.awaitility.Awaitility.await;
22+
23+
@ExtendWith(SpringExtension.class)
24+
@ContextConfiguration(classes = { CacheConfiguration.class })
25+
public class SpringCacheTest {
26+
27+
@Autowired
28+
CacheManager cacheManager;
29+
30+
@Test
31+
@DisplayName("Tests cache get, put, putIfAbsent, clear")
32+
public void putGetClearTest() {
33+
Cache cache = Objects.requireNonNull(cacheManager.getCache("dev"));
34+
35+
// cache should be listed in cacheManager
36+
Assertions.assertTrue(cacheManager.getCacheNames().contains("dev"));
37+
38+
// Test put/get
39+
cache.put("4/20", 420);
40+
Assertions.assertEquals(420, Objects.requireNonNull(cache.get("4/20")).get());
41+
42+
// Test putIfAbsent
43+
Assertions.assertEquals(420, Objects.requireNonNull(cache.putIfAbsent("4/20", 69)).get());
44+
Assertions.assertEquals(420, Objects.requireNonNull(cache.get("4/20")).get());
45+
46+
// Test clear
47+
cache.clear();
48+
Assertions.assertNull(cache.get("4/20"));
49+
}
50+
51+
@Test
52+
@DisplayName("Tests the registration and usage of a custom cache")
53+
public void registerCustomCacheTest() {
54+
XanthicSpringCacheManager xanthicSpringCacheManager = (XanthicSpringCacheManager) cacheManager;
55+
xanthicSpringCacheManager.registerCache("my-custom-cache", spec -> {
56+
spec.maxSize(1L);
57+
});
58+
59+
// registration check
60+
Assertions.assertTrue(xanthicSpringCacheManager.getCustomCacheNames().contains("my-custom-cache"), "getCustomCacheNames should contain my-custom-cache");
61+
62+
// cache available
63+
Cache cache = cacheManager.getCache("my-custom-cache");
64+
Assertions.assertNotNull(cache, "my-custom-cache should not be null");
65+
}
66+
67+
@Test
68+
@DisplayName("Tests the eviction of entries based on max size")
69+
public void evictionTest() {
70+
XanthicSpringCacheManager xanthicSpringCacheManager = (XanthicSpringCacheManager) cacheManager;
71+
xanthicSpringCacheManager.registerCache("small-cache", spec -> {
72+
spec.maxSize(2L);
73+
});
74+
75+
Cache cache = Objects.requireNonNull(cacheManager.getCache("small-cache"));
76+
cache.put("first", 1);
77+
cache.put("second", 2);
78+
cache.put("third", 3);
79+
80+
Assertions.assertEquals(2, Objects.requireNonNull(cache.get("second")).get());
81+
Assertions.assertEquals(3, Objects.requireNonNull(cache.get("third")).get());
82+
await().atLeast(100, TimeUnit.MILLISECONDS)
83+
.atMost(5, TimeUnit.SECONDS)
84+
.until(() -> cache.get("first") == null);
85+
}
86+
87+
@Test
88+
@DisplayName("Tests the eviction of entries based on max size")
89+
public void valueLoaderTest() {
90+
XanthicSpringCacheManager xanthicSpringCacheManager = (XanthicSpringCacheManager) cacheManager;
91+
xanthicSpringCacheManager.registerCache("value-cache", spec -> {
92+
spec.maxSize(100L);
93+
});
94+
Cache cache = Objects.requireNonNull(cacheManager.getCache("value-cache"));
95+
96+
AtomicInteger callCounter = new AtomicInteger(0);
97+
Callable<String> valueLoader = () -> {
98+
callCounter.incrementAndGet();
99+
return "value-loaded";
100+
};
101+
102+
String value = cache.get("key", valueLoader);
103+
Assertions.assertEquals("value-loaded", value);
104+
Assertions.assertEquals(1, callCounter.get(), "Value loader should only be called once");
105+
106+
// Check that the valueLoader is not called again
107+
value = cache.get("key", valueLoader);
108+
Assertions.assertEquals("value-loaded", value);
109+
Assertions.assertEquals(1, callCounter.get(), "Value loader should still be called only once");
110+
}
111+
112+
@Test
113+
@DisplayName("Tests the eviction of entries based on max size")
114+
public void valueLoaderConcurrentTest() throws InterruptedException {
115+
XanthicSpringCacheManager xanthicSpringCacheManager = (XanthicSpringCacheManager) cacheManager;
116+
xanthicSpringCacheManager.registerCache("value-cache", spec -> {
117+
spec.maxSize(100L);
118+
});
119+
Cache cache = Objects.requireNonNull(cacheManager.getCache("value-cache"));
120+
121+
AtomicInteger callCounter = new AtomicInteger(0);
122+
Callable<String> valueLoader = () -> {
123+
callCounter.incrementAndGet();
124+
return "value-loaded";
125+
};
126+
127+
int numThreads = 10;
128+
ExecutorService executorService = Executors.newFixedThreadPool(numThreads);
129+
for (int i = 0; i < numThreads; i++) {
130+
executorService.submit(() -> {
131+
String value = cache.get("key", valueLoader);
132+
Assertions.assertEquals("value-loaded", value);
133+
});
134+
}
135+
executorService.awaitTermination(5, TimeUnit.SECONDS);
136+
137+
String value = cache.get("key", valueLoader);
138+
Assertions.assertEquals("value-loaded", value);
139+
Assertions.assertEquals(1, callCounter.get(), "Value loader should only be called once");
140+
}
141+
142+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package io.github.xanthic.cache.springjdk17.config;
2+
3+
import io.github.xanthic.cache.api.domain.ExpiryType;
4+
import io.github.xanthic.cache.springjdk17.XanthicSpringCacheManager;
5+
import org.springframework.cache.CacheManager;
6+
import org.springframework.context.annotation.Bean;
7+
import org.springframework.context.annotation.Configuration;
8+
9+
@Configuration
10+
public class CacheConfiguration {
11+
12+
@Bean
13+
public CacheManager cacheManager() {
14+
XanthicSpringCacheManager cacheManager = new XanthicSpringCacheManager(spec -> {
15+
spec.expiryType(ExpiryType.POST_ACCESS);
16+
});
17+
cacheManager.registerCache("my-custom-cache", spec -> {
18+
spec.maxSize(10L);
19+
});
20+
return cacheManager;
21+
}
22+
23+
}

0 commit comments

Comments
 (0)