A Spring Boot 4.1.1 module whose test suite is the evidence for the article: 19 tests producing 22 transcripts under docs/output/, plus 12 documentation chapters. Findings the build pins: - @EnableCaching has no exposeProxy attribute; the widely-copied @EnableCaching(exposeProxy = true) does not compile. - Two methods sharing a cache name and an argument type share a key space, and one silently serves the other's answers. - beforeInvocation = true is NOT deferred by TransactionAwareCacheManagerProxy on 7.0.9 - doEvict picks evictIfPresent, which the decorator does not intercept. - Four of five invalid declarations start a clean context and throw at the first call. - Caffeine on the classpath silently displaces the simple provider.
154 lines
5.9 KiB
Java
154 lines
5.9 KiB
Java
package com.ankurm.caching;
|
|
|
|
import org.junit.jupiter.api.Test;
|
|
import org.springframework.boot.autoconfigure.AutoConfigurations;
|
|
import org.springframework.boot.cache.autoconfigure.CacheAutoConfiguration;
|
|
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
|
|
import org.springframework.cache.annotation.CacheEvict;
|
|
import org.springframework.cache.annotation.Cacheable;
|
|
import org.springframework.cache.annotation.EnableCaching;
|
|
import org.springframework.context.annotation.Bean;
|
|
import org.springframework.context.annotation.Configuration;
|
|
|
|
/**
|
|
* Declarations the abstraction rejects, and when it tells you. Some of these fail while the
|
|
* context is still starting, which is the good case; others wait for the first call.
|
|
*
|
|
* <p>Every message below is the framework's own, captured from a real failed context.
|
|
*/
|
|
class InvalidDeclarationsTest {
|
|
|
|
private final ApplicationContextRunner runner = new ApplicationContextRunner()
|
|
.withConfiguration(AutoConfigurations.of(CacheAutoConfiguration.class))
|
|
.withPropertyValues("spring.cache.type=simple");
|
|
|
|
@Test
|
|
void whatTheAbstractionRefusesAndWhen() {
|
|
try (Transcript t = new Transcript("20-invalid-declarations.txt",
|
|
"Declarations that are rejected, and how late you find out")) {
|
|
|
|
t.line("1. key and keyGenerator together");
|
|
runner.withUserConfiguration(BothKeyAndGenerator.class).run(context -> {
|
|
report(t, context.getStartupFailure());
|
|
if (context.getStartupFailure() == null) {
|
|
probe(t, context::getBean, BothKeyAndGenerator.Svc.class);
|
|
}
|
|
});
|
|
|
|
t.section("2. sync = true with unless");
|
|
runner.withUserConfiguration(SyncWithUnless.class).run(context -> {
|
|
report(t, context.getStartupFailure());
|
|
probe(t, context::getBean, SyncWithUnless.Svc.class);
|
|
});
|
|
|
|
t.section("3. sync = true across two caches");
|
|
runner.withUserConfiguration(SyncTwoCaches.class).run(context -> {
|
|
report(t, context.getStartupFailure());
|
|
probe(t, context::getBean, SyncTwoCaches.Svc.class);
|
|
});
|
|
|
|
t.section("4. @Cacheable and @CacheEvict on one method");
|
|
runner.withUserConfiguration(CacheableAndEvict.class).run(context -> {
|
|
report(t, context.getStartupFailure());
|
|
probe(t, context::getBean, CacheableAndEvict.Svc.class);
|
|
});
|
|
|
|
t.section("5. a cache name that spring.cache.cache-names does not declare");
|
|
new ApplicationContextRunner()
|
|
.withConfiguration(AutoConfigurations.of(CacheAutoConfiguration.class))
|
|
.withPropertyValues("spring.cache.type=simple", "spring.cache.cache-names=known")
|
|
.withUserConfiguration(UndeclaredCache.class)
|
|
.run(context -> {
|
|
report(t, context.getStartupFailure());
|
|
probe(t, context::getBean, UndeclaredCache.Svc.class);
|
|
});
|
|
|
|
t.line("");
|
|
t.line("Only the first of these is a compile-time-shaped mistake. The rest start a");
|
|
t.line("perfectly healthy application and throw on a code path that may not be hit");
|
|
t.line("for hours.");
|
|
}
|
|
}
|
|
|
|
private void report(Transcript t, Throwable startupFailure) {
|
|
if (startupFailure == null) {
|
|
t.line(" startup : clean");
|
|
return;
|
|
}
|
|
Throwable cause = root(startupFailure);
|
|
t.line(" startup : FAILED - %s", cause.getClass().getName());
|
|
t.line(" %s", cause.getMessage());
|
|
}
|
|
|
|
private static Throwable root(Throwable t) {
|
|
Throwable cause = t;
|
|
while (cause.getCause() != null && cause.getCause() != cause) {
|
|
cause = cause.getCause();
|
|
}
|
|
return cause;
|
|
}
|
|
|
|
private <T> void probe(Transcript t, java.util.function.Function<Class<T>, T> lookup, Class<T> type) {
|
|
try {
|
|
T bean = lookup.apply(type);
|
|
Object result = type.getMethod("call", String.class).invoke(bean, "k");
|
|
t.line(" first call: returned %s", result);
|
|
} catch (Exception e) {
|
|
Throwable cause = root(e);
|
|
t.line(" first call: %s", cause.getClass().getName());
|
|
t.line(" %s", cause.getMessage());
|
|
}
|
|
}
|
|
|
|
@Configuration
|
|
@EnableCaching
|
|
static class BothKeyAndGenerator {
|
|
@Bean Svc svc() { return new Svc(); }
|
|
static class Svc {
|
|
@Cacheable(cacheNames = "c", key = "#a", keyGenerator = "simpleKeyGenerator")
|
|
public String call(String a) { return "v:" + a; }
|
|
}
|
|
}
|
|
|
|
@Configuration
|
|
@EnableCaching
|
|
static class SyncWithUnless {
|
|
@Bean Svc svc() { return new Svc(); }
|
|
static class Svc {
|
|
@Cacheable(cacheNames = "c", sync = true, unless = "#result != null")
|
|
public String call(String a) { return "v:" + a; }
|
|
}
|
|
}
|
|
|
|
@Configuration
|
|
@EnableCaching
|
|
static class SyncTwoCaches {
|
|
@Bean Svc svc() { return new Svc(); }
|
|
static class Svc {
|
|
@Cacheable(cacheNames = {"c1", "c2"}, sync = true)
|
|
public String call(String a) { return "v:" + a; }
|
|
}
|
|
}
|
|
|
|
@Configuration
|
|
@EnableCaching
|
|
static class CacheableAndEvict {
|
|
@Bean Svc svc() { return new Svc(); }
|
|
static class Svc {
|
|
@Cacheable(cacheNames = "c", sync = true)
|
|
@CacheEvict(cacheNames = "c")
|
|
public String call(String a) { return "v:" + a; }
|
|
}
|
|
}
|
|
|
|
@Configuration
|
|
@EnableCaching
|
|
static class UndeclaredCache {
|
|
@Bean Svc svc() { return new Svc(); }
|
|
static class Svc {
|
|
@Cacheable("unknown")
|
|
public String call(String a) { return "v:" + a; }
|
|
}
|
|
}
|
|
}
|