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.

JavaCreated Updated 2 min readhistorical

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 postProcessBeanDefinitionRegistry methods of all BeanFactoryPostProcessor instances.
  • One of them is ConfigurationClassPostProcessor. Its postProcessBeanDefinitionRegistry method parses the @Import annotations of all @Configuration classes and calls the selectImport methods of ImportSelector instances as well as the registerBeanDefinitions methods of ImportBeanDefinitionRegistrar instances.
  • CacheConfigurationImportSelector in CacheAutoConfiguration is one such ImportSelector. Its selectImport method imports the SimpleCacheConfiguration configuration class, which then generates the ConcurrentMapCacheManager cache-management class.
  • CachingConfigurationSelector from EnableCaching is also one of them. Its selectImport method imports AutoProxyRegistrar.
  • During Spring startup, one step of refreshing the context is registerBeanPostProcessors, which registers all BeanPostProcessors in the container. AutoProxyRegistrar is one of them.
  • During Spring startup, one step of refreshing the context is finishBeanFactoryInitialization. When the TestService bean is generated, the postProcessAfterBeanInitialization method of AutoProxyRegistrar is called when the custom initialization method is invoked. This method returns a proxy object for TestService.
  • When TestController calls a TestService method, the call first goes through the proxy class. The proxy class calls the CacheInterceptor interceptor and finally enters operations on the ConcurrentMapCache class.
  • The final operations of the ConcurrentMapCache class 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