• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

TAKETODAY / today-infrastructure / 19575886058

21 Nov 2025 03:51PM UTC coverage: 84.413% (+0.003%) from 84.41%
19575886058

push

github

TAKETODAY
:white_check_mark:

61710 of 78169 branches covered (78.94%)

Branch coverage included in aggregate %.

145709 of 167549 relevant lines covered (86.97%)

3.71 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

86.32
today-context/src/main/java/infra/cache/support/CaffeineCacheManager.java
1
/*
2
 * Copyright 2017 - 2025 the original author or authors.
3
 *
4
 * This program is free software: you can redistribute it and/or modify
5
 * it under the terms of the GNU General Public License as published by
6
 * the Free Software Foundation, either version 3 of the License, or
7
 * (at your option) any later version.
8
 *
9
 * This program is distributed in the hope that it will be useful,
10
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
 * GNU General Public License for more details.
13
 *
14
 * You should have received a copy of the GNU General Public License
15
 * along with this program. If not, see [https://www.gnu.org/licenses/]
16
 */
17

18
package infra.cache.support;
19

20
import com.github.benmanes.caffeine.cache.AsyncCache;
21
import com.github.benmanes.caffeine.cache.AsyncCacheLoader;
22
import com.github.benmanes.caffeine.cache.CacheLoader;
23
import com.github.benmanes.caffeine.cache.Caffeine;
24
import com.github.benmanes.caffeine.cache.CaffeineSpec;
25

26
import org.jspecify.annotations.Nullable;
27

28
import java.util.Arrays;
29
import java.util.Collection;
30
import java.util.Collections;
31
import java.util.Map;
32
import java.util.concurrent.ConcurrentHashMap;
33
import java.util.concurrent.CopyOnWriteArrayList;
34
import java.util.function.Supplier;
35

36
import infra.cache.Cache;
37
import infra.cache.CacheManager;
38
import infra.cache.interceptor.CacheAspectSupport;
39
import infra.lang.Assert;
40
import infra.util.ObjectUtils;
41

42
/**
43
 * {@link CacheManager} implementation that lazily builds {@link CaffeineCache}
44
 * instances for each {@link #getCache} request. Also supports a 'static' mode
45
 * where the set of cache names is pre-defined through {@link #setCacheNames},
46
 * with no dynamic creation of further cache regions at runtime.
47
 *
48
 * <p>The configuration of the underlying cache can be fine-tuned through a
49
 * {@link Caffeine} builder or {@link CaffeineSpec}, passed into this
50
 * CacheManager through {@link #setCaffeine}/{@link #setCaffeineSpec}.
51
 * A {@link CaffeineSpec}-compliant expression value can also be applied
52
 * via the {@link #setCacheSpecification "cacheSpecification"} bean property.
53
 *
54
 * <p>Supports the {@link Cache#retrieve(Object)} and
55
 * {@link Cache#retrieve(Object, Supplier)} operations through Caffeine's
56
 * {@link AsyncCache}, when configured via {@link #setAsyncCacheMode}.
57
 *
58
 * <p>Requires Caffeine 3.0 or higher.
59
 *
60
 * @author Ben Manes
61
 * @author Juergen Hoeller
62
 * @author Stephane Nicoll
63
 * @author Sam Brannen
64
 * @author <a href="https://github.com/TAKETODAY">Harry Yang</a>
65
 * @see CaffeineCache
66
 * @since 2020-08-15 20:05
67
 */
68
public class CaffeineCacheManager implements CacheManager {
69

70
  private Caffeine<Object, Object> cacheBuilder;
71

72
  @Nullable
73
  private AsyncCacheLoader<Object, Object> cacheLoader;
74

75
  private boolean asyncCacheMode = false;
6✔
76

77
  private boolean allowNullValues = true;
6✔
78

79
  private volatile boolean dynamic = true;
6✔
80

81
  private final ConcurrentHashMap<String, Cache> cacheMap = new ConcurrentHashMap<>(16);
12✔
82

83
  private final CopyOnWriteArrayList<String> customCacheNames = new CopyOnWriteArrayList<>();
10✔
84

85
  /**
86
   * Construct a dynamic CaffeineCacheManager,
87
   * lazily creating cache instances as they are being requested.
88
   */
89
  public CaffeineCacheManager() {
2✔
90
    this.cacheBuilder = Caffeine.newBuilder();
3✔
91
  }
1✔
92

93
  /**
94
   * @since 4.0
95
   */
96
  public CaffeineCacheManager(Caffeine<Object, Object> cacheBuilder) {
2✔
97
    Assert.notNull(cacheBuilder, "cacheBuilder is required");
3✔
98
    this.cacheBuilder = cacheBuilder;
3✔
99
  }
1✔
100

101
  /**
102
   * Construct a static CaffeineCacheManager,
103
   * managing caches for the specified cache names only.
104
   */
105
  public CaffeineCacheManager(String... cacheNames) {
106
    this(Caffeine.newBuilder());
3✔
107
    setCacheNames(Arrays.asList(cacheNames));
4✔
108
  }
1✔
109

110
  /**
111
   * Specify the set of cache names for this CacheManager's 'static' mode.
112
   * <p>The number of caches and their names will be fixed after a call
113
   * to this method, with no creation of further cache regions at runtime.
114
   * <p>Note that this method replaces existing caches of the given names
115
   * and prevents the creation of further cache regions from here on - but
116
   * does <i>not</i> remove unrelated existing caches. For a full reset,
117
   * consider calling {@link #resetCaches()} before calling this method.
118
   * <p>Calling this method with a {@code null} collection argument resets
119
   * the mode to 'dynamic', allowing for further creation of caches again.
120
   *
121
   * @see #resetCaches()
122
   */
123
  public void setCacheNames(@Nullable Collection<String> cacheNames) {
124
    if (cacheNames != null) {
2✔
125
      for (String name : cacheNames) {
10✔
126
        this.cacheMap.put(name, createCaffeineCache(name));
8✔
127
      }
1✔
128
      this.dynamic = false;
4✔
129
    }
130
    else {
131
      this.dynamic = true;
3✔
132
    }
133
  }
1✔
134

135
  /**
136
   * Set the Caffeine to use for building each individual
137
   * {@link CaffeineCache} instance.
138
   *
139
   * @see #createNativeCaffeineCache
140
   * @see com.github.benmanes.caffeine.cache.Caffeine#build()
141
   */
142
  public void setCaffeine(Caffeine<Object, Object> caffeine) {
143
    Assert.notNull(caffeine, "Caffeine is required");
3✔
144
    doSetCaffeine(caffeine);
3✔
145
  }
1✔
146

147
  /**
148
   * Set the {@link CaffeineSpec} to use for building each individual
149
   * {@link CaffeineCache} instance.
150
   *
151
   * @see #createNativeCaffeineCache
152
   * @see com.github.benmanes.caffeine.cache.Caffeine#from(CaffeineSpec)
153
   */
154
  public void setCaffeineSpec(CaffeineSpec caffeineSpec) {
155
    doSetCaffeine(Caffeine.from(caffeineSpec));
4✔
156
  }
1✔
157

158
  /**
159
   * Set the Caffeine cache specification String to use for building each
160
   * individual {@link CaffeineCache} instance. The given value needs to
161
   * comply with Caffeine's {@link CaffeineSpec} (see its javadoc).
162
   *
163
   * @see #createNativeCaffeineCache
164
   * @see com.github.benmanes.caffeine.cache.Caffeine#from(String)
165
   */
166
  public void setCacheSpecification(String cacheSpecification) {
167
    doSetCaffeine(Caffeine.from(cacheSpecification));
4✔
168
  }
1✔
169

170
  private void doSetCaffeine(Caffeine<Object, Object> cacheBuilder) {
171
    if (!ObjectUtils.nullSafeEquals(this.cacheBuilder, cacheBuilder)) {
5✔
172
      this.cacheBuilder = cacheBuilder;
3✔
173
      refreshCommonCaches();
2✔
174
    }
175
  }
1✔
176

177
  /**
178
   * Set the Caffeine CacheLoader to use for building each individual
179
   * {@link CaffeineCache} instance, turning it into a LoadingCache.
180
   *
181
   * @see #createNativeCaffeineCache
182
   * @see com.github.benmanes.caffeine.cache.Caffeine#build(CacheLoader)
183
   * @see com.github.benmanes.caffeine.cache.LoadingCache
184
   */
185
  public void setCacheLoader(CacheLoader<Object, Object> cacheLoader) {
186
    if (!ObjectUtils.nullSafeEquals(this.cacheLoader, cacheLoader)) {
5✔
187
      this.cacheLoader = cacheLoader;
3✔
188
      refreshCommonCaches();
2✔
189
    }
190
  }
1✔
191

192
  /**
193
   * Set the Caffeine AsyncCacheLoader to use for building each individual
194
   * {@link CaffeineCache} instance, turning it into a LoadingCache.
195
   * <p>This implicitly switches the {@link #setAsyncCacheMode "asyncCacheMode"}
196
   * flag to {@code true}.
197
   *
198
   * @see #createAsyncCaffeineCache
199
   * @see Caffeine#buildAsync(AsyncCacheLoader)
200
   * @see com.github.benmanes.caffeine.cache.LoadingCache
201
   * @since 4.0
202
   */
203
  public void setAsyncCacheLoader(AsyncCacheLoader<Object, Object> cacheLoader) {
204
    if (!ObjectUtils.nullSafeEquals(this.cacheLoader, cacheLoader)) {
×
205
      this.cacheLoader = cacheLoader;
×
206
      this.asyncCacheMode = true;
×
207
      refreshCommonCaches();
×
208
    }
209
  }
×
210

211
  /**
212
   * Set the common cache type that this cache manager builds to async.
213
   * This applies to {@link #setCacheNames} as well as on-demand caches.
214
   * <p>Individual cache registrations (such as {@link #registerCustomCache(String, AsyncCache)}
215
   * and {@link #registerCustomCache(String, com.github.benmanes.caffeine.cache.Cache)})
216
   * are not dependent on this setting.
217
   * <p>By default, this cache manager builds regular native Caffeine caches.
218
   * To switch to async caches which can also be used through the synchronous API
219
   * but come with support for {@code Cache#retrieve}, set this flag to {@code true}.
220
   * <p>Note that while null values in the cache are tolerated in async cache mode,
221
   * the recommendation is to disallow null values through
222
   * {@link #setAllowNullValues setAllowNullValues(false)}. This makes the semantics
223
   * of CompletableFuture-based access simpler and optimizes retrieval performance
224
   * since a Caffeine-provided CompletableFuture handle does not have to get wrapped.
225
   * <p>If you come here for the adaptation of reactive types such as a Reactor
226
   * {@code Mono} or {@code Flux} onto asynchronous caching, we recommend the standard
227
   * arrangement for caching the produced values asynchronously in 4.0 through enabling
228
   * this Caffeine mode. If this is not immediately possible/desirable for existing
229
   * apps, you may set the system property "infra.cache.reactivestreams.ignore=true"
230
   * to restore 4.0 behavior where reactive handles are treated as regular values.
231
   *
232
   * @see Caffeine#buildAsync()
233
   * @see Cache#retrieve(Object)
234
   * @see Cache#retrieve(Object, Supplier)
235
   * @see CacheAspectSupport#IGNORE_REACTIVESTREAMS_PROPERTY_NAME
236
   * @since 4.0
237
   */
238
  public void setAsyncCacheMode(boolean asyncCacheMode) {
239
    if (this.asyncCacheMode != asyncCacheMode) {
4!
240
      this.asyncCacheMode = asyncCacheMode;
3✔
241
      refreshCommonCaches();
2✔
242
    }
243
  }
1✔
244

245
  /**
246
   * Specify whether to accept and convert {@code null} values for all caches
247
   * in this cache manager.
248
   * <p>Default is "true", despite Caffeine itself not supporting {@code null} values.
249
   * An internal holder object will be used to store user-level {@code null}s.
250
   */
251
  public void setAllowNullValues(boolean allowNullValues) {
252
    if (this.allowNullValues != allowNullValues) {
4!
253
      this.allowNullValues = allowNullValues;
3✔
254
      refreshCommonCaches();
2✔
255
    }
256
  }
1✔
257

258
  /**
259
   * Return whether this cache manager accepts and converts {@code null} values
260
   * for all of its caches.
261
   */
262
  public boolean isAllowNullValues() {
263
    return this.allowNullValues;
3✔
264
  }
265

266
  @Override
267
  @Nullable
268
  public Cache getCache(String name) {
269
    Cache cache = cacheMap.get(name);
6✔
270
    if (cache == null && this.dynamic) {
5✔
271
      cache = cacheMap.computeIfAbsent(name, this::createCaffeineCache);
8✔
272
    }
273
    return cache;
2✔
274
  }
275

276
  @Override
277
  public Collection<String> getCacheNames() {
278
    return Collections.unmodifiableSet(this.cacheMap.keySet());
×
279
  }
280

281
  /**
282
   * Reset this cache manager's caches, removing them completely for on-demand
283
   * re-creation in 'dynamic' mode, or simply clearing their entries otherwise.
284
   *
285
   * @since 5.0
286
   */
287
  public void resetCaches() {
288
    this.cacheMap.values().forEach(Cache::clear);
5✔
289
    if (this.dynamic) {
3✔
290
      this.cacheMap.keySet().retainAll(this.customCacheNames);
7✔
291
    }
292
  }
1✔
293

294
  /**
295
   * Remove the specified cache from this cache manager, applying to
296
   * custom caches as well as dynamically registered caches at runtime.
297
   *
298
   * @param name the name of the cache
299
   * @since 5.0
300
   */
301
  public void removeCache(String name) {
302
    this.customCacheNames.remove(name);
5✔
303
    this.cacheMap.remove(name);
5✔
304
  }
1✔
305

306
  /**
307
   * Register the given native Caffeine Cache instance with this cache manager,
308
   * adapting it to Framework's cache API for exposure through {@link #getCache}.
309
   * Any number of such custom caches may be registered side by side.
310
   * <p>This allows for custom settings per cache (as opposed to all caches
311
   * sharing the common settings in the cache manager's configuration) and
312
   * is typically used with the Caffeine builder API:
313
   * {@code registerCustomCache("myCache", Caffeine.newBuilder().maximumSize(10).build())}
314
   * <p>Note that any other caches, whether statically specified through
315
   * {@link #setCacheNames} or dynamically built on demand, still operate
316
   * with the common settings in the cache manager's configuration.
317
   *
318
   * @param name the name of the cache
319
   * @param cache the custom Caffeine Cache instance to register
320
   * @see #adaptCaffeineCache(String, com.github.benmanes.caffeine.cache.Cache)
321
   */
322
  public void registerCustomCache(String name, com.github.benmanes.caffeine.cache.Cache<Object, Object> cache) {
323
    this.customCacheNames.add(name);
5✔
324
    this.cacheMap.put(name, adaptCaffeineCache(name, cache));
9✔
325
  }
1✔
326

327
  /**
328
   * Register the given Caffeine AsyncCache instance with this cache manager,
329
   * adapting it to Infra cache API for exposure through {@link #getCache}.
330
   * Any number of such custom caches may be registered side by side.
331
   * <p>This allows for custom settings per cache (as opposed to all caches
332
   * sharing the common settings in the cache manager's configuration) and
333
   * is typically used with the Caffeine builder API:
334
   * {@code registerCustomCache("myCache", Caffeine.newBuilder().maximumSize(10).buildAsync())}
335
   * <p>Note that any other caches, whether statically specified through
336
   * {@link #setCacheNames} or dynamically built on demand, still operate
337
   * with the common settings in the cache manager's configuration.
338
   *
339
   * @param name the name of the cache
340
   * @param cache the custom Caffeine AsyncCache instance to register
341
   * @see #adaptCaffeineCache(String, AsyncCache)
342
   * @since 4.0
343
   */
344
  public void registerCustomCache(String name, AsyncCache<Object, Object> cache) {
345
    this.customCacheNames.add(name);
×
346
    this.cacheMap.put(name, adaptCaffeineCache(name, cache));
×
347
  }
×
348

349
  /**
350
   * Adapt the given new native Caffeine Cache instance to Framework's {@link Cache}
351
   * abstraction for the specified cache name.
352
   *
353
   * @param name the name of the cache
354
   * @param cache the native Caffeine Cache instance
355
   * @return the FrameworkCaffeineCache adapter (or a decorator thereof)
356
   * @see CaffeineCache
357
   * @see #isAllowNullValues()
358
   */
359
  protected Cache adaptCaffeineCache(String name, com.github.benmanes.caffeine.cache.Cache<Object, Object> cache) {
360
    return new CaffeineCache(name, cache, isAllowNullValues());
8✔
361
  }
362

363
  /**
364
   * Adapt the given new Caffeine AsyncCache instance to Infra {@link Cache}
365
   * abstraction for the specified cache name.
366
   *
367
   * @param name the name of the cache
368
   * @param cache the Caffeine AsyncCache instance
369
   * @return the Infra CaffeineCache adapter (or a decorator thereof)
370
   * @see CaffeineCache#CaffeineCache(String, AsyncCache, boolean)
371
   * @see #isAllowNullValues()
372
   * @since 4.0
373
   */
374
  protected Cache adaptCaffeineCache(String name, AsyncCache<Object, Object> cache) {
375
    return new CaffeineCache(name, cache, isAllowNullValues());
8✔
376
  }
377

378
  /**
379
   * Build a common {@link CaffeineCache} instance for the specified cache name,
380
   * using the common Caffeine configuration specified on this cache manager.
381
   * <p>Delegates to {@link #adaptCaffeineCache} as the adaptation method to
382
   * Framework's cache abstraction (allowing for centralized decoration etc),
383
   * passing in a freshly built native Caffeine Cache instance.
384
   *
385
   * @param name the name of the cache
386
   * @return the FrameworkCaffeineCache adapter (or a decorator thereof)
387
   * @see #adaptCaffeineCache
388
   * @see #createNativeCaffeineCache
389
   */
390
  protected Cache createCaffeineCache(String name) {
391
    return (this.asyncCacheMode ? adaptCaffeineCache(name, createAsyncCaffeineCache(name)) :
11✔
392
            adaptCaffeineCache(name, createNativeCaffeineCache(name)));
6✔
393
  }
394

395
  /**
396
   * Build a common Caffeine Cache instance for the specified cache name,
397
   * using the common Caffeine configuration specified on this cache manager.
398
   *
399
   * @param name the name of the cache
400
   * @return the native Caffeine Cache instance
401
   * @see #createCaffeineCache
402
   */
403
  protected com.github.benmanes.caffeine.cache.Cache<Object, Object> createNativeCaffeineCache(String name) {
404
    if (this.cacheLoader != null) {
3✔
405
      if (this.cacheLoader instanceof CacheLoader<Object, Object> regularCacheLoader) {
9!
406
        return this.cacheBuilder.build(regularCacheLoader);
5✔
407
      }
408
      else {
409
        throw new IllegalStateException(
×
410
                "Cannot create regular Caffeine Cache with async-only cache loader: " + this.cacheLoader);
411
      }
412
    }
413
    return this.cacheBuilder.build();
4✔
414
  }
415

416
  /**
417
   * Build a common Caffeine AsyncCache instance for the specified cache name,
418
   * using the common Caffeine configuration specified on this cache manager.
419
   *
420
   * @param name the name of the cache
421
   * @return the Caffeine AsyncCache instance
422
   * @see #createCaffeineCache
423
   * @since 4.0
424
   */
425
  protected AsyncCache<Object, Object> createAsyncCaffeineCache(String name) {
426
    return (this.cacheLoader != null ? this.cacheBuilder.buildAsync(this.cacheLoader) :
4!
427
            this.cacheBuilder.buildAsync());
3✔
428
  }
429

430
  /**
431
   * Recreate the common caches with the current state of this manager.
432
   */
433
  private void refreshCommonCaches() {
434
    for (Map.Entry<String, Cache> entry : this.cacheMap.entrySet()) {
12✔
435
      if (!this.customCacheNames.contains(entry.getKey())) {
6✔
436
        entry.setValue(createCaffeineCache(entry.getKey()));
8✔
437
      }
438
    }
1✔
439
  }
1✔
440

441
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc