View Javadoc
1   /*
2    *    Copyright 2010-2026 the original author or authors.
3    *
4    *    Licensed under the Apache License, Version 2.0 (the "License");
5    *    you may not use this file except in compliance with the License.
6    *    You may obtain a copy of the License at
7    *
8    *       https://www.apache.org/licenses/LICENSE-2.0
9    *
10   *    Unless required by applicable law or agreed to in writing, software
11   *    distributed under the License is distributed on an "AS IS" BASIS,
12   *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   *    See the License for the specific language governing permissions and
14   *    limitations under the License.
15   */
16  package org.mybatis.caches.ehcache;
17  
18  import java.io.Serializable;
19  import java.nio.file.Path;
20  import java.time.Duration;
21  import java.util.concurrent.locks.ReadWriteLock;
22  
23  import org.apache.ibatis.cache.Cache;
24  import org.ehcache.Cache.Entry;
25  import org.ehcache.PersistentCacheManager;
26  import org.ehcache.config.builders.CacheConfigurationBuilder;
27  import org.ehcache.config.builders.CacheManagerBuilder;
28  import org.ehcache.config.builders.ExpiryPolicyBuilder;
29  import org.ehcache.config.builders.ResourcePoolsBuilder;
30  import org.ehcache.config.units.EntryUnit;
31  import org.ehcache.config.units.MemoryUnit;
32  
33  /**
34   * Cache adapter for Ehcache 3.
35   *
36   * @author Simone Tripodi
37   */
38  public abstract class AbstractEhcacheCache implements Cache {
39  
40    /** Placeholder stored in Ehcache 3 for entries whose actual value is {@code null}. */
41    private static final Object NULL_VALUE = new NullValue();
42  
43    /**
44     * The cache manager reference. A {@link PersistentCacheManager} is used so that individual caches may optionally
45     * configure a disk tier via {@link #setMaxBytesLocalDisk(long)}.
46     */
47    protected static PersistentCacheManager CACHE_MANAGER = CacheManagerBuilder.newCacheManagerBuilder()
48        .with(
49            CacheManagerBuilder.persistence(Path.of(System.getProperty("java.io.tmpdir"), "ehcache-mybatis").toString()))
50        .build(true);
51  
52    /**
53     * The cache id (namespace).
54     */
55    protected final String id;
56  
57    /**
58     * The cache instance (lazily initialised on first use).
59     */
60    protected org.ehcache.Cache<Object, Object> cache;
61  
62    protected long timeToIdleSeconds;
63    protected long timeToLiveSeconds;
64    protected long maxEntriesLocalHeap;
65    protected long maxEntriesLocalDisk;
66    protected long maxBytesLocalDisk;
67    protected String memoryStoreEvictionPolicy;
68  
69    /**
70     * Instantiates a new abstract ehcache cache.
71     *
72     * @param id
73     *          the cache id (namespace)
74     */
75    public AbstractEhcacheCache(final String id) {
76      if (id == null) {
77        throw new IllegalArgumentException("Cache instances require an ID");
78      }
79      this.id = id;
80      // Remove any pre-existing cache so this instance always starts with a fresh default configuration.
81      if (CACHE_MANAGER.getCache(id, Object.class, Object.class) != null) {
82        CACHE_MANAGER.removeCache(id);
83      }
84    }
85  
86    /**
87     * Returns the underlying Ehcache 3 cache, creating it on first use with the current configuration.
88     */
89    protected synchronized org.ehcache.Cache<Object, Object> getOrCreateCache() {
90      if (cache == null) {
91        cache = buildAndRegisterCache();
92      }
93      return cache;
94    }
95  
96    /**
97     * Builds and registers a new Ehcache 3 cache instance using the current configuration fields.
98     */
99    protected org.ehcache.Cache<Object, Object> buildAndRegisterCache() {
100     if (CACHE_MANAGER.getCache(id, Object.class, Object.class) != null) {
101       CACHE_MANAGER.removeCache(id);
102     }
103     long heapEntries = maxEntriesLocalHeap > 0 ? maxEntriesLocalHeap : Long.MAX_VALUE / 2;
104     ResourcePoolsBuilder poolsBuilder = ResourcePoolsBuilder.newResourcePoolsBuilder().heap(heapEntries,
105         EntryUnit.ENTRIES);
106     if (maxBytesLocalDisk > 0) {
107       poolsBuilder = poolsBuilder.disk(maxBytesLocalDisk, MemoryUnit.B);
108     }
109     CacheConfigurationBuilder<Object, Object> builder = CacheConfigurationBuilder
110         .newCacheConfigurationBuilder(Object.class, Object.class, poolsBuilder).withExpiry(buildExpiryPolicy());
111     if (maxBytesLocalDisk > 0) {
112       // Disk and off-heap tiers require a Serializer since entries cannot be stored as object references.
113       // ObjectSerializer uses standard Java serialisation; cached values must implement Serializable.
114       builder = builder.withKeySerializer(ObjectSerializer.class).withValueSerializer(ObjectSerializer.class);
115     }
116     CACHE_MANAGER.createCache(id, builder.build());
117     return CACHE_MANAGER.getCache(id, Object.class, Object.class);
118   }
119 
120   private org.ehcache.expiry.ExpiryPolicy<Object, Object> buildExpiryPolicy() {
121     if (timeToLiveSeconds > 0) {
122       return ExpiryPolicyBuilder.timeToLiveExpiration(Duration.ofSeconds(timeToLiveSeconds));
123     }
124     if (timeToIdleSeconds > 0) {
125       return ExpiryPolicyBuilder.timeToIdleExpiration(Duration.ofSeconds(timeToIdleSeconds));
126     }
127     return ExpiryPolicyBuilder.noExpiration();
128   }
129 
130   /**
131    * {@inheritDoc}
132    */
133   @Override
134   public void clear() {
135     getOrCreateCache().clear();
136   }
137 
138   /**
139    * {@inheritDoc}
140    */
141   @Override
142   public String getId() {
143     return id;
144   }
145 
146   /**
147    * {@inheritDoc}
148    */
149   @Override
150   public Object getObject(Object key) {
151     Object value = getOrCreateCache().get(new HashKeyWrapper(key));
152     return value instanceof NullValue ? null : value;
153   }
154 
155   /**
156    * {@inheritDoc}
157    */
158   @Override
159   public int getSize() {
160     int count = 0;
161     for (Entry<Object, Object> ignored : getOrCreateCache()) {
162       count++;
163     }
164     return count;
165   }
166 
167   /**
168    * {@inheritDoc}
169    */
170   @Override
171   public void putObject(Object key, Object value) {
172     getOrCreateCache().put(new HashKeyWrapper(key), value == null ? NULL_VALUE : value);
173   }
174 
175   /**
176    * {@inheritDoc}
177    */
178   @Override
179   public Object removeObject(Object key) {
180     Object obj = getObject(key);
181     getOrCreateCache().remove(new HashKeyWrapper(key));
182     return obj;
183   }
184 
185   /**
186    * {@inheritDoc}
187    */
188   public void unlock(Object key) {
189   }
190 
191   /**
192    * {@inheritDoc}
193    */
194   @Override
195   public boolean equals(Object obj) {
196     if (this == obj) {
197       return true;
198     }
199     if (obj == null) {
200       return false;
201     }
202     if (!(obj instanceof Cache)) {
203       return false;
204     }
205 
206     Cache otherCache = (Cache) obj;
207     return id.equals(otherCache.getId());
208   }
209 
210   /**
211    * {@inheritDoc}
212    */
213   @Override
214   public int hashCode() {
215     return id.hashCode();
216   }
217 
218   @Override
219   public ReadWriteLock getReadWriteLock() {
220     return null;
221   }
222 
223   /**
224    * {@inheritDoc}
225    */
226   @Override
227   public String toString() {
228     return "EHCache {" + id + "}";
229   }
230 
231   // DYNAMIC PROPERTIES
232 
233   /**
234    * Sets the time to idle for an element before it expires. Is only used if the element is not eternal. If the cache
235    * has already been initialised the configuration change takes effect immediately by recreating the cache.
236    *
237    * @param timeToIdleSeconds
238    *          the default amount of time to live for an element from its last accessed or modified date
239    */
240   public void setTimeToIdleSeconds(long timeToIdleSeconds) {
241     this.timeToIdleSeconds = timeToIdleSeconds;
242     recreateCacheIfInitialized();
243   }
244 
245   /**
246    * Sets the time to live for an element before it expires. Is only used if the element is not eternal. If the cache
247    * has already been initialised the configuration change takes effect immediately by recreating the cache.
248    *
249    * @param timeToLiveSeconds
250    *          the default amount of time to live for an element from its creation date
251    */
252   public void setTimeToLiveSeconds(long timeToLiveSeconds) {
253     this.timeToLiveSeconds = timeToLiveSeconds;
254     recreateCacheIfInitialized();
255   }
256 
257   /**
258    * Sets the maximum objects to be held in memory (0 = no limit). If the cache has already been initialised the
259    * configuration change takes effect immediately by recreating the cache.
260    *
261    * @param maxEntriesLocalHeap
262    *          The maximum number of elements in heap, before they are evicted (0 == no limit)
263    */
264   public void setMaxEntriesLocalHeap(long maxEntriesLocalHeap) {
265     this.maxEntriesLocalHeap = maxEntriesLocalHeap;
266     recreateCacheIfInitialized();
267   }
268 
269   /**
270    * Sets the maximum number elements on Disk. 0 means unlimited.
271    * <p>
272    * Note: this property is retained for compatibility but has no effect in Ehcache 3, which does not support an
273    * entry-count limit for the disk tier. Use {@link #setMaxBytesLocalDisk(long)} to configure disk storage instead.
274    * </p>
275    *
276    * @param maxEntriesLocalDisk
277    *          the maximum number of Elements to allow on the disk. 0 means unlimited.
278    */
279   public void setMaxEntriesLocalDisk(long maxEntriesLocalDisk) {
280     this.maxEntriesLocalDisk = maxEntriesLocalDisk;
281     recreateCacheIfInitialized();
282   }
283 
284   /**
285    * Sets the maximum bytes to be used for the disk tier. When greater than zero a disk resource pool is added to the
286    * cache, allowing entries evicted from the heap to overflow to disk. If set to zero (the default) no disk tier is
287    * configured and the cache is heap-only.
288    *
289    * @param maxBytesLocalDisk
290    *          the maximum number of bytes to allocate on disk. 0 means no disk tier (heap-only).
291    */
292   public void setMaxBytesLocalDisk(long maxBytesLocalDisk) {
293     this.maxBytesLocalDisk = maxBytesLocalDisk;
294     recreateCacheIfInitialized();
295   }
296 
297   /**
298    * Sets the eviction policy. Stored for informational purposes; Ehcache 3 manages its own eviction strategy.
299    *
300    * @param memoryStoreEvictionPolicy
301    *          a String representation of the policy. One of "LRU", "LFU" or "FIFO".
302    */
303   public void setMemoryStoreEvictionPolicy(String memoryStoreEvictionPolicy) {
304     this.memoryStoreEvictionPolicy = memoryStoreEvictionPolicy;
305     recreateCacheIfInitialized();
306   }
307 
308   /**
309    * Recreates the underlying Ehcache 3 cache with the current configuration if the cache has already been initialised.
310    * Called by property setters when a configuration change is requested after first use.
311    */
312   protected synchronized void recreateCacheIfInitialized() {
313     if (cache != null) {
314       cache = buildAndRegisterCache();
315     }
316   }
317 
318   /**
319    * Placeholder used to represent a cached {@code null} value. Ehcache 3 does not permit null values, so this sentinel
320    * is stored and translated back to {@code null} on retrieval.
321    */
322   private static final class NullValue implements Serializable {
323     private static final long serialVersionUID = 1L;
324   }
325 
326 }