NOTE
Spring Boot Cache Auto-Configuration Principle
How the cache manager and cache classes are generated, how cache proxy classes are generated, and the overall execution flow.
This is a historical learning note and may contain outdated or incomplete understanding.
How are the cache manager and cache classes generated?
1. Auto-Configuration Class Pre-Imported by spring-boot-starter
@CacheAutoConfiguration
The cache auto-configuration class is CacheAutoConfiguration. It imports a CacheConfigurationImportSelector.
@Configuration
@ConditionalOnClass(CacheManager.class)
@ConditionalOnBean(CacheAspectSupport.class)
@ConditionalOnMissingBean(value = CacheManager.class, name = "cacheResolver")
@EnableConfigurationProperties(CacheProperties.class)
@AutoConfigureBefore(HibernateJpaAutoConfiguration.class)
@AutoConfigureAfter({ CouchbaseAutoConfiguration.class, HazelcastAutoConfiguration.class,
RedisAutoConfiguration.class })
@Import(CacheConfigurationImportSelector.class)// Imports some components
public class CacheAutoConfiguration {
}
We know that when Spring Boot starts, it needs to refresh the container. One step calls the postProcessBeanDefinitionRegistry methods of all BeanFactoryPostProcessor instances. It gets all ImportSelector instances and calls their selectImport methods. Therefore, we mainly look at the selectImport method of CacheConfigurationImportSelector.
1.1. Import SimpleCacheConfiguration by Default
CacheConfigurationImportSelector
static class CacheConfigurationImportSelector implements ImportSelector {
@Override
public String[] selectImports(AnnotationMetadata importingClassMetadata) {
// Get all predefined enum values for cache components
CacheType[] types = CacheType.values();
String[] imports = new String[types.length];
// Traverse the enum and get the corresponding configuration class from
// the [cache component name : cache configuration class] definitions in CacheConfigurations
for (int i = 0; i < types.length; i++) {
imports[i] = CacheConfigurations.getConfigurationClass(types[i]);
}
// All cache configuration classes are as follows
//0 = "org.springframework.boot.autoconfigure.cache.GenericCacheConfiguration"
//1 = "org.springframework.boot.autoconfigure.cache.JCacheCacheConfiguration"
//2 = "org.springframework.boot.autoconfigure.cache.EhCacheCacheConfiguration"
//3 = "org.springframework.boot.autoconfigure.cache.HazelcastCacheConfiguration"
//4 = "org.springframework.boot.autoconfigure.cache.InfinispanCacheConfiguration"
//5 = "org.springframework.boot.autoconfigure.cache.CouchbaseCacheConfiguration"
//6 = "org.springframework.boot.autoconfigure.cache.RedisCacheConfiguration"
//7 = "org.springframework.boot.autoconfigure.cache.CaffeineCacheConfiguration"
//8 = "org.springframework.boot.autoconfigure.cache.GuavaCacheConfiguration"
//9 = "org.springframework.boot.autoconfigure.cache.SimpleCacheConfiguration"// Enable debug=true in the configuration file and you can see this is the default
//10 = "org.springframework.boot.autoconfigure.cache.NoOpCacheConfiguration"
return imports;
}
}
1.2. Import ConcurrentMapCacheManager into the Container
Take org.springframework.boot.autoconfigure.cache.SimpleCacheConfiguration as an example.
@Configuration
@ConditionalOnMissingBean(CacheManager.class)
@Conditional(CacheCondition.class)
class SimpleCacheConfiguration {
private final CacheProperties cacheProperties;
private final CacheManagerCustomizers customizerInvoker;
SimpleCacheConfiguration(CacheProperties cacheProperties,
CacheManagerCustomizers customizerInvoker) {
this.cacheProperties = cacheProperties;
this.customizerInvoker = customizerInvoker;
}
// Its purpose is to inject ConcurrentMapCacheManager into the IoC container
@Bean
public ConcurrentMapCacheManager cacheManager() {
ConcurrentMapCacheManager cacheManager = new ConcurrentMapCacheManager();
List<String> cacheNames = this.cacheProperties.getCacheNames();
if (!cacheNames.isEmpty()) {
cacheManager.setCacheNames(cacheNames);
}
return this.customizerInvoker.customize(cacheManager);
}
}
1.2.1. Obtain Cache Classes Through the Cache Manager
It is a Map. The key is cacheName, and the value is Cache [which is actually also a Map].
public class ConcurrentMapCacheManager implements CacheManager, BeanClassLoaderAware {
// Store Cache classes in ConcurrentHashMap. The actual cached data is in the Cache class
private final ConcurrentMap<String, Cache> cacheMap = new ConcurrentHashMap<String, Cache>(16);
public ConcurrentMapCacheManager() {
}
@Override
public Cache getCache(String name) {
// Get cache from ConcurrentMap
Cache cache = this.cacheMap.get(name);
// If it is empty, create a new Cache and put it into ConcurrentMap
if (cache == null && this.dynamic) {
// A lock is added here
synchronized (this.cacheMap) {
// Double check??
cache = this.cacheMap.get(name);
if (cache == null) {
// Create a cache instance
cache = createConcurrentMapCache(name);
this.cacheMap.put(name, cache);
}
}
}
return cache;
}
protected Cache createConcurrentMapCache(String name) {
SerializationDelegate actualSerialization = (isStoreByValue() ? this.serialization : null);
// ConcurrentMapCache is used
return new ConcurrentMapCache(name, new ConcurrentHashMap<Object, Object>(256),
isAllowNullValues(), actualSerialization);
}
}
1.2.2. Cache Class Operations
public class ConcurrentMapCache extends AbstractValueAdaptingCache {
private final String name;
// ConcurrentMap is still used as the container for cached data
private final ConcurrentMap<Object, Object> store;
private final SerializationDelegate serialization;
protected ConcurrentMapCache(String name, ConcurrentMap<Object, Object> store,
boolean allowNullValues, 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;
}
// Cache operations are basically Map operations
@Override
public void put(Object key, Object value) {
this.store.put(key, toStoreValue(value));
}
@Override
public void evict(Object key) {
this.store.remove(key);
}
@Override
public void clear() {
this.store.clear();
}
@Override
protected Object lookup(Object key) {
return this.store.get(key);
}
@SuppressWarnings("unchecked")
@Override
public <T> T get(Object key, Callable<T> valueLoader) {
// Try efficient lookup on the ConcurrentHashMap first...
ValueWrapper storeValue = get(key);
if (storeValue != null) {
return (T) storeValue.get();
}
// No value found -> load value within full synchronization.
synchronized (this.store) {
storeValue = get(key);
if (storeValue != null) {
return (T) storeValue.get();
}
T value;
try {
value = valueLoader.call();
}
catch (Throwable ex) {
throw new ValueRetrievalException(key, valueLoader, ex);
}
put(key, value);
return value;
}
}
}
2. Generation of Proxy Classes
2.1. Enable Cache Proxy Class Configuration
Through this annotation, we import a CachingConfigurationSelector.
@EnableCaching
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(CachingConfigurationSelector.class)// Imports CachingConfigurationSelector
public @interface EnableCaching
{
boolean proxyTargetClass() default false;
AdviceMode mode() default AdviceMode.PROXY;
int order() default Ordered.LOWEST_PRECEDENCE;
}
CachingConfigurationSelector extends AdviceModeImportSelector. We want to look at the selectImport method of AdviceModeImportSelector.
2.1.1. Import AutoProxyRegistrar and ProxyCachingConfiguration into the Container
AdviceModeImportSelector.selectImports
@Override
public final String[] selectImports(AnnotationMetadata importingClassMetadata) {
Class<?> annType = GenericTypeResolver.resolveTypeArgument(getClass(), AdviceModeImportSelector.class);
AnnotationAttributes attributes = AnnotationConfigUtils.attributesFor(importingClassMetadata, annType);
if (attributes == null) {
throw new IllegalArgumentException(String.format(
"@%s is not present on importing class '%s' as expected",
annType.getSimpleName(), importingClassMetadata.getClassName()));
}
// Get the proxy mode. The default is AdviceMode.PROXY
AdviceMode adviceMode = attributes.getEnum(getAdviceModeAttributeName());
// Call CachingConfigurationSelector's selectImport method
String[] imports = selectImports(adviceMode);
if (imports == null) {
throw new IllegalArgumentException("Unknown AdviceMode: " + adviceMode);
}
return imports;
}
public String[] selectImports(AdviceMode adviceMode) {
switch (adviceMode) {
case PROXY:
// This branch is used
return getProxyImports();
case ASPECTJ:
return getAspectJImports();
default:
return null;
}
}
private String[] getProxyImports() {
List<String> result = new ArrayList<String>(3);
// Imports AutoProxyRegistrar and ProxyCachingConfiguration
result.add(AutoProxyRegistrar.class.getName());
result.add(ProxyCachingConfiguration.class.getName());
if (jsr107Present && jcacheImplPresent) {
result.add(PROXY_JCACHE_CONFIGURATION_CLASS);
}
return StringUtils.toStringArray(result);
}
The main role of CachingConfigurationSelector is to import AutoProxyRegistrar and ProxyCachingConfiguration. We only need to study AutoProxyRegistrar to understand how the proxy class is generated.
2.1.2. AutoProxyRegistrar Injects InfrastructureAdvisorAutoProxyCreator, Which Generates Proxy Objects, into the Container
public class AutoProxyRegistrar implements ImportBeanDefinitionRegistrar {// It is an ImportBeanDefinitionRegistrar; focus on its registerBeanDefinitions method
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
boolean candidateFound = false;
Set<String> annoTypes = importingClassMetadata.getAnnotationTypes();
for (String annoType : annoTypes) {
AnnotationAttributes candidate = AnnotationConfigUtils.attributesFor(importingClassMetadata, annoType);
if (candidate == null) {
continue;
}
Object mode = candidate.get("mode");
Object proxyTargetClass = candidate.get("proxyTargetClass");
if (mode != null && proxyTargetClass != null && AdviceMode.class == mode.getClass() &&
Boolean.class == proxyTargetClass.getClass()) {
candidateFound = true;
if (mode == AdviceMode.PROXY) {
// This code first injects InfrastructureAdvisorAutoProxyCreator into the container
AopConfigUtils.registerAutoProxyCreatorIfNecessary(registry);
if ((Boolean) proxyTargetClass) {
AopConfigUtils.forceAutoProxyCreatorToUseClassProxying(registry);
return;
}
}
}
}
if (!candidateFound && logger.isWarnEnabled()) {
String name = getClass().getSimpleName();
logger.warn(String.format("%s was imported but no annotations were found " +
"having both 'mode' and 'proxyTargetClass' attributes of type " +
"AdviceMode and boolean respectively. This means that auto proxy " +
"creator registration and configuration may not have occurred as " +
"intended, and components may not be proxied as expected. Check to " +
"ensure that %s has been @Import'ed on the same class where these " +
"annotations are declared; otherwise remove the import of %s " +
"altogether.", name, name, name));
}
}
}
InfrastructureAdvisorAutoProxyCreator extends AbstractAdvisorAutoProxyCreator, and the latter extends AbstractAutoProxyCreator. The principle is the same as 1. Injecting the AspectJAnnotationAutoProxyCreator Component into the Container.
The key is the postProcessAfterInitialization method of AbstractAutoProxyCreator. It is called while the TestService bean is being instantiated -> assigned -> custom initialization method called, and then it generates and returns a proxy class.
2.1.3. The Proxy Object Intercepts Calls to the Proxied Method and Enters Cache Operations
The proxy class calls the CacheInterceptor interceptor and enters the operations of the ConcurrentMapCache class.
3. Summary
- During Spring startup, one step of refreshing the context is invokeBeanFactoryPostProcessors, which calls the
postProcessBeanDefinitionRegistrymethods of allBeanFactoryPostProcessorinstances. - One of them is
ConfigurationClassPostProcessor. ItspostProcessBeanDefinitionRegistrymethod parses the@Importannotations of all@Configurationclasses and calls theselectImportmethods ofImportSelectorinstances as well as theregisterBeanDefinitionsmethods ofImportBeanDefinitionRegistrarinstances. CacheConfigurationImportSelectorinCacheAutoConfigurationis one suchImportSelector. ItsselectImportmethod imports theSimpleCacheConfigurationconfiguration class, which then generates theConcurrentMapCacheManagercache-management class.CachingConfigurationSelectorfromEnableCachingis also one of them. ItsselectImportmethod importsAutoProxyRegistrar.- During Spring startup, one step of refreshing the context is registerBeanPostProcessors, which registers all BeanPostProcessors in the container.
AutoProxyRegistraris one of them. - During Spring startup, one step of refreshing the context is finishBeanFactoryInitialization. When the
TestServicebean is generated, thepostProcessAfterBeanInitializationmethod ofAutoProxyRegistraris called when the custom initialization method is invoked. This method returns a proxy object forTestService. - When
TestControllercalls aTestServicemethod, the call first goes through the proxy class. The proxy class calls theCacheInterceptorinterceptor and finally enters operations on theConcurrentMapCacheclass. - The final operations of the
ConcurrentMapCacheclass are as follows:
Before calling the method, the cacheNames of @Cacheable are used to call ConcurrentMapCacheManager.getCache to obtain the corresponding Cache class. If one does not exist, it is created.
Then ConcurrentMapCache.lookup is called. The key is generated by generateKey in CacheAspectSupport.
SimpleKeyGenerator
public static Object generateKey(Object... params) {
// Length is 0: new SimpleKey();
if (params.length == 0) {
return SimpleKey.EMPTY;
}
// Length is 1: return params directly
if (params.length == 1) {
Object param = params[0];
if (param != null && !param.getClass().isArray()) {
return param;
}
}
// Combine all params
return new SimpleKey(params);
}
If it is not found, the target method is called, and the result is put into ConcurrentMapCache through its put method, including storing null.
The EnableXX annotations all import BeanPostProcessor instances into the container.
The XXXAutoconfiguration classes first import @Configuration into the container, and then @Bean.
Discussion
Sign in with GitHub to comment. Discussions are stored as GitHub Issues.View on GitHub