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

TAKETODAY / today-infrastructure / 18154768944

01 Oct 2025 07:26AM UTC coverage: 81.882% (-0.005%) from 81.887%
18154768944

push

github

web-flow
Merge pull request #290 from TAKETODAY/dev/jspecify

jspecify

59788 of 78013 branches covered (76.64%)

Branch coverage included in aggregate %.

141239 of 167496 relevant lines covered (84.32%)

3.6 hits per line

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

79.46
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 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) {
×
97
    Assert.notNull(cacheBuilder, "cacheBuilder is required");
×
98
    this.cacheBuilder = cacheBuilder;
×
99
  }
×
100

101
  /**
102
   * Construct a static CaffeineCacheManager,
103
   * managing caches for the specified cache names only.
104
   */
105
  public CaffeineCacheManager(String... cacheNames) {
2✔
106
    this.cacheBuilder = 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 to this method,
113
   * with no creation of further cache regions at runtime.
114
   * <p>Calling this with a {@code null} collection argument resets the
115
   * mode to 'dynamic', allowing for further creation of caches again.
116
   */
117
  public void setCacheNames(@Nullable Collection<String> cacheNames) {
118
    if (cacheNames != null) {
2✔
119
      for (String name : cacheNames) {
10✔
120
        this.cacheMap.put(name, createCaffeineCache(name));
8✔
121
      }
1✔
122
      this.dynamic = false;
4✔
123
    }
124
    else {
125
      this.dynamic = true;
3✔
126
    }
127
  }
1✔
128

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

141
  /**
142
   * Set the {@link CaffeineSpec} to use for building each individual
143
   * {@link CaffeineCache} instance.
144
   *
145
   * @see #createNativeCaffeineCache
146
   * @see com.github.benmanes.caffeine.cache.Caffeine#from(CaffeineSpec)
147
   */
148
  public void setCaffeineSpec(CaffeineSpec caffeineSpec) {
149
    doSetCaffeine(Caffeine.from(caffeineSpec));
4✔
150
  }
1✔
151

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

164
  private void doSetCaffeine(Caffeine<Object, Object> cacheBuilder) {
165
    if (!ObjectUtils.nullSafeEquals(this.cacheBuilder, cacheBuilder)) {
5✔
166
      this.cacheBuilder = cacheBuilder;
3✔
167
      refreshCommonCaches();
2✔
168
    }
169
  }
1✔
170

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

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

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

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

252
  /**
253
   * Return whether this cache manager accepts and converts {@code null} values
254
   * for all of its caches.
255
   */
256
  public boolean isAllowNullValues() {
257
    return this.allowNullValues;
3✔
258
  }
259

260
  @Override
261
  public Collection<String> getCacheNames() {
262
    return Collections.unmodifiableSet(this.cacheMap.keySet());
×
263
  }
264

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

275
  /**
276
   * Register the given native Caffeine Cache instance with this cache manager,
277
   * adapting it to Framework's cache API for exposure through {@link #getCache}.
278
   * Any number of such custom caches may be registered side by side.
279
   * <p>This allows for custom settings per cache (as opposed to all caches
280
   * sharing the common settings in the cache manager's configuration) and
281
   * is typically used with the Caffeine builder API:
282
   * {@code registerCustomCache("myCache", Caffeine.newBuilder().maximumSize(10).build())}
283
   * <p>Note that any other caches, whether statically specified through
284
   * {@link #setCacheNames} or dynamically built on demand, still operate
285
   * with the common settings in the cache manager's configuration.
286
   *
287
   * @param name the name of the cache
288
   * @param cache the custom Caffeine Cache instance to register
289
   * @see #adaptCaffeineCache(String, com.github.benmanes.caffeine.cache.Cache)
290
   */
291
  public void registerCustomCache(String name, com.github.benmanes.caffeine.cache.Cache<Object, Object> cache) {
292
    this.customCacheNames.add(name);
5✔
293
    this.cacheMap.put(name, adaptCaffeineCache(name, cache));
9✔
294
  }
1✔
295

296
  /**
297
   * Register the given Caffeine AsyncCache instance with this cache manager,
298
   * adapting it to Infra cache API for exposure through {@link #getCache}.
299
   * Any number of such custom caches may be registered side by side.
300
   * <p>This allows for custom settings per cache (as opposed to all caches
301
   * sharing the common settings in the cache manager's configuration) and
302
   * is typically used with the Caffeine builder API:
303
   * {@code registerCustomCache("myCache", Caffeine.newBuilder().maximumSize(10).buildAsync())}
304
   * <p>Note that any other caches, whether statically specified through
305
   * {@link #setCacheNames} or dynamically built on demand, still operate
306
   * with the common settings in the cache manager's configuration.
307
   *
308
   * @param name the name of the cache
309
   * @param cache the custom Caffeine AsyncCache instance to register
310
   * @see #adaptCaffeineCache(String, AsyncCache)
311
   * @since 4.0
312
   */
313
  public void registerCustomCache(String name, AsyncCache<Object, Object> cache) {
314
    this.customCacheNames.add(name);
×
315
    this.cacheMap.put(name, adaptCaffeineCache(name, cache));
×
316
  }
×
317

318
  /**
319
   * Remove the specified cache from this cache manager, applying to
320
   * custom caches as well as dynamically registered caches at runtime.
321
   *
322
   * @param name the name of the cache
323
   * @since 5.0
324
   */
325
  public void removeCache(String name) {
326
    this.customCacheNames.remove(name);
×
327
    this.cacheMap.remove(name);
×
328
  }
×
329

330
  /**
331
   * Adapt the given new native Caffeine Cache instance to Framework's {@link Cache}
332
   * abstraction for the specified cache name.
333
   *
334
   * @param name the name of the cache
335
   * @param cache the native Caffeine Cache instance
336
   * @return the FrameworkCaffeineCache adapter (or a decorator thereof)
337
   * @see CaffeineCache
338
   * @see #isAllowNullValues()
339
   */
340
  protected Cache adaptCaffeineCache(String name, com.github.benmanes.caffeine.cache.Cache<Object, Object> cache) {
341
    return new CaffeineCache(name, cache, isAllowNullValues());
8✔
342
  }
343

344
  /**
345
   * Adapt the given new Caffeine AsyncCache instance to Infra {@link Cache}
346
   * abstraction for the specified cache name.
347
   *
348
   * @param name the name of the cache
349
   * @param cache the Caffeine AsyncCache instance
350
   * @return the Infra CaffeineCache adapter (or a decorator thereof)
351
   * @see CaffeineCache#CaffeineCache(String, AsyncCache, boolean)
352
   * @see #isAllowNullValues()
353
   * @since 4.0
354
   */
355
  protected Cache adaptCaffeineCache(String name, AsyncCache<Object, Object> cache) {
356
    return new CaffeineCache(name, cache, isAllowNullValues());
8✔
357
  }
358

359
  /**
360
   * Build a common {@link CaffeineCache} instance for the specified cache name,
361
   * using the common Caffeine configuration specified on this cache manager.
362
   * <p>Delegates to {@link #adaptCaffeineCache} as the adaptation method to
363
   * Framework's cache abstraction (allowing for centralized decoration etc),
364
   * passing in a freshly built native Caffeine Cache instance.
365
   *
366
   * @param name the name of the cache
367
   * @return the FrameworkCaffeineCache adapter (or a decorator thereof)
368
   * @see #adaptCaffeineCache
369
   * @see #createNativeCaffeineCache
370
   */
371
  protected Cache createCaffeineCache(String name) {
372
    return (this.asyncCacheMode ? adaptCaffeineCache(name, createAsyncCaffeineCache(name)) :
11✔
373
            adaptCaffeineCache(name, createNativeCaffeineCache(name)));
6✔
374
  }
375

376
  /**
377
   * Build a common Caffeine Cache instance for the specified cache name,
378
   * using the common Caffeine configuration specified on this cache manager.
379
   *
380
   * @param name the name of the cache
381
   * @return the native Caffeine Cache instance
382
   * @see #createCaffeineCache
383
   */
384
  protected com.github.benmanes.caffeine.cache.Cache<Object, Object> createNativeCaffeineCache(String name) {
385
    if (this.cacheLoader != null) {
3✔
386
      if (this.cacheLoader instanceof CacheLoader<Object, Object> regularCacheLoader) {
9!
387
        return this.cacheBuilder.build(regularCacheLoader);
5✔
388
      }
389
      else {
390
        throw new IllegalStateException(
×
391
                "Cannot create regular Caffeine Cache with async-only cache loader: " + this.cacheLoader);
392
      }
393
    }
394
    return this.cacheBuilder.build();
4✔
395
  }
396

397
  /**
398
   * Build a common Caffeine AsyncCache instance for the specified cache name,
399
   * using the common Caffeine configuration specified on this cache manager.
400
   *
401
   * @param name the name of the cache
402
   * @return the Caffeine AsyncCache instance
403
   * @see #createCaffeineCache
404
   * @since 4.0
405
   */
406
  protected AsyncCache<Object, Object> createAsyncCaffeineCache(String name) {
407
    return (this.cacheLoader != null ? this.cacheBuilder.buildAsync(this.cacheLoader) :
4!
408
            this.cacheBuilder.buildAsync());
3✔
409
  }
410

411
  /**
412
   * Recreate the common caches with the current state of this manager.
413
   */
414
  private void refreshCommonCaches() {
415
    for (Map.Entry<String, Cache> entry : this.cacheMap.entrySet()) {
12✔
416
      if (!this.customCacheNames.contains(entry.getKey())) {
6✔
417
        entry.setValue(createCaffeineCache(entry.getKey()));
8✔
418
      }
419
    }
1✔
420
  }
1✔
421

422
}
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