Spring Cache cache details

Keywords: Java Spring Cache

preface

Spring's cache cache is similar to java's JDBC. It defines a set of specifications. Third party caching needs to implement this set of specifications in order to use the caching function through the Spring API. The core interfaces of this specification are CacheManager and cache. CacheMananger is the entry to get the cache. Cache is an interface that implements cache logic. Let's look at these two interfaces.

CacheManager

Let's first look at its source code:

public interface CacheManager {

	/**
	 * Get the cache associated with the given name.
	 * <p>Note that the cache may be lazily created at runtime if the
	 * native provider supports it.
	 * @param name the cache identifier (must not be {@code null})
	 * @return the associated cache, or {@code null} if such a cache
	 * does not exist or could be not created
	 */
	@Nullable
	Cache getCache(String name);

	/**
	 * Get a collection of the cache names known by this manager.
	 * @return the names of all caches known by the cache manager
	 */
	Collection<String> getCacheNames();

}

You can see that two methods are defined in the interface. The getCache method returns the cache object according to the specified name. The getCacheNames method returns all cache objects in the cache manager. Therefore, the function of cache manager is to obtain cache objects.

Spring provides us with the simplest CacheManager implementation class, ConcurrentMapCacheManager. Let's look at its implementation.

public class ConcurrentMapCacheManager implements CacheManager, BeanClassLoaderAware {

	private final ConcurrentMap<String, Cache> cacheMap = new ConcurrentHashMap<>(16);

	private boolean dynamic = true;

	private boolean allowNullValues = true;

	private boolean storeByValue = false;

	@Nullable
	private SerializationDelegate serialization;


	/**
	 * Construct a dynamic ConcurrentMapCacheManager,
	 * lazily creating cache instances as they are being requested.
	 */
	public ConcurrentMapCacheManager() {
	}

	/**
	 * Construct a static ConcurrentMapCacheManager,
	 * managing caches for the specified cache names only.
	 */
	public ConcurrentMapCacheManager(String... cacheNames) {
		setCacheNames(Arrays.asList(cacheNames));
	}


	/**
	 * Specify the set of cache names for this CacheManager's 'static' mode.
	 * <p>The number of caches and their names will be fixed after a call to this method,
	 * with no creation of further cache regions at runtime.
	 * <p>Calling this with a {@code null} collection argument resets the
	 * mode to 'dynamic', allowing for further creation of caches again.
	 */
	public void setCacheNames(@Nullable Collection<String> cacheNames) {
		if (cacheNames != null) {
			for (String name : cacheNames) {
				this.cacheMap.put(name, createConcurrentMapCache(name));
			}
			this.dynamic = false;
		}
		else {
			this.dynamic = true;
		}
	}

	/**
	 * Specify whether to accept and convert {@code null} values for all caches
	 * in this cache manager.
	 * <p>Default is "true", despite ConcurrentHashMap itself not supporting {@code null}
	 * values. An internal holder object will be used to store user-level {@code null}s.
	 * <p>Note: A change of the null-value setting will reset all existing caches,
	 * if any, to reconfigure them with the new null-value requirement.
	 */
	public void setAllowNullValues(boolean allowNullValues) {
		if (allowNullValues != this.allowNullValues) {
			this.allowNullValues = allowNullValues;
			// Need to recreate all Cache instances with the new null-value configuration...
			recreateCaches();
		}
	}

	/**
	 * Return whether this cache manager accepts and converts {@code null} values
	 * for all of its caches.
	 */
	public boolean isAllowNullValues() {
		return this.allowNullValues;
	}

	/**
	 * Specify whether this cache manager stores a copy of each entry ({@code true}
	 * or the reference ({@code false} for all of its caches.
	 * <p>Default is "false" so that the value itself is stored and no serializable
	 * contract is required on cached values.
	 * <p>Note: A change of the store-by-value setting will reset all existing caches,
	 * if any, to reconfigure them with the new store-by-value requirement.
	 * @since 4.3
	 */
	public void setStoreByValue(boolean storeByValue) {
		if (storeByValue != this.storeByValue) {
			this.storeByValue = storeByValue;
			// Need to recreate all Cache instances with the new store-by-value configuration...
			recreateCaches();
		}
	}

	/**
	 * Return whether this cache manager stores a copy of each entry or
	 * a reference for all its caches. If store by value is enabled, any
	 * cache entry must be serializable.
	 * @since 4.3
	 */
	public boolean isStoreByValue() {
		return this.storeByValue;
	}

	@Override
	public void setBeanClassLoader(ClassLoader classLoader) {
		this.serialization = new SerializationDelegate(classLoader);
		// Need to recreate all Cache instances with new ClassLoader in store-by-value mode...
		if (isStoreByValue()) {
			recreateCaches();
		}
	}


	@Override
	public Collection<String> getCacheNames() {
		return Collections.unmodifiableSet(this.cacheMap.keySet());
	}

	@Override
	@Nullable
	public Cache getCache(String name) {
		Cache cache = this.cacheMap.get(name);
		if (cache == null && this.dynamic) {
			synchronized (this.cacheMap) {
				cache = this.cacheMap.get(name);
				if (cache == null) {
					cache = createConcurrentMapCache(name);
					this.cacheMap.put(name, cache);
				}
			}
		}
		return cache;
	}

	private void recreateCaches() {
		for (Map.Entry<String, Cache> entry : this.cacheMap.entrySet()) {
			entry.setValue(createConcurrentMapCache(entry.getKey()));
		}
	}

	/**
	 * Create a new ConcurrentMapCache instance for the specified cache name.
	 * @param name the name of the cache
	 * @return the ConcurrentMapCache (or a decorator thereof)
	 */
	protected Cache createConcurrentMapCache(String name) {
		SerializationDelegate actualSerialization = (isStoreByValue() ? this.serialization : null);
		return new ConcurrentMapCache(name, new ConcurrentHashMap<>(256), isAllowNullValues(), actualSerialization);
	}

}

As can be seen from the source code, the name and Cache objects are stored in the Cache manager through CurrentHashMap. Getting the Cache object through name is actually getting the value value in the Map object. The Cache object is also the ConcurrentMapCache object provided by Spring. Inside the object, the CurrentMap object also stores cached data. Therefore, Spring provides us with a Cache in the form of Map. So when doing some small projects, you can use this simple Cache manager to store the Cache.
We can also customize the cache manager to obtain cache objects. However, manufacturers like redis have provided us with corresponding implementation classes, so we generally do not need to customize them.

Cache interface

public interface Cache {
 
	/**
	 * Return the cache name.
	 */
	String getName();
 
	/**
	 * Return the underlying native cache provider.
	 */
	Object getNativeCache();
 
	//Main methods of obtaining cache
	@Nullable
	ValueWrapper get(Object key);
 
	
	@Nullable
	<T> T get(Object key, @Nullable Class<T> type);
 
	
	@Nullable
	<T> T get(Object key, Callable<T> valueLoader);
 
	//Main methods of adding cache
	void put(Object key, @Nullable Object value);
 
	
	@Nullable
	default ValueWrapper putIfAbsent(Object key, @Nullable Object value) {
		ValueWrapper existingValue = get(key);
		if (existingValue == null) {
			put(key, value);
		}
		return existingValue;
	}
	void evict(Object key);
 
	default boolean evictIfPresent(Object key) {
		evict(key);
		return false;
	}
 
	void clear();
 
	
	default boolean invalidate() {
		clear();
		return false;
	}
 
 
	
	@FunctionalInterface
	interface ValueWrapper {
		@Nullable
		Object get();
	}
 
	@SuppressWarnings("serial")
	class ValueRetrievalException extends RuntimeException {
 
		@Nullable
		private final Object key;
 
		public ValueRetrievalException(@Nullable Object key, Callable<?> loader, Throwable ex) {
			super(String.format("Value for key '%s' could not be loaded using '%s'", key, loader), ex);
			this.key = key;
		}
 
		@Nullable
		public Object getKey() {
			return this.key;
		}
	}
 
}
cache Custom implementation class for

public class MapCaffeCache implements Cache {
 
//	private ConcurrentMapCache mapCache = new ConcurrentMapCache("mapCache");
 
	private com.github.benmanes.caffeine.cache.@NonNull Cache<Object, Object> mapCache = Caffeine.newBuilder()
			.expireAfterWrite(5, TimeUnit.SECONDS).expireAfterAccess(5, TimeUnit.SECONDS).maximumSize(5).build();
	private com.github.benmanes.caffeine.cache.@NonNull Cache<Object, Object> caffeCache = Caffeine.newBuilder()
			.expireAfterWrite(1, TimeUnit.MINUTES).expireAfterAccess(1, TimeUnit.MINUTES).maximumSize(100).build();
 
	@Autowired
	private StringRedisTemplate redisTemplate;
 
	private String name = "userCache";
 
	@Override
	public String getName() {
		return this.name;
	}
 
	@Override
	public Object getNativeCache() {
		return this;
	}
 
	@Override
	public ValueWrapper get(Object key) {
 
		@Nullable
		Object ob = mapCache.getIfPresent(key);
		// If the L1 cache has data returned directly, the L2 cache will not be triggered
		if (ob != null) {
			System.out.println(String.format("Cache L1 (CaffeineCache) :: %s = %s", key, ob));
			SimpleValueWrapper valueWrapper = new SimpleValueWrapper(ob);
			return valueWrapper;
		}
 
		Object obj = caffeCache.getIfPresent(key);
		if (obj != null) {
			SimpleValueWrapper valueWrapper2 = new SimpleValueWrapper(obj);
			System.out.println(String.format("Cache L2 (CaffeineCache) :: %s = %s", key, obj));
			// If there is data in the L2 cache, it is updated to the L1 cache
			mapCache.put(key, obj);
			return valueWrapper2;
		}
		return null;
	}
 
	@Override
	public <T> T get(Object key, Class<T> type) {
		return (T) get(key).get();
	}
 
	@Override
	public <T> T get(Object key, Callable<T> valueLoader) {
		return (T) get(key).get();
	}
 
	@Override
	public void put(Object key, Object value) {
		mapCache.put(key, value);
		caffeCache.put(key, value);
		//When nginx builds a cluster, the redis subscription / publish function is used to synchronize the cache consistency of each project point
		redisTemplate.convertAndSend("ch1", key + ":>" + value);
	}
 
	@Override
	public void evict(Object key) {
		mapCache.asMap().remove(key);
		caffeCache.asMap().remove(key);
	}
 
	@Override
	public void clear() {
		mapCache.asMap().clear();
		caffeCache.asMap().clear();
	}
 
}

It can be seen that put,get and other methods are defined in the interface, which is to store and take values into the cache. This interface is a specification for storing and fetching values into their third-party caches. Generally, we do not need to customize the implementation of this interface, which is also implemented by a third-party cache vendor.
Moreover, the methods of storing and fetching Cache in Cache do not need to be called by ourselves. Spring will call these methods in the Cache aspect. Therefore, using spring's Cache saves us the code to access the Cache.

@Cacheable annotation

There are cache managers and cache objects. How does Spring access the cache? This involves the @ Cacheable annotation used by our users.
We modify the method with @ Cacheable annotation. Spring AOP generates a proxy object. When calling the method, the proxy object judges whether there is a cache. If there is a cache, the cache will be called. The target method will not be called. If there is no cache, the target method will be called and the return value will be stored in the cache.
@For the specific usage of Cacheable, see @Detailed explanation of Cacheable

Cache configuration process

1. Use the @ EnableCaching annotation to enable the caching function.
2. Configure the CacheManager class
3. Configure Cache class
4. Modify the method to be cached with @ Cacheable annotation.

Posted by lives4him06 on Mon, 20 Sep 2021 05:33:31 -0700