001/* 002 * Copyright 2002-2020 the original author or authors. 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * https://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016 017package org.springframework.cache.caffeine; 018 019import java.util.Arrays; 020import java.util.Collection; 021import java.util.Collections; 022import java.util.Map; 023import java.util.concurrent.ConcurrentHashMap; 024import java.util.concurrent.CopyOnWriteArrayList; 025 026import com.github.benmanes.caffeine.cache.CacheLoader; 027import com.github.benmanes.caffeine.cache.Caffeine; 028import com.github.benmanes.caffeine.cache.CaffeineSpec; 029 030import org.springframework.cache.Cache; 031import org.springframework.cache.CacheManager; 032import org.springframework.lang.Nullable; 033import org.springframework.util.Assert; 034import org.springframework.util.ObjectUtils; 035 036/** 037 * {@link CacheManager} implementation that lazily builds {@link CaffeineCache} 038 * instances for each {@link #getCache} request. Also supports a 'static' mode 039 * where the set of cache names is pre-defined through {@link #setCacheNames}, 040 * with no dynamic creation of further cache regions at runtime. 041 * 042 * <p>The configuration of the underlying cache can be fine-tuned through a 043 * {@link Caffeine} builder or {@link CaffeineSpec}, passed into this 044 * CacheManager through {@link #setCaffeine}/{@link #setCaffeineSpec}. 045 * A {@link CaffeineSpec}-compliant expression value can also be applied 046 * via the {@link #setCacheSpecification "cacheSpecification"} bean property. 047 * 048 * <p>Requires Caffeine 2.1 or higher. 049 * 050 * @author Ben Manes 051 * @author Juergen Hoeller 052 * @author Stephane Nicoll 053 * @author Sam Brannen 054 * @since 4.3 055 * @see CaffeineCache 056 */ 057public class CaffeineCacheManager implements CacheManager { 058 059 private Caffeine<Object, Object> cacheBuilder = Caffeine.newBuilder(); 060 061 @Nullable 062 private CacheLoader<Object, Object> cacheLoader; 063 064 private boolean allowNullValues = true; 065 066 private boolean dynamic = true; 067 068 private final Map<String, Cache> cacheMap = new ConcurrentHashMap<>(16); 069 070 private final Collection<String> customCacheNames = new CopyOnWriteArrayList<>(); 071 072 073 /** 074 * Construct a dynamic CaffeineCacheManager, 075 * lazily creating cache instances as they are being requested. 076 */ 077 public CaffeineCacheManager() { 078 } 079 080 /** 081 * Construct a static CaffeineCacheManager, 082 * managing caches for the specified cache names only. 083 */ 084 public CaffeineCacheManager(String... cacheNames) { 085 setCacheNames(Arrays.asList(cacheNames)); 086 } 087 088 089 /** 090 * Specify the set of cache names for this CacheManager's 'static' mode. 091 * <p>The number of caches and their names will be fixed after a call to this method, 092 * with no creation of further cache regions at runtime. 093 * <p>Calling this with a {@code null} collection argument resets the 094 * mode to 'dynamic', allowing for further creation of caches again. 095 */ 096 public void setCacheNames(@Nullable Collection<String> cacheNames) { 097 if (cacheNames != null) { 098 for (String name : cacheNames) { 099 this.cacheMap.put(name, createCaffeineCache(name)); 100 } 101 this.dynamic = false; 102 } 103 else { 104 this.dynamic = true; 105 } 106 } 107 108 /** 109 * Set the Caffeine to use for building each individual 110 * {@link CaffeineCache} instance. 111 * @see #createNativeCaffeineCache 112 * @see com.github.benmanes.caffeine.cache.Caffeine#build() 113 */ 114 public void setCaffeine(Caffeine<Object, Object> caffeine) { 115 Assert.notNull(caffeine, "Caffeine must not be null"); 116 doSetCaffeine(caffeine); 117 } 118 119 /** 120 * Set the {@link CaffeineSpec} to use for building each individual 121 * {@link CaffeineCache} instance. 122 * @see #createNativeCaffeineCache 123 * @see com.github.benmanes.caffeine.cache.Caffeine#from(CaffeineSpec) 124 */ 125 public void setCaffeineSpec(CaffeineSpec caffeineSpec) { 126 doSetCaffeine(Caffeine.from(caffeineSpec)); 127 } 128 129 /** 130 * Set the Caffeine cache specification String to use for building each 131 * individual {@link CaffeineCache} instance. The given value needs to 132 * comply with Caffeine's {@link CaffeineSpec} (see its javadoc). 133 * @see #createNativeCaffeineCache 134 * @see com.github.benmanes.caffeine.cache.Caffeine#from(String) 135 */ 136 public void setCacheSpecification(String cacheSpecification) { 137 doSetCaffeine(Caffeine.from(cacheSpecification)); 138 } 139 140 private void doSetCaffeine(Caffeine<Object, Object> cacheBuilder) { 141 if (!ObjectUtils.nullSafeEquals(this.cacheBuilder, cacheBuilder)) { 142 this.cacheBuilder = cacheBuilder; 143 refreshCommonCaches(); 144 } 145 } 146 147 /** 148 * Set the Caffeine CacheLoader to use for building each individual 149 * {@link CaffeineCache} instance, turning it into a LoadingCache. 150 * @see #createNativeCaffeineCache 151 * @see com.github.benmanes.caffeine.cache.Caffeine#build(CacheLoader) 152 * @see com.github.benmanes.caffeine.cache.LoadingCache 153 */ 154 public void setCacheLoader(CacheLoader<Object, Object> cacheLoader) { 155 if (!ObjectUtils.nullSafeEquals(this.cacheLoader, cacheLoader)) { 156 this.cacheLoader = cacheLoader; 157 refreshCommonCaches(); 158 } 159 } 160 161 /** 162 * Specify whether to accept and convert {@code null} values for all caches 163 * in this cache manager. 164 * <p>Default is "true", despite Caffeine itself not supporting {@code null} values. 165 * An internal holder object will be used to store user-level {@code null}s. 166 */ 167 public void setAllowNullValues(boolean allowNullValues) { 168 if (this.allowNullValues != allowNullValues) { 169 this.allowNullValues = allowNullValues; 170 refreshCommonCaches(); 171 } 172 } 173 174 /** 175 * Return whether this cache manager accepts and converts {@code null} values 176 * for all of its caches. 177 */ 178 public boolean isAllowNullValues() { 179 return this.allowNullValues; 180 } 181 182 183 @Override 184 public Collection<String> getCacheNames() { 185 return Collections.unmodifiableSet(this.cacheMap.keySet()); 186 } 187 188 @Override 189 @Nullable 190 public Cache getCache(String name) { 191 return this.cacheMap.computeIfAbsent(name, cacheName -> 192 this.dynamic ? createCaffeineCache(cacheName) : null); 193 } 194 195 196 /** 197 * Register the given native Caffeine Cache instance with this cache manager, 198 * adapting it to Spring's cache API for exposure through {@link #getCache}. 199 * Any number of such custom caches may be registered side by side. 200 * <p>This allows for custom settings per cache (as opposed to all caches 201 * sharing the common settings in the cache manager's configuration) and 202 * is typically used with the Caffeine builder API: 203 * {@code registerCustomCache("myCache", Caffeine.newBuilder().maximumSize(10).build())} 204 * <p>Note that any other caches, whether statically specified through 205 * {@link #setCacheNames} or dynamically built on demand, still operate 206 * with the common settings in the cache manager's configuration. 207 * @param name the name of the cache 208 * @param cache the custom Caffeine Cache instance to register 209 * @since 5.2.8 210 * @see #adaptCaffeineCache 211 */ 212 public void registerCustomCache(String name, com.github.benmanes.caffeine.cache.Cache<Object, Object> cache) { 213 this.customCacheNames.add(name); 214 this.cacheMap.put(name, adaptCaffeineCache(name, cache)); 215 } 216 217 /** 218 * Adapt the given new native Caffeine Cache instance to Spring's {@link Cache} 219 * abstraction for the specified cache name. 220 * @param name the name of the cache 221 * @param cache the native Caffeine Cache instance 222 * @return the Spring CaffeineCache adapter (or a decorator thereof) 223 * @since 5.2.8 224 * @see CaffeineCache 225 * @see #isAllowNullValues() 226 */ 227 protected Cache adaptCaffeineCache(String name, com.github.benmanes.caffeine.cache.Cache<Object, Object> cache) { 228 return new CaffeineCache(name, cache, isAllowNullValues()); 229 } 230 231 /** 232 * Build a common {@link CaffeineCache} instance for the specified cache name, 233 * using the common Caffeine configuration specified on this cache manager. 234 * <p>Delegates to {@link #adaptCaffeineCache} as the adaptation method to 235 * Spring's cache abstraction (allowing for centralized decoration etc), 236 * passing in a freshly built native Caffeine Cache instance. 237 * @param name the name of the cache 238 * @return the Spring CaffeineCache adapter (or a decorator thereof) 239 * @see #adaptCaffeineCache 240 * @see #createNativeCaffeineCache 241 */ 242 protected Cache createCaffeineCache(String name) { 243 return adaptCaffeineCache(name, createNativeCaffeineCache(name)); 244 } 245 246 /** 247 * Build a common Caffeine Cache instance for the specified cache name, 248 * using the common Caffeine configuration specified on this cache manager. 249 * @param name the name of the cache 250 * @return the native Caffeine Cache instance 251 * @see #createCaffeineCache 252 */ 253 protected com.github.benmanes.caffeine.cache.Cache<Object, Object> createNativeCaffeineCache(String name) { 254 return (this.cacheLoader != null ? this.cacheBuilder.build(this.cacheLoader) : this.cacheBuilder.build()); 255 } 256 257 /** 258 * Recreate the common caches with the current state of this manager. 259 */ 260 private void refreshCommonCaches() { 261 for (Map.Entry<String, Cache> entry : this.cacheMap.entrySet()) { 262 if (!this.customCacheNames.contains(entry.getKey())) { 263 entry.setValue(createCaffeineCache(entry.getKey())); 264 } 265 } 266 } 267 268}