CacheRegistryConfiguration.java
/*
* Copyright (c) 2007-2026 MetaSolutions AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.entrystore.rest.springboot.configuration;
import com.github.benmanes.caffeine.cache.Cache;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cache.CacheManager;
import org.springframework.cache.caffeine.CaffeineCacheManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.crypto.password.PasswordEncoder;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
/**
* Publishes a single {@link CacheManager} that wraps the application's hand-built Caffeine caches
* so they appear under the Actuator {@code /management/caches} endpoint. Without a managed
* {@code CacheManager} the endpoint reports an empty {@code cacheManagers}, because the caches are
* created directly via {@code Caffeine.newBuilder()} and are otherwise invisible to Spring.
*
* <p>This is the registration half of the {@link CaffeineCacheSource} registry; see that interface
* for why the REST layer registers native caches instead of adopting {@code @EnableCaching}.
*/
@Slf4j
@Configuration
public class CacheRegistryConfiguration {
@Bean
public CacheManager cacheManager(List<CaffeineCacheSource> sources, PasswordEncoder passwordEncoder) {
var allSources = new ArrayList<>(sources);
// passwordEncoder is published under type PasswordEncoder (the cache-bearing CachingPasswordEncoder
// is only installed when basic auth is enabled), so type-based List<CaffeineCacheSource> injection
// does not reliably include it — add it explicitly when it happens to carry a cache.
if (passwordEncoder instanceof CaffeineCacheSource pe && !allSources.contains(pe)) {
allSources.add(pe);
}
var manager = new CaffeineCacheManager();
// Non-dynamic: serve only the caches registered below. A stray getCache() miss must not
// silently materialise an empty cache that then shows up under /caches.
manager.setCacheNames(List.of());
var registered = new LinkedHashMap<String, Cache<?, ?>>();
var owners = new HashMap<String, String>();
for (CaffeineCacheSource source : allSources) {
Map<String, Cache<?, ?>> caches = source.caffeineCaches();
if (caches == null) {
throw new IllegalStateException(source.getClass().getName()
+ " returned null from caffeineCaches() — it must return a (possibly empty) map");
}
caches.forEach((name, cache) -> {
if (cache == null) {
throw new IllegalStateException("Null cache for name '" + name + "' from "
+ source.getClass().getName());
}
if (registered.putIfAbsent(name, cache) == null) {
owners.put(name, source.getClass().getName());
manager.registerCustomCache(name, asObjectCache(cache));
} else {
log.warn("Duplicate cache name '{}' from {}; keeping the registration from {}",
name, source.getClass().getName(), owners.get(name));
}
});
}
log.info("Exposed {} Caffeine cache(s) under the actuator caches endpoint: {}",
registered.size(), registered.keySet());
return manager;
}
// CaffeineCacheManager.registerCustomCache requires Cache<Object, Object>; the sources hold
// concretely-typed caches (e.g. Cache<String, Integer>). The cast is safe by erasure (both are the
// same runtime class) and the actuator only invokes type-agnostic operations on the cache
// (getNativeCache().getClass() for reporting, clear() for eviction) — never typed key/value reads.
@SuppressWarnings("unchecked")
private static Cache<Object, Object> asObjectCache(Cache<?, ?> cache) {
return (Cache<Object, Object>) cache;
}
}