001/*
002 * Copyright 2002-2016 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.ConcurrentMap;
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.util.Assert;
033import org.springframework.util.ObjectUtils;
034
035/**
036 * {@link CacheManager} implementation that lazily builds {@link CaffeineCache}
037 * instances for each {@link #getCache} request. Also supports a 'static' mode
038 * where the set of cache names is pre-defined through {@link #setCacheNames},
039 * with no dynamic creation of further cache regions at runtime.
040 *
041 * <p>The configuration of the underlying cache can be fine-tuned through a
042 * {@link Caffeine} builder or {@link CaffeineSpec}, passed into this
043 * CacheManager through {@link #setCaffeine}/{@link #setCaffeineSpec}.
044 * A {@link CaffeineSpec}-compliant expression value can also be applied
045 * via the {@link #setCacheSpecification "cacheSpecification"} bean property.
046 *
047 * <p>Requires Caffeine 2.1 or higher.
048 *
049 * @author Ben Manes
050 * @author Juergen Hoeller
051 * @author Stephane Nicoll
052 * @since 4.3
053 * @see CaffeineCache
054 */
055public class CaffeineCacheManager implements CacheManager {
056
057        private final ConcurrentMap<String, Cache> cacheMap = new ConcurrentHashMap<String, Cache>(16);
058
059        private boolean dynamic = true;
060
061        private Caffeine<Object, Object> cacheBuilder = Caffeine.newBuilder();
062
063        private CacheLoader<Object, Object> cacheLoader;
064
065        private boolean allowNullValues = true;
066
067
068        /**
069         * Construct a dynamic CaffeineCacheManager,
070         * lazily creating cache instances as they are being requested.
071         */
072        public CaffeineCacheManager() {
073        }
074
075        /**
076         * Construct a static CaffeineCacheManager,
077         * managing caches for the specified cache names only.
078         */
079        public CaffeineCacheManager(String... cacheNames) {
080                setCacheNames(Arrays.asList(cacheNames));
081        }
082
083
084        /**
085         * Specify the set of cache names for this CacheManager's 'static' mode.
086         * <p>The number of caches and their names will be fixed after a call to this method,
087         * with no creation of further cache regions at runtime.
088         * <p>Calling this with a {@code null} collection argument resets the
089         * mode to 'dynamic', allowing for further creation of caches again.
090         */
091        public void setCacheNames(Collection<String> cacheNames) {
092                if (cacheNames != null) {
093                        for (String name : cacheNames) {
094                                this.cacheMap.put(name, createCaffeineCache(name));
095                        }
096                        this.dynamic = false;
097                }
098                else {
099                        this.dynamic = true;
100                }
101        }
102
103        /**
104         * Set the Caffeine to use for building each individual
105         * {@link CaffeineCache} instance.
106         * @see #createNativeCaffeineCache
107         * @see com.github.benmanes.caffeine.cache.Caffeine#build()
108         */
109        public void setCaffeine(Caffeine<Object, Object> caffeine) {
110                Assert.notNull(caffeine, "Caffeine must not be null");
111                doSetCaffeine(caffeine);
112        }
113
114        /**
115         * Set the {@link CaffeineSpec} to use for building each individual
116         * {@link CaffeineCache} instance.
117         * @see #createNativeCaffeineCache
118         * @see com.github.benmanes.caffeine.cache.Caffeine#from(CaffeineSpec)
119         */
120        public void setCaffeineSpec(CaffeineSpec caffeineSpec) {
121                doSetCaffeine(Caffeine.from(caffeineSpec));
122        }
123
124        /**
125         * Set the Caffeine cache specification String to use for building each
126         * individual {@link CaffeineCache} instance. The given value needs to
127         * comply with Caffeine's {@link CaffeineSpec} (see its javadoc).
128         * @see #createNativeCaffeineCache
129         * @see com.github.benmanes.caffeine.cache.Caffeine#from(String)
130         */
131        public void setCacheSpecification(String cacheSpecification) {
132                doSetCaffeine(Caffeine.from(cacheSpecification));
133        }
134
135        /**
136         * Set the Caffeine CacheLoader to use for building each individual
137         * {@link CaffeineCache} instance, turning it into a LoadingCache.
138         * @see #createNativeCaffeineCache
139         * @see com.github.benmanes.caffeine.cache.Caffeine#build(CacheLoader)
140         * @see com.github.benmanes.caffeine.cache.LoadingCache
141         */
142        public void setCacheLoader(CacheLoader<Object, Object> cacheLoader) {
143                if (!ObjectUtils.nullSafeEquals(this.cacheLoader, cacheLoader)) {
144                        this.cacheLoader = cacheLoader;
145                        refreshKnownCaches();
146                }
147        }
148
149        /**
150         * Specify whether to accept and convert {@code null} values for all caches
151         * in this cache manager.
152         * <p>Default is "true", despite Caffeine itself not supporting {@code null} values.
153         * An internal holder object will be used to store user-level {@code null}s.
154         */
155        public void setAllowNullValues(boolean allowNullValues) {
156                if (this.allowNullValues != allowNullValues) {
157                        this.allowNullValues = allowNullValues;
158                        refreshKnownCaches();
159                }
160        }
161
162        /**
163         * Return whether this cache manager accepts and converts {@code null} values
164         * for all of its caches.
165         */
166        public boolean isAllowNullValues() {
167                return this.allowNullValues;
168        }
169
170
171        @Override
172        public Collection<String> getCacheNames() {
173                return Collections.unmodifiableSet(this.cacheMap.keySet());
174        }
175
176        @Override
177        public Cache getCache(String name) {
178                Cache cache = this.cacheMap.get(name);
179                if (cache == null && this.dynamic) {
180                        synchronized (this.cacheMap) {
181                                cache = this.cacheMap.get(name);
182                                if (cache == null) {
183                                        cache = createCaffeineCache(name);
184                                        this.cacheMap.put(name, cache);
185                                }
186                        }
187                }
188                return cache;
189        }
190
191        /**
192         * Create a new CaffeineCache instance for the specified cache name.
193         * @param name the name of the cache
194         * @return the Spring CaffeineCache adapter (or a decorator thereof)
195         */
196        protected Cache createCaffeineCache(String name) {
197                return new CaffeineCache(name, createNativeCaffeineCache(name), isAllowNullValues());
198        }
199
200        /**
201         * Create a native Caffeine Cache instance for the specified cache name.
202         * @param name the name of the cache
203         * @return the native Caffeine Cache instance
204         */
205        protected com.github.benmanes.caffeine.cache.Cache<Object, Object> createNativeCaffeineCache(String name) {
206                if (this.cacheLoader != null) {
207                        return this.cacheBuilder.build(this.cacheLoader);
208                }
209                else {
210                        return this.cacheBuilder.build();
211                }
212        }
213
214        private void doSetCaffeine(Caffeine<Object, Object> cacheBuilder) {
215                if (!ObjectUtils.nullSafeEquals(this.cacheBuilder, cacheBuilder)) {
216                        this.cacheBuilder = cacheBuilder;
217                        refreshKnownCaches();
218                }
219        }
220
221        /**
222         * Create the known caches again with the current state of this manager.
223         */
224        private void refreshKnownCaches() {
225                for (Map.Entry<String, Cache> entry : this.cacheMap.entrySet()) {
226                        entry.setValue(createCaffeineCache(entry.getKey()));
227                }
228        }
229
230}