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}