Design and Implementation of a Local Cache Component in Spring Boot
This article explains how to build a robust, thread‑safe local cache for Spring Boot applications by defining cache domain models, abstract managers, a centralized registry, and scheduled refresh tasks, while providing complete Java code examples and best‑practice recommendations.
Introduction
When implementing a local cache, developers often resort to using a thread‑safe ConcurrentHashMap together with SpringBoot's @Scheduled annotation. Although functional, this approach is neither elegant nor safe for production use.
Proposed Architecture
A well‑structured local cache should consist of the following components:
DAL layer that produces DAO and DO objects and defines cache domain models.
Cache name definition with initialization order.
Data repository that converts data models to cache models.
Cache manager that extends an abstract manager ( AbstractCacheManager ).
Cache API exposing basic operations such as putAll , get , and getCacheInfo .
Bean configuration that allows plug‑in registration of managers and repositories.
Core Interfaces and Classes
CacheNameDomain defines the cache name, description and order:
package com.example.test.localcache;
public interface CacheNameDomain {
int getOrder();
String getName();
String getDescription();
}CacheManager is the contract for all cache managers:
package com.example.test.localcache;
import org.springframework.core.Ordered;
public interface CacheManager extends Ordered {
void initCache();
void refreshCache();
CacheNameDomain getCacheName();
void dumpCache();
long getCacheSize();
}The abstract base class provides common lifecycle handling and logging:
package com.example.test.localcache;
import com.example.test.localcache.manager.CacheManagerRegistry;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.InitializingBean;
public abstract class AbstractCacheManager implements CacheManager, InitializingBean {
protected static final Logger LOGGER = LoggerFactory.getLogger(AbstractCacheManager.class);
protected abstract String getCacheInfo();
protected abstract void loadingCache();
protected abstract long getSize();
@Override
public void afterPropertiesSet() {
CacheManagerRegistry.register(this);
}
@Override
public void initCache() {
String description = getCacheName().getDescription();
LOGGER.info("start init {}", description);
loadingCache();
afterInitCache();
LOGGER.info("{} end init", description);
}
@Override
public void refreshCache() {
String description = getCacheName().getDescription();
LOGGER.info("start refresh {}", description);
loadingCache();
afterRefreshCache();
LOGGER.info("{} end refresh", description);
}
@Override
public int getOrder() {
return getCacheName().getOrder();
}
@Override
public void dumpCache() {
String description = getCacheName().getDescription();
LOGGER.info("start print {}\n{}", description, getCacheInfo());
LOGGER.info("{} end print", description);
}
@Override
public long getCacheSize() {
LOGGER.info("Cache Size Count: {}", getSize());
return getSize();
}
protected void afterInitCache() { /* optional post‑init actions */ }
protected void afterRefreshCache() { /* optional post‑refresh actions */ }
}A central registry collects all cache managers and provides unified operations:
package com.example.test.localcache.manager;
import com.example.test.localcache.CacheManager;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.core.OrderComparator;
import org.springframework.stereotype.Component;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
@Component
public final class CacheManagerRegistry implements InitializingBean {
private static final Logger logger = LoggerFactory.getLogger(CacheManagerRegistry.class);
private static Map
managerMap = new ConcurrentHashMap<>();
public static void register(CacheManager cacheManager) {
String cacheName = resolveCacheName(cacheManager.getCacheName().getName());
managerMap.put(cacheName, cacheManager);
}
public static void refreshCache(String cacheName) {
CacheManager cm = managerMap.get(resolveCacheName(cacheName));
if (cm == null) { logger.warn("cache manager not exist, cacheName={}", cacheName); return; }
cm.refreshCache();
cm.dumpCache();
}
public static long getCacheSize(String cacheName) {
CacheManager cm = managerMap.get(resolveCacheName(cacheName));
if (cm == null) { logger.warn("cache manager not exist, cacheName={}", cacheName); return 0; }
return cm.getCacheSize();
}
public static List
getCacheNameList() {
List
list = new ArrayList<>();
managerMap.forEach((k, v) -> list.add(k));
return list;
}
@Override
public void afterPropertiesSet() throws Exception { startup(); }
private void startup() {
try { deployCompletion(); }
catch (Exception e) { logger.error("Cache Component Init Fail:", e); throw new RuntimeException("启动加载失败", e); }
}
private void deployCompletion() {
List
managers = new ArrayList<>(managerMap.values());
Collections.sort(managers, new OrderComparator());
logger.info("cache manager component extensions:");
for (CacheManager cm : managers) {
logger.info(cm.getCacheName().getName() + " ==> " + cm.getClass().getSimpleName());
}
for (CacheManager cm : managers) {
cm.initCache();
cm.dumpCache();
}
}
private static String resolveCacheName(String cacheName) { return cacheName.toUpperCase(); }
}A scheduled trigger periodically refreshes all registered caches in order:
package com.example.test.localcache.manager;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import java.text.MessageFormat;
import java.util.List;
@Component
public class CacheManagerTrigger {
private static final Logger LOGGER = LoggerFactory.getLogger(CacheManagerTrigger.class);
@Scheduled(fixedRate = 1000 * 60, initialDelay = 1000 * 60)
private static void refreshCache() {
List
cacheNameList = getCacheList();
LOGGER.info("start refresh instruction, cacheNameList={}", cacheNameList);
if (CollectionUtils.isEmpty(cacheNameList)) { LOGGER.warn("cache name list are empty"); return; }
long totalCacheSize = 0;
for (String name : cacheNameList) {
CacheManagerRegistry.refreshCache(name);
totalCacheSize += CacheManagerRegistry.getCacheSize(name);
}
LOGGER.info(MessageFormat.format("缓存刷新成功,缓存管理器:{0}个,总缓存条目数量:{1}条", cacheNameList.size(), totalCacheSize));
}
private static List
getCacheList() { return CacheManagerRegistry.getCacheNameList(); }
}Example Implementation – SysConfigCacheManager
The following concrete manager caches system configuration data using a ConcurrentHashMap and a re‑entrant lock to avoid read/write conflicts:
package com.example.test.manger;
import com.example.test.localcache.AbstractCacheManager;
import com.example.test.localcache.CacheNameDomain;
import com.example.test.localcache.constant.CacheNameEnum;
import com.example.test.localcache.util.CacheMessageUtil;
import org.springframework.stereotype.Component;
import java.util.concurrent.*;
@Component
public class SysConfigCacheManager extends AbstractCacheManager {
private static final Lock LOCK = new ReentrantLock();
private static ConcurrentMap
CACHE;
@Override
protected String getCacheInfo() { return CacheMessageUtil.toString(CACHE); }
@Override
protected void loadingCache() {
LOCK.lock();
try {
CACHE = new ConcurrentHashMap<>();
CACHE.put("key1", "value1");
CACHE.put("key2", "value2");
CACHE.put("key3", "value3");
} finally { LOCK.unlock(); }
}
@Override
protected long getSize() { return CACHE == null ? 0 : CACHE.size(); }
@Override
public CacheNameDomain getCacheName() { return CacheNameEnum.SYS_CONFIG; }
}This manager is automatically registered via AbstractCacheManager.afterPropertiesSet() , and its cache will be refreshed every minute by CacheManagerTrigger .
Conclusion
The article demonstrates a clean, extensible way to build local caches in Spring Boot without external services. Developers can replace the ConcurrentHashMap with Redis or another distributed store for higher scalability, while reusing the same manager, registry, and trigger infrastructure.
Selected Java Interview Questions
A professional Java tech channel sharing common knowledge to help developers fill gaps. Follow us!
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.