Understanding Spring Boot Caching with JCache, Annotations, and Redis Integration
This article explains Spring Boot's caching mechanism based on the JSR‑107 JCache specification, details core interfaces and implementations like Cache, AbstractValueAdaptingCache, and ConcurrentMapCache, demonstrates cache annotations such as @Cacheable, @CachePut and @CacheEvict, and shows how to replace the default in‑memory cache with Redis using Docker, StringRedisTemplate, and custom serialization.
Spring Boot caching relies on the JSR‑107 (JCache) specification, which defines core interfaces such as CachingProvider , CacheManager , Cache , Entry and Expiry .
The Cache interface abstracts operations like get , put and evict , and Spring provides implementations such as ConcurrentMapCache and AbstractValueAdaptingCache that handle null values and optional serialization.
Spring’s cache abstraction is enabled with @EnableCaching and uses annotations @Cacheable , @CachePut and @CacheEvict to declaratively manage method results, supporting key generation, conditions, SpEL expressions and sync options.
For production use, Redis can replace the default in‑memory cache. The article shows how to start a Redis container with Docker, configure StringRedisTemplate and RedisTemplate , and customize serialization to store objects as JSON instead of Java serialization.
Custom CacheManager and RedisCacheManager examples illustrate how to control cache prefixes and serialization strategies, allowing Spring to manage Redis‑backed caches alongside other cache implementations.
public interface Cache {
String getName();
Object getNativeCache();
@Nullable
Cache.ValueWrapper get(Object key);
@Nullable
T get(Object key, @Nullable Class
type);
@Nullable
T get(Object key, Callable
valueLoader);
void put(Object key, @Nullable Object value);
@Nullable
ValueWrapper putIfAbsent(Object key, @Nullable Object value);
void evict(Object key);
boolean evictIfPresent(Object key);
void clear();
boolean invalidate();
interface ValueWrapper {
@Nullable Object get();
}
}
public abstract class AbstractValueAdaptingCache implements Cache {
private final boolean allowNullValues;
protected AbstractValueAdaptingCache(boolean allowNullValues) { this.allowNullValues = allowNullValues; }
public final boolean isAllowNullValues() { return this.allowNullValues; }
@Nullable
public ValueWrapper get(Object key) { return toValueWrapper(lookup(key)); }
@Nullable
protected abstract Object lookup(Object key);
@Nullable
protected Object fromStoreValue(@Nullable Object storeValue) { return this.allowNullValues && storeValue == NullValue.INSTANCE ? null : storeValue; }
@Nullable
protected Object toStoreValue(@Nullable Object userValue) { if (userValue == null) { if (this.allowNullValues) return NullValue.INSTANCE; else throw new IllegalArgumentException("Null values not allowed"); } return userValue; }
@Nullable
protected ValueWrapper toValueWrapper(@Nullable Object storeValue) { return storeValue != null ? new SimpleValueWrapper(fromStoreValue(storeValue)) : null; }
}
public class ConcurrentMapCache extends AbstractValueAdaptingCache {
private final String name;
private final ConcurrentMap
store;
private final SerializationDelegate serialization;
public ConcurrentMapCache(String name) { this(name, new ConcurrentHashMap(256), true); }
public ConcurrentMapCache(String name, boolean allowNullValues) { this(name, new ConcurrentHashMap(256), allowNullValues); }
protected ConcurrentMapCache(String name, ConcurrentMap
store, boolean allowNullValues, @Nullable SerializationDelegate serialization) {
super(allowNullValues);
Assert.notNull(name, "Name must not be null");
Assert.notNull(store, "Store must not be null");
this.name = name;
this.store = store;
this.serialization = serialization;
}
public String getName() { return this.name; }
public ConcurrentMap
getNativeCache() { return this.store; }
@Nullable
protected Object lookup(Object key) { return this.store.get(key); }
public void put(Object key, @Nullable Object value) { this.store.put(key, toStoreValue(value)); }
public void evict(Object key) { this.store.remove(key); }
public boolean evictIfPresent(Object key) { return this.store.remove(key) != null; }
public void clear() { this.store.clear(); }
public boolean invalidate() { boolean notEmpty = !this.store.isEmpty(); this.store.clear(); return notEmpty; }
}Top Architect
Top Architect focuses on sharing practical architecture knowledge, covering enterprise, system, website, large‑scale distributed, and high‑availability architectures, plus architecture adjustments using internet technologies. We welcome idea‑driven, sharing‑oriented architects to exchange and learn together.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.