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    protected 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   @Override
131   public void clear() {
132     getOrCreateCache().clear();
133   }
134 
135   @Override
136   public String getId() {
137     return id;
138   }
139 
140   @Override
141   public Object getObject(Object key) {
142     Object value = getOrCreateCache().get(new HashKeyWrapper(key));
143     return value instanceof NullValue ? null : value;
144   }
145 
146   @Override
147   public int getSize() {
148     int size = 0;
149     for (@SuppressWarnings("unused")
150     Entry<Object, Object> entry : getOrCreateCache()) {
151       size++;
152     }
153     return size;
154   }
155 
156   @Override
157   public void putObject(Object key, Object value) {
158     getOrCreateCache().put(new HashKeyWrapper(key), value == null ? NULL_VALUE : value);
159   }
160 
161   @Override
162   public Object removeObject(Object key) {
163     Object obj = getObject(key);
164     getOrCreateCache().remove(new HashKeyWrapper(key));
165     return obj;
166   }
167 
168   @Override
169   public boolean equals(Object obj) {
170     if (this == obj) {
171       return true;
172     }
173     if (obj == null) {
174       return false;
175     }
176     if (!(obj instanceof Cache)) {
177       return false;
178     }
179 
180     Cache otherCache = (Cache) obj;
181     return id.equals(otherCache.getId());
182   }
183 
184   @Override
185   public int hashCode() {
186     return id.hashCode();
187   }
188 
189   @Override
190   public ReadWriteLock getReadWriteLock() {
191     return null;
192   }
193 
194   @Override
195   public String toString() {
196     return "EHCache {" + id + "}";
197   }
198 
199   // DYNAMIC PROPERTIES
200 
201   /**
202    * Sets the time to idle for an element before it expires. Is only used if the element is not eternal. If the cache
203    * has already been initialised the configuration change takes effect immediately by recreating the cache.
204    *
205    * @param timeToIdleSeconds
206    *          the default amount of time to live for an element from its last accessed or modified date
207    */
208   public void setTimeToIdleSeconds(long timeToIdleSeconds) {
209     this.timeToIdleSeconds = timeToIdleSeconds;
210     recreateCacheIfInitialized();
211   }
212 
213   /**
214    * Sets the time to live for an element before it expires. Is only used if the element is not eternal. If the cache
215    * has already been initialised the configuration change takes effect immediately by recreating the cache.
216    *
217    * @param timeToLiveSeconds
218    *          the default amount of time to live for an element from its creation date
219    */
220   public void setTimeToLiveSeconds(long timeToLiveSeconds) {
221     this.timeToLiveSeconds = timeToLiveSeconds;
222     recreateCacheIfInitialized();
223   }
224 
225   /**
226    * Sets the maximum objects to be held in memory (0 = no limit). If the cache has already been initialised the
227    * configuration change takes effect immediately by recreating the cache.
228    *
229    * @param maxEntriesLocalHeap
230    *          The maximum number of elements in heap, before they are evicted (0 == no limit)
231    */
232   public void setMaxEntriesLocalHeap(long maxEntriesLocalHeap) {
233     this.maxEntriesLocalHeap = maxEntriesLocalHeap;
234     recreateCacheIfInitialized();
235   }
236 
237   /**
238    * Sets the maximum number elements on Disk. 0 means unlimited.
239    * <p>
240    * Note: this property is retained for compatibility but has no effect in Ehcache 3, which does not support an
241    * entry-count limit for the disk tier. Use {@link #setMaxBytesLocalDisk(long)} to configure disk storage instead.
242    * </p>
243    *
244    * @param maxEntriesLocalDisk
245    *          the maximum number of Elements to allow on the disk. 0 means unlimited.
246    */
247   public void setMaxEntriesLocalDisk(long maxEntriesLocalDisk) {
248     this.maxEntriesLocalDisk = maxEntriesLocalDisk;
249     recreateCacheIfInitialized();
250   }
251 
252   /**
253    * Sets the maximum bytes to be used for the disk tier. When greater than zero a disk resource pool is added to the
254    * cache, allowing entries evicted from the heap to overflow to disk. If set to zero (the default) no disk tier is
255    * configured and the cache is heap-only.
256    *
257    * @param maxBytesLocalDisk
258    *          the maximum number of bytes to allocate on disk. 0 means no disk tier (heap-only).
259    */
260   public void setMaxBytesLocalDisk(long maxBytesLocalDisk) {
261     this.maxBytesLocalDisk = maxBytesLocalDisk;
262     recreateCacheIfInitialized();
263   }
264 
265   /**
266    * Sets the eviction policy. Stored for informational purposes; Ehcache 3 manages its own eviction strategy.
267    *
268    * @param memoryStoreEvictionPolicy
269    *          a String representation of the policy. One of "LRU", "LFU" or "FIFO".
270    */
271   public void setMemoryStoreEvictionPolicy(String memoryStoreEvictionPolicy) {
272     this.memoryStoreEvictionPolicy = memoryStoreEvictionPolicy;
273     recreateCacheIfInitialized();
274   }
275 
276   /**
277    * Recreates the underlying Ehcache 3 cache with the current configuration if the cache has already been initialised.
278    * Called by property setters when a configuration change is requested after first use.
279    */
280   protected synchronized void recreateCacheIfInitialized() {
281     if (cache != null) {
282       cache = buildAndRegisterCache();
283     }
284   }
285 
286   /**
287    * Placeholder used to represent a cached {@code null} value. Ehcache 3 does not permit null values, so this sentinel
288    * is stored and translated back to {@code null} on retrieval.
289    */
290   private static final class NullValue implements Serializable {
291     private static final long serialVersionUID = 1L;
292   }
293 
294 }