Spring Cache takes you flying

Spring version 3.1 introduces the annotation based cache technology and provides a set of abstract cache implementation scheme. It uses the cache through annotation and flexibly uses different cache components based on configuration. The code is quite flexible and extensible. This paper analyzes the code art of Spring Cache based on the Spring 5.x source code.

Open Spring Cache

It is easy for Spring to provide Cache capability. You only need to add @ EnableCaching annotation to the startup class:

@Configuration
@EnableCaching
public class ServerMain {

}

Add the EnableCaching annotation on the startup class to inject Cache related components into the Spring startup, and obtain the execution information corresponding to the Cache through Proxy or AspectJ.

If you want to modify some basic management information corresponding to the underlying Cache during startup, you can override the relevant methods provided by CachingConfigurerSupport on the startup class:

@EnableCaching
@SpringBootApplication
public class WebDemoApplication extends CachingConfigurerSupport {

    public static void main(String[] args) {
        SpringApplication.run(WebDemoApplication.class, args);
    }


    @Override
    public CacheManager cacheManager() {
        return super.cacheManager();
    }

    @Override
    public KeyGenerator keyGenerator() {
        return super.keyGenerator();
    }
}

In the above example code, the construction and implementation of CacheManager and KeyGenerator are rewritten. The functions implemented are Cache management mode and Cache key generation mode respectively.

Learn about Spring Cache execution management from Bean loading mechanism

Starting with the startup class, we can easily see the overall Cache management mode:

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import({CachingConfigurationSelector.class})
public @interface EnableCaching {
    boolean proxyTargetClass() default false;

    AdviceMode mode() default AdviceMode.PROXY;

    int order() default 2147483647;
}
  • proxyTargetClass: false indicates that JDK proxy is used, and true indicates that cglib proxy is used.

  • Mode: Specifies the mode of AOP. When the value is AdviceMode.PROXY, Spring aop is used, and when the value is AdviceMode.ASPECTJ, AspectJ is used.

When EnableCaching is used, import the CachingConfigurationSelector class. Let's see what information is loaded:

public class CachingConfigurationSelector extends AdviceModeImportSelector<EnableCaching> {

    public String[] selectImports(AdviceMode adviceMode) {
        switch(adviceMode) {
            case PROXY:
                return this.getProxyImports();
            case ASPECTJ:
                return this.getAspectJImports();
            default:
                return null;
        }
    }
    ......
    ......
    ......

    static {
        ClassLoader classLoader = CachingConfigurationSelector.class.getClassLoader();
        jsr107Present = ClassUtils.isPresent("javax.cache.Cache", classLoader);
        jcacheImplPresent = ClassUtils.isPresent("org.springframework.cache.jcache.config.ProxyJCacheConfiguration", classLoader);
    }


}

You can see that the main thing is to load the corresponding CacheConfiguration through different proxy methods:

  • If it is a JDK Proxy, load the autoproxyregister class and ProxyCachingConfiguration class;
  • If it is AspectJ, load the AspectJCachingConfiguration class.

Spring Boot uses JDK Proxy by default. Let's look at the use of JDK Proxy.

Autoproxyregister is used to create proxy objects. Internally, an autoproxyregister is registered in the IOC container by calling the aopconfigutils.registerautoproxycreator ifnecessary (Registry) method. The autoproxyregister finally injected into the container is an infrastructure advisor AutoProxyCreator type:

@Nullable
public static BeanDefinition registerAutoProxyCreatorIfNecessary(BeanDefinitionRegistry registry, @Nullable Object source) {
    return registerOrEscalateApcAsRequired(InfrastructureAdvisorAutoProxyCreator.class, registry, source);
}

The infrastructure Advisor autoproxycreator type will only automatically create proxy objects for Advisor of infrastructure type. It will only find qualified beans to create proxies. From the source code, it can be seen that only the role is beandefinition.role_ The conditions for the infrastruture are met.

ProxyCachingConfiguration creates three bean s. CacheOperationSource focuses on how to obtain all intercepted aspects,

The CacheInterceptor solves what to do with the intercepted aspect.

public class ProxyCachingConfiguration extends AbstractCachingConfiguration {
   
    @Bean(
        name = {"org.springframework.cache.config.internalCacheAdvisor"}
    )
    @Role(2)
    public BeanFactoryCacheOperationSourceAdvisor cacheAdvisor(CacheOperationSource cacheOperationSource, CacheInterceptor cacheInterceptor) {
        BeanFactoryCacheOperationSourceAdvisor advisor = new BeanFactoryCacheOperationSourceAdvisor();
        advisor.setCacheOperationSource(cacheOperationSource);
        advisor.setAdvice(cacheInterceptor);
        if (this.enableCaching != null) {
            advisor.setOrder((Integer)this.enableCaching.getNumber("order"));
        }

        return advisor;
    }
    ......
}

Beanfactory cacheoperationsourceadvisor is an Advisor implemented by Spring Cache. It will execute the Advice of CacheInterceptor for all methods that can fetch cacheoperations.

Tips:

The essence of the creation process of Spring AOP is to implement a BeanPostProcessor, create a Proxy in the process of creating a bean, bind all the advisor s applicable to the bean for the Proxy, and finally expose it to the container.

Several key concepts of AOP in Spring: advisor, advice and pointcut

advice = behavior inserted in slice interception

Pointcut = pointcut of the slice

advisor = advice + pointcut

The pointcut implementation class inside beanfactory cacheoperationsourceadvisor is CacheOperationSourcePointcut. The logic of pointcut is as follows:

abstract class CacheOperationSourcePointcut extends StaticMethodMatcherPointcut implements Serializable {
	
    private class CacheOperationSourceClassFilter implements ClassFilter {
        private CacheOperationSourceClassFilter() {
        }

        public boolean matches(Class<?> clazz) {
            if (CacheManager.class.isAssignableFrom(clazz)) {
                return false;
            } else {
                CacheOperationSource cas = CacheOperationSourcePointcut.this.getCacheOperationSource();
                return cas == null || cas.isCandidateClass(clazz);
            }
        }
    }
}

You can see that the pointcut is mainly to judge whether there are annotations on the method to be cut in: CacheOperationSourcePointcut.this.getCacheOperationSource().

There is another noteworthy class in ProxyCachingConfiguration - AnnotationCacheOperationSource:

@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
public CacheOperationSource cacheOperationSource() {
    return new AnnotationCacheOperationSource();
}

CacheOperationSource holds a list of cacheannotation parsers. CacheAnnotationParser has only one implementation class: SpringCacheAnnotationParser:

public class SpringCacheAnnotationParser implements CacheAnnotationParser, Serializable {

    private static final Set<Class<? extends Annotation>> CACHE_OPERATION_ANNOTATIONS = new LinkedHashSet<>(8);

    static {
        CACHE_OPERATION_ANNOTATIONS.add(Cacheable.class);
        CACHE_OPERATION_ANNOTATIONS.add(CacheEvict.class);
        CACHE_OPERATION_ANNOTATIONS.add(CachePut.class);
        CACHE_OPERATION_ANNOTATIONS.add(Caching.class);
    }
    ......
    @Nullable
	private Collection<CacheOperation> parseCacheAnnotations(
			DefaultCacheConfig cachingConfig, AnnotatedElement ae, boolean localOnly) {

		Collection<? extends Annotation> anns = (localOnly ?
				AnnotatedElementUtils.getAllMergedAnnotations(ae, CACHE_OPERATION_ANNOTATIONS) :
				AnnotatedElementUtils.findAllMergedAnnotations(ae, CACHE_OPERATION_ANNOTATIONS));
		if (anns.isEmpty()) {
			return null;
		}

		final Collection<CacheOperation> ops = new ArrayList<>(1);
		anns.stream().filter(ann -> ann instanceof Cacheable).forEach(
				ann -> ops.add(parseCacheableAnnotation(ae, cachingConfig, (Cacheable) ann)));
		anns.stream().filter(ann -> ann instanceof CacheEvict).forEach(
				ann -> ops.add(parseEvictAnnotation(ae, cachingConfig, (CacheEvict) ann)));
		anns.stream().filter(ann -> ann instanceof CachePut).forEach(
				ann -> ops.add(parsePutAnnotation(ae, cachingConfig, (CachePut) ann)));
		anns.stream().filter(ann -> ann instanceof Caching).forEach(
				ann -> parseCachingAnnotation(ae, cachingConfig, (Caching) ann, ops));
		return ops;
	}
    ......
}

You can see that the function of Parser is to parse the method corresponding to the annotation into CacheOperation and save it.

Seeing this, we began to know where to find, what to intercept and what to operate. Next, continue to drill down on CacheOperation and CacheInterceptor.

CacheOperation

There is only one method in the CacheOperationSource interface:

public interface CacheOperationSource {
    default boolean isCandidateClass(Class<?> targetClass) {
        return true;
    }

    @Nullable
    Collection<CacheOperation> getCacheOperations(Method method, @Nullable Class<?> targetClass);
}

There is only one method in the interface: obtain the corresponding CacheOperation through the interface and class name. CacheOperation is an abstract encapsulation of cache operations. It has three implementation classes:

The three implementation classes correspond to @ CacheEvict, @ CachePut and @ Cacheable annotations respectively.

CacheInterceptor

The function of CacheInterceptor is to actually intercept the target method:

public class CacheInterceptor extends CacheAspectSupport implements MethodInterceptor, Serializable {

    @Override
    @Nullable
    public Object invoke(final MethodInvocation invocation) throws Throwable {
        Method method = invocation.getMethod();

        CacheOperationInvoker aopAllianceInvoker = () -> {
            try {
                return invocation.proceed();
            }
            catch (Throwable ex) {
                throw new CacheOperationInvoker.ThrowableWrapper(ex);
            }
        };

        Object target = invocation.getThis();
        Assert.state(target != null, "Target must not be null");
        try {
            return execute(aopAllianceInvoker, target, method, invocation.getArguments());
        }
        catch (CacheOperationInvoker.ThrowableWrapper th) {
            throw th.getOriginal();
        }
    }

}

The code of CacheInterceptor is very concise. It encapsulates the function logic to be executed in the form of function, and finally passes the function to execute() of the parent class. Obviously, the final execution of the target method is invocation. Processed (). Let's go directly to the parent class cachespectsupport to see the relevant code logic:

public abstract class CacheAspectSupport extends AbstractCacheInvoker
		implements BeanFactoryAware, InitializingBean, SmartInitializingSingleton {

	protected final Log logger = LogFactory.getLog(getClass());

	private final Map<CacheOperationCacheKey, CacheOperationMetadata> metadataCache = new ConcurrentHashMap<>(1024);

	private final CacheOperationExpressionEvaluator evaluator = new CacheOperationExpressionEvaluator();

	@Nullable
	private CacheOperationSource cacheOperationSource;

	private SingletonSupplier<KeyGenerator> keyGenerator = SingletonSupplier.of(SimpleKeyGenerator::new);

	@Nullable
	private SingletonSupplier<CacheResolver> cacheResolver;
	
    ......
}

Some attributes extracted above:

Map<CacheOperationCacheKey, CacheOperationMetadata> metadataCache

This Map caches the basic attribute information corresponding to all annotated classes or methods.

CacheOperationExpressionEvaluator evaluator

Parse some processors that can write el expressions, such as condition, key, unless, etc.

SingletonSupplier<KeyGenerator> keyGenerator

The key generator uses SimpleKeyGenerator by default. Note that SingletonSupplier is a new class in spring 5.1 and implements the interface java.util.function.Supplier, which is mainly used to fault-tolerant null values.

Then look at the relevant methods:

public abstract class CacheAspectSupport extends AbstractCacheInvoker
		implements BeanFactoryAware, InitializingBean, SmartInitializingSingleton {
    
    
    // This interface is derived from SmartInitializingSingleton and is invoked after instantiating all the singleton Bean.
    //You can see that the CacheManager is instantiated here. We will talk about the role of the CacheManager later
 	@Override
	public void afterSingletonsInstantiated() {
		if (getCacheResolver() == null) {
			// Lazily initialize cache resolver via default cache manager...
			Assert.state(this.beanFactory != null, "CacheResolver or BeanFactory must be set on cache aspect");
			try {
				setCacheManager(this.beanFactory.getBean(CacheManager.class));
			}
			catch (NoUniqueBeanDefinitionException ex) {
				throw new IllegalStateException("No CacheResolver specified, and no unique bean of type " +
						"CacheManager found. Mark one as primary or declare a specific CacheManager to use.", ex);
			}
			catch (NoSuchBeanDefinitionException ex) {
				throw new IllegalStateException("No CacheResolver specified, and no bean of type CacheManager found. " +
						"Register a CacheManager bean or remove the @EnableCaching annotation from your configuration.", ex);
			}
		}
		this.initialized = true;
	}   
    
   
    //Encapsulate CacheOperationMetadata according to CacheOperation
    protected CacheOperationMetadata getCacheOperationMetadata(
			CacheOperation operation, Method method, Class<?> targetClass) {

		CacheOperationCacheKey cacheKey = new CacheOperationCacheKey(operation, method, targetClass);
		CacheOperationMetadata metadata = this.metadataCache.get(cacheKey);
		if (metadata == null) {
			KeyGenerator operationKeyGenerator;
			if (StringUtils.hasText(operation.getKeyGenerator())) {
				operationKeyGenerator = getBean(operation.getKeyGenerator(), KeyGenerator.class);
			}
			else {
				operationKeyGenerator = getKeyGenerator();
			}
			CacheResolver operationCacheResolver;
			if (StringUtils.hasText(operation.getCacheResolver())) {
				operationCacheResolver = getBean(operation.getCacheResolver(), CacheResolver.class);
			}
			else if (StringUtils.hasText(operation.getCacheManager())) {
				CacheManager cacheManager = getBean(operation.getCacheManager(), CacheManager.class);
				operationCacheResolver = new SimpleCacheResolver(cacheManager);
			}
			else {
				operationCacheResolver = getCacheResolver();
				Assert.state(operationCacheResolver != null, "No CacheResolver/CacheManager set");
			}
			metadata = new CacheOperationMetadata(operation, method, targetClass,
					operationKeyGenerator, operationCacheResolver);
			this.metadataCache.put(cacheKey, metadata);
		}
		return metadata;
	}
    
    
    
    
    //Implementation of real execution target method + cache
	@Nullable
	protected Object execute(CacheOperationInvoker invoker, Object target, Method method, Object[] args) {
		// Check whether aspect is enabled (to cope with cases where the AJ is pulled in automatically)
        // If it has been initialized (with CacheManager and CacheResolver), execute here
		if (this.initialized) {
            //getTargetClass gets the original Class proxy
			Class<?> targetClass = getTargetClass(target);
            //In short, all cacheoperations on the method are obtained and finally executed one by one
			CacheOperationSource cacheOperationSource = getCacheOperationSource();
			if (cacheOperationSource != null) {
                // CacheOperationContexts is a very important private inner class
				// Note that it is plural! Is not a singular CacheOperationContext  
                // So it executes one by one like holding multiple annotation contexts
				Collection<CacheOperation> operations = cacheOperationSource.getCacheOperations(method, targetClass);
				if (!CollectionUtils.isEmpty(operations)) {
					return execute(invoker, method,
							new CacheOperationContexts(operations, method, args, target, targetClass));
				}
			}
		}
		// If it is not initialized, execute the target method directly and do not execute the cache operation
		return invoker.invoke();
	}

    
    
    
    //Internally called execute method 
	@Nullable
	private Object execute(final CacheOperationInvoker invoker, Method method, CacheOperationContexts contexts) {
		// Judge whether to execute synchronously
		if (contexts.isSynchronized()) {
			CacheOperationContext context = contexts.get(CacheableOperation.class).iterator().next();
			if (isConditionPassing(context, CacheOperationExpressionEvaluator.NO_RESULT)) {
				Object key = generateKey(context, CacheOperationExpressionEvaluator.NO_RESULT);
				Cache cache = context.getCaches().iterator().next();
				try {
					return wrapCacheValue(method, handleSynchronizedGet(invoker, key, cache));
				}
				catch (Cache.ValueRetrievalException ex) {
					// Directly propagate ThrowableWrapper from the invoker,
					// or potentially also an IllegalArgumentException etc.
					ReflectionUtils.rethrowRuntimeException(ex.getCause());
				}
			}
			else {
				// No caching required, only call the underlying method
				return invokeOperation(invoker);
			}
		}


		
		// In the case of sync=false, go here

		
		// Process any early occurrences beforeinvocation = true will be executed first here~~~
		
		// The @ CacheEvict annotation is processed first ~ ~ ~ for the method of real execution, see: performCacheEvict
		// context.getCaches() takes out all caches to see if cache.evict(key) is executed; Method is also cache.clear();
		// Note that context.isConditionPassing(result); condition conditions take effect here, and you can use #result
		// context.generateKey(result) can also use #result
		// @CacheEvict has no unless attribute
		processCacheEvicts(contexts.get(CacheEvictOperation.class), true,
				CacheOperationExpressionEvaluator.NO_RESULT);

		// Execute @ Cacheable to see if the cache can be hit
		Cache.ValueWrapper cacheHit = findCachedItem(contexts.get(CacheableOperation.class));

		// If the cache misses, prepare a cachePutRequest
		// Because @ Cacheable can't hit the target for the first time. In the end, it must perform a put operation so that it can hit the target next time
		List<CachePutRequest> cachePutRequests = new ArrayList<>();
		if (cacheHit == null) {
			collectPutRequests(contexts.get(CacheableOperation.class),
					CacheOperationExpressionEvaluator.NO_RESULT, cachePutRequests);
		}

		Object cacheValue;
		Object returnValue;
		// If the cache hits and there is no @ CachePut, it will be returned directly
		if (cacheHit != null && !hasCachePut(contexts)) {
			cacheValue = cacheHit.get();
			returnValue = wrapCacheValue(method, cacheValue);
		}
		else {
			// In case of cachePut, execute the target method first and then put the cache
			returnValue = invokeOperation(invoker);
			cacheValue = unwrapReturnValue(returnValue);
		}

		// Encapsulating cacheput objects
		collectPutRequests(contexts.get(CachePutOperation.class), cacheValue, cachePutRequests);

		// Where cacheput is implemented uniformly
		for (CachePutRequest cachePutRequest : cachePutRequests) {
			cachePutRequest.apply(cacheValue);
		}

		// cacheEvict is executed last
		processCacheEvicts(contexts.get(CacheEvictOperation.class), false, cacheValue);

		return returnValue;
	}
    
    
    
   
	//Cache attribute context object
	private class CacheOperationContexts {

		private final MultiValueMap<Class<? extends CacheOperation>, CacheOperationContext> contexts;
		//Check whether the annotation is configured with a switch for synchronous execution
		private final boolean sync;

		public CacheOperationContexts(Collection<? extends CacheOperation> operations, Method method,
				Object[] args, Object target, Class<?> targetClass) {
			
            //Encapsulate all cache operations on the current method into a map object
			this.contexts = new LinkedMultiValueMap<>(operations.size());
			for (CacheOperation op : operations) {
				this.contexts.add(op.getClass(), getOperationContext(op, method, args, target, targetClass));
			}
            //Synchronous execution depends on this function
			this.sync = determineSyncFlag(method);
		}

		public Collection<CacheOperationContext> get(Class<? extends CacheOperation> operationClass) {
			Collection<CacheOperationContext> result = this.contexts.get(operationClass);
			return (result != null ? result : Collections.emptyList());
		}

		public boolean isSynchronized() {
			return this.sync;
		}

        //Only @ Cacheable has sync attribute, so you only need to look at CacheableOperation
		private boolean determineSyncFlag(Method method) {
			List<CacheOperationContext> cacheOperationContexts = this.contexts.get(CacheableOperation.class);
			if (cacheOperationContexts == null) {  // no @Cacheable operation at all
				return false;
			}
			boolean syncEnabled = false;
            //As long as there is a @ Cacheable sync=true, it will be true, and there is check logic below
			for (CacheOperationContext cacheOperationContext : cacheOperationContexts) {
				if (((CacheableOperation) cacheOperationContext.getOperation()).isSync()) {
					syncEnabled = true;
					break;
				}
			}
            // Execute sync=true check logic
			if (syncEnabled) {
                // When sync=true, there can be no other cache operations, that is, @ Cacheable(sync=true) can only be used alone
				if (this.contexts.size() > 1) {
					throw new IllegalStateException(
							"@Cacheable(sync=true) cannot be combined with other cache operations on '" + method + "'");
				}
                //@When Cacheable(sync=true), multiple @ Cacheable are not allowed
				if (cacheOperationContexts.size() > 1) {
					throw new IllegalStateException(
							"Only one @Cacheable(sync=true) entry is allowed on '" + method + "'");
				}
                // Get the only @ Cacheable
				CacheOperationContext cacheOperationContext = cacheOperationContexts.iterator().next();
				CacheableOperation operation = (CacheableOperation) cacheOperationContext.getOperation();
                //@When Cacheable(sync=true), only one cacheName can be used
				if (cacheOperationContext.getCaches().size() > 1) {
					throw new IllegalStateException(
							"@Cacheable(sync=true) only allows a single cache on '" + operation + "'");
				}
                //When sync=true, the unless attribute is not supported and cannot be written
				if (StringUtils.hasText(operation.getUnless())) {
					throw new IllegalStateException(
							"@Cacheable(sync=true) does not support unless attribute on '" + operation + "'");
				}
				return true;
			}
			return false;
		}
	}

    
}

The comments on the execute related code are very clear. You can read it several times. There's another point here. Where is the final get Cache or put Cache operation? There was a code in the execute method just now:

Cache.ValueWrapper cacheHit = findCachedItem(contexts.get(CacheableOperation.class));

Here, we first check whether the Cache exists in the Cache, and then go in to see a findCaches method:

@Nullable
private Cache.ValueWrapper findInCaches(CacheOperationContext context, Object key) {
    for (Cache cache : context.getCaches()) {
        Cache.ValueWrapper wrapper = doGet(cache, key);
        if (wrapper != null) {
            if (logger.isTraceEnabled()) {
                logger.trace("Cache entry for key '" + key + "' found in cache '" + cache.getName() + "'");
            }
            return wrapper;
        }
    }
    return null;
}

Focus on the doGet method. You can see that this method is in the parent class AbstractCacheInvoker, and there are doPut, doEvict and doClear in the same class.

Little knowledge

@Effect of Cacheable annotation sync=true

In a multithreaded environment, multiple operations use the same parameters to synchronously call the same key. By default, the cache does not lock any resources, so multiple calculations may be caused. In this case, the sync attribute can lock the bottom layer, so that only one thread operates, and other threads are blocked until the cache returns results after updating.

Summary

So far, let's summarize the above contents:

BeanFactoryCacheOperationSourceAdvisor

Configure the aspect and interception implementation of Cache. The intercepted object is the resolved CacheOperation object.

Each CacheOperation is encapsulated as a CacheOperationContext object during execution (a method may be modified by multiple annotations), and finally the Cache object Cache is resolved through the CacheResolver.

CacheOperation

Encapsulates the attribute information of @ CachePut, @ Cacheable and @ CacheEvict, so that this object can be directly operated to execute logic during interception.

The work of parsing the code corresponding to the annotation as CacheOperation is completed by cacheannotation parser.

CacheInterceptor

The CacheInterceptor performs real method execution and Cache operations. Finally, the four do methods provided by its parent class are called to process the Cache.

The above overall process starts Spring to intercept and inject the class or method where the relevant annotation is located, so as to realize the Cache logic. Limited to space, this article will discuss these contents temporarily. In the next article, we will look at how Spring supports the underlying scheme of multi Cache (local Cache, Redis, Guava Cache and cafeine Cache).

Posted by keiron77 on Mon, 01 Nov 2021 04:53:50 -0700